houzhongjian
2024-09-14 818a0170d8f2950d52cc7300a302356bbc523236
提交 | 用户 | 时间
e7c126 1 package com.iailab.module.system.service.oauth2;
H 2
3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.map.MapUtil;
5 import cn.hutool.core.util.IdUtil;
6 import cn.hutool.core.util.ObjectUtil;
7 import cn.hutool.core.util.StrUtil;
8 import com.iailab.framework.common.enums.UserTypeEnum;
9 import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants;
10 import com.iailab.framework.common.pojo.PageResult;
11 import com.iailab.framework.common.util.date.DateUtils;
12 import com.iailab.framework.security.core.LoginUser;
13 import com.iailab.framework.tenant.core.context.TenantContextHolder;
14 import com.iailab.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO;
15 import com.iailab.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
16 import com.iailab.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
17 import com.iailab.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO;
18 import com.iailab.module.system.dal.dataobject.user.AdminUserDO;
19 import com.iailab.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper;
20 import com.iailab.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper;
21 import com.iailab.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO;
22 import com.iailab.module.system.service.user.AdminUserService;
23 import org.springframework.context.annotation.Lazy;
24 import org.springframework.stereotype.Service;
25 import org.springframework.transaction.annotation.Transactional;
26
27 import javax.annotation.Resource;
28 import java.time.LocalDateTime;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Map;
32
33 import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception0;
34 import static com.iailab.framework.common.util.collection.CollectionUtils.convertSet;
35
36 /**
37  * OAuth2.0 Token Service 实现类
38  *
39  * @author iailab
40  */
41 @Service
42 public class OAuth2TokenServiceImpl implements OAuth2TokenService {
43
44     @Resource
45     private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
46     @Resource
47     private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;
48
49     @Resource
50     private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO;
51
52     @Resource
53     private OAuth2ClientService oauth2ClientService;
54     @Resource
55     @Lazy // 懒加载,避免循环依赖
56     private AdminUserService adminUserService;
57
58     @Override
59     @Transactional
60     public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List<String> scopes) {
61         OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
62         // 创建刷新令牌
63         OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes);
64         // 创建访问令牌
65         return createOAuth2AccessToken(refreshTokenDO, clientDO);
66     }
67
68     @Override
69     public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) {
70         // 查询访问令牌
71         OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
72         if (refreshTokenDO == null) {
73             throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌");
74         }
75
76         // 校验 Client 匹配
77         OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
78         if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) {
79             throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确");
80         }
81
82         // 移除相关的访问令牌
83         List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
84         if (CollUtil.isNotEmpty(accessTokenDOs)) {
85             oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId));
86             oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken));
87         }
88
89         // 已过期的情况下,删除刷新令牌
90         if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) {
91             oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId());
92             throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期");
93         }
94
95         // 创建访问令牌
96         return createOAuth2AccessToken(refreshTokenDO, clientDO);
97     }
98
99     @Override
100     public OAuth2AccessTokenDO getAccessToken(String accessToken) {
101         // 优先从 Redis 中获取
102         OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
103         if (accessTokenDO != null) {
104             return accessTokenDO;
105         }
106
107         // 获取不到,从 MySQL 中获取
108         accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
109         // 如果在 MySQL 存在,则往 Redis 中写入
110         if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
111             oauth2AccessTokenRedisDAO.set(accessTokenDO);
112         }
113         return accessTokenDO;
114     }
115
116     @Override
117     public OAuth2AccessTokenDO checkAccessToken(String accessToken) {
118         OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);
119         if (accessTokenDO == null) {
120             throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在");
121         }
122         if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
123             throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期");
124         }
125         return accessTokenDO;
126     }
127
128     @Override
129     public OAuth2AccessTokenDO removeAccessToken(String accessToken) {
130         // 删除访问令牌
131         OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken);
132         if (accessTokenDO == null) {
133             return null;
134         }
135         oauth2AccessTokenMapper.deleteById(accessTokenDO.getId());
136         oauth2AccessTokenRedisDAO.delete(accessToken);
137         // 删除刷新令牌
138         oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken());
139         return accessTokenDO;
140     }
141
142     @Override
143     public PageResult<OAuth2AccessTokenDO> getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) {
144         return oauth2AccessTokenMapper.selectPage(reqVO);
145     }
146
147     private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
148         OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
149                 .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
150                 .setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType()))
151                 .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
152                 .setRefreshToken(refreshTokenDO.getRefreshToken())
153                 .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds()));
154         accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
155         oauth2AccessTokenMapper.insert(accessTokenDO);
156         // 记录到 Redis 中
157         oauth2AccessTokenRedisDAO.set(accessTokenDO);
158         return accessTokenDO;
159     }
160
161     private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List<String> scopes) {
162         OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
163                 .setUserId(userId).setUserType(userType)
164                 .setClientId(clientDO.getClientId()).setScopes(scopes)
165                 .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
166         oauth2RefreshTokenMapper.insert(refreshToken);
167         return refreshToken;
168     }
169
170     /**
171      * 加载用户信息,方便 {@link com.iailab.framework.security.core.LoginUser} 获取到昵称、部门等信息
172      *
173      * @param userId 用户编号
174      * @param userType 用户类型
175      * @return 用户信息
176      */
177     private Map<String, String> buildUserInfo(Long userId, Integer userType) {
178         if (userType.equals(UserTypeEnum.ADMIN.getValue())) {
179             AdminUserDO user = adminUserService.getUser(userId);
180             return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname())
181                     .put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(user.getDeptId())).build();
182         } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) {
183             // 注意:目前 Member 暂时不读取,可以按需实现
184             return Collections.emptyMap();
185         }
186         return null;
187     }
188
189     private static String generateAccessToken() {
190         return IdUtil.fastSimpleUUID();
191     }
192
193     private static String generateRefreshToken() {
194         return IdUtil.fastSimpleUUID();
195     }
196
197 }