package com.iailab.module.system.service.oauth2; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.iailab.framework.common.enums.UserTypeEnum; import com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants; import com.iailab.framework.common.pojo.PageResult; import com.iailab.framework.common.util.date.DateUtils; import com.iailab.framework.security.core.LoginUser; import com.iailab.framework.tenant.core.context.TenantContextHolder; import com.iailab.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; import com.iailab.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import com.iailab.module.system.dal.dataobject.oauth2.OAuth2ClientDO; import com.iailab.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; import com.iailab.module.system.dal.dataobject.user.AdminUserDO; import com.iailab.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; import com.iailab.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; import com.iailab.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; import com.iailab.module.system.service.user.AdminUserService; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.Map; import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception0; import static com.iailab.framework.common.util.collection.CollectionUtils.convertSet; /** * OAuth2.0 Token Service 实现类 * * @author iailab */ @Service public class OAuth2TokenServiceImpl implements OAuth2TokenService { @Resource private OAuth2AccessTokenMapper oauth2AccessTokenMapper; @Resource private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; @Resource private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; @Resource private OAuth2ClientService oauth2ClientService; @Resource @Lazy // 懒加载,避免循环依赖 private AdminUserService adminUserService; @Override @Transactional public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); // 创建刷新令牌 OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); // 创建访问令牌 return createOAuth2AccessToken(refreshTokenDO, clientDO); } @Override public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { // 查询访问令牌 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); if (refreshTokenDO == null) { throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌"); } // 校验 Client 匹配 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) { throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确"); } // 移除相关的访问令牌 List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); if (CollUtil.isNotEmpty(accessTokenDOs)) { oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); } // 已过期的情况下,删除刷新令牌 if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) { oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId()); throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期"); } // 创建访问令牌 return createOAuth2AccessToken(refreshTokenDO, clientDO); } @Override public OAuth2AccessTokenDO getAccessToken(String accessToken) { // 优先从 Redis 中获取 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken); if (accessTokenDO != null) { return accessTokenDO; } // 获取不到,从 MySQL 中获取 accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); // 如果在 MySQL 存在,则往 Redis 中写入 if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) { oauth2AccessTokenRedisDAO.set(accessTokenDO); } return accessTokenDO; } @Override public OAuth2AccessTokenDO checkAccessToken(String accessToken) { OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); if (accessTokenDO == null) { throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在"); } if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) { throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期"); } return accessTokenDO; } @Override public OAuth2AccessTokenDO removeAccessToken(String accessToken) { // 删除访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); if (accessTokenDO == null) { return null; } oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); oauth2AccessTokenRedisDAO.delete(accessToken); // 删除刷新令牌 oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); return accessTokenDO; } @Override public PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) { return oauth2AccessTokenMapper.selectPage(reqVO); } private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()) .setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType())) .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) .setRefreshToken(refreshTokenDO.getRefreshToken()) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())); accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 oauth2AccessTokenMapper.insert(accessTokenDO); // 记录到 Redis 中 oauth2AccessTokenRedisDAO.set(accessTokenDO); return accessTokenDO; } private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes) { OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) .setUserId(userId).setUserType(userType) .setClientId(clientDO.getClientId()).setScopes(scopes) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds())); oauth2RefreshTokenMapper.insert(refreshToken); return refreshToken; } /** * 加载用户信息,方便 {@link com.iailab.framework.security.core.LoginUser} 获取到昵称、部门等信息 * * @param userId 用户编号 * @param userType 用户类型 * @return 用户信息 */ private Map buildUserInfo(Long userId, Integer userType) { if (userType.equals(UserTypeEnum.ADMIN.getValue())) { AdminUserDO user = adminUserService.getUser(userId); return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname()) .put(LoginUser.INFO_KEY_DEPT_ID, StrUtil.toStringOrNull(user.getDeptId())).build(); } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) { // 注意:目前 Member 暂时不读取,可以按需实现 return Collections.emptyMap(); } return null; } private static String generateAccessToken() { return IdUtil.fastSimpleUUID(); } private static String generateRefreshToken() { return IdUtil.fastSimpleUUID(); } }