Commit a3687132b68188f5ffd4c53ebbff3a1835d3e291

Authored by YunaiV
1 parent 86e6c04e

增加 refresh token 接口,并接入到前端项目

yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
@@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 import cn.iocoder.yudao.framework.common.util.collection.SetUtils; 6 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
7 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; 7 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
8 import cn.iocoder.yudao.framework.security.config.SecurityProperties; 8 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
9 -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;  
10 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*; 9 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
11 import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; 10 import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
12 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; 11 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
@@ -34,6 +33,7 @@ import java.util.Set; @@ -34,6 +33,7 @@ import java.util.Set;
34 33
35 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 34 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
36 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 35 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  36 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization;
37 import static java.util.Collections.singleton; 37 import static java.util.Collections.singleton;
38 38
39 @Api(tags = "管理后台 - 认证") 39 @Api(tags = "管理后台 - 认证")
@@ -68,13 +68,20 @@ public class AuthController { @@ -68,13 +68,20 @@ public class AuthController {
68 @ApiOperation("登出系统") 68 @ApiOperation("登出系统")
69 @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 69 @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
70 public CommonResult<Boolean> logout(HttpServletRequest request) { 70 public CommonResult<Boolean> logout(HttpServletRequest request) {
71 - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); 71 + String token = obtainAuthorization(request, securityProperties.getTokenHeader());
72 if (StrUtil.isNotBlank(token)) { 72 if (StrUtil.isNotBlank(token)) {
73 authService.logout(token); 73 authService.logout(token);
74 } 74 }
75 return success(true); 75 return success(true);
76 } 76 }
77 77
  78 + @PostMapping("/refresh-token")
  79 + @ApiOperation("刷新令牌")
  80 + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 TODO 接口文档
  81 + public CommonResult<AuthLoginRespVO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
  82 + return success(authService.refreshToken(refreshToken));
  83 + }
  84 +
78 @GetMapping("/get-permission-info") 85 @GetMapping("/get-permission-info")
79 @ApiOperation("获取登录用户的权限信息") 86 @ApiOperation("获取登录用户的权限信息")
80 public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() { 87 public CommonResult<AuthPermissionInfoRespVO> getPermissionInfo() {
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2AccessTokenMapper.java
@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; @@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; 4 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
5 import org.apache.ibatis.annotations.Mapper; 5 import org.apache.ibatis.annotations.Mapper;
6 6
  7 +import java.util.List;
  8 +
7 @Mapper 9 @Mapper
8 public interface OAuth2AccessTokenMapper extends BaseMapperX<OAuth2AccessTokenDO> { 10 public interface OAuth2AccessTokenMapper extends BaseMapperX<OAuth2AccessTokenDO> {
9 11
@@ -11,22 +13,8 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX&lt;OAuth2AccessTokenDO @@ -11,22 +13,8 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX&lt;OAuth2AccessTokenDO
11 return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken); 13 return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken);
12 } 14 }
13 15
14 -// default OAuth2AccessTokenDO selectByUserIdAndUserType(Integer userId, Integer userType) {  
15 -// return selectOne(new QueryWrapper<OAuth2AccessTokenDO>()  
16 -// .eq("user_id", userId).eq("user_type", userType));  
17 -// }  
18 -//  
19 -// default int deleteByUserIdAndUserType(Integer userId, Integer userType) {  
20 -// return delete(new QueryWrapper<OAuth2AccessTokenDO>()  
21 -// .eq("user_id", userId).eq("user_type", userType));  
22 -// }  
23 -//  
24 -// default int deleteByRefreshToken(String refreshToken) {  
25 -// return delete(new QueryWrapper<OAuth2AccessTokenDO>().eq("refresh_token", refreshToken));  
26 -// }  
27 -//  
28 -// default List<OAuth2AccessTokenDO> selectListByRefreshToken(String refreshToken) {  
29 -// return selectList(new QueryWrapper<OAuth2AccessTokenDO>().eq("refresh_token", refreshToken));  
30 -// } 16 + default List<OAuth2AccessTokenDO> selectListByRefreshToken(String refreshToken) {
  17 + return selectList(OAuth2AccessTokenDO::getRefreshToken, refreshToken);
  18 + }
31 19
32 } 20 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/auth/OAuth2RefreshTokenMapper.java
1 package cn.iocoder.yudao.module.system.dal.mysql.auth; 1 package cn.iocoder.yudao.module.system.dal.mysql.auth;
2 2
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
3 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
4 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO; 5 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO;
5 -import com.baomidou.mybatisplus.core.mapper.BaseMapper;  
6 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Mapper;
7 7
8 @Mapper 8 @Mapper
9 -public interface OAuth2RefreshTokenMapper extends BaseMapper<OAuth2RefreshTokenDO> { 9 +public interface OAuth2RefreshTokenMapper extends BaseMapperX<OAuth2RefreshTokenDO> {
10 10
11 default int deleteByRefreshToken(String refreshToken) { 11 default int deleteByRefreshToken(String refreshToken) {
12 return delete(new LambdaQueryWrapperX<OAuth2RefreshTokenDO>() 12 return delete(new LambdaQueryWrapperX<OAuth2RefreshTokenDO>()
13 .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken)); 13 .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken));
14 } 14 }
15 15
  16 + default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) {
  17 + return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken);
  18 + }
  19 +
16 } 20 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/auth/OAuth2AccessTokenRedisDAO.java
1 package cn.iocoder.yudao.module.system.dal.redis.auth; 1 package cn.iocoder.yudao.module.system.dal.redis.auth;
2 2
  3 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; 5 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
5 import org.springframework.data.redis.core.StringRedisTemplate; 6 import org.springframework.data.redis.core.StringRedisTemplate;
6 import org.springframework.stereotype.Repository; 7 import org.springframework.stereotype.Repository;
7 8
8 import javax.annotation.Resource; 9 import javax.annotation.Resource;
  10 +import java.util.Collection;
  11 +import java.util.List;
9 import java.util.concurrent.TimeUnit; 12 import java.util.concurrent.TimeUnit;
10 13
11 import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; 14 import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN;
@@ -39,6 +42,11 @@ public class OAuth2AccessTokenRedisDAO { @@ -39,6 +42,11 @@ public class OAuth2AccessTokenRedisDAO {
39 stringRedisTemplate.delete(redisKey); 42 stringRedisTemplate.delete(redisKey);
40 } 43 }
41 44
  45 + public void deleteList(Collection<String> accessTokens) {
  46 + List<String> redisKeys = CollectionUtils.convertList(accessTokens, OAuth2AccessTokenRedisDAO::formatKey);
  47 + stringRedisTemplate.delete(redisKeys);
  48 + }
  49 +
42 private static String formatKey(String accessToken) { 50 private static String formatKey(String accessToken) {
43 return String.format(OAUTH2_ACCESS_TOKEN.getKeyTemplate(), accessToken); 51 return String.format(OAUTH2_ACCESS_TOKEN.getKeyTemplate(), accessToken);
44 } 52 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java
@@ -21,6 +21,7 @@ public class SecurityConfiguration { @@ -21,6 +21,7 @@ public class SecurityConfiguration {
21 // 登录的接口 21 // 登录的接口
22 registry.antMatchers(buildAdminApi("/system/auth/login")).permitAll(); 22 registry.antMatchers(buildAdminApi("/system/auth/login")).permitAll();
23 registry.antMatchers(buildAdminApi("/system/auth/logout")).permitAll(); 23 registry.antMatchers(buildAdminApi("/system/auth/logout")).permitAll();
  24 + registry.antMatchers(buildAdminApi("/system/auth/refresh-token")).permitAll();
24 // 社交登陆的接口 25 // 社交登陆的接口
25 registry.antMatchers(buildAdminApi("/system/auth/social-auth-redirect")).permitAll(); 26 registry.antMatchers(buildAdminApi("/system/auth/social-auth-redirect")).permitAll();
26 registry.antMatchers(buildAdminApi("/system/auth/social-quick-login")).permitAll(); 27 registry.antMatchers(buildAdminApi("/system/auth/social-quick-login")).permitAll();
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java
@@ -59,4 +59,12 @@ public interface AdminAuthService { @@ -59,4 +59,12 @@ public interface AdminAuthService {
59 */ 59 */
60 AuthLoginRespVO socialBindLogin(@Valid AuthSocialBindLoginReqVO reqVO); 60 AuthLoginRespVO socialBindLogin(@Valid AuthSocialBindLoginReqVO reqVO);
61 61
  62 + /**
  63 + * 刷新访问令牌
  64 + *
  65 + * @param refreshToken 刷新令牌
  66 + * @return 登录结果
  67 + */
  68 + AuthLoginRespVO refreshToken(String refreshToken);
  69 +
62 } 70 }
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -192,6 +192,12 @@ public class AdminAuthServiceImpl implements AdminAuthService { @@ -192,6 +192,12 @@ public class AdminAuthServiceImpl implements AdminAuthService {
192 return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); 192 return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
193 } 193 }
194 194
  195 + @Override
  196 + public AuthLoginRespVO refreshToken(String refreshToken) {
  197 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientIdEnum.DEFAULT.getId());
  198 + return AuthConvert.INSTANCE.convert(accessTokenDO);
  199 + }
  200 +
195 private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { 201 private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
196 // 插入登陆日志 202 // 插入登陆日志
197 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); 203 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenService.java
@@ -30,9 +30,10 @@ public interface OAuth2TokenService { @@ -30,9 +30,10 @@ public interface OAuth2TokenService {
30 * 参考 DefaultTokenServices 的 refreshAccessToken 方法 30 * 参考 DefaultTokenServices 的 refreshAccessToken 方法
31 * 31 *
32 * @param refreshToken 刷新令牌 32 * @param refreshToken 刷新令牌
  33 + * @param clientId 客户端编号
33 * @return 访问令牌的信息 34 * @return 访问令牌的信息
34 */ 35 */
35 - OAuth2AccessTokenDO refreshAccessToken(String refreshToken); 36 + OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId);
36 37
37 /** 38 /**
38 * 获得访问令牌 39 * 获得访问令牌
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/OAuth2TokenServiceImpl.java
1 package cn.iocoder.yudao.module.system.service.auth; 1 package cn.iocoder.yudao.module.system.service.auth;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.hutool.core.util.IdUtil; 4 import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.core.util.ObjectUtil;
4 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; 6 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
5 import cn.iocoder.yudao.framework.common.util.date.DateUtils; 7 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
6 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; 8 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
@@ -15,8 +17,10 @@ import org.springframework.transaction.annotation.Transactional; @@ -15,8 +17,10 @@ import org.springframework.transaction.annotation.Transactional;
15 17
16 import javax.annotation.Resource; 18 import javax.annotation.Resource;
17 import java.util.Calendar; 19 import java.util.Calendar;
  20 +import java.util.List;
18 21
19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 22 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
20 24
21 /** 25 /**
22 * OAuth2.0 Token Service 实现类 26 * OAuth2.0 Token Service 实现类
@@ -44,15 +48,38 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -44,15 +48,38 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
44 // 创建刷新令牌 48 // 创建刷新令牌
45 OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO); 49 OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO);
46 // 创建访问令牌 50 // 创建访问令牌
47 - OAuth2AccessTokenDO accessTokenDO = createOAuth2AccessToken(refreshTokenDO, clientDO);  
48 - // 记录到 Redis 中  
49 - oauth2AccessTokenRedisDAO.set(accessTokenDO);  
50 - return accessTokenDO; 51 + return createOAuth2AccessToken(refreshTokenDO, clientDO);
51 } 52 }
52 53
53 @Override 54 @Override
54 - public OAuth2AccessTokenDO refreshAccessToken(String refreshToken) {  
55 - return null; 55 + public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, Long clientId) {
  56 + // 查询访问令牌
  57 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
  58 + if (refreshTokenDO == null) {
  59 + throw exception(GlobalErrorCodeConstants.BAD_REQUEST, "无效的刷新令牌");
  60 + }
  61 +
  62 + // 校验 Client 匹配
  63 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
  64 + if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) {
  65 + throw exception(GlobalErrorCodeConstants.BAD_REQUEST, "刷新令牌的客户端编号不正确");
  66 + }
  67 +
  68 + // 移除相关的访问令牌
  69 + List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
  70 + if (CollUtil.isNotEmpty(accessTokenDOs)) {
  71 + oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
  72 + oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken));
  73 + }
  74 +
  75 + // 已过期的情况下,删除刷新令牌
  76 + if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
  77 + oauth2AccessTokenMapper.deleteById(refreshTokenDO.getId());
  78 + throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "刷新令牌已过期");
  79 + }
  80 +
  81 + // 创建访问令牌
  82 + return createOAuth2AccessToken(refreshTokenDO, clientDO);
56 } 83 }
57 84
58 @Override 85 @Override
@@ -76,10 +103,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -76,10 +103,10 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
76 public OAuth2AccessTokenDO checkAccessToken(String accessToken) { 103 public OAuth2AccessTokenDO checkAccessToken(String accessToken) {
77 OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); 104 OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);
78 if (accessTokenDO == null) { 105 if (accessTokenDO == null) {
79 - throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "Token 不存在"); 106 + throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "访问令牌不存在");
80 } 107 }
81 if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) { 108 if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
82 - throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "Token 已过期"); 109 + throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "访问令牌已过期");
83 } 110 }
84 return accessTokenDO; 111 return accessTokenDO;
85 } 112 }
@@ -98,38 +125,16 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { @@ -98,38 +125,16 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
98 return accessTokenDO; 125 return accessTokenDO;
99 } 126 }
100 127
101 -  
102 -// @Override  
103 -// @Transactional  
104 -// public OAuth2AccessTokenRespDTO refreshAccessToken(OAuth2RefreshAccessTokenReqDTO refreshAccessTokenDTO) {  
105 -// OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectById(refreshAccessTokenDTO.getRefreshToken());  
106 -// // 校验刷新令牌是否合法  
107 -// if (refreshTokenDO == null) { // 不存在  
108 -// throw ServiceExceptionUtil.exception(OAUTH2_REFRESH_TOKEN_NOT_FOUND);  
109 -// }  
110 -// if (refreshTokenDO.getExpiresTime().getTime() < System.currentTimeMillis()) { // 已过期  
111 -// throw ServiceExceptionUtil.exception(OAUTH_REFRESH_TOKEN_EXPIRED);  
112 -// }  
113 -//  
114 -// // 标记 refreshToken 对应的 accessToken 都不合法  
115 -// // 这块的实现,参考了 Spring Security OAuth2 的代码  
116 -// List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshAccessTokenDTO.getRefreshToken());  
117 -// accessTokenDOs.forEach(accessTokenDO -> deleteOAuth2AccessToken(accessTokenDO.getId()));  
118 -//  
119 -// // 创建访问令牌  
120 -// OAuth2AccessTokenDO oauth2AccessTokenDO = createOAuth2AccessToken(refreshTokenDO, refreshAccessTokenDTO.getCreateIp());  
121 -// // 返回访问令牌  
122 -// return OAuth2Convert.INSTANCE.convert(oauth2AccessTokenDO);  
123 -// }  
124 -  
125 private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { 128 private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
126 - OAuth2AccessTokenDO accessToken = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) 129 + OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
127 .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getId()) 130 .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getId())
128 .setRefreshToken(refreshTokenDO.getRefreshToken()) 131 .setRefreshToken(refreshTokenDO.getRefreshToken())
129 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds())); 132 .setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
130 - accessToken.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号  
131 - oauth2AccessTokenMapper.insert(accessToken);  
132 - return accessToken; 133 + accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
  134 + oauth2AccessTokenMapper.insert(accessTokenDO);
  135 + // 记录到 Redis 中
  136 + oauth2AccessTokenRedisDAO.set(accessTokenDO);
  137 + return accessTokenDO;
133 } 138 }
134 139
135 private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) { 140 private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) {
yudao-ui-admin/src/api/login.js
1 import request from '@/utils/request' 1 import request from '@/utils/request'
  2 +import {getRefreshToken} from "@/utils/auth";
  3 +import service from "@/utils/request";
2 4
3 // 登录方法 5 // 登录方法
4 export function login(username, password, code, uuid) { 6 export function login(username, password, code, uuid) {
@@ -99,3 +101,11 @@ export function smsLogin(mobile, code) { @@ -99,3 +101,11 @@ export function smsLogin(mobile, code) {
99 } 101 }
100 }) 102 })
101 } 103 }
  104 +
  105 +// 刷新访问令牌
  106 +export function refreshToken() {
  107 + return service({
  108 + url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken(),
  109 + method: 'post'
  110 + })
  111 +}
yudao-ui-admin/src/store/modules/user.js
@@ -3,8 +3,6 @@ import {getAccessToken, setToken, removeToken, getRefreshToken} from &#39;@/utils/au @@ -3,8 +3,6 @@ import {getAccessToken, setToken, removeToken, getRefreshToken} from &#39;@/utils/au
3 3
4 const user = { 4 const user = {
5 state: { 5 state: {
6 - accessToken: getAccessToken(),  
7 - refreshToken: getRefreshToken(),  
8 id: 0, // 用户编号 6 id: 0, // 用户编号
9 name: '', 7 name: '',
10 avatar: '', 8 avatar: '',
@@ -16,10 +14,6 @@ const user = { @@ -16,10 +14,6 @@ const user = {
16 SET_ID: (state, id) => { 14 SET_ID: (state, id) => {
17 state.id = id 15 state.id = id
18 }, 16 },
19 - SET_TOKEN: (state, token) => {  
20 - state.accessToken = token.accessToken  
21 - state.refreshToken = token.refreshToken  
22 - },  
23 SET_NAME: (state, name) => { 17 SET_NAME: (state, name) => {
24 state.name = name 18 state.name = name
25 }, 19 },
@@ -46,7 +40,6 @@ const user = { @@ -46,7 +40,6 @@ const user = {
46 res = res.data; 40 res = res.data;
47 // 设置 token 41 // 设置 token
48 setToken(res) 42 setToken(res)
49 - commit('SET_TOKEN', res)  
50 resolve() 43 resolve()
51 }).catch(error => { 44 }).catch(error => {
52 reject(error) 45 reject(error)
@@ -64,7 +57,6 @@ const user = { @@ -64,7 +57,6 @@ const user = {
64 res = res.data; 57 res = res.data;
65 // 设置 token 58 // 设置 token
66 setToken(res) 59 setToken(res)
67 - commit('SET_TOKEN', res)  
68 resolve() 60 resolve()
69 }).catch(error => { 61 }).catch(error => {
70 reject(error) 62 reject(error)
@@ -84,7 +76,6 @@ const user = { @@ -84,7 +76,6 @@ const user = {
84 res = res.data; 76 res = res.data;
85 // 设置 token 77 // 设置 token
86 setToken(res) 78 setToken(res)
87 - commit('SET_TOKEN', res)  
88 resolve() 79 resolve()
89 }).catch(error => { 80 }).catch(error => {
90 reject(error) 81 reject(error)
@@ -100,7 +91,6 @@ const user = { @@ -100,7 +91,6 @@ const user = {
100 res = res.data; 91 res = res.data;
101 // 设置 token 92 // 设置 token
102 setToken(res) 93 setToken(res)
103 - commit('SET_TOKEN', res)  
104 resolve() 94 resolve()
105 }).catch(error => { 95 }).catch(error => {
106 reject(error) 96 reject(error)
@@ -148,7 +138,6 @@ const user = { @@ -148,7 +138,6 @@ const user = {
148 LogOut({ commit, state }) { 138 LogOut({ commit, state }) {
149 return new Promise((resolve, reject) => { 139 return new Promise((resolve, reject) => {
150 logout(state.token).then(() => { 140 logout(state.token).then(() => {
151 - commit('SET_TOKEN', '')  
152 commit('SET_ROLES', []) 141 commit('SET_ROLES', [])
153 commit('SET_PERMISSIONS', []) 142 commit('SET_PERMISSIONS', [])
154 removeToken() 143 removeToken()
yudao-ui-admin/src/utils/auth.js
@@ -8,7 +8,7 @@ export function getAccessToken() { @@ -8,7 +8,7 @@ export function getAccessToken() {
8 } 8 }
9 9
10 export function getRefreshToken() { 10 export function getRefreshToken() {
11 - return Cookies.get(AccessTokenKey) 11 + return Cookies.get(RefreshTokenKey)
12 } 12 }
13 13
14 export function setToken(token) { 14 export function setToken(token) {
yudao-ui-admin/src/utils/request.js
1 import axios from 'axios' 1 import axios from 'axios'
2 import { Notification, MessageBox, Message } from 'element-ui' 2 import { Notification, MessageBox, Message } from 'element-ui'
3 import store from '@/store' 3 import store from '@/store'
4 -import { getAccessToken } from '@/utils/auth' 4 +import {getAccessToken, getRefreshToken, setToken} from '@/utils/auth'
5 import errorCode from '@/utils/errorCode' 5 import errorCode from '@/utils/errorCode'
6 import Cookies from "js-cookie"; 6 import Cookies from "js-cookie";
7 import {getPath, getTenantEnable} from "@/utils/ruoyi"; 7 import {getPath, getTenantEnable} from "@/utils/ruoyi";
  8 +import {refreshToken} from "@/api/login";
8 9
9 // 是否显示重新登录 10 // 是否显示重新登录
10 export let isRelogin = { show: false }; 11 export let isRelogin = { show: false };
  12 +// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 实现
  13 +// 请求队列
  14 +let requestList = []
  15 +// 是否正在刷新中
  16 +let isRefreshToken = false
11 17
12 axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' 18 axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
13 // 创建axios实例 19 // 创建axios实例
@@ -60,29 +66,43 @@ service.interceptors.request.use(config =&gt; { @@ -60,29 +66,43 @@ service.interceptors.request.use(config =&gt; {
60 }) 66 })
61 67
62 // 响应拦截器 68 // 响应拦截器
63 -service.interceptors.response.use(res => { 69 +service.interceptors.response.use( async res => {
64 // 未设置状态码则默认成功状态 70 // 未设置状态码则默认成功状态
65 const code = res.data.code || 200; 71 const code = res.data.code || 200;
66 // 获取错误信息 72 // 获取错误信息
67 const msg = errorCode[code] || res.data.msg || errorCode['default'] 73 const msg = errorCode[code] || res.data.msg || errorCode['default']
68 if (code === 401) { 74 if (code === 401) {
69 - if (!isRelogin.show) {  
70 - isRelogin.show = true;  
71 - MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {  
72 - confirmButtonText: '重新登录',  
73 - cancelButtonText: '取消',  
74 - type: 'warning'  
75 - }  
76 - ).then(() => {  
77 - isRelogin.show = false;  
78 - store.dispatch('LogOut').then(() => {  
79 - location.href = getPath('/index'); 75 + // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
  76 + if (!isRefreshToken) {
  77 + isRefreshToken = true;
  78 + // 1. 如果获取不到刷新令牌,则只能执行登出操作
  79 + if (!getRefreshToken()) {
  80 + return handleAuthorized();
  81 + }
  82 + // 2. 进行刷新访问令牌
  83 + try {
  84 + const refreshTokenRes = await refreshToken()
  85 + // 2.1 刷新成功,则回放队列的请求 + 当前请求
  86 + setToken(refreshTokenRes.data)
  87 + requestList.forEach(cb => cb())
  88 + return service(res.config)
  89 + } catch (e) {
  90 + // 2.1 刷新失败,则只能执行登出操作
  91 + return handleAuthorized();
  92 + } finally {
  93 + requestList = []
  94 + isRefreshToken = false
  95 + }
  96 + } else {
  97 + // 添加到队列,等待刷新获取到新的令牌
  98 + return new Promise(resolve => {
  99 + requestList.push(() => {
  100 + config.headers['Authorization'] = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  101 + resolve(service(config))
80 }) 102 })
81 - }).catch(() => {  
82 - isRelogin.show = false;  
83 - }); 103 + })
84 } 104 }
85 - return Promise.reject('无效的会话,或者会话已过期,请重新登录。') 105 + return handleAuthorized();
86 } else if (code === 500) { 106 } else if (code === 500) {
87 Message({ 107 Message({
88 message: msg, 108 message: msg,
@@ -138,4 +158,24 @@ export function getBaseHeader() { @@ -138,4 +158,24 @@ export function getBaseHeader() {
138 } 158 }
139 } 159 }
140 160
  161 +function handleAuthorized() {
  162 + if (!isRelogin.show) {
  163 + isRelogin.show = true;
  164 + MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
  165 + confirmButtonText: '重新登录',
  166 + cancelButtonText: '取消',
  167 + type: 'warning'
  168 + }
  169 + ).then(() => {
  170 + isRelogin.show = false;
  171 + store.dispatch('LogOut').then(() => {
  172 + location.href = getPath('/index');
  173 + })
  174 + }).catch(() => {
  175 + isRelogin.show = false;
  176 + });
  177 + }
  178 + return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
  179 +}
  180 +
141 export default service 181 export default service