添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

项目启动后会自动寻找 UserDetailsService 实现类;
执行 UserDetailsService 的唯一方法 loadUserByName(String username) 并返回 UserDetail 类,注意,返回的 UserDetail 是根据用户名去数据库查询到用户信息;
拿到 UserDetail 后会对 UserDetail 进行一个预检查,检查 用户是否存在,是否被锁定等等等
全部认证成功后会调用 AuthenticationSuccess 成功处理类,失败则调用 AuthenticationFailHandler 类;
此时对于前后端分离项目而言,调用成功处理类,将验证结果返回给前端,前台拿到返回信息后,保存token致本地,然后每次请求都会拼接到head中。

认证处理流程

认证处理流程

  • 获取用户名与密码。
  • 拿用户名与密码构建 UsernamePasswordAuthenticationToken 对象。
  • 实例化 UsernamePasswordAuthenticationToken 之后调用了 setDetails(request,authRequest) 将请求的信息设到 UsernamePasswordAuthenticationToken 中去,包括ip、session等内。
  • 然后去调用 AuthenticationManager AuthenticationManager 本身不包含验证的逻辑,它的作用是用来管理 AuthenticationProvider
  • UsernamePasswordAuthenticationToken 对象其实是 Authentication 接口的实现, Authentication 封装的是用户的验证信息。

  • 用户名与密码设为本地变量
  • super((Collection)null); 中collection代表权限列表,在这传了一个 null 进去是因为刚开始并没有进行认证,因此用户此时没有任何权限,并且设置没有认证的信息 setAuthenticated(false)
  • 校验逻辑

    真正的校验逻辑写在 AuthenticationProvider 中,

  • 遍历 Provider 集合。因为不同的登陆方式认证逻辑是不一样的,可能是微信等社交平台登陆,也可能是用户名密码登陆。 AuthenticationManager 其实是将 AuthenticationProvider 收集起来,然后登陆的时候挨个去 AuthenticationProvider 中问你这种验证逻辑支不支持此次登陆的方式,根据传进来的 Authentication 类型会挑出一个适合的 Provider 来进行校验处理。
  • 挑一种 Provider 进行判断,调用 Provider.supports() 方法,判断 Provider 是否支持 authentication.getClass() 的类型。
  • 真正的执行校验逻辑。
  • provider.authenticate(authentication) 中, authenticate DaoAuthenticationProvider 类中的一个方法。

    DaoAuthenticationProvider 继承了 AbstractUserDetailsAuthenticationProvider 。实际上 authenticate 的校验逻辑写在了 AbstractUserDetailsAuthenticationProvider 抽象类中。

  • 首先实例化 UserDetails 对象,调用了 retrieveUser 方法获取到了一个 user 对象, retrieveUser 是一个抽象方法。
  • 如果没拿到信息就会抛出异常,如果查到了就会去调用 preAuthenticationChecks check(user) 方法去进行预检查。在预检查中进行了三个检查,因为 UserDetail 类中有四个布尔类型,去检查其中的三个, 用户是否锁定 用户是否过期 用户是否可用
  • 预检查之后紧接着去调用了 additionalAuthenticationChecks 方法去进行附加检查,这个方法也是一个抽象方法, 检查密码是否匹配 ,在 DaoAuthenticationProvider 中去具体实现,在里面进行了加密解密去校验当前的密码是否匹配。
  • 如果通过了预检查和附加检查,还会进行厚检查,检查4个布尔中的最后一个, 检查身份认证是否已过期
  • 所有的检查都通过,则认为用户认证是成功的。用户认证成功之后,会将这些认证信息和user传递进去,调用 createSuccessAuthentication 方法。
  • 在这个方法中同样会实例化一个user,但是这个方法不会调用之前传两个参数的函数,而是会调用三个参数的构造函数。这个时候,在调 super 的构造函数中不会再传 null ,会将 authorities 权限设进去,之后将用户密码设进去,最后 setAuthenticated(true) ,代表验证已经通过。

    最后创建一个 authentication 会沿着验证的这条线返回回去。

    在拿到 authentication 后,会调用 successHandler.onAuthenticationSuccess(request, response, authResult); ,这个就是住在调用我们自己写的认证成功的处理器。
    如果验证成功,则在这条路中调用我们系统的业务逻辑。

    认证失败

    如果在任何一处发生问题,就会抛出异常,在AbstractAuthenticationProcessingFilter中进行捕获,然后进行 unsuccessfulAuthentication ,这里调用 failureHandler.onAuthenticationFailure(request, response, failed); ,也就是调用我们自己定义的认证失败的处理器。
    AbstractAuthenticationProcessingFilter.java:unsuccessfulAuthentication()

    认证结果如何在多个请求之间共享

    多个请求之间共享肯定是放在session中,但是它是什么时候,把什么东西放到了session中,什么时候在session中读出来。

    什么时候共享

    在验证成功最后会调用我们自定义的 successHandler 登陆成功处理器,在调用这个方法之前会调用 SecurityContextHolder.getContext().setAuthentication(authResult); ,会将我们验证成功的那个 Authentication 放到 SecurityContext 中,然后再放到 SecurityContextHolder 中。

    SecurityContextImpl 是SecurityContext的一个实现类,它包装了 authentication ,重写了 hashcode 方法和 equals 方法去保证 authentication 的唯一。

    怎么共享

    SecurityContextHolder ThreadLocal 的一个封装, ThreadLocal 是线程绑定的一个map,在同一个线程里在这个方法里往 ThreadLocal 里设置的变量是可以在另一个线程中读取到的。可以理解为 SecurityContextHolder 是一个线程级的全局变量,在一个线程中操作 ThreadLocal 中的数据会影响另一个线程。

    setAuthentication(authResult) ,将 authentication 放在当前的线程 SecurityContextHolder 中去,在整个认证处理过程中,在任何一个方法里,通过 SecurityContextHolder 的静态方法,都能讲 authentication 读出来。

    登录成功后,登录所有的请求都会通过 SecurityContextPersisenceFilter SecurityContextHolder 拿那个 authentication SecurityContextPersisenceFilter 在整个过滤器的最前面。

    SecurityContextPersisenceFilter 的作用:

  • 当请求进入过滤器链时,先进它,检查 session 是否有 securityContext 。如果有,就把从 securityContext 中从 session 取出,放到线程中。如果没有,则跳过。
  • 当响应最后过它时,检查线程。如果线程中有 securityContext ,就发出来放到 session 中去。
  • 这样不同的请求,可以从线程中拿到相同的用户认证信息。整个请求与相应的过程都是在一个线程中完成的,因此在线程的其他位置,随时可以用 SecurityContextHolder 来拿到用户认证信息。

    获取认证用户信息

    如何用 SecurityContextHolder 来拿到用户认证信息

    可以使用SecurityContextHolder去获取用户的认证信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // UserController.java

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
    @GetMapping("/me")
    public Object getCurrentUser(){
    return SecurityContextHolder.getContext().getAuthentication();
    }
    }

    可以直接获取 authentication

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // UserController.java

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
    @GetMapping("/me")
    public Object getCurrentUser(Authentication authentication){
    return authentication;
    }
    }

    如果不需要返回全部的用户信息,可以使用 @AuthenticationPrincipal 注解,返回部分用户信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // UserController.java

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
    @GetMapping("/me")
    public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails){
    return userDetails;
    }
    }