Commit a3687132b68188f5ffd4c53ebbff3a1835d3e291
1 parent
86e6c04e
增加 refresh token 接口,并接入到前端项目
Showing
13 changed files
with
154 additions
and
87 deletions
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<OAuth2AccessTokenDO | @@ -11,22 +13,8 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX<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 '@/utils/au | @@ -3,8 +3,6 @@ import {getAccessToken, setToken, removeToken, getRefreshToken} from '@/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 => { | @@ -60,29 +66,43 @@ service.interceptors.request.use(config => { | ||
| 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 |