Spring Security 认证流程(一)

129 阅读6分钟

Spring Security 认证流程(一)

稍微了解一点Spring Security的都知道,Spring Security主要有两大功能:认证和授权。网上有许多相关教程,但是大多比较晦涩。

我相信,刚刚接触到安全框架的同学,应该有许多人和我一样,有种无从下手的感觉。 但是今天看到两个大佬的分析笔记,受益匪浅,在这里记录一下自己的理解。大佬们的链接我会放在帖子底部。

基础使用

Spring Security最简单的应用就是用户登录功能了。 实现这点需要的工作只有3步:

1. 进行security核心配置

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.cors().and().csrf().disable()
            .authorizeRequests();
    }

    //密码加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //用它帮我们进行认证操作,调用这个Bean的authenticate方法会由Spring Security自动帮我们做认证。
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

在上述SpringSecurityConfig.java中,配置了密码加密方式BCryptPasswordEncoder(),将Bean组件authenticationManager()交给了spring容器。

2. 登录认证具体实现

实现访问接口

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthService authService;

    @PostMapping("/login")
    public ApiResult login(@Valid @RequestBody LoginInfo loginInfo) {
        return authService.login(loginInfo.getLoginAccount(), loginInfo.getPassword());
    }
      ...
}

接口中调用了authService.login()的服务,需要进行实现。

实现对应服务

   @Slf4j
   @Service
   public class AuthServiceImpl implements AuthService {
       @Autowired
       private AuthenticationManager authenticationManager;
       @Autowired
       private JwtProvider jwtProvider;
       @Autowired
       private Cache caffeineCache;


       @Override
       public ApiResult login(String loginAccount, String password) {

           log.debug("进入login方法");
           // 1 创建UsernamePasswordAuthenticationToken
           UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
           // 2 认证
           Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
           // 3 保存认证信息
           SecurityContextHolder.getContext().setAuthentication(authentication);
           // 4 生成自定义token
           AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());
         
           return ApiResult.ok(accessToken);
       }

       ...
   }

这里一共4个步骤,大概只有前四步是比较陌生的:

  1. 传入用户名和密码创建了一个UsernamePasswordAuthenticationToken对象,这是我们前面说过的Authentication的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication对象。
  2. 使用我们先前已经声明过的Bean:authenticationManager调用它的authenticate方法进行认证,返回一个认证完成的Authentication对象。(在配置类中,使用@Bean方法声明的)
  3. 认证完成没有出现异常,就会走到第三步,使用SecurityContextHolder获取SecurityContext之后,将认证完成之后的Authentication对象,放入上下文对象。
  4. 从Authentication对象中拿到我们的UserDetails对象,之前我们说过,认证后的Authentication对象调用它的getPrincipal()方法就可以拿到我们先前数据库查询后组装出来的UserDetails对象,然后创建token。

到这里,其实就已经完成了,感觉很简单,因为主要认证操作都会由authenticationManager.authenticate()帮我们完成。

authenticate()实际也是去查对象数据库,如何查数据库呢?查询数据库的方法就是调用userDetailsService中的loadUserByUsername()。

大家一般都会自定义实现一下UserDetailsService,就像下面这样。

3. 自定义实现UserDetailsService

@Slf4j
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleInfoService roleInfoService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("开始登陆验证,用户名为: {}", s);

        // 根据用户名验证用户
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(UserInfo::getUsername, s);
        UserInfo userInfo = userService.getOne(queryWrapper);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用户名不存在,登陆失败。");
        }

        // 构建UserDetail对象
        UserDetails userDetail = new UserDetails();
        userDetail.setUserInfo(userInfo);
     
        return userDetail;
    }
}

完成以上步骤就可以去进行简单测试了。

ps:基本的实体类、统一返回结果类ApiResult还有token生成工具类jwtProvider 需要自己提前准备好。

原理分析

image.png 如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。

我们上面这个案例基本都是绿色部分的内容。

案例解析

下面是security中一种表单登录模式的流程示意图。 image.png

在一开始的配置过程中,我们就使用了formLogin方法。这是我们在会经常用到的一段代码,大家应该都比较熟悉,加上.formLogin()后就代表着我们采用了form表单认证方式,框架会自动帮助我们进行用户认证。后续部分介绍的就是框架帮助我们进行用户认证的具体方法。 image.png

  1. HttpSecurity.formLogion() 方法会返回一个 FormLoginConfigurer 对象。这个构造器对象内部就包含着一个UsernamePasswordAuthenticationFilter 过滤器。
  2. UsernamePasswordAuthenticationFilter 过滤器的构造函数内绑定了 POST 类型的 /login 请求,也就是说,如果配置了 formLogin 的相关信息,那么在使用 POST 类型的 /login URL进行登录的时候就会被这个过滤器拦截,并进行登录验证 这就是加入.formLogion()就会进行用户身份验证的原因。
  3. 此外,UsernamePasswordAuthenticationFilter 过滤器会解析请求,获得username和password,封装到UsernamePasswordAuthenticationToken 中,然后委托给AuthenticationManager,交由 AuthenticationManager 完成实际的登录认证过程。自身只要在登录成功之后,将认证后的 Authentication 对象存储到请求线程上下文,以方便授权阶段使用就可以了。这就是UsernamePasswordAuthenticationFilter 过滤器的主要工作:解析请求+委托认证+成功存储上下文
  4. 其实到 AuthenticationManager这里大家就又会比较熟悉了,因为在前面的案例中,我们也主动调用authenticationManager.authenticate()进行过用户认证。其实那就是一种手动的简化版身份认证流程。authenticationManager只是一个接口,具体的实现方法在ProviderManager中,然而ProviderManager又将任务委托给AuthenticationProvider。这里就是一系列的委托再委托,直到交给AuthenticationProvider
  5. 所以AuthenticationProvider需要完成AuthenticationManager的工作:用户身份验证,即需要完成:获取验证需要的用户信息的工作 + 密码验证,账号状态验证等逻辑验证的工作。检查和验证的工作基本一致,但是用户信息加载方式常有不同,而我们常用的DaoAuthenticationProvide就是一种通过DAO方式获取验证需要的用户信息的一个具体实现类。
  6. UserDetailsService则是被DaoAuthenticationProvider 调用,获取验证信息的一个工具类。前面的案例中我们也自定义实现了该类,核心就是这个loadUserByUsername(String username)方法。

总结:

  • 整体流程:HttpSecurity.formLogion()作为入口,生成一个包含UsernamePasswordAuthenticationFilter 过滤器的对象,所以才能实现表单登录认证。

  • UsernamePasswordAuthenticationFilter过滤器工作流程:过滤器解析请求获得username和password,委托给AuthenticationManager进行身份认证,成功后存储在上下文中。

  • AuthenticationManager工作流程:将任务多级下派到一个合适的AuthenticationProvider中,获取验证需要的用户信息,并进行多方面验证(密码,账号状态等)。

  • UserDetailsService就是一个获取验证需要的用户信息的工具类。

后续会介绍,如何结合jwt,进行登录认证优化。

作为一个未正式工作的学生,十分感谢各位大佬们的经验分享,也希望我的分享可以帮助到大家。如果有哪些错误的地方,也请大家不吝指出。

本篇文章是在学习两位大佬笔记后的收获整理,下面的链接是大佬们原文。

参考链接

条理清晰,剖析源码:www.cnblogs.com/xifengxiaom…

案例介绍,浅显易懂:juejin.cn/post/684668…