本期介绍的是在oauth2.0中 , 通过调用oauth/token接口 , 框架是如何给我们申请到JWT令牌的 , 内部做了些什么事情 ?
在分析源码之前 , 我们首先需要知道的是我们需要具备哪些调试条件 , 不然会发现许多奇奇怪怪的错误 (比如通过/oauth/token时出现401)
1.一张oauth2.0的内置表(oauth_client_details)
注意:这里的密码需要用Bcript加密 , 因为源码内部是用Bcript解密的
2.两把钥匙:
一本是后缀为jks的私钥
另一本是后缀为key的公钥
准备工作做好了 , 我们来看下申请后的效果图
下面附上oauth的部分源码 , 这个类正是我们申请令牌的入口 :
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.provider.endpoint;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.BadClientCredentialsException;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.common.exceptions.UnsupportedGrantTypeException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2RequestValidator;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* <p>
* Endpoint for token requests as described in the OAuth2 spec. Clients post requests with a <code>grant_type</code>
* parameter (e.g. "authorization_code") and other parameters as determined by the grant type. Supported grant types are
* handled by the provided {@link #setTokenGranter(org.springframework.security.oauth2.provider.TokenGranter) token
* granter}.
* </p>
*
* <p>
* Clients must be authenticated using a Spring Security {@link Authentication} to access this endpoint, and the client
* id is extracted from the authentication token. The best way to arrange this (as per the OAuth2 spec) is to use HTTP
* basic authentication for this endpoint with standard Spring Security support.
* </p>
*
* @author Dave Syer
*
*/
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
private Set<HttpMethod> allowedRequestMethods = new HashSet<HttpMethod>(Arrays.asList(HttpMethod.POST));
@RequestMapping(value = "/oauth/token", method=RequestMethod.GET)
public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!allowedRequestMethods.contains(HttpMethod.GET)) {
throw new HttpRequestMethodNotSupportedException("GET");
}
return postAccessToken(principal, parameters);
}
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
/**
* @param principal the currently authentication principal
* @return a client id if there is one in the principal
*/
protected String getClientId(Principal principal) {
Authentication client = (Authentication) principal;
if (!client.isAuthenticated()) {
throw new InsufficientAuthenticationException("The client is not authenticated.");
}
String clientId = client.getName();
if (client instanceof OAuth2Authentication) {
// Might be a client and user combined authentication
clientId = ((OAuth2Authentication) client).getOAuth2Request().getClientId();
}
return clientId;
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
if (logger.isInfoEnabled()) {
logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return getExceptionTranslator().translate(e);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
if (logger.isWarnEnabled()) {
logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return getExceptionTranslator().translate(e);
}
@ExceptionHandler(ClientRegistrationException.class)
public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
if (logger.isWarnEnabled()) {
logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return getExceptionTranslator().translate(new BadClientCredentialsException());
}
@ExceptionHandler(OAuth2Exception.class)
public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
if (logger.isWarnEnabled()) {
logger.warn("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
}
return getExceptionTranslator().translate(e);
}
private ResponseEntity<OAuth2AccessToken> getResponse(OAuth2AccessToken accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
headers.set("Content-Type", "application/json;charset=UTF-8");
return new ResponseEntity<OAuth2AccessToken>(accessToken, headers, HttpStatus.OK);
}
private boolean isRefreshTokenRequest(Map<String, String> parameters) {
return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null;
}
private boolean isAuthCodeRequest(Map<String, String> parameters) {
return "authorization_code".equals(parameters.get("grant_type")) && parameters.get("code") != null;
}
public void setOAuth2RequestValidator(OAuth2RequestValidator oAuth2RequestValidator) {
this.oAuth2RequestValidator = oAuth2RequestValidator;
}
public void setAllowedRequestMethods(Set<HttpMethod> allowedRequestMethods) {
this.allowedRequestMethods = allowedRequestMethods;
}
}
看到这个源码也许我们会想 : "这好简单, 接口都提供好了 , 直接调用就行了 ! "
没错 ! 那我们就调用一下…
首先我们可以快速的分析出 , 这个接口方法上的第二个参数就是我们调用时所添加的param参数 , 但奇怪的是 , 还有一个参数为Principal , 对象内部有一个显眼的字符串 , 那这个字符串是怎么来的呢 ?
答案揭晓:
在我们申请/oauth/token的时候 , 其实还添加了一个参数 , 在Authorization中将type设置为Basic Auth模式 , 同时将我们的证书数据库中的oauth_client_details表中的client_id和client_secret填入其中 , 框架就会封装为一个Principal对象 (这里的password为null是因为框架认为密码是不可泄露的 , 所以将其设置为受保护模式 ,bug模式下无法查看其密码)
OK , 理解完参数之后 , 我们来看方法内部的逻辑处理
- 判断该Principal对象是否属于Authentication , 因为Authentication继承了Principal , 这部分属于参数校验 .
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
- 获取client_id
String clientId = getClientId(principal);
protected String getClientId(Principal principal) {
Authentication client = (Authentication) principal;
if (!client.isAuthenticated()) {
throw new InsufficientAuthenticationException("The client is not authenticated.");
}
String clientId = client.getName();
if (client instanceof OAuth2Authentication) {
// Might be a client and user combined authentication
clientId = ((OAuth2Authentication) client).getOAuth2Request().getClientId();
}
return clientId;
}
- 数据库查询
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
getClientDetailsService()方法很简单 , 就一个赋值操作
protected ClientDetailsService getClientDetailsService() {
return clientDetailsService;
}
private ClientDetailsService clientDetailsService;
既然是赋值操作 , 那么我们看看这个接口在做什么?
我们发现 , 这个接口提供了一个方法 , 注释也写的很明白 , 通过客户端id加载一个客户端
public interface ClientDetailsService {
/**
* Load a client by the client id. This method must not return null.
*
* @param clientId The client id.
* @return The client details (never null).
* @throws ClientRegistrationException If the client account is locked, expired, disabled, or invalid for any other reason.
*/
ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException;
}
既然是接口 , 那么我们就去找找实现类
发现只有两个 , 太开心了!!!
于是乎, 我在两个实现类的方法上都打了断点 , 看看他走哪个类就行了
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
ClientDetails details;
try {
details = jdbcTemplate.queryForObject(selectClientDetailsSql, new ClientDetailsRowMapper(), clientId);
}
catch (EmptyResultDataAccessException e) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
return details;
}
后来发现原来走的是JdbcClientDetailsService类 , 当然 , 我们知道他肯定会去查数据库的 , 所以走jdbc也是理所当然
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
ClientDetails details;
try {
details = jdbcTemplate.queryForObject(selectClientDetailsSql, new ClientDetailsRowMapper(), clientId);
}
catch (EmptyResultDataAccessException e) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
return details;
}
这个方法里面就一行方法代码 , 从方法名就可以看出 , 这就是一条查询语句而已 , 那我们直接看参数吧
private String selectClientDetailsSql = DEFAULT_SELECT_STATEMENT;
我们发现方法的第一参数其实是一个字符串 , 我们接着往上找这个字符串
private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
突然发现了一个where , 那我们基本可以确定下来这是一条sql语句了 , 那我们继续把完整的sql语句找出来
private static final String BASE_FIND_STATEMENT = "select client_id, " + CLIENT_FIELDS
+ " from oauth_client_details";
private static final String CLIENT_FIELDS = "client_secret, " + CLIENT_FIELDS_FOR_UPDATE;
private static final String CLIENT_FIELDS_FOR_UPDATE = "resource_ids, scope, "
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove";
可真啰嗦 , 一条完整的sql语句终于浮出水面
我们来看看完整版sql
select client_id,client_secret,resource_ids, scope,authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity,refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?
而且我们看到这个方法的最后一位参数为clientId , 正是我们输入在postman的Authorization中的username , 那么整条sql语句即为
select client_id,client_secret,resource_ids, scope,authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity,refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = 'modeling'
突然发现 , 这条sql所要查询的内容不就是我们在数据库中添加的oauth_client_details表吗?
这回知道为什么一开始需要添加这张表了吧 !! 源码中强制需要
查询后的返回值长这样
可以看到 , 表中的数据已经从数据库中拿出来了
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
authenticatedClient对象就是我们在debug控制台看到的内容了
ok , 获取到了authenticatedClient对象之后 , 我们来到下一行代码
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
protected OAuth2RequestFactory getOAuth2RequestFactory() {
return oAuth2RequestFactory;
}
getOAuth2RequestFactory()简单的工厂模式 , 生成工厂对象 , 我们着重来看后面这个方法
public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {
String clientId = requestParameters.get(OAuth2Utils.CLIENT_ID);
if (clientId == null) {
// if the clientId wasn't passed in in the map, we add pull it from the authenticated client object
clientId = authenticatedClient.getClientId();
}
else {
// otherwise, make sure that they match
if (!clientId.equals(authenticatedClient.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
String grantType = requestParameters.get(OAuth2Utils.GRANT_TYPE);
Set<String> scopes = extractScopes(requestParameters, clientId);
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
return tokenRequest;
}
- 方法的第一行代码就说明了从我们的请求参数中拿到client_id (这就是为什么我们在其他教程中可以看到携带client_id和client_secret参数来请求jwt的原因)
- 如果参数中没有client_id , 那么就会从authenticatedClient对象中去获取
- 接着我们看到代码继续获取grant_type , 即password模式
- 我们来到下一行代码
Set<String> scopes = extractScopes(requestParameters, clientId);
private Set<String> extractScopes(Map<String, String> requestParameters, String clientId) {
Set<String> scopes = OAuth2Utils.parseParameterList(requestParameters.get(OAuth2Utils.SCOPE));
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if ((scopes == null || scopes.isEmpty())) {
// If no scopes are specified in the incoming data, use the default values registered with the client
// (the spec allows us to choose between this option and rejecting the request completely, so we'll take the
// least obnoxious choice as a default).
scopes = clientDetails.getScope();
}
if (checkUserScopes) {
scopes = checkUserScopes(scopes, clientDetails);
}
return scopes;
}
从请求参数中获取scope的值 , 当然我们也没有传值 , 所以返回的是null.
那如果参数中没有 , 又该怎么办呢?
于是 ,他又去数据库中查询了一次 , 拿到了scope的值
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
同样的代码 , 同样的味道!!!
if ((scopes == null || scopes.isEmpty())) {
// If no scopes are specified in the incoming data, use the default values registered with the client
// (the spec allows us to choose between this option and rejecting the request completely, so we'll take the
// least obnoxious choice as a default).
scopes = clientDetails.getScope();
}
在if判断中明确指出 : 如果参数中拿不到值 , 那么就从clientDetails对象中去获取
我们来到下一行代码
if (checkUserScopes) {
scopes = checkUserScopes(scopes, clientDetails);
}
有人问了 , 这个checkUserScopes是什么?
private boolean checkUserScopes = false;
在本类的属性中可以看到 , 这个初始默认值为false , 所以不走if内部逻辑
所以最终的scope为我们数据库中设置的app
出方法之后我们来看下一行主方法代码
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
很好 , 一个简单的带参构造器创建对象
最后把tokenRequest这个对象返回
至此 , 该方法结束
我们来到下一行主方法postAccessToken代码
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
一个简单的逻辑判断 , client_id我们知道是有的 , 所以我们会进第一层判断 , 但是不会走第二层判断 , 因为他俩是相等的 , 为什么会有这层校验呢 ?
因为我们在上述代码中可以知道 , 我们外部的client_id是通过查询数据库获得的 , 而我们的tokenRequest.getClientId()可以通过参数携带获得 , 也可以从数据库中获取 , 如果是通过参数携带获得 , 那么就有可能出现不一致的情况 , 所以他的报错信息中也很明确的说“Given client ID does not match authenticated client”
我们看下一行代码
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
有人又要问了 , 这个oAuth2RequestValidator又是什么呀 , 我们来到本类属性中
private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
很好 , 是一个多态 , 我们暂且称之为默认身份验证器
那么他在验证什么呢?
private void validateScope(Set<String> requestScopes, Set<String> clientScopes) {
if (clientScopes != null && !clientScopes.isEmpty()) {
for (String scope : requestScopes) {
if (!clientScopes.contains(scope)) {
throw new InvalidScopeException("Invalid scope: " + scope, clientScopes);
}
}
}
if (requestScopes.isEmpty()) {
throw new InvalidScopeException("Empty scope (either the client or the user is not allowed the requested scopes)");
}
}
还是一样的配方 , 校验参数中的scope和数据库中的scope是否存在包含关系 , 以及简单的非空判断
一句话概括就是: 校验scope是否合法
下一行代码
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
校验grant_type的合法性
接下来就比较重要了
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
还是一个判断 , 即校验流程 , 但是我们看到判断条件是一个方法 , 我们来看下这个方法
private boolean isAuthCodeRequest(Map<String, String> parameters) {
return "authorization_code".equals(parameters.get("grant_type")) && parameters.get("code") != null;
}
它在校验我们的grant_type是否为authorization_code并且这个code值不能为空
其实 ,这就是我们熟知的授权吗模式 , 这行代码就在询问我们 , 本次登陆是否为授权吗模式登陆
显然不是!! 我们的grant_type为 password ,是密码模式
我们来到下一行代码
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
同样也进入if中的方法中
private boolean isRefreshTokenRequest(Map<String, String> parameters) {
return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null;
}
判断当前是否为刷新令牌操作 , 很显然也不是
我们来到下一行代码
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
发现它不判断了 , 那么我们可以大致猜出 , 他要开大了(生成jwt了)
首先走进第一个方法
protected TokenGranter getTokenGranter() {
return tokenGranter;
}
没啥, 一个tokenGranter对象而已 .
这个对象执行了一个方法------grant , 并且带上了grant_type和tokenRequest , 我们可以暂且认为它带上了所以我们已知的参数
进入这个grant方法后 , 我们发现这是一个接口
public interface TokenGranter {
OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}
继续debug , 发现进入的是AuthorizationServerEndpointsConfigurer类
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;
}
然后我们来看一下delegate的内部值
内部还是授权模式
所以它是有值的 , 不会走if内部
我们看下一行代码
delegate.grant(grantType, tokenRequest);
这是一个方法 , 我们来到该方法
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
发现是一个循环 , 那tokenGranters是什么呢
private final List<TokenGranter> tokenGranters;
一个TokenGranter对象的集合
我们通过debug来看一下这个集合里有什么
我悟了 , 这不就是oauth的几种授权模式嘛
循环体内有还有一个grant方法 , 我们进去看看
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);
}
逻辑清晰起来了家人们!
判断我们输入的grant_type是属于哪种登陆类型 , 循环判断 , 如果不属于我们的类型 ,那直接返回null , 只有匹配上了才会继续执行.
经过几次循环后 , 终于到了password模式(为什么要把password放到最后 , 好烦!)
继续看下面的代码
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
又是熟悉的味道 , 获取client_id , 然后从数据库中查询生成ClientDetails对象(搞得厉害)
直接跳过 , 来到下一行代码
validateGrantType(grantType, client);
进入方法
protected void validateGrantType(String grantType, ClientDetails clientDetails) {
Collection<String> authorizedGrantTypes = clientDetails.getAuthorizedGrantTypes();
if (authorizedGrantTypes != null && !authorizedGrantTypes.isEmpty()
&& !authorizedGrantTypes.contains(grantType)) {
throw new InvalidClientException("Unauthorized grant type: " + grantType);
}
}
- 从ClientDetails对象中取出了grant_type集合 ,注意 , 这个grant_type是我们在数据库中定义的 , 忘记的家人们可以翻到顶端准备阶段中查看那张表
- 紧接着判断我们的password是否在数据库表中存在该字段 , 很明显是存在password的 , 所以放行
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));
}
我们看到这里有两个方法 , 第一个是createAccessToken() , 第二个是 getOAuth2Authentication
我们先来看第二个方法 , 由于我们是password模式 , 所以我们的进入该类的子类 , 我们来看下这个子类 .
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.provider.password;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
/**
* @author Dave Syer
*
*/
public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "password";
private final AuthenticationManager authenticationManager;
public ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
@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);
}
}
这个类就是根据我们grant_type的参数值而选择走的类了 , 如果是grant_type是其他模式 ,则不会走这个类
重点来看重写方法
@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);
}
- 新建了一个带参集合 , 创建该集合是就把我们传递的参数放进了集合中
- 从集合中获取username和password , 同时又移除了password
- 那么我们可以知道集合中只剩两个参数了 , 即 grant_type 和 username .
- 以多态的形式创建了一个对象 , 并将username和password注入其中
我们来看一些这个类的结构
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.authentication;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
/**
* An {@link org.springframework.security.core.Authentication} implementation that is
* designed for simple presentation of a username and password.
* <p>
* The <code>principal</code> and <code>credentials</code> should be set with an
* <code>Object</code> that provides the respective property via its
* <code>Object.toString()</code> method. The simplest such <code>Object</code> to use is
* <code>String</code>.
*
* @author Ben Alex
*/
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
private Object credentials;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
找到我们需要的构造器
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
我们发现 , 我们的username属性值被放入了该对象的principal属性中 , 我们的password被放入了该对象的credentials中 , 同时默认设置 Authenticated = false
注 : 虽然password被集合移除了 , 但是在移除之前被拿出来了 , 所以这个password还是存在的 , 只是集合中不存在了
5.
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
将集合中的两个属性(grant_type和username)设置为对象的details.
来看下一行代码
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());
}
又是一个方法 , 该方法的明面意思是进行身份验证 , 我们进入该方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (this.delegate != null) {
return this.delegate.authenticate(authentication);
} else {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
this.delegate = (AuthenticationManager)this.delegateBuilder.getObject();
this.delegateBuilder = null;
}
}
return this.delegate.authenticate(authentication);
}
}
这部分代码的大致意思是 : 如果有delegate对象 , 那就执行authenticate()方法 , 如果没有 , 就创建一个 , 然后执行authenticate() , 我们不追究他是怎么创建的 , 我们直接看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;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// 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 e) {
lastException = parentException = e;
}
}
if (result != null) {
if (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 than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
. . . 有点长了. . .
那我们一步一步来吧
首先看传进来的参数Authentication里有什么
ok , 就是熟悉的账号密码 , 加个grant_type
接下来我们一步一步看
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
- 获取了参数对象的字节码文件(这是要干嘛?)
- 设置了一些初始值
- 我们来看下一行代码 , 循环判断
for (AuthenticationProvider provider : getProviders()) {
······
}
getProviders()方法就是一个简单的获取变量的过程
public List<AuthenticationProvider> getProviders() {
return providers;
}
我们来看下这个变量
private List<AuthenticationProvider> providers = Collections.emptyList();
我们可以看到 , 他的默认值是一个空集合
那么 , 他是如何被赋值的呢
大家可以找到AuthenticationManagerBuilder类 , 内部有这两个方法
public AuthenticationManagerBuilder authenticationProvider(
AuthenticationProvider authenticationProvider) {
this.authenticationProviders.add(authenticationProvider);
return this;
}
@Override
protected ProviderManager performBuild() throws Exception {
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
ProviderManager providerManager = new ProviderManager(authenticationProviders,
parentAuthenticationManager);
if (eraseCredentials != null) {
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}
集合内部的对象就是从这里放入的 , 并且在项目启动时就会初始化 , 不做过多的解释 (大家可以参考其他博客
https://blog.csdn.net/andy_zhang2007/article/details/90200868
)
我们继续来到for循环内部
在第一次debug时 , 我们可以看到 , 集合内部只有一个值
而且是一个看不懂的key+message
看不懂咱就略过····
关于suports()方法 , 注释解释为
如果此AuthenticationProvider支持指定的Authentication对象,则返回true 。
那我想这么一个看不懂的东西应该不会支持的
继续debug
果然continue了 , 既然集合里只有一个值 , 那循环其实已经结束了
我们来到循坏体外的代码中
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// 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 e) {
lastException = parentException = e;
}
}
if (result != null) {
if (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 than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
很巧 , 第一个if判断我们就进了 , 因为我们的成员变量parent是存在的 , 进入之后我们发现 , 又回到了最初的起点 (美其名曰递归)
又来到循环体内 , 这次我们可以看到的provider有点样子了 , 不像上次的看不懂的key
那我们可以估计 , 他是支持的 , 所以不会continue , 进入下面的校验
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
接着 , 我们进入
result = provider.authenticate(authentication);
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// 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);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
- 断言了对象类型
- 三元表达式取出Principal作为username
- 从缓存中去找user对象
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
该成员变量userCache为NullUserCache
private UserCache userCache = new NullUserCache();
我们来看一下这个类
public class NullUserCache implements UserCache {
public UserDetails getUserFromCache(String username) {
return null;
}
public void putUserInCache(UserDetails user) {
}
public void removeUserFromCache(String username) {
}
}
发现getUserFromCache()方法直接return null;
所以我们第一次获取的UserDetails是null
- 我们来到下一行代码
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
进入这个方法
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
} catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
接着进入第一行代码 prepareTimingAttackProtection(); 的方法中
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
很奇怪 , 他对 userNotFoundPassword 字符串进行了加密
那么我们知道了第一行代码就是对一个字符串的加密
- 又去数据库中查询了一次 , 不做解释了
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
- 最后返回了这个对象
我们来到下一行代码
preAuthenticationChecks.check(user);
进入方法
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
logger.debug("User account is locked");
throw new LockedException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"));
}
if (!user.isEnabled()) {
logger.debug("User account is disabled");
throw new DisabledException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.disabled",
"User is disabled"));
}
if (!user.isAccountNonExpired()) {
logger.debug("User account is expired");
throw new AccountExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.expired",
"User account has expired"));
}
}
}
一个简单的校验 , 我们根据图片就可以判断时候进入
发现一个也不会进 , 直接退出了方法
那我们直接来到下一个方法
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
这个方法可重要了 , 我们的密码就是在这里校验的
- 首先做一层简单校验 , authentication对象是否为null
- 取出我们输入的密码
- 使用Bcript进行密码校验
方法结束 , 我们来到下一个方法
postAuthenticationChecks.check(user);
进入方法
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
logger.debug("User account credentials have expired");
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
}
}
}
在之前的user信息中可以知道CredentialsNonExpired属性是true , 所以也不会进
来到下一行代码
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
或许大家在之前没注意到cacheWasUsed属性 , 我帮大家再贴出来
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
···
}
所以在这一步中 , 我们的cacheWasUsed是false , 所以会进if判断中 , 其实这个变量的意思就是这个缓存变量有没有被使用过 , 第一次的时候使用过一次 , 所以后续都是被使用过的
我们进入putUserInCache()方法中
public void putUserInCache(UserDetails user) {
}
啥也没干
下一行代码
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
private boolean forcePrincipalAsString = false;
将user赋值给了principalToReturn变量
再来看最后一行代码
return createSuccessAuthentication(principalToReturn, authentication, user);
进入该方法
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
看if内部的操作 , 好像是在修改密码 , 那我们当然不是 , 所以不走if
直接到最后一行代码
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
通过构造器创建了一个对象 , 美其名曰result
我们进入该构造器
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
简单的用户属性赋值操作 , 那么我们可以认为这个reuslt其实就是我们的用户信息
返回上层
if (result != null) {
copyDetails(authentication, result);
break;
}
进入copyDetails()方法中
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
我们知道reult是有值的 , 所以if判断中dest.getDetails() == null不成立 , 不会进循环 , 什么也不做就退出了.
来到下一行代码
if (result == null && parent != null) {
···
}
内部代码我就不贴了 , 因为result!=null , 根本不会进
我们重点来看下一段代码
if (result != null) {
if (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 than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
这里就会进了
private boolean eraseCredentialsAfterAuthentication = true;
我们来看下一行代码
((CredentialsContainer) result).eraseCredentials();
翻译过来就是擦除凭证 , 方法如其名 , 我们进入方法内部看看
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
credentials被擦除了 , 再看看super.eraseCredentials();
public void eraseCredentials() {
eraseSecret(getCredentials());
eraseSecret(getPrincipal());
eraseSecret(details);
}
private void eraseSecret(Object secret) {
if (secret instanceof CredentialsContainer) {
((CredentialsContainer) secret).eraseCredentials();
}
}
public void eraseCredentials() {
password = null;
}
所以最后把密码也擦除了
我们来看下一行代码
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
eventPublisher对象为DefaultAuthenticationEventPublisher类 , 官方介绍为:
The default strategy for publishing authentication events.
发布身份验证事件的默认策略。
publishAuthenticationSuccess()方法的官方解释为
发布认证成功
返回的result即是我们的用户信息
好的 , 我们赶紧退回到getOAuth2Authentication()方法中
userAuth = authenticationManager.authenticate(userAuth);
这行代码算是告一段落了
那我们直接来看最后两句
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
我们进入createOAuth2Request()方法
public OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest) {
return tokenRequest.createOAuth2Request(client);
}
再进入createOAuth2Request()方法
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);
}
可以看到 , OAuth2Request这个对象中 , 包含了一些client的基础信息 , 同时也过滤了敏感信息
getOAuth2Authentication(client, tokenRequest) 这个方法算是告一段落了
那接下来我们来看看createAccessToken()这个方法
@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;
}
}
// @Transactional注解表示事务一致性 , 要么全部执行 , 要么全部不执行
我们来看第一句代码
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
@Override
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
// We don't want to accidentally issue a token, and we have no way to reconstruct the refresh token
return null;
}
返回null
所以existingAccessToken对象为null
所以我们直接来到下面的if代码中
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
进入方法
private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
return null;
}
int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
String value = UUID.randomUUID().toString();
if (validitySeconds > 0) {
return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
+ (validitySeconds * 1000L)));
}
return new DefaultOAuth2RefreshToken(value);
}
if中的方法为isSupportRefreshToken , 翻译为是否支持令牌刷新 , 显然是支持的 , 那我们来看一下他是怎么校验的吧
protected boolean isSupportRefreshToken(OAuth2Request clientAuth) {
if (clientDetailsService != null) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
return client.getAuthorizedGrantTypes().contains("refresh_token");
}
return this.supportRefreshToken;
}
还是通过数据库拿到了client信息 , 同时查询在grant_type中是否包含了refresh_token字符串
下一句代码
int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
获取令牌过期时间 , 我们进入该方法
protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) {
if (clientDetailsService != null) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
Integer validity = client.getRefreshTokenValiditySeconds();
if (validity != null) {
return validity;
}
}
return refreshTokenValiditySeconds;
}
还是同样的配方
第三行代码 , 利用UUID设置了一个随机字符串
String value = UUID.randomUUID().toString(); //8d85a4a1-5deb-40cf-a3f4-fa14a4dce277
我们记住段字符串
我们继续看代码
if (validitySeconds > 0) {
return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
+ (validitySeconds * 1000L)));
}
根据我们生成的随机字符串和过期时间作为构造参数生成了一个对象
此时我们可以看到我们的refreshToken对象的参数值
我们来到下一句代码
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
进入方法
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
- 通过一个随机字符串创建了一个对象
- 获取过期时间 (不要怀疑 , 和之前的操作一样 ,从数据库里根据client_id查询client信息 )
- 设置token的其他信息
- 生成token的具体方法
我们进入该方法
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
String tokenId = result.getValue();
if (!info.containsKey(TOKEN_ID)) {
info.put(TOKEN_ID, tokenId);
}
else {
tokenId = (String) info.get(TOKEN_ID);
}
result.setAdditionalInformation(info);
result.setValue(encode(result, authentication));
OAuth2RefreshToken refreshToken = result.getRefreshToken();
if (refreshToken != null) {
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
encodedRefreshToken.setValue(refreshToken.getValue());
// Refresh tokens do not expire unless explicitly of the right type
encodedRefreshToken.setExpiration(null);
try {
Map<String, Object> claims = objectMapper
.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey(TOKEN_ID)) {
encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
}
}
catch (IllegalArgumentException e) {
}
Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
accessToken.getAdditionalInformation());
refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
encode(encodedRefreshToken, authentication));
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
}
result.setRefreshToken(token);
}
return result;
}
我们来看一下流程
5. 根据之前的token对象生成一个result对象 , 该对象和之前的token属性值一样
来到下一句代码
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
String tokenId = result.getValue();
if (!info.containsKey(TOKEN_ID)) {
info.put(TOKEN_ID, tokenId);
}
else {
tokenId = (String) info.get(TOKEN_ID);
}
新建了一个链表哈希Map , 同时将reult中的随机数作为tokenId放入了集合中
而这个TOKEN_ID常量就是我们熟知的 jti ,没错 ,他只是一个UUID的字符串而已
public static final String TOKEN_ID = AccessTokenConverter.JTI;
final String JTI = "jti";
从命名为TOKEN_ID我们也可以知道 , 这个jti的作用就是作为jwt的唯一标识符
我们来看下一行代码
result.setAdditionalInformation(info);
info集合中有一个jti
放入了result的information中
我们来看下一行代码
result.setValue(encode(result, authentication));
把result和authentication作为参数进行了加密操作
其中 , result是client的相关信息 , authentication是用户的相关信息
我们进入该方法
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert access token to JSON", e);
}
String token = JwtHelper.encode(content, signer).getEncoded();
return token;
}
第一行代码其实就是将我们的client信息和用户信息整合在一起 , 并生成一个json字符串
我们可以来看一下该字符串
{"birthday":946684800000,"address":"浙江/杭州","gender":1,"signature":null,"weixinId":null,"authorities":["admin"],"client_id":"modeling","password":null,"scope":["app"],"nickname":"管理员","id":1,"exp":1631069633,"jti":"a9bc8c3d-d011-4076-8ce3-2f73ddeee800","username":"123456"}
我们来看下一行代码
String token = JwtHelper.encode(content, signer).getEncoded();
这显然是一步加密生成真正的jwt操作了 , 我们先看一下参数 , content就是我们刚刚的json , 而后面的signer我们来关注一下
private Signer signer = new MacSigner(verifierKey);
private String verifierKey = new RandomValueStringGenerator().generate();
private Signer signer = new MacSigner(verifierKey);
signer是一个签名对象 , 我们来看看他的类构造
public class MacSigner implements SignerVerifier {
private static final String DEFAULT_ALGORITHM = "HMACSHA256";
private final String algorithm;
private final SecretKey key;
//···
}
可以看到 , 该对象有两个成员变量 , 外加一个常量 ,
algorithm代表算法 , 而在没有设置算法机制的情况下 , 默认使用该常量算法“HMACSHA256”
key代表密钥
从图片中可以看到 , 其实这就是我们的公钥
参数知道了 ,我们来看encode()方法
public static Jwt encode(CharSequence content, Signer signer) {
return encode(content, signer, Collections.<String, String>emptyMap());
}
public static Jwt encode(CharSequence content, Signer signer,
Map<String, String> headers) {
JwtHeader header = JwtHeaderHelper.create(signer, headers);
byte[] claims = utf8Encode(content);
byte[] crypto = signer
.sign(concat(b64UrlEncode(header.bytes()), PERIOD, b64UrlEncode(claims)));
return new JwtImpl(header, claims, crypto);
}
我们来看参数的第一行代码
JwtHeader header = JwtHeaderHelper.create(signer, headers);
进入方法
static JwtHeader create(Signer signer, Map<String, String> params) {
Map<String, String> map = new LinkedHashMap<String, String>(params);
map.put("alg", sigAlg(signer.algorithm()));
HeaderParameters p = new HeaderParameters(map);
return new JwtHeader(serializeParams(p), p);
}
创建了一个map , 同时把当前算法模式“HMACSHA256”放入其中, 同时根据这些属性创建了一个JwtHeader对象
我们来看一下该对象中有什么
一个是算法类型 , 一个是token类型 , 还有一个byte数组
private static byte[] serializeParams(HeaderParameters params) {
StringBuilder builder = new StringBuilder("{");
appendField(builder, "alg", params.alg);
if (params.typ != null) {
appendField(builder, "typ", params.typ);
}
for (Entry<String, String> entry : params.map.entrySet()) {
appendField(builder, entry.getKey(), entry.getValue());
}
builder.append("}");
return utf8Encode(builder.toString());
}
该数组也是通过加密算法模式“HMACSHA256”和token类型“JWT”生成
我们来到下一步
byte[] claims = utf8Encode(content);
将内容也转化成byte数组
byte[] crypto = signer
.sign(concat(b64UrlEncode(header.bytes()), PERIOD, b64UrlEncode(claims)));
继续算法加密
加密之后最后一步 , 就是获取了
String token = JwtHelper.encode(content, signer).getEncoded();
@Override
public String getEncoded() {
return utf8Decode(bytes());
}
public static String utf8Decode(ByteBuffer bytes) {
try {
return UTF8.newDecoder().decode(bytes).toString();
}
catch (CharacterCodingException e) {
throw new RuntimeException(e);
}
}
又是一步解密过程
最后 , 我们的token出世了
所以真正的token生成其实是在result的value属性中
result.setValue(encode(result, authentication));
我们回到上层的下一行代码
OAuth2RefreshToken refreshToken = result.getRefreshToken();
接下来就是刷新令牌的生成
首先 , 将result中的refreshToken字段的随机字符串取出 , 赋值给refreshToken对象
接着是生成一个对象
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
我们来看一下该对象的构造方法
public DefaultOAuth2AccessToken(OAuth2AccessToken accessToken) {
this(accessToken.getValue());
setAdditionalInformation(accessToken.getAdditionalInformation());
setRefreshToken(accessToken.getRefreshToken());
setExpiration(accessToken.getExpiration());
setScope(accessToken.getScope());
setTokenType(accessToken.getTokenType());
}
可以看到 , 该构造方法植入了client(准确来说应该是Token对象)的相关信息
我们来看一下生成的对象属性
对象生成后 , 又进行了属性的相关操作
encodedRefreshToken.setValue(refreshToken.getValue());
// Refresh tokens do not expire unless explicitly of the right type
encodedRefreshToken.setExpiration(null);
我们可以看到 , 他将refreshToken的随机字符串设置为了value。,同时取消了过期时间
接下去的大篇幅 , 就是在做刷新令牌的加密操作了 , 方式和生成Jwt走的方式一致 , 在此不一一细讲了
try {
Map<String, Object> claims = objectMapper
.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey(TOKEN_ID)) {
encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
}
}
catch (IllegalArgumentException e) {
}
Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
accessToken.getAdditionalInformation());
refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
encode(encodedRefreshToken, authentication));
/**
* Field name for token id.
*/
public static final String TOKEN_ID = AccessTokenConverter.JTI;
/**
* Field name for access token id.
*/
public static final String ACCESS_TOKEN_ID = AccessTokenConverter.ATI;
JTI是令牌的唯一标志
ATI是申请令牌的唯一标志
要注意的是 , refreshToken可能会生成两次 , 因为在后面还会有一个if判断
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
}
加上了一个过期时间 , 并重新生成刷新令牌
至此 ,我们的accessToken就差不多成型了
我们退回方法上层
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;
tokenStore.storeAccessToken()方法什么也没做
@Override
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
}
tokenStore.storeRefreshToken()方法也什么都没做
@Override
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
}
注意 , 这两个方法走的是JwtTokenStore类
返回上层方法
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
至此 , 我们的getAccessToken()方法算是完成了
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
······
return getAccessToken(client, tokenRequest);
}
grant() 方法也结束了
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
外层的grant()方法也结束了
我们来到接口层
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
······
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
至此 , 我们的token就完全浮出水面了 , 通过response返回了将token返回
最终在客户端生成的样子如下图(由于Debug了好几次 , 每次的token和jti都不相同 , 请忽略值)
历经几天的考究和打磨 , oauth的生成令牌的源码终究破译了 , 但其实这只是其中的九牛一毛 , 我们知道 , oauth还提供了多样化的登录方式 , 而我们只走了密码模式的登陆方式 , 我们不仅需要了解其登录方式的底层逻辑 , 同时 , 在探究源码的过程中 , 一些设计模式 , 代码的严谨性 , 以及整体的架构思路 , 都是值得我们关注和学习的 .