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 加入
还未添加个人简介
评论