spring security 登录流程解析 (用户名、密码模式)
- 2021 年 12 月 02 日
本文字数:8557 字
阅读完需:约 28 分钟
客户端请求
/oauth/token地址,这个在 spring security 框架中的 TokenEndpoint 类之中如果是 OAuth2Authentication 认证的话,则需要获取去客户端 ID(getClientId(principal))
通过 clientId 创建 ClientDetails 对象,默认从数据库
oauth_client_details这张表获取相关配置,该表主要配置了授权方式(authorized_grant_types 字段),比如有password,app,refresh_token,authorization_code,client_credentials
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);通过请求参数 parameters 和 authenticatedClient 对象创建 TokenRequest
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);获取 token
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);根据配置的 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 加入
还未添加个人简介











评论