/* * Copyright (c) 2018-2999 广州市蓝海创新科技有限公司 All rights reserved. * * https://www.mall4j.com/ * * 未经允许,不可做商业用途! * * 版权所有,侵权必究! */ package com.yami.trading.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.hutool.core.util.StrUtil; import cn.hutool.crypto.symmetric.AES; import com.yami.trading.bean.syspara.domain.Syspara; import com.yami.trading.common.constants.OauthCacheNames; import com.yami.trading.common.enums.YamiHttpStatus; import com.yami.trading.common.exception.YamiShopBindException; import com.yami.trading.common.serializer.redis.KryoRedisSerializer; import com.yami.trading.common.util.PrincipalUtil; import com.yami.trading.security.common.bo.TokenInfoBO; import com.yami.trading.security.common.bo.UserInfoInTokenBO; import com.yami.trading.security.common.enums.SysTypeEnum; import com.yami.trading.security.common.vo.TokenInfoVO; import com.yami.trading.service.syspara.SysparaService; 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.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.stereotype.Component; 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 * * @author FrozenWatermelon * @date 2020/7/2 */ @Component 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 redisTemplate; private final RedisSerializer redisSerializer; private final StringRedisTemplate stringRedisTemplate; @Autowired private SysparaService sysparaService; public TokenStore(RedisTemplate redisTemplate, StringRedisTemplate stringRedisTemplate) { this.redisTemplate = redisTemplate; this.redisSerializer = new KryoRedisSerializer<>(); this.stringRedisTemplate = stringRedisTemplate; } /** * 将用户的部分信息存储在token中,并返回token信息 * @param userInfoInToken 用户在token中的信息 * @return token信息 */ public TokenInfoBO storeAccessToken(UserInfoInTokenBO userInfoInToken) { 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 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 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) 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 禁用用户 if (expiresIn==-1){ // 通过refresh_token获取用户的access_token从而刷新token connection.set(refreshKey, accessToken.getBytes(StandardCharsets.UTF_8)); // 通过access_token保存用户的租户id,用户id,uid connection.set(accessKey, Objects.requireNonNull(redisSerializer.serialize(userInfoInToken))); } else { 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())); return tokenInfoBO; } private int getExpiresIn(int sysType) { Syspara admin_token_expires_time = sysparaService.find("admin_token_expires_time"); // 3600秒 int expiresIn = 3600; // 普通用户token过期时间 15天 if (Objects.equals(sysType, SysTypeEnum.ORDINARY.value())) { expiresIn = -1; // expiresIn = expiresIn * 24 * 300; } // 系统管理员的token过期时间 2小时分钟 if (Objects.equals(sysType, SysTypeEnum.ADMIN.value())) { if(Objects.nonNull(admin_token_expires_time)) { Double times = admin_token_expires_time.getDouble(); if(Objects.nonNull(times)) { expiresIn = (int) (expiresIn * times); } }else { expiresIn = (int) (expiresIn * 0.5); } } return expiresIn; } /** * 根据accessToken 获取用户信息 * @param accessToken accessToken * @param needDecrypt 是否需要解密 * @return 用户信息 */ public UserInfoInTokenBO getUserInfoByAccessToken(String accessToken, boolean needDecrypt) { if (StrUtil.isBlank(accessToken)) { throw new YamiShopBindException(403, "accessToken is blank"); } String realAccessToken; if (needDecrypt) { realAccessToken = decryptToken(accessToken); } else { realAccessToken = accessToken; } UserInfoInTokenBO userInfoInTokenBO = (UserInfoInTokenBO) redisTemplate.opsForValue() .get(getAccessKey(realAccessToken)); if (userInfoInTokenBO == null) { throw new YamiShopBindException(403, "accessToken已过期"); } int expireIn= getExpiresIn(userInfoInTokenBO.getSysType()); if (expireIn>-1){ redisTemplate.opsForValue() .set(getAccessKey(realAccessToken), userInfoInTokenBO, getExpiresIn(userInfoInTokenBO.getSysType()), TimeUnit.SECONDS); } return userInfoInTokenBO; } /** * 刷新token,并返回新的token * @param refreshToken * @return */ public TokenInfoBO refreshToken(String refreshToken) { if (StrUtil.isBlank(refreshToken)) { throw new YamiShopBindException(403, "refreshToken is blank"); } String realRefreshToken = decryptToken(refreshToken); String accessToken = stringRedisTemplate.opsForValue().get(getRefreshToAccessKey(realRefreshToken)); if (StrUtil.isBlank(accessToken)) { throw new YamiShopBindException(403, "refreshToken已过期"); } UserInfoInTokenBO userInfoInTokenBO = getUserInfoByAccessToken(accessToken, false); // 删除旧的refresh_token stringRedisTemplate.delete(getRefreshToAccessKey(realRefreshToken)); // 删除旧的access_token stringRedisTemplate.delete(getAccessKey(accessToken)); // 保存一份新的token return storeAccessToken(userInfoInTokenBO); } /** * 删除全部的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 tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidKey, size); // // if (CollUtil.isEmpty(tokenInfoBoList)) { // return; // } List tokenInfoBoList = new ArrayList<>(); while (true) { String tokenInfo = stringRedisTemplate.opsForSet().pop(uidKey); if (StrUtil.isNotBlank(tokenInfo)) { tokenInfoBoList.add(tokenInfo); } else { break; } } 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(403,"token error"); // } } catch (Exception e) { throw new YamiShopBindException(403,"token error"); } // 防止解密后的token是脚本,从而对redis进行攻击,uuid只能是数字和小写字母 if (!PrincipalUtil.isSimpleChar(decryptToken)) { throw new YamiShopBindException(403,"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; } public TokenInfoVO storeAndGetVo(UserInfoInTokenBO userInfoInToken) { TokenInfoBO tokenInfoBO = storeAccessToken(userInfoInToken); TokenInfoVO tokenInfoVO = new TokenInfoVO(); tokenInfoVO.setAccessToken(tokenInfoBO.getAccessToken()); tokenInfoVO.setRefreshToken(tokenInfoBO.getRefreshToken()); tokenInfoVO.setExpiresIn(tokenInfoBO.getExpiresIn()); return tokenInfoVO; } public void deleteCurrentToken(String accessToken) { String decryptToken = decryptToken(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 tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidKey, size); if (CollUtil.isEmpty(tokenInfoBoList)) { return; } String dbAccessToken = null; String dbRefreshToken = null; List 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) connection -> { connection.sAdd(uidKey.getBytes(StandardCharsets.UTF_8), ArrayUtil.toArray(list, byte[].class)); return null; }); } } }