• spring security 用户认证原理


    目录

    一、从过滤器链开始

    二、获取用户信息

    三、用户名和密码认证流程

    四、构建成功的认证对象

    五、认证信息放入SecurityContext


    一、从过滤器链开始

    当我们执行认证逻辑,在获取用户信息时,可打一个断点,跟踪程序的调用链

    在程序的调用链中,找到一个 StandardWrapperValve 的记录,StandardWrapperValve.class 类位于 tomcat 核心包中,跟踪到这个调用位置(StandardWrapperValve#invoke),可以看到过滤器链的调用

    二、获取用户信息

    在调用链的一系列过滤器中,找到 UsernamePasswordAuthenticationFilter.calss(用户名和密码认证过滤器)

    用户名和密码认证过滤器继承了 AbstractAuthenticationProcessingFilter.calss,该过滤器的 #doFilter 方法中,调用了用户名和密码认证过滤器中用户认证的逻辑

    1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    2. throws IOException, ServletException {
    3. HttpServletRequest request = (HttpServletRequest) req;
    4. HttpServletResponse response = (HttpServletResponse) res;
    5. if (!requiresAuthentication(request, response)) {
    6. // 返回点 -> 需要关注
    7. chain.doFilter(request, response);
    8. return;
    9. }
    10. if (logger.isDebugEnabled()) {
    11. logger.debug("Request is to process authentication");
    12. }
    13. Authentication authResult;
    14. try {
    15. // 调用认证逻辑-> 返回认证结果
    16. authResult = attemptAuthentication(request, response);
    17. if (authResult == null) {
    18. // return immediately as subclass has indicated that it hasn't completed
    19. // authentication
    20. return;
    21. }
    22. sessionStrategy.onAuthentication(authResult, request, response);
    23. }
    24. catch (InternalAuthenticationServiceException failed) {
    25. logger.error(
    26. "An internal error occurred while trying to authenticate the user.",
    27. failed);
    28. // 扩展点
    29. unsuccessfulAuthentication(request, response, failed);
    30. return;
    31. }
    32. catch (AuthenticationException failed) {
    33. // 扩展点 Authentication failed 认证失败
    34. unsuccessfulAuthentication(request, response, failed);
    35. return;
    36. }
    37. // Authentication success 认证成功
    38. if (continueChainBeforeSuccessfulAuthentication) {
    39. chain.doFilter(request, response);
    40. }
    41. // 扩展点 -> AuthenticationSuccessHandler
    42. successfulAuthentication(request, response, chain, authResult);
    43. }

    调用 attemptAuthentication(request, response) 抽象方法

    UsernamePasswordAuthenticationFilter#attemptAuthentication 实现了该抽象方法

    1. public Authentication attemptAuthentication(HttpServletRequest request,
    2. HttpServletResponse response) throws AuthenticationException {
    3. // 1-必须为 post 方法
    4. if (postOnly && !request.getMethod().equals("POST")) {
    5. throw new AuthenticationServiceException(
    6. "Authentication method not supported: " + request.getMethod());
    7. }
    8. // 2-解析用户名和密码 -> 固定解析参数名称 'username','password'
    9. String username = obtainUsername(request);
    10. String password = obtainPassword(request);
    11. if (username == null) {
    12. username = "";
    13. }
    14. if (password == null) {
    15. password = "";
    16. }
    17. username = username.trim();
    18. // 3-封装到认证的token,此时认证状态为 false -> setAuthenticated(false);
    19. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    20. username, password);
    21. // Allow subclasses to set the "details" property
    22. setDetails(request, authRequest);
    23. // 4-调用认证管理器进行认证 -> 认证核心逻辑
    24. return this.getAuthenticationManager().authenticate(authRequest);
    25. }

    方法 this.getAuthenticationManager().authenticate(authRequest); 调用认证管理器的具体实现进行权限验证

    具体认证管理器的实现逻辑为 ProviderManager.class#authenticate

    1. public Authentication authenticate(Authentication authentication)
    2. throws AuthenticationException {
    3. Class<? extends Authentication> toTest = authentication.getClass();
    4. AuthenticationException lastException = null;
    5. AuthenticationException parentException = null;
    6. Authentication result = null;
    7. Authentication parentResult = null;
    8. boolean debug = logger.isDebugEnabled();
    9. // providers 列表循环
    10. for (AuthenticationProvider provider : getProviders()) {
    11. if (!provider.supports(toTest)) {
    12. continue;
    13. }
    14. if (debug) {
    15. logger.debug("Authentication attempt using "
    16. + provider.getClass().getName());
    17. }
    18. try {
    19. // 具体的provider -> 结果返回,关键的认证方法
    20. result = provider.authenticate(authentication);
    21. if (result != null) {
    22. copyDetails(authentication, result);
    23. break;
    24. }
    25. }
    26. catch (AccountStatusException e) {
    27. prepareException(e, authentication);
    28. // SEC-546: Avoid polling additional providers if auth failure is due to
    29. // invalid account status
    30. throw e;
    31. }
    32. catch (InternalAuthenticationServiceException e) {
    33. prepareException(e, authentication);
    34. throw e;
    35. }
    36. catch (AuthenticationException e) {
    37. lastException = e;
    38. }
    39. }
    40. if (result == null && parent != null) {
    41. // Allow the parent to try.
    42. try {
    43. result = parentResult = parent.authenticate(authentication);
    44. }
    45. catch (ProviderNotFoundException e) {
    46. // ignore as we will throw below if no other exception occurred prior to
    47. // calling parent and the parent
    48. // may throw ProviderNotFound even though a provider in the child already
    49. // handled the request
    50. }
    51. catch (AuthenticationException e) {
    52. lastException = parentException = e;
    53. }
    54. }
    55. if (result != null) {
    56. if (eraseCredentialsAfterAuthentication
    57. && (result instanceof CredentialsContainer)) {
    58. // Authentication is complete. Remove credentials and other secret data
    59. // from authentication
    60. ((CredentialsContainer) result).eraseCredentials();
    61. }
    62. // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
    63. // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
    64. if (parentResult == null) {
    65. eventPublisher.publishAuthenticationSuccess(result);
    66. }
    67. return result;
    68. }
    69. // Parent was null, or didn't authenticate (or throw an exception).
    70. if (lastException == null) {
    71. lastException = new ProviderNotFoundException(messages.getMessage(
    72. "ProviderManager.providerNotFound",
    73. new Object[] { toTest.getName() },
    74. "No AuthenticationProvider found for {0}"));
    75. }
    76. // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
    77. // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
    78. if (parentException == null) {
    79. prepareException(lastException, authentication);
    80. }
    81. throw lastException;
    82. }

    具体调用到哪一个 ProviderManager#authenticate 的实现,我们可以通过断点来定位

    此时,我们回到原来断点的调用链,可以看到代码的调用层次如下

    1. -> ProviderManager#authenticate
    2. -> AbstractUserDetailsAuthenticationProvider#authenticate 认证
    3. -> DaoAuthenticationProvider#retrieveUser // 获取用户信息

    执行逻辑调用到了 AbstractUserDetailsAuthenticationProvider#authenticate 方法中,该方法中有多个获取用户信息的扩展点,支持用户进行自定义

    1. public Authentication authenticate(Authentication authentication)
    2. throws AuthenticationException {
    3. Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    4. () -> messages.getMessage(
    5. "AbstractUserDetailsAuthenticationProvider.onlySupports",
    6. "Only UsernamePasswordAuthenticationToken is supported"));
    7. // Determine username
    8. String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
    9. : authentication.getName();
    10. boolean cacheWasUsed = true;
    11. // 1-从cache中获取用户信息 -> 可是实现 UserCache 接口自定义、redis等
    12. UserDetails user = this.userCache.getUserFromCache(username);
    13. if (user == null) {
    14. cacheWasUsed = false;
    15. try {
    16. // 2-从定义的方法中获取用户信息(实现接口自定义)
    17. user = retrieveUser(username,
    18. (UsernamePasswordAuthenticationToken) authentication);
    19. }
    20. catch (UsernameNotFoundException notFound) {
    21. logger.debug("User '" + username + "' not found");
    22. if (hideUserNotFoundExceptions) {
    23. throw new BadCredentialsException(messages.getMessage(
    24. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    25. "Bad credentials"));
    26. }
    27. else {
    28. throw notFound;
    29. }
    30. }
    31. Assert.notNull(user,
    32. "retrieveUser returned null - a violation of the interface contract");
    33. }
    34. try {
    35. // 3-检验用户是否被锁、是否可用、过期等 -> UserDetails 属性(可以自定义逻辑)
    36. preAuthenticationChecks.check(user);
    37. // 4-密码认证逻辑 -> 该类中的additionalAuthenticationChecks实现
    38. additionalAuthenticationChecks(user,
    39. (UsernamePasswordAuthenticationToken) authentication);
    40. }
    41. catch (AuthenticationException exception) {
    42. if (cacheWasUsed) {
    43. // There was a problem, so try again after checking
    44. // we're using latest data (i.e. not from the cache)
    45. cacheWasUsed = false;
    46. user = retrieveUser(username,
    47. (UsernamePasswordAuthenticationToken) authentication);
    48. preAuthenticationChecks.check(user);
    49. additionalAuthenticationChecks(user,
    50. (UsernamePasswordAuthenticationToken) authentication);
    51. }
    52. else {
    53. throw exception;
    54. }
    55. }
    56. postAuthenticationChecks.check(user);
    57. if (!cacheWasUsed) {
    58. // 设置用户缓存
    59. this.userCache.putUserInCache(user);
    60. }
    61. Object principalToReturn = user;
    62. if (forcePrincipalAsString) {
    63. principalToReturn = user.getUsername();
    64. }
    65. // 5-创建成功的权限验证结果 -> 构建成功的认证对象
    66. return createSuccessAuthentication(principalToReturn, authentication, user);
    67. }

    因为我们使用的是从数据库获取用户信息,所以程序最终会执行到下边的方法中来

    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);

    最终进入  DaoAuthenticationProvider.class#retrieveUser 方法,此方法中会去获取用户信息 -> 会调用我们自己实现的获取用户信息的方法

    1. protected final UserDetails retrieveUser(String username,
    2. UsernamePasswordAuthenticationToken authentication)
    3. throws AuthenticationException {
    4. prepareTimingAttackProtection();
    5. try {
    6. // 获取用户信息->调用我们自己实现的获取用户信息的方法
    7. UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
    8. if (loadedUser == null) {
    9. throw new InternalAuthenticationServiceException(
    10. "UserDetailsService returned null, which is an interface contract violation");
    11. }
    12. return loadedUser;
    13. }
    14. catch (UsernameNotFoundException ex) {
    15. mitigateAgainstTimingAttack(authentication);
    16. throw ex;
    17. }
    18. catch (InternalAuthenticationServiceException ex) {
    19. throw ex;
    20. }
    21. catch (Exception ex) {
    22. throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
    23. }
    24. }

    调用我们自己实现的 loadUserByUsername() 方法

    三、用户名和密码认证流程

    获取用户信息后,回到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中,该方法后边代码具体实现了密码验证的逻辑

    1. public Authentication authenticate(Authentication authentication)
    2. throws AuthenticationException {
    3. Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    4. () -> messages.getMessage(
    5. "AbstractUserDetailsAuthenticationProvider.onlySupports",
    6. "Only UsernamePasswordAuthenticationToken is supported"));
    7. // Determine username
    8. String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
    9. : authentication.getName();
    10. boolean cacheWasUsed = true;
    11. // 1-从cache中获取用户信息 -> 可是实现 UserCache 接口自定义、redis等
    12. UserDetails user = this.userCache.getUserFromCache(username);
    13. if (user == null) {
    14. cacheWasUsed = false;
    15. try {
    16. // 2-从定义的方法中获取用户信息(实现接口自定义)
    17. user = retrieveUser(username,
    18. (UsernamePasswordAuthenticationToken) authentication);
    19. }
    20. catch (UsernameNotFoundException notFound) {
    21. logger.debug("User '" + username + "' not found");
    22. if (hideUserNotFoundExceptions) {
    23. throw new BadCredentialsException(messages.getMessage(
    24. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    25. "Bad credentials"));
    26. }
    27. else {
    28. throw notFound;
    29. }
    30. }
    31. Assert.notNull(user,
    32. "retrieveUser returned null - a violation of the interface contract");
    33. }
    34. try {
    35. // 3-检验用户是否被锁、是否可用、过期等 -> UserDetails 属性(可以自定义逻辑)
    36. preAuthenticationChecks.check(user);
    37. // 4-密码认证逻辑 -> 该类中的additionalAuthenticationChecks实现
    38. additionalAuthenticationChecks(user,
    39. (UsernamePasswordAuthenticationToken) authentication);
    40. }
    41. catch (AuthenticationException exception) {
    42. if (cacheWasUsed) {
    43. // There was a problem, so try again after checking
    44. // we're using latest data (i.e. not from the cache)
    45. cacheWasUsed = false;
    46. user = retrieveUser(username,
    47. (UsernamePasswordAuthenticationToken) authentication);
    48. preAuthenticationChecks.check(user);
    49. additionalAuthenticationChecks(user,
    50. (UsernamePasswordAuthenticationToken) authentication);
    51. }
    52. else {
    53. throw exception;
    54. }
    55. }
    56. postAuthenticationChecks.check(user);
    57. if (!cacheWasUsed) {
    58. // 设置用户缓存
    59. this.userCache.putUserInCache(user);
    60. }
    61. Object principalToReturn = user;
    62. if (forcePrincipalAsString) {
    63. principalToReturn = user.getUsername();
    64. }
    65. // 5-创建成功的权限验证结果 -> 构建成功的认证对象
    66. return createSuccessAuthentication(principalToReturn, authentication, user);
    67. }

    密码验证的具体逻辑

    1. protected void additionalAuthenticationChecks(UserDetails userDetails,
    2. UsernamePasswordAuthenticationToken authentication)
    3. throws AuthenticationException {
    4. if (authentication.getCredentials() == null) {
    5. logger.debug("Authentication failed: no credentials provided");
    6. throw new BadCredentialsException(messages.getMessage(
    7. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    8. "Bad credentials"));
    9. }
    10. String presentedPassword = authentication.getCredentials().toString();
    11. // 对加密后的密码进行匹配
    12. if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
    13. logger.debug("Authentication failed: password does not match stored value");
    14. throw new BadCredentialsException(messages.getMessage(
    15. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    16. "Bad credentials"));
    17. }
    18. }

    执行断点截图

    至此用户名和密码认证完成,密码认证通过后,接着会获取用户权限,并构建成功的认证对象

    四、构建成功的认证对象

    回到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中

    1. public Authentication authenticate(Authentication authentication)
    2. throws AuthenticationException {
    3. Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    4. () -> messages.getMessage(
    5. "AbstractUserDetailsAuthenticationProvider.onlySupports",
    6. "Only UsernamePasswordAuthenticationToken is supported"));
    7. // Determine username
    8. String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
    9. : authentication.getName();
    10. boolean cacheWasUsed = true;
    11. // 1-从cache中获取用户信息 -> 可是实现 UserCache 接口自定义、redis等
    12. UserDetails user = this.userCache.getUserFromCache(username);
    13. if (user == null) {
    14. cacheWasUsed = false;
    15. try {
    16. // 2-从定义的方法中获取用户信息(实现接口自定义)
    17. user = retrieveUser(username,
    18. (UsernamePasswordAuthenticationToken) authentication);
    19. }
    20. catch (UsernameNotFoundException notFound) {
    21. logger.debug("User '" + username + "' not found");
    22. if (hideUserNotFoundExceptions) {
    23. throw new BadCredentialsException(messages.getMessage(
    24. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    25. "Bad credentials"));
    26. }
    27. else {
    28. throw notFound;
    29. }
    30. }
    31. Assert.notNull(user,
    32. "retrieveUser returned null - a violation of the interface contract");
    33. }
    34. try {
    35. // 3-检验用户是否被锁、是否可用、过期等 -> UserDetails 属性(可以自定义逻辑)
    36. preAuthenticationChecks.check(user);
    37. // 4-密码认证逻辑 -> 该类中的additionalAuthenticationChecks实现
    38. additionalAuthenticationChecks(user,
    39. (UsernamePasswordAuthenticationToken) authentication);
    40. }
    41. catch (AuthenticationException exception) {
    42. if (cacheWasUsed) {
    43. // There was a problem, so try again after checking
    44. // we're using latest data (i.e. not from the cache)
    45. cacheWasUsed = false;
    46. user = retrieveUser(username,
    47. (UsernamePasswordAuthenticationToken) authentication);
    48. preAuthenticationChecks.check(user);
    49. additionalAuthenticationChecks(user,
    50. (UsernamePasswordAuthenticationToken) authentication);
    51. }
    52. else {
    53. throw exception;
    54. }
    55. }
    56. postAuthenticationChecks.check(user);
    57. if (!cacheWasUsed) {
    58. // 设置用户缓存
    59. this.userCache.putUserInCache(user);
    60. }
    61. Object principalToReturn = user;
    62. if (forcePrincipalAsString) {
    63. principalToReturn = user.getUsername();
    64. }
    65. // 5-创建成功的权限验证结果 -> 构建成功的认证对象
    66. return createSuccessAuthentication(principalToReturn, authentication, user);
    67. }

    createSuccessAuthentication(principalToReturn, authentication, user); 构建的认证成功结果如下

    五、认证信息放入SecurityContext

    UsernamePasswordAuthenticationFilter 继承了认证过滤器 AbstractAuthenticationProcessingFilter,实现了具体的认证逻辑,用户认证时会先执行AbstractAuthenticationProcessingFilter#doFilter

    1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    2. throws IOException, ServletException {
    3. HttpServletRequest request = (HttpServletRequest) req;
    4. HttpServletResponse response = (HttpServletResponse) res;
    5. if (!requiresAuthentication(request, response)) {
    6. chain.doFilter(request, response);
    7. return;
    8. }
    9. if (logger.isDebugEnabled()) {
    10. logger.debug("Request is to process authentication");
    11. }
    12. Authentication authResult;
    13. try {
    14. // 认证逻辑 -> UsernamePasswordAuthenticationFilter
    15. authResult = attemptAuthentication(request, response);
    16. if (authResult == null) {
    17. // return immediately as subclass has indicated that it hasn't completed
    18. // authentication
    19. return;
    20. }
    21. sessionStrategy.onAuthentication(authResult, request, response);
    22. }
    23. catch (InternalAuthenticationServiceException failed) {
    24. logger.error(
    25. "An internal error occurred while trying to authenticate the user.",
    26. failed);
    27. unsuccessfulAuthentication(request, response, failed);
    28. return;
    29. }
    30. catch (AuthenticationException failed) {
    31. // Authentication failed
    32. unsuccessfulAuthentication(request, response, failed);
    33. return;
    34. }
    35. // Authentication success
    36. if (continueChainBeforeSuccessfulAuthentication) {
    37. chain.doFilter(request, response);
    38. }
    39. // 成功认证后的操作
    40. successfulAuthentication(request, response, chain, authResult);
    41. }

    认证通过后,doFilter 方法中调用了 以下方法

    successfulAuthentication(request, response, chain, authResult);

    该方法也在 AbstractAuthenticationProcessingFilter 过滤器中, 在AbstractAuthenticationProcessingFilter#successfulAuthentication 方法中,把认证结果存入到了 SecurityContext 中

    1. protected void successfulAuthentication(HttpServletRequest request,
    2. HttpServletResponse response, FilterChain chain, Authentication authResult)
    3. throws IOException, ServletException {
    4. if (logger.isDebugEnabled()) {
    5. logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
    6. + authResult);
    7. }
    8. // 把认证结果存入到 SecurityContext 中
    9. SecurityContextHolder.getContext().setAuthentication(authResult);
    10. rememberMeServices.loginSuccess(request, response, authResult);
    11. // Fire event
    12. if (this.eventPublisher != null) {
    13. eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
    14. authResult, this.getClass()));
    15. }
    16. successHandler.onAuthenticationSuccess(request, response, authResult);
    17. }

    至此,整个认证流程完成。

     补充用户认证流程。

  • 相关阅读:
    Java 线程
    Spring Boot框架
    【笔记版】cgroup大摸底
    PID控制原理基本介绍(图解)
    在CentOS下安装MySQL
    《安富莱嵌入式周报》第291期:分分钟设计数字芯片,单片机版JS,神经网络DSP,microPLC,FatFS升级至V0.15,微软Arm64 VS正式版发布
    Gartner:2024 年十大战略技术趋势
    为什么mac上有的软件删除不掉?
    在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
    个人博客系列-后端项目-RBAC角色管理(6)
  • 原文地址:https://blog.csdn.net/swadian2008/article/details/126492531