目录
学习链接
一、客户端凭证模式源码分析
因为项目配置了jdbc存储的方式(还有其他方式可以配置),所以以下的源码分析都是通过jdbc操作数据库。。
其他存储方式:
具体的接口在:TokenEndpoint
类中(一开始我没找到入口,调试过程中,发现了这个类)
1)前置环境
springboot版本:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/>
</parent>
相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
客户端备案表(oauth_client_details):
作用:保存第三方应用的备案信息,同时认证系统会将这些信息分配给第三方应用,第三方应用可以通过这些信息来获取访问令牌token,和微信公众号开发、微信开发中的appid、secret类似。
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`)
VALUES ('zszstudy', 'warehouse-admin', '$2a$10$6KX0/28LknA31sEHa5NVauLPB8eB6hNsR.lbVuH9z8VQ7.N4jzlMO', 'read,write', 'client_credentials,authorization_code,password,refresh_token,implicit', 'http://bugstack.cn', 'user', 7199, 2592000, NULL, 'true');
token保存表(oauth_access_token):
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(255) NOT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2)获取token测试
调用如下接口生成token:
curl --location --request POST 'http://127.0.0.1:8091/oauth/token?grant_type=client_credentials' \
--header 'Authorization: Basic enN6c3R1ZHk6MTIzNDU2' \
--header 'Cookie: JSESSIONID=E0FEF6DBF88FFAE8DCEA923D1D1477A2'
结果如下:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2FyZWhvdXNlLWFkbWluIl0sInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE3MzcwNDIwNjYsImF1dGhvcml0aWVzIjpbInVzZXIiXSwianRpIjoiYTBkZDYzMDMtNDQyMy00YzBjLWJlMmUtMGFjMzI1ODYyMTRkIiwiY2xpZW50X2lkIjoienN6c3R1ZHkifQ.GKP0amXQRNQJKdZ1A9rTfXKrb8-oZwc1Du19j4__Uus",
"token_type": "bearer",
"expires_in": 7198,
"scope": "read write",
"jti": "a0dd6303-4423-4c0c-be2e-0ac32586214d"
}
并在token保存表生成了记录:
3)获取token流程
1.首先在JdbcClientDetailsService
的loadClientByClientId
方法打上断点作为入口:
可以看到会通过jdbc来操作数据库,根据clientid去备案表获取应用信息
2.校验密码是否正确:
AbstractUserDetailsAuthenticationProvider#authenticate()
DaoAuthenticationProvider#additionalAuthenticationChecks()
将输入的密码加密后在和数据库中的密码对比是否一致
因为原密码是123456,所以这里会匹配失败,抛出异常:
3.密码输入正确,就会进行token的生成
主要处理逻辑在TokenEndpoint#postAccessToken
中:
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.");
} else {
String clientId = this.getClientId(principal);
// ①备案表中的应用信息
ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
// ②构建获取token的参数
TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
} else {
if (authenticatedClient != null) {
this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
} else if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
} else {
if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
this.logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.emptySet());
}
if (this.isRefreshTokenRequest(parameters)) {
tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
}
// ③生成token
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
} else {
return this.getResponse(token);
}
}
}
}
}
①应用备案信息:
②token请求参数:
4.生成token
前面第三点中,调用了下面的方法:
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
最终会执行到AbstractTokenGranter#getAccessToken
方法:
它里面又调用了DefaultTokenServices#createAccessToken
方法,它就是具体处理token生成的方法:
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// ①判断是否生成了token,见下面第一点分析
OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 如果存在token,则进入以下流程
if (existingAccessToken != null) {
// 如果token没有失效,则返回
if (!existingAccessToken.isExpired()) {
this.tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
// 如果token失效了,且刷新token不为空,则删除oauth_refresh_token表中的刷新token
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
this.tokenStore.removeRefreshToken(refreshToken);
}
// 删除token
this.tokenStore.removeAccessToken(existingAccessToken);
}
// ②如果刷新token为null,则生成一个有过期时间的刷新token,见下面的第二点分析
if (refreshToken == null) {
refreshToken = this.createRefreshToken(authentication);
} else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = this.createRefreshToken(authentication);
}
}
// ③生成真正的访问token
OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
// 将token存到表中oauth_access_token
this.tokenStore.storeAccessToken(accessToken, authentication);
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
// 如果刷新token不为空,则保存到oauth_refresh_token中
this.tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
从这里可以的出,当多次调用接口时,在相同参数且token没有过期的情况下,都会返回数据库中同一个token
①判断是否已经生成了token:
JdbcTokenStore#getAccessToken
:
// 查找token的sql
private String selectAccessTokenFromAuthenticationSql = "select token_id, token from oauth_access_token where authentication_id = ?";
// 删除token的sql
private String deleteAccessTokenSql = "delete from oauth_access_token where token_id = ?";
// 新增token的sql
private String insertAccessTokenSql = "insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)";
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
OAuth2AccessToken accessToken = null;
// 1.使用md5加密备案表中的client_id和scope,见下文生成key的流程
String key = this.authenticationKeyGenerator.extractKey(authentication);
try {
// 2.根据key去token表中查找token信息
accessToken = (OAuth2AccessToken)this.jdbcTemplate.queryForObject(this.selectAccessTokenFromAuthenticationSql, new RowMapper<OAuth2AccessToken>() {
public OAuth2AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException {
return JdbcTokenStore.this.deserializeAccessToken(rs.getBytes(2));
}
}, new Object[]{key});
} catch (EmptyResultDataAccessException var5) {
if (LOG.isDebugEnabled()) {
LOG.debug("Failed to find access token for authentication " + authentication);
}
} catch (IllegalArgumentException var6) {
LOG.error("Could not extract access token for authentication " + authentication, var6);
}
// 3.判断token是否为null,第一次调用接口肯定是null
if (accessToken != null && !key.equals(this.authenticationKeyGenerator.extractKey(this.readAuthentication(accessToken.getValue())))) {
// 4.如果不为null,则删除旧的token,插入新的token
this.removeAccessToken(accessToken.getValue());
this.storeAccessToken(accessToken, authentication);
}
return accessToken;
}
生成key的流程:
②refresh token的过期时间:
③生成访问token:
最终会生成一个很长的字符串:
4)校验token测试
调用如下接口校验生成的token:
curl --location 'http://127.0.0.1:8091/oauth/check_token' \
--header 'Authorization: ••••••' \
--header 'Cookie: JSESSIONID=B32041879B077D9FF298F8EB3CE4B459' \
--form 'token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2FyZWhvdXNlLWFkbWluIl0sInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE3MzcxODc3ODYsImF1dGhvcml0aWVzIjpbInVzZXIiXSwianRpIjoiNGYwMDU1MjMtYTg4OS00NzRmLTlhNjAtZDRiNDE2YjVlZjQzIiwiY2xpZW50X2lkIjoienN6c3R1ZHkifQ.d0mT33s7kk6x7GbNm_hozUrLEHqubRjzTKL25_7T86Q"'
正确结果:
{
"aud": [
"warehouse-admin"
],
"scope": [
"read",
"write"
],
"exp": 1737187786,
"authorities": [
"user"
],
"jti": "4f005523-a889-474f-9a60-d4b416b5ef43",
"client_id": "zszstudy"
}
客户端密钥错误的情况:
{
"timestamp": "2025-01-18T06:31:08.790+00:00",
"status": 401,
"error": "Unauthorized",
"path": "/oauth/check_token"
}
token错误的情况:
{
"error": "invalid_token",
"error_description": "Token was not recognised"
}
5)校验token流程
1.老样子,首先在JdbcClientDetailsService
的loadClientByClientId
方法打上断点作为入口,先根据客户端的clientId获取oauth_client_details
表中的信息:
2.同样的,它这个接口也需要先校验下客户端的密钥是否正确,这样即使被别人获取到token也无法使用,还得需要密钥:
因为原密码是123456,我输入了1234567,所以这里会匹配失败,抛出异常
3.如果密码输入正确,接下来就会去校验传递过来的token:
CheckTokenEndpoint#checkToken
@RequestMapping({"/oauth/check_token"})
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
// ①去oauth_client_details表中获取之前生成的token,见下面分析
OAuth2AccessToken token = this.resourceServerTokenServices.readAccessToken(value);
// 如果获取不到,说明传递过来的token有问题,抛出异常
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
// 如果token过期,也抛出异常
} else if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
} else {
// 查找相关信息
OAuth2Authentication authentication = this.resourceServerTokenServices.loadAuthentication(token.getValue());
// 组装数据成一个map返回
return this.accessTokenConverter.convertAccessToken(token, authentication);
}
}
①查找token的调用链路:
DefaultTokenServices#readAccessToken
JdbcTokenStore#readAccessToken
:这个就是查找数据库中的token核心代码:
private String selectAccessTokenSql = "select token_id, token from oauth_access_token where token_id = ?";
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken accessToken = null;
try {
// 根据tokenid查找oauth_access_token表中的token
accessToken = (OAuth2AccessToken)this.jdbcTemplate.queryForObject(this.selectAccessTokenSql, new RowMapper<OAuth2AccessToken>() {
public OAuth2AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException {
return JdbcTokenStore.this.deserializeAccessToken(rs.getBytes(2));
}
// 解析token,获取tokenid
}, new Object[]{this.extractTokenKey(tokenValue)});
} catch (EmptyResultDataAccessException var4) {
if (LOG.isInfoEnabled()) {
LOG.info("Failed to find access token for token " + tokenValue);
}
} catch (IllegalArgumentException var5) {
LOG.warn("Failed to deserialize access token for " + tokenValue, var5);
this.removeAccessToken(tokenValue);
}
return accessToken;
}
解析token的代码:
解析token失败的情况:
二、密码模式源码分析
具体的接口在:TokenEndpoint
类中
1)前置环境
第三方应用对应的账户表:
CREATE TABLE `oauth_account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '账号ID',
`client_id` varchar(50) NOT NULL COMMENT '客户端ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(200) DEFAULT NULL COMMENT '密码',
`mobile` varchar(13) DEFAULT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`enabled` tinyint(1) DEFAULT NULL COMMENT '账号可用',
`account_non_expired` tinyint(1) DEFAULT '1' COMMENT '账号未过期',
`credentials_non_expired` tinyint(1) DEFAULT '1' COMMENT '密码未过期',
`account_non_locked` tinyint(1) DEFAULT '1' COMMENT '账号未锁定',
`account_non_deleted` tinyint(1) DEFAULT '1' COMMENT '账号未删除',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `user_idx` (`client_id`,`username`,`password`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='自定义认证中心账号表';
INSERT INTO `oauth_account` (`id`, `client_id`, `username`, `password`, `mobile`, `email`, `enabled`, `account_non_expired`, `credentials_non_expired`, `account_non_locked`, `account_non_deleted`, `created_time`, `updated_time`)
VALUES (1, 'zszstudy', 'zhou22', '$2a$10$6KX0/28LknA31sEHa5NVauLPB8eB6hNsR.lbVuH9z8VQ7.N4jzlMO', '13500002222', '523088136@qq.com', 1, 1, 1, 1, 1, '2025-01-09 13:16:57', '2025-01-18 17:12:55');
客户端备案表(oauth_client_details):
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL,
`resource_ids` varchar(255) DEFAULT NULL,
`client_secret` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`)
VALUES ('zszstudy', 'warehouse-admin', '$2a$10$6KX0/28LknA31sEHa5NVauLPB8eB6hNsR.lbVuH9z8VQ7.N4jzlMO', 'read,write', 'client_credentials,authorization_code,password,refresh_token,implicit', 'http://bugstack.cn', 'user', 7199, 2592000, NULL, 'true');
token保存表(oauth_access_token):
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(255) NOT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(255) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2)获取token测试
调用如下接口生成token:
curl --location --request POST 'http://127.0.0.1:8091/oauth/token' \
--header 'Authorization: Basic enN6c3R1ZHk6MTIzNDU2' \
--form 'grant_type="password"' \
--form 'username="zhou22"' \
--form 'password="1234561"'
结果如下:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2FyZWhvdXNlLWFkbWluIl0sImV4cCI6MTczNzE5ODk1OSwidXNlcl9uYW1lIjoiemhvdTIyIiwianRpIjoiYmQzNDZlOGYtNGY0Ni00ZDA2LWI3OTktZTJkNDFhN2FlZTMwIiwiY2xpZW50X2lkIjoienN6c3R1ZHkiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.Ejf0H5MEp2bNFnqAs2bO-aEmwuUggM_asHDmLTkRGQY",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2FyZWhvdXNlLWFkbWluIl0sInVzZXJfbmFtZSI6Inpob3UyMiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJiZDM0NmU4Zi00ZjQ2LTRkMDYtYjc5OS1lMmQ0MWE3YWVlMzAiLCJleHAiOjE3Mzk3ODM3NTcsImp0aSI6IjZkMWRiYTcwLTc3ZGMtNDhjMC05NTZlLTBiYTRkMTBiOGQ4MCIsImNsaWVudF9pZCI6InpzenN0dWR5In0.1BWnmK70uXE62xaY_ItREOHAMr42Ft1QaEd1sCDTaVQ",
"expires_in": 6449,
"scope": "read write",
"account_info": {
"id": null,
"clientId": "zszstudy",
"username": "zhou22",
"mobile": "13500002222",
"email": "523088136@qq.com"
}
}
3)获取token流程
1.和前面客户端凭证模式的获取token流程一样,首先会校验客户端密钥是否正确
2.如果客户端密钥正确,则走自定义的UserDetailsService
接口的实现类OauthAccountUserDetailsService
来查找用户信息,并使用这些用户相关信息来生成token,不再是使用oauth_client_details
(备案表)中的信息来生成token:
查找的sql如下:
<select id="loadUserByUsername" resultMap="dataMap">
select client_id, username, password, mobile, email,
enabled, account_non_expired, credentials_non_expired, account_non_locked, account_non_deleted,
created_time, updated_time
from oauth_account
where client_id = #{clientId} and username = #{userName} and account_non_deleted = true
limit 1
</select>
3.拿到这里的用户信息之后,剩下的流程就和第一点一样了,重新走了一遍校验密码的流程,如果正确则用这个用户信息来生成token,不在过多分析。。
存在疑问:密码模式为什么会走两次校验流程,一次是oauth_client_details表,一次是oauth_account表,不清楚是动态代理的拦截器链机制,还是过滤器链【调试过程中碰到了很多过滤器和拦截器链相关放行方法】,又或者其他方式的处理,有时间再多研究下,暂时先记住结论。
不知道是不是和下面这个有关系:
private final List<TokenGranter> tokenGranters;
// CompositeTokenGranter#grant
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
Iterator i$ = this.tokenGranters.iterator();
OAuth2AccessToken grant;
do {
if (!i$.hasNext()) {
return null;
}
TokenGranter granter = (TokenGranter)i$.next();
grant = granter.grant(grantType, tokenRequest);
} while(grant == null);
return grant;
}
4)刷新token测试
调用如下接口测试:
**注意:**refresh token是前面获取token时生成的
curl --location --request POST 'http://127.0.0.1:8091/oauth/token' \
--header 'Authorization: Basic enN6c3R1ZHk6MTIzNDU2' \
--form 'grant_type="refresh_token"' \
--form 'refresh_token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2FyZWhvdXNlLWFkbWluIl0sInVzZXJfbmFtZSI6Inpob3UyMiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI2YzMyZTE4MC0xNDYzLTRiNDMtYmY3ZS1jMTY5ZmQ2NTEwOTEiLCJleHAiOjE3Mzk3ODU4OTQsImp0aSI6IjljMGZjODUxLWU5MzUtNGEzYi04N2M5LTE5YjI4ZWVlNTI4OSIsImNsaWVudF9pZCI6InpzenN0dWR5In0.kDah8myiXE1BO0goFhbPbKUBT8NtYb8f1uzS24ZrGI4"'
测试结果:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2FyZWhvdXNlLWFkbWluIl0sImV4cCI6MTczNzIwODU3MCwidXNlcl9uYW1lIjoiemhvdTIyIiwianRpIjoiZmRkZDkxODktMTE3YS00ZjI3LTgwNTItN2JiN2NhN2NiNDJlIiwiY2xpZW50X2lkIjoienN6c3R1ZHkiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.SWHarqe-Qbyg3n6_WVou4CrqrzY6eYRFghCMsoyThBo",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2FyZWhvdXNlLWFkbWluIl0sInVzZXJfbmFtZSI6Inpob3UyMiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJmZGRkOTE4OS0xMTdhLTRmMjctODA1Mi03YmI3Y2E3Y2I0MmUiLCJleHAiOjE3Mzk3OTMzNzAsImp0aSI6IjlkMTg2ZGM1LTk0YjQtNDlmYy05OGExLTMyYjMwZDAzMTgyOSIsImNsaWVudF9pZCI6InpzenN0dWR5In0.N6JNPB2wAMsBqMiRsSBNv5SUjS2zI1OpZ7vauHrL14c",
"expires_in": 6911,
"scope": "read write",
"account_info": {
"id": null,
"clientId": "zszstudy",
"username": "zhou22",
"mobile": "13500002222",
"email": "523088136@qq.com"
}
}
5)刷新token流程
先说结论:所谓的刷新token,就是重新生成access token和refresh token,再将原来的access token和refresh token删除,把新的access token和refresh token插入到表中。
注意:新旧token的值是不一样的
大致流程:
1.和前面客户端凭证模式的获取token流程一样,首先会校验客户端密钥是否正确
2.如果客户端密钥正确,接下来就会根据grantType的值来做相应的处理,比如这里的值为refresh_token:
TokenEndpoint#postAccessToken
3.就会由RefreshTokenGranter
来处理:
4.因为我故意输出了refresh_token,所以它在oauth_refresh_token
表获取不到对应的数据就抛出异常:
相关sql:
private String selectRefreshTokenSql = "select token_id, token from oauth_refresh_token where token_id = ?";
5.如果refresh_token解析正常,则会根据refresh_token删除旧的access_token:
DefaultTokenServices#refreshAccessToken
@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
throws AuthenticationException {
if (!supportRefreshToken) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
// 1.解析refresh_token,通过解析出来的token_id查找数据
OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
// 2.如果为空直接抛出异常
if (refreshToken == null) {
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
}
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
if (this.authenticationManager != null && !authentication.isClientOnly()) {
// The client has already been authenticated, but the user authentication might be old now, so give it a
// chance to re-authenticate.
Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
user = authenticationManager.authenticate(user);
Object details = authentication.getDetails();
authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
authentication.setDetails(details);
}
String clientId = authentication.getOAuth2Request().getClientId();
if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
}
// clear out any access tokens already associated with the refresh
// token.
// 3、通过refresh_token删除access_token,见下面的第一点
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
if (isExpired(refreshToken)) {
tokenStore.removeRefreshToken(refreshToken);
throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
}
authentication = createRefreshedAuthentication(authentication, tokenRequest);
if (!reuseRefreshToken) {
// 4.通过token_id删除refresh_token,见下面的第一点分析
tokenStore.removeRefreshToken(refreshToken);
// 5.重新生成refresh_token
refreshToken = createRefreshToken(authentication);
}
// 6.重新生成access_token,见下面第二点分析
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
// 7.分别保存access_token和refresh_token
tokenStore.storeAccessToken(accessToken, authentication);
if (!reuseRefreshToken) {
tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
}
return accessToken;
}
注意:oauth_refresh_token表中的authentication字段为blob类型,它的值如下,包含了应用账户信息
①删除token操作
相关sql:
private static final String DEFAULT_ACCESS_TOKEN_DELETE_FROM_REFRESH_TOKEN_STATEMENT =
"delete from oauth_access_token where refresh_token = ?";
private static final String DEFAULT_REFRESH_TOKEN_DELETE_STATEMENT =
"delete from oauth_refresh_token where token_id = ?";
②生成access_token
首先会调用DefaultTokenServices#createAccessToken
:
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;
}
接着会调用到自定义的刷新token方法:
最后返回该access_token.
三、微信公众号开发的Oauth2应用
原文链接:https://blog.51cto.com/u_16217279/12417444
在微信公众号开发中,access_token 是一个非常重要的令牌,用于授权和调用微信开放平台的各类 API。根据不同的场景,可以通过不同的方式获取 access_token。主要有两种常见的获取方式:
1)通过 app_id 和 app_secret 获取的 access_token
- 这种 access_token 是全局唯一的,用于调用大多数微信公众平台接口。
- 通常用于服务器端的 API 调用,如获取用户基本信息、管理素材、发送模板消息等。
获取方式:
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=YOUR_APPID&secret=YOUR_APPSECRET
返回示例:
{
"access_token": "ACCESS_TOKEN",
"expires_in": 7200
}
2)通过用户授权的 code 获取的 access_token
- 这种 access_token 是针对特定用户的,用于获取用户的授权信息。
- 通常用于网页授权(OAuth2.0)场景,用户在网页上进行授权后,服务器端可以通过 code 换取 access_token 和 openid。
获取方式:
GET https://api.weixin.qq.com/sns/oauth2/access_token?appid=YOUR_APPID&secret=YOUR_APPSECRET&code=AUTH_CODE&grant_type=authorization_code
返回示例:
{
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"openid": "OPENID",
"scope": "SCOPE"
}
3)区别
使用范围:
- 通过 app_id 和 app_secret 获取的 access_token:用于调用大多数微信公众平台的 API,是服务器端调用 API 的凭证。
- 通过 code 获取的 access_token:用于获取用户的授权信息,主要用于网页授权场景。
获取方式:
- 通过 app_id 和 app_secret 获取的 access_token:直接通过微信公众号的 app_id 和 app_secret 获取。
- 通过 code 获取的 access_token:需要用户在网页上进行授权,获取到 code 后,再通过 code 和 app_id、app_secret 换取。
有效期和刷新:
- 通过 app_id 和 app_secret 获取的 access_token:有效期为 2 小时,过期后需要重新获取。
- 通过 code 获取的 access_token:有效期为 2 小时,可以使用 refresh_token 刷新 access_token。
关联对象:
- 通过 app_id 和 app_secret 获取的 access_token:与微信公众号全局关联。
- 通过 code 获取的 access_token:与特定用户关联。
4)总结
- 通过 app_id 和 app_secret 获取的 access_token:用于全局接口调用,是服务器端操作的凭证,对应前面所说的客户端凭证模式。
- 通过 code 获取的 access_token:用于获取用户授权信息,主要用于网页授权场景,与特定用户关联,对应前面所说的授权模型。
四、总结
这周有空都在调试代码,查看spring security oauth2的认证授权流程,调试了几天之后,脑子有点蒙蒙了,只能初步了解一些流程原理,不得不说这个框架确实有点东西。。
- 授权模型(用户认证接口)和密码模式的认证都和用户相关,所以都会经过自定义的
UserDetailsService
接口的实现类OauthAccountUserDetailsService
来查找用户信息,再由框架来进行授权用户的密码校验 - 客户端凭证式:主要用于服务器与服务器之间的通信,适用于没有用户上下文的场景,所以它的认证并没有经过
OauthAccountUserDetailsService
的处理,由框架内部实现了认证流程,底层是通过JDBC操作数据库中的oauth_client_details
、oauth_access_token
、oauth_refresh_token
表,该方式不支持刷新令牌,调试源码发现依旧会保存一个刷新token到oauth_refresh_token
表中。
收获:
如果以后碰到需要实现Oauth2的场景,并且不能引入Spring Security框架,也可以基于源码分析的思想来实现一个轻量级的Oauth2功能,可以基于它的库表设计,以及流程的处理逻辑来实现。。