写点什么

spring security 登录流程解析 (用户名、密码模式)

作者:Tracy-wen
  • 2021 年 12 月 02 日
  • 本文字数:8557 字

    阅读完需:约 28 分钟

  1. 客户端请求/oauth/token地址,这个在 spring security 框架中的 TokenEndpoint 类之中

  2. 如果是 OAuth2Authentication 认证的话,则需要获取去客户端 ID(getClientId(principal))

  3. 通过 clientId 创建 ClientDetails 对象,默认从数据库oauth_client_details这张表获取相关配置,该表主要配置了授权方式(authorized_grant_types 字段),比如有password,app,refresh_token,authorization_code,client_credentials


   ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
复制代码


  1. 通过请求参数 parameters 和 authenticatedClient 对象创建 TokenRequest


   TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
复制代码


  1. 获取 tokenOAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

  2. 根据配置的 TokenGranter 对象的 grant 方法来授权获取 token


     public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {         @Override       public void configure(AuthorizationServerEndpointsConfigurer endpoints) {         endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)             .tokenServices(tokenServices())             .tokenStore(redisTokenStore).tokenEnhancer(tokenEnhancer()).userDetailsService(userDetailsService)             .authenticationManager(authenticationManager).reuseRefreshTokens(false)             .pathMapping("/oauth/confirm_access", "/token/confirm_access")             .exceptionTranslator(new PigWebResponseExceptionTranslator());         setTokenGranter(endpoints);       }       private void setTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {         // 获取默认授权类型         TokenGranter tokenGranter = endpoints.getTokenGranter();         ArrayList<TokenGranter> tokenGranters = new ArrayList<>(Arrays.asList(tokenGranter));         ResourceOwnerCustomeAppTokenGranter resourceOwnerCustomeAppTokenGranter = new ResourceOwnerCustomeAppTokenGranter(             authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(),             endpoints.getOAuth2RequestFactory());         tokenGranters.add(resourceOwnerCustomeAppTokenGranter);         CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranters);         endpoints.tokenGranter(compositeTokenGranter);       }     }
复制代码


  • 接下来看下 TokenGranter 的 grant 方法


     public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {              //1.遍历当前授权者对象支持的授权方式         for (TokenGranter granter : tokenGranters) {                   //调用授权方法并判断是否和请求参数的授权方式是否匹配           OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);           if (grant!=null) {             return grant;           }         }         return null;       }
复制代码


 最终会调用到配置类的匿名授权者
复制代码


     private TokenGranter tokenGranter() {         if (tokenGranter == null) {           tokenGranter = new TokenGranter() {             private CompositeTokenGranter delegate;                  @Override             public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {               if (delegate == null) {                 delegate = new CompositeTokenGranter(getDefaultTokenGranters());               }               return delegate.grant(grantType, tokenRequest);             }           };         }         return tokenGranter;       }
复制代码


而这边的getDefaultTokenGranters方法则会初始化一个默认的 TokenGranter 列表,包含所有的类型的 Granter


     private List<TokenGranter> getDefaultTokenGranters() {         ClientDetailsService clientDetails = clientDetailsService();         AuthorizationServerTokenServices tokenServices = tokenServices();         AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();         OAuth2RequestFactory requestFactory = requestFactory();              List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();         tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,             requestFactory));         tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));         ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);         tokenGranters.add(implicit);         tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));         if (authenticationManager != null) {           tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,               clientDetails, requestFactory));         }         return tokenGranters;       }
复制代码


同时遍历所有的的 granter 与当前 type 匹配的 granter 对象,还是调用public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest),该方法会调用具体的 grant 方法,比如拿ResourceOwnerPasswordTokenGranter类型来说,该子类没有重写 grant 方法,所以我们需要看他父类的grant方法,内容如下:


       public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {              //1.判断类型是否与请求参数的类型匹配,如果不匹配直接返回null         if (!this.grantType.equals(grantType)) {           return null;         }             //2.通过客户端ID获取客户端对象         String clientId = tokenRequest.getClientId();         ClientDetails client = clientDetailsService.loadClientByClientId(clientId);         validateGrantType(grantType, client);              if (logger.isDebugEnabled()) {           logger.debug("Getting access token for: " + clientId);         }              //3.调用真正的获取token方法         return getAccessToken(client, tokenRequest);            }
复制代码


我们可以看到 getAccessToken 方法,在创建 token 之前会先获取一个 OAuth2Authentication 对象,该对象获取的过程就是验证我们密码是否匹配的过程


       protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {         return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));       }
复制代码


通过创建 OAuth2Request 来创建 OAuth2Authentication


       public OAuth2Request createOAuth2Request(ClientDetails client) {         Map<String, String> requestParameters = getRequestParameters();         HashMap<String, String> modifiable = new HashMap<String, String>(requestParameters);         // Remove password if present to prevent leaks         modifiable.remove("password");         modifiable.remove("client_secret");         // Add grant type so it can be retrieved from OAuth2Request         modifiable.put("grant_type", grantType);         return new OAuth2Request(modifiable, client.getClientId(), client.getAuthorities(), true, this.getScope(),             client.getResourceIds(), null, null, null);       }
复制代码


     @Override       protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {              Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());         String username = parameters.get("username");         String password = parameters.get("password");         // Protect from downstream leaks of password         parameters.remove("password");             //1.根据传入的用户名密码,创建UsernamePasswordAuthenticationToken对象         Authentication userAuth = new UsernamePasswordAuthenticationToken     UsernamePasswordAuthenticationToken(username, password);         ((AbstractAuthenticationToken) userAuth).setDetails(parameters);         try {                  //2.验证用户名密码是否正确           userAuth = authenticationManager.authenticate(userAuth);         }         catch (AccountStatusException ase) {           //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)           throw new InvalidGrantException(ase.getMessage());         }         catch (BadCredentialsException e) {           // If the username/password are wrong the spec says we should send 400/invalid grant           throw new InvalidGrantException(e.getMessage());         }         if (userAuth == null || !userAuth.isAuthenticated()) {           throw new InvalidGrantException("Could not authenticate user: " + username);         }                  OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);             return new OAuth2Authentication(storedOAuth2Request, userAuth);       }
复制代码


实际上调用的是org.springframework.security.authentication.ProviderManager#authenticate方法


     public Authentication authenticate(Authentication authentication) throws AuthenticationException {         Class<? extends Authentication> toTest = authentication.getClass();         AuthenticationException lastException = null;         AuthenticationException parentException = null;         Authentication result = null;         Authentication parentResult = null;         int currentPosition = 0;         int size = this.providers.size();              //……此处省去部分代码         if (result == null && this.parent != null) {           // Allow the parent to try.           try {                      //此处就是调用密码验证的地方             parentResult = this.parent.authenticate(authentication);             result = parentResult;           }           catch (ProviderNotFoundException ex) {             // ignore as we will throw below if no other exception occurred prior to             // calling parent and the parent             // may throw ProviderNotFound even though a provider in the child already             // handled the request           }           catch (AuthenticationException ex) {             parentException = ex;             lastException = ex;           }         }         if (result != null) {           if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {             // Authentication is complete. Remove credentials and other secret data             // from authentication             ((CredentialsContainer) result).eraseCredentials();           }           // If the parent AuthenticationManager was attempted and successful then it           // will publish an AuthenticationSuccessEvent           // This check prevents a duplicate AuthenticationSuccessEvent if the parent           // AuthenticationManager already published it           if (parentResult == null) {             this.eventPublisher.publishAuthenticationSuccess(result);           }                return result;         }            //此处省去部分代码       }
复制代码


以上的 parentResult = this.parent.authenticate(authentication)实际上是调用了 parent 的 authenticate 方法,实际上还是 ProviderManager 对象,只是对象持有的 providers 不一样,parent 持有的对象是 DaoAuthenticationProvider 对象,而 DaoAuthenticationProvider 又没有实现 authenticate 方法,所以调用父类的 authenticate 方法,也就是org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate


     public Authentication authenticate(Authentication authentication) throws AuthenticationException {         Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,             () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",                 "Only UsernamePasswordAuthenticationToken is supported"));              //1、通过请求参数获取username         String username = determineUsername(authentication);         boolean cacheWasUsed = true;         UserDetails user = this.userCache.getUserFromCache(username);         if (user == null) {           cacheWasUsed = false;           try {                      //2、通过用户名获取用户对象             user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);           }           catch (UsernameNotFoundException ex) {             this.logger.debug("Failed to find user '" + username + "'");             if (!this.hideUserNotFoundExceptions) {               throw ex;             }             throw new BadCredentialsException(this.messages                 .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));           }           Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");         }         try {                 //3、初步验证用户对象是否正常           this.preAuthenticationChecks.check(user);                 //4、验证用户信息是否正常           additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);         }         catch (AuthenticationException ex) {           if (!cacheWasUsed) {             throw ex;           }           // There was a problem, so try again after checking           // we're using latest data (i.e. not from the cache)           cacheWasUsed = false;           user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);           this.preAuthenticationChecks.check(user);           additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);         }         this.postAuthenticationChecks.check(user);         if (!cacheWasUsed) {           this.userCache.putUserInCache(user);         }         Object principalToReturn = user;         if (this.forcePrincipalAsString) {           principalToReturn = user.getUsername();         }         return createSuccessAuthentication(principalToReturn, authentication, user);       }
复制代码


additionalAuthenticationChecks该方法主要完成了密码匹配的工作,spring security 默认是使用 BCrypt 加密算法 BCryptPasswordEncoder


     protected void additionalAuthenticationChecks(UserDetails userDetails,           UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {         if (authentication.getCredentials() == null) {           this.logger.debug("Failed to authenticate since no credentials provided");           throw new BadCredentialsException(this.messages               .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));         }         String presentedPassword = authentication.getCredentials().toString();         if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {           this.logger.debug("Failed to authenticate since password does not match stored value");           throw new BadCredentialsException(this.messages               .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));         }       }     //最终调用的是BCryptPasswordEncoder的matches方法
复制代码


万事俱备,我来看下获取 token 的方法,以下是默认的创建 token 的方法(DefaultTokenServices),我们也可以自定义创建 AccessToken,可以自定义 token 的存储方式(tokenStore)比如将其存到 redis 中等等


     @Transactional       public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {              OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);         OAuth2RefreshToken refreshToken = null;         if (existingAccessToken != null) {           if (existingAccessToken.isExpired()) {             if (existingAccessToken.getRefreshToken() != null) {               refreshToken = existingAccessToken.getRefreshToken();               // The token store could remove the refresh token when the               // access token is removed, but we want to               // be sure...               tokenStore.removeRefreshToken(refreshToken);             }             tokenStore.removeAccessToken(existingAccessToken);           }           else {             // Re-store the access token in case the authentication has changed             tokenStore.storeAccessToken(existingAccessToken, authentication);             return existingAccessToken;           }         }              // Only create a new refresh token if there wasn't an existing one         // associated with an expired access token.         // Clients might be holding existing refresh tokens, so we re-use it in         // the case that the old access token         // expired.         if (refreshToken == null) {           refreshToken = createRefreshToken(authentication);         }         // But the refresh token itself might need to be re-issued if it has         // expired.         else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {           ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;           if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {             refreshToken = createRefreshToken(authentication);           }         }              OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);         tokenStore.storeAccessToken(accessToken, authentication);         // In case it was modified         refreshToken = accessToken.getRefreshToken();         if (refreshToken != null) {           tokenStore.storeRefreshToken(refreshToken, authentication);         }         return accessToken;            }
复制代码


用户头像

Tracy-wen

关注

还未添加个人签名 2020.08.25 加入

还未添加个人简介

评论

发布
暂无评论
spring security登录流程解析(用户名、密码模式)