SpringSecurity的Oauth2学习笔记


学习链接

一、客户端凭证模式源码分析

因为项目配置了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.首先在JdbcClientDetailsServiceloadClientByClientId 方法打上断点作为入口:
在这里插入图片描述

可以看到会通过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.老样子,首先在JdbcClientDetailsServiceloadClientByClientId 方法打上断点作为入口,先根据客户端的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_detailsoauth_access_tokenoauth_refresh_token表,该方式不支持刷新令牌,调试源码发现依旧会保存一个刷新token到oauth_refresh_token 表中。

收获:

如果以后碰到需要实现Oauth2的场景,并且不能引入Spring Security框架,也可以基于源码分析的思想来实现一个轻量级的Oauth2功能,可以基于它的库表设计,以及流程的处理逻辑来实现。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhou22-codeWalker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值