这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战
前两篇(Spring Security 认证流程 和 Spring Security 自定义认证逻辑 )简单介绍了 Spring Security 认证的流程以及增加自定义认证逻辑的方法。在自定义认证逻辑的那篇里,有一个认证成功后进行后续处理的 Handler 类,可以在登录成功后,将当前登录的用户信息放在会话当中,完成完整机遇会话的登录流程。
不过,现在很多实践中,都是以 Spring Security OAuth 做认证和授权的,在认证成功后,要返回一个访问令牌,于是想到了怎样将手机验证码的认证方式 OAuth 认证的流程中。
第一步就是从 Spring Security OAuth 源码分析其实现原理,然后对功能进行扩展。这篇我们先分析最常用也相对简单的密码模式(也叫资源所有者模式,下文统称密码模式)。
由于内容的连贯性,建议你先读前文提到的两篇文章。以下内容需要你了解 OAuth 的原理,可以阅读这篇文章回顾一下。
请求方式
密码模式的认证请求方式如下:
POST HOST:PORT/oauth/token
Authorization: Basic <Base64("clientId:clientSecret")>
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=user&password=123456&scope=admin
在 Spring Security OAuth 中,这个请求的默认 URI 是 /oauth/token,需要在 Header 中添加客户端的认证信息,在请求参数中提供授权类型、用户的认证信息等。
请求处理
这个请求在 TokenEndpoint 类中处理,我们可以找到如下源代码:
从其中可以看到,无论是 GET 和 POST 请求都会在 POST 请求的方法中来处理。方法中大部分都是各种封装和验证的代码,不在这里贴了,可以自行查看。
方法最终返回的令牌,来自末尾处的一行代码:
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
这一行代码,就是我们分析原理的线索。但看这行代码,可以分析出,这里需要获取一个 TokenGranter(getTokenGranter方法的返回值类型),然后将请求信息作为参数,调用它的 grant 方法,或得到 Token。
TokenGranter
我们顺着这个思路,找到 TokenGranter 类,查看一下它的实现类:
可以分析出,这些实现类,基本上对应了 OAuth 中的集中授权模式,抽象类 AbstractTokenGranter 是它们的父类。
还有一个 CompositeTokenGranter 类,从它的名字,以及类中包含了一个 List<TokenGranter> tokenGranters 属性,大致可以分析出它主要用来组织多个不同类型的 Granter。
这篇我们主要分析密码模式,所以我们下面找到 ResourceOwnerPasswordTokenGranter 查看它的源码。
ResourceOwnerPasswordTokenGranter
之前分析到,最终的 Token 是通过 TokenGranter 的 grant 方法获得的,我们可以找到这个类的 grant 方法在它的父类 AbstractTokenGranter 中。代码不多,直接贴出来:
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
以上代码中,校验了客户端的一些信息,然后调用了 getAccessToken 方法,然后在其中,调用了 getOAuth2Authentication,这个方法是在我们要分析的子类 ResourceOwnerPasswordTokenGranter 中重写了的。
我们找到这个方法的代码:
@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");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
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);
}
从代码中不难发现熟悉的配方,我们看到了 UsernamePasswordAuthenticationToken 和 authenticationManager.authenticate(userAuth) 方法的调用代码。
看过之前的文章 Spring Security 认证流程 就知道了,这是 Spring Security 用户名/密码认证的处理流程。
如果认证通过,没有抛出异常,最终会返回 new OAuth2Authentication(storedOAuth2Request, userAuth),之后,在负累中通过 tokenServices.createAccessToken 生成访问令牌。