mirror of
https://gitee.com/gz-yami/mall4j.git
synced 2026-03-22 09:17:16 +08:00
sa-token改造
This commit is contained in:
@@ -24,14 +24,9 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Spring Boot</td>
|
||||
<td>2.1.6.RELEASE</td>
|
||||
<td>3.0.4</td>
|
||||
<td>MVC核心框架</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spring Security oauth2</td>
|
||||
<td>2.1.5.RELEASE</td>
|
||||
<td>认证和授权框架</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MyBatis</td>
|
||||
<td>3.5.0</td>
|
||||
@@ -39,22 +34,17 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MyBatisPlus</td>
|
||||
<td>3.1.0</td>
|
||||
<td>3.5.3.1</td>
|
||||
<td>基于mybatis,使用lambda表达式的</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swagger-UI</td>
|
||||
<td>2.9.2</td>
|
||||
<td>4.0.0</td>
|
||||
<td>文档生产工具</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hibernator-Validator</td>
|
||||
<td>6.0.17.Final</td>
|
||||
<td>验证框架</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>redisson</td>
|
||||
<td>3.10.6</td>
|
||||
<td>3.19.3</td>
|
||||
<td>对redis进行封装、集成分布式锁等</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -64,19 +54,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>log4j2</td>
|
||||
<td>2.11.2</td>
|
||||
<td>2.17.2</td>
|
||||
<td>更快的log日志工具</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>fst</td>
|
||||
<td>2.57</td>
|
||||
<td>更快的序列化和反序列化工具</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>orika</td>
|
||||
<td>1.5.4</td>
|
||||
<td>更快的bean复制工具</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>lombok</td>
|
||||
<td>1.18.8</td>
|
||||
@@ -84,13 +64,13 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>hutool</td>
|
||||
<td>4.5.0</td>
|
||||
<td>5.8.15</td>
|
||||
<td>更适合国人的java工具集</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>swagger-bootstrap</td>
|
||||
<td>1.9.3</td>
|
||||
<td>基于swagger,更便于国人使用的swagger ui</td>
|
||||
<td>xxl-job</td>
|
||||
<td>2.3.1</td>
|
||||
<td>定时任务</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -112,7 +92,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>jdk</td>
|
||||
<td>1.8+</td>
|
||||
<td>17</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mysql</td>
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -33,11 +33,13 @@
|
||||
<aliyun-dysmsapi.version>1.1.0</aliyun-dysmsapi.version>
|
||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
||||
<redisson.version>3.19.3</redisson.version>
|
||||
<transmittable-thread-local.version>2.12.1</transmittable-thread-local.version>
|
||||
<transmittable-thread-local.version>2.14.2</transmittable-thread-local.version>
|
||||
<log4j.version>2.19.0</log4j.version>
|
||||
<knife4j.version>4.0.0</knife4j.version>
|
||||
<xxl-job.version>2.3.1</xxl-job.version>
|
||||
<spring-cloud-commons.version>4.0.1</spring-cloud-commons.version>
|
||||
<satoken.version>1.34.0</satoken.version>
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -25,3 +25,14 @@ mybatis-plus:
|
||||
field-strategy: NOT_NULL
|
||||
# 默认数据库表下划线命名
|
||||
table-underline: true
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: Authorization
|
||||
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
||||
is-concurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个token(不共用,避免登出时导致其他用户也登出)
|
||||
is-share: false
|
||||
# token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
token-style: uuid
|
||||
# 是否输出操作日志
|
||||
is-log: false
|
||||
|
||||
@@ -29,3 +29,14 @@ mybatis-plus:
|
||||
management:
|
||||
server:
|
||||
add-application-context-header: false
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: Authorization
|
||||
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
||||
is-concurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个token(不共用,避免登出时导致其他用户也登出)
|
||||
is-share: false
|
||||
# token风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
token-style: uuid
|
||||
# 是否输出操作日志
|
||||
is-log: false
|
||||
|
||||
@@ -30,4 +30,9 @@ public interface OauthCacheNames {
|
||||
* 根据uid获取保存的token key缓存使用的key
|
||||
*/
|
||||
String UID_TO_ACCESS = OAUTH_TOKEN_PREFIX + "uid_to_access:";
|
||||
|
||||
/**
|
||||
* 保存token的用户信息使用的key
|
||||
*/
|
||||
String USER_INFO = OAUTH_TOKEN_PREFIX + "user_info:";
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public class ServerResponseEntity<T> implements Serializable {
|
||||
|
||||
public ServerResponseEntity() {
|
||||
// 版本号
|
||||
this.version = "mall4j.v230410";
|
||||
this.version = "mall4j.v230424";
|
||||
}
|
||||
|
||||
public static <T> ServerResponseEntity<T> success(T data) {
|
||||
|
||||
@@ -28,6 +28,23 @@
|
||||
<artifactId>captcha</artifactId>
|
||||
<version>1.3.0</version>
|
||||
</dependency>
|
||||
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>${satoken.version}</version>
|
||||
</dependency>
|
||||
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-dao-redis-jackson</artifactId>
|
||||
<version>${satoken.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>${fastjson.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
package com.yami.shop.security.common.filter;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.yami.shop.common.exception.YamiShopBindException;
|
||||
@@ -22,6 +23,7 @@ import com.yami.shop.security.common.util.AuthUserContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
@@ -51,6 +53,9 @@ public class AuthFilter implements Filter {
|
||||
@Autowired
|
||||
private TokenStore tokenStore;
|
||||
|
||||
@Value("${sa-token.token-name}")
|
||||
private String tokenName;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
@@ -72,7 +77,7 @@ public class AuthFilter implements Filter {
|
||||
}
|
||||
}
|
||||
|
||||
String accessToken = req.getHeader("Authorization");
|
||||
String accessToken = req.getHeader(tokenName);
|
||||
// 也许需要登录,不登陆也能用的uri
|
||||
boolean mayAuth = pathMatcher.match(AuthConfigAdapter.MAYBE_AUTH_URI, requestUri);
|
||||
|
||||
@@ -82,6 +87,13 @@ public class AuthFilter implements Filter {
|
||||
try {
|
||||
// 如果有token,就要获取token
|
||||
if (StrUtil.isNotBlank(accessToken)) {
|
||||
// 校验登录,并从缓存中取出用户信息
|
||||
try {
|
||||
StpUtil.checkLogin();
|
||||
} catch (Exception e) {
|
||||
httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
|
||||
return;
|
||||
}
|
||||
userInfoInToken = tokenStore.getUserInfoByAccessToken(accessToken, true);
|
||||
}
|
||||
else if (!mayAuth) {
|
||||
|
||||
@@ -33,6 +33,10 @@ public class PasswordManager {
|
||||
public String passwordSignKey;
|
||||
|
||||
public String decryptPassword(String data) {
|
||||
// 在使用oracle的JDK时,JAR包必须签署特殊的证书才能使用。
|
||||
// 解决方案 1.使用openJDK或者非oracle的JDK(建议) 2.添加证书
|
||||
// hutool的aes报错可以打开下面那段代码
|
||||
// SecureUtil.disableBouncyCastle();
|
||||
AES aes = new AES(passwordSignKey.getBytes(StandardCharsets.UTF_8));
|
||||
String decryptStr;
|
||||
String decryptPassword;
|
||||
|
||||
@@ -9,34 +9,26 @@
|
||||
*/
|
||||
package com.yami.shop.security.common.manager;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.symmetric.AES;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.yami.shop.common.constants.OauthCacheNames;
|
||||
import com.yami.shop.common.response.ResponseEnum;
|
||||
import com.yami.shop.common.exception.YamiShopBindException;
|
||||
import com.yami.shop.common.util.PrincipalUtil;
|
||||
import com.yami.shop.common.response.ResponseEnum;
|
||||
import com.yami.shop.security.common.bo.TokenInfoBO;
|
||||
import com.yami.shop.security.common.bo.UserInfoInTokenBO;
|
||||
import com.yami.shop.security.common.enums.SysTypeEnum;
|
||||
import com.yami.shop.security.common.vo.TokenInfoVO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* token管理 1. 登陆返回token 2. 刷新token 3. 清除用户过去token 4. 校验token
|
||||
@@ -49,96 +41,49 @@ public class TokenStore {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TokenStore.class);
|
||||
|
||||
/**
|
||||
* 用于aes签名的key,16位
|
||||
*/
|
||||
@Value("${auth.token.signKey:-mall4j--mall4j-}")
|
||||
public String tokenSignKey;
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final RedisSerializer<Object> redisSerializer;
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
public TokenStore(RedisTemplate<String, Object> redisTemplate,
|
||||
StringRedisTemplate stringRedisTemplate, GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer) {
|
||||
public TokenStore(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.redisSerializer = genericJackson2JsonRedisSerializer;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将用户的部分信息存储在token中,并返回token信息
|
||||
* @param userInfoInToken 用户在token中的信息
|
||||
* @return token信息
|
||||
* 以Sa-Token技术生成token,并返回token信息
|
||||
* @param userInfoInToken
|
||||
* @return
|
||||
*/
|
||||
public TokenInfoBO storeAccessToken(UserInfoInTokenBO userInfoInToken) {
|
||||
public TokenInfoBO storeAccessSaToken(UserInfoInTokenBO userInfoInToken) {
|
||||
// token生成
|
||||
int timeoutSecond = getExpiresIn(userInfoInToken.getSysType());
|
||||
String uid = this.getUid(userInfoInToken.getSysType().toString(), userInfoInToken.getUserId());
|
||||
StpUtil.login(uid, timeoutSecond);
|
||||
String token = StpUtil.getTokenValue();
|
||||
// 用户信息存入缓存
|
||||
String keyName = OauthCacheNames.USER_INFO + token;
|
||||
redisTemplate.delete(keyName);
|
||||
redisTemplate.opsForValue().set(
|
||||
keyName,
|
||||
JSON.toJSONString(userInfoInToken),
|
||||
timeoutSecond,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
// 数据封装返回(token不用加密)
|
||||
TokenInfoBO tokenInfoBO = new TokenInfoBO();
|
||||
String accessToken = IdUtil.simpleUUID();
|
||||
String refreshToken = IdUtil.simpleUUID();
|
||||
|
||||
tokenInfoBO.setUserInfoInToken(userInfoInToken);
|
||||
tokenInfoBO.setExpiresIn(getExpiresIn(userInfoInToken.getSysType()));
|
||||
|
||||
String uidToAccessKeyStr = getUserIdToAccessKey(getApprovalKey(userInfoInToken));
|
||||
String accessKeyStr = getAccessKey(accessToken);
|
||||
String refreshToAccessKeyStr = getRefreshToAccessKey(refreshToken);
|
||||
|
||||
// 一个用户会登陆很多次,每次登陆的token都会存在 uid_to_access里面
|
||||
// 但是每次保存都会更新这个key的时间,而key里面的token有可能会过期,过期就要移除掉
|
||||
List<byte[]> existsAccessTokensBytes = new ArrayList<>();
|
||||
// 新的token数据
|
||||
existsAccessTokensBytes.add((accessToken + StrUtil.COLON + refreshToken).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Long size = redisTemplate.opsForSet().size(uidToAccessKeyStr);
|
||||
if (size != null && size != 0) {
|
||||
List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidToAccessKeyStr, size);
|
||||
if (tokenInfoBoList != null) {
|
||||
for (String accessTokenWithRefreshToken : tokenInfoBoList) {
|
||||
String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
|
||||
String accessTokenData = accessTokenWithRefreshTokenArr[0];
|
||||
if (BooleanUtil.isTrue(stringRedisTemplate.hasKey(getAccessKey(accessTokenData)))) {
|
||||
existsAccessTokensBytes.add(accessTokenWithRefreshToken.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
|
||||
|
||||
long expiresIn = tokenInfoBO.getExpiresIn();
|
||||
|
||||
byte[] uidKey = uidToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] refreshKey = refreshToAccessKeyStr.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] accessKey = accessKeyStr.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
connection.sAdd(uidKey, ArrayUtil.toArray(existsAccessTokensBytes, byte[].class));
|
||||
|
||||
// 通过uid + sysType 保存access_token,当需要禁用用户的时候,可以根据uid + sysType 禁用用户
|
||||
connection.expire(uidKey, expiresIn);
|
||||
|
||||
// 通过refresh_token获取用户的access_token从而刷新token
|
||||
connection.setEx(refreshKey, expiresIn, accessToken.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 通过access_token保存用户的租户id,用户id,uid
|
||||
connection.setEx(accessKey, expiresIn, Objects.requireNonNull(redisSerializer.serialize(userInfoInToken)));
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// 返回给前端是加密的token
|
||||
tokenInfoBO.setAccessToken(encryptToken(accessToken,userInfoInToken.getSysType()));
|
||||
tokenInfoBO.setRefreshToken(encryptToken(refreshToken,userInfoInToken.getSysType()));
|
||||
|
||||
tokenInfoBO.setExpiresIn(timeoutSecond);
|
||||
tokenInfoBO.setAccessToken(token);
|
||||
tokenInfoBO.setRefreshToken(token);
|
||||
return tokenInfoBO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算过期时间(单位:秒)
|
||||
* @param sysType
|
||||
* @return
|
||||
*/
|
||||
private int getExpiresIn(int sysType) {
|
||||
// 3600秒
|
||||
int expiresIn = 3600;
|
||||
|
||||
// 普通用户token过期时间 1小时
|
||||
if (Objects.equals(sysType, SysTypeEnum.ORDINARY.value())) {
|
||||
expiresIn = expiresIn * 24 * 30;
|
||||
@@ -160,20 +105,12 @@ public class TokenStore {
|
||||
if (StrUtil.isBlank(accessToken)) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"accessToken is blank");
|
||||
}
|
||||
String realAccessToken;
|
||||
if (needDecrypt) {
|
||||
realAccessToken = decryptToken(accessToken);
|
||||
String keyName = OauthCacheNames.USER_INFO + accessToken;
|
||||
Object redisCache = redisTemplate.opsForValue().get(keyName);
|
||||
if (redisCache == null) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"登录过期,请重新登录");
|
||||
}
|
||||
else {
|
||||
realAccessToken = accessToken;
|
||||
}
|
||||
UserInfoInTokenBO userInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue()
|
||||
.get(getAccessKey(realAccessToken));
|
||||
|
||||
if (userInfoInTokenBO == null) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"accessToken 已过期");
|
||||
}
|
||||
return userInfoInTokenBO;
|
||||
return JSON.parseObject(redisCache.toString(), UserInfoInTokenBO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,106 +122,43 @@ public class TokenStore {
|
||||
if (StrUtil.isBlank(refreshToken)) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"refreshToken is blank");
|
||||
}
|
||||
String realRefreshToken = decryptToken(refreshToken);
|
||||
String accessToken = stringRedisTemplate.opsForValue().get(getRefreshToAccessKey(realRefreshToken));
|
||||
|
||||
if (StrUtil.isBlank(accessToken)) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"refreshToken 已过期");
|
||||
}
|
||||
UserInfoInTokenBO userInfoInTokenBO = getUserInfoByAccessToken(accessToken,
|
||||
false);
|
||||
|
||||
// 删除旧的refresh_token
|
||||
stringRedisTemplate.delete(getRefreshToAccessKey(realRefreshToken));
|
||||
// 删除旧的access_token
|
||||
stringRedisTemplate.delete(getAccessKey(accessToken));
|
||||
// 删除旧token
|
||||
UserInfoInTokenBO userInfoInTokenBO = getUserInfoByAccessToken(refreshToken, false);
|
||||
this.deleteCurrentToken(refreshToken);
|
||||
// 保存一份新的token
|
||||
return storeAccessToken(userInfoInTokenBO);
|
||||
return storeAccessSaToken(userInfoInTokenBO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除全部的token
|
||||
* 删除指定用户的全部的token
|
||||
*/
|
||||
public void deleteAllToken(String sysType, String userId) {
|
||||
String uidKey = getUserIdToAccessKey(getApprovalKey(sysType, userId));
|
||||
Long size = redisTemplate.opsForSet().size(uidKey);
|
||||
if (size == null || size == 0) {
|
||||
return;
|
||||
}
|
||||
List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidKey, size);
|
||||
|
||||
if (CollUtil.isEmpty(tokenInfoBoList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String accessTokenWithRefreshToken : tokenInfoBoList) {
|
||||
String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
|
||||
String accessToken = accessTokenWithRefreshTokenArr[0];
|
||||
String refreshToken = accessTokenWithRefreshTokenArr[1];
|
||||
redisTemplate.delete(getRefreshToAccessKey(refreshToken));
|
||||
redisTemplate.delete(getAccessKey(accessToken));
|
||||
}
|
||||
redisTemplate.delete(uidKey);
|
||||
|
||||
}
|
||||
|
||||
private static String getApprovalKey(UserInfoInTokenBO userInfoInToken) {
|
||||
return getApprovalKey(userInfoInToken.getSysType().toString(), userInfoInToken.getUserId());
|
||||
}
|
||||
|
||||
private static String getApprovalKey(String sysType, String userId) {
|
||||
return userId == null? sysType : sysType + StrUtil.COLON + userId;
|
||||
}
|
||||
|
||||
private String encryptToken(String accessToken,Integer sysType) {
|
||||
AES aes = new AES(tokenSignKey.getBytes(StandardCharsets.UTF_8));
|
||||
return aes.encryptBase64(accessToken + System.currentTimeMillis() + sysType);
|
||||
}
|
||||
|
||||
private String decryptToken(String data) {
|
||||
AES aes = new AES(tokenSignKey.getBytes(StandardCharsets.UTF_8));
|
||||
String decryptStr;
|
||||
String decryptToken;
|
||||
try {
|
||||
decryptStr = aes.decryptStr(data);
|
||||
decryptToken = decryptStr.substring(0,32);
|
||||
// 创建token的时间,token使用时效性,防止攻击者通过一堆的尝试找到aes的密码,虽然aes是目前几乎最好的加密算法
|
||||
long createTokenTime = Long.parseLong(decryptStr.substring(32,45));
|
||||
// 系统类型
|
||||
int sysType = Integer.parseInt(decryptStr.substring(45));
|
||||
// token的过期时间
|
||||
int expiresIn = getExpiresIn(sysType);
|
||||
long second = 1000L;
|
||||
if (System.currentTimeMillis() - createTokenTime > expiresIn * second) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"token error");
|
||||
// 删除用户缓存
|
||||
String uid = this.getUid(sysType, userId);
|
||||
List<String> tokens = StpUtil.getTokenValueListByLoginId(uid);
|
||||
if (!CollectionUtils.isEmpty(tokens)) {
|
||||
List<String> keyNames = new ArrayList<>();
|
||||
for (String token : tokens) {
|
||||
keyNames.add(OauthCacheNames.USER_INFO + token);
|
||||
}
|
||||
redisTemplate.delete(keyNames);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"token error");
|
||||
}
|
||||
|
||||
// 防止解密后的token是脚本,从而对redis进行攻击,uuid只能是数字和小写字母
|
||||
if (!PrincipalUtil.isSimpleChar(decryptToken)) {
|
||||
throw new YamiShopBindException(ResponseEnum.UNAUTHORIZED,"token error");
|
||||
}
|
||||
return decryptToken;
|
||||
}
|
||||
|
||||
public String getAccessKey(String accessToken) {
|
||||
return OauthCacheNames.ACCESS + accessToken;
|
||||
}
|
||||
|
||||
public String getUserIdToAccessKey(String approvalKey) {
|
||||
return OauthCacheNames.UID_TO_ACCESS + approvalKey;
|
||||
}
|
||||
|
||||
public String getRefreshToAccessKey(String refreshToken) {
|
||||
return OauthCacheNames.REFRESH_TO_ACCESS + refreshToken;
|
||||
// 移除token
|
||||
StpUtil.logout(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成token,并返回token展示信息
|
||||
* @param userInfoInToken
|
||||
* @return
|
||||
*/
|
||||
public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInToken) {
|
||||
TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInToken);
|
||||
|
||||
if (!userInfoInToken.getEnabled()){
|
||||
// 用户已禁用,请联系客服
|
||||
throw new YamiShopBindException("yami.user.disabled");
|
||||
}
|
||||
TokenInfoBO tokenInfoBO = storeAccessSaToken(userInfoInToken);
|
||||
// 数据封装返回
|
||||
TokenInfoVO tokenInfoVO = new TokenInfoVO();
|
||||
tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken());
|
||||
tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken());
|
||||
@@ -292,41 +166,25 @@ public class TokenStore {
|
||||
return tokenInfoVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前登录的token
|
||||
* @param accessToken 令牌
|
||||
*/
|
||||
public void deleteCurrentToken(String accessToken) {
|
||||
String decryptToken = decryptToken(accessToken);
|
||||
// 删除用户缓存
|
||||
String keyName = OauthCacheNames.USER_INFO + accessToken;
|
||||
redisTemplate.delete(keyName);
|
||||
// 移除token
|
||||
StpUtil.logoutByTokenValue(accessToken);
|
||||
}
|
||||
|
||||
UserInfoInTokenBO userInfoInToken = getUserInfoByAccessToken(accessToken, true);
|
||||
|
||||
String uidKey = getUserIdToAccessKey(getApprovalKey(userInfoInToken.getSysType().toString(), userInfoInToken.getUserId()));
|
||||
Long size = redisTemplate.opsForSet().size(uidKey);
|
||||
if (size == null || size == 0) {
|
||||
return;
|
||||
}
|
||||
List<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidKey, size);
|
||||
|
||||
if (CollUtil.isEmpty(tokenInfoBoList)) {
|
||||
return;
|
||||
}
|
||||
String dbAccessToken = null;
|
||||
String dbRefreshToken = null;
|
||||
List<byte[]> list = new ArrayList<>();
|
||||
for (String accessTokenWithRefreshToken : tokenInfoBoList) {
|
||||
String[] accessTokenWithRefreshTokenArr = accessTokenWithRefreshToken.split(StrUtil.COLON);
|
||||
dbAccessToken = accessTokenWithRefreshTokenArr[0];
|
||||
if (decryptToken.equals(dbAccessToken)) {
|
||||
dbRefreshToken = accessTokenWithRefreshTokenArr[1];
|
||||
redisTemplate.delete(getRefreshToAccessKey(dbRefreshToken));
|
||||
redisTemplate.delete(getAccessKey(dbAccessToken));
|
||||
continue;
|
||||
}
|
||||
list.add(accessTokenWithRefreshToken.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
if (CollUtil.isNotEmpty(list)) {
|
||||
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
|
||||
connection.sAdd(uidKey.getBytes(StandardCharsets.UTF_8), ArrayUtil.toArray(list, byte[].class));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 生成各系统唯一uid
|
||||
* @param sysType 系统类型
|
||||
* @param userId 用户id
|
||||
* @return
|
||||
*/
|
||||
private String getUid(String sysType, String userId) {
|
||||
return sysType + ":" + userId;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user