前言:所有代码来源于mall4j开源版本(gtiee https://gitee.com/gz-yami/mall4j?utm_source=alading&utm_campaign=repo),仅供学习使用,详细代码请看源代码。
一、token存储处理类TokenStore;
1.定义了生成accessToken和refreshToken的方法;
2.根据accessToken 获取用户信息的方法
3.刷新token,并返回新的token的方法
4.删除用户全部token的方法
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.shop.common.constants.OauthCacheNames;
import com.yami.shop.common.enums.YamiHttpStatus;
import com.yami.shop.common.exception.YamiShopBindException;
import com.yami.shop.common.serializer.redis.KryoRedisSerializer;
import com.yami.shop.common.util.PrincipalUtil;
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.RedisSerializer;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/** * 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<String, Object> redisTemplate; private final RedisSerializer<Object> redisSerializer; private final StringRedisTemplate stringRedisTemplate; public TokenStore(RedisTemplate<String, Object> 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<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); List<String> tokenInfoBoList = new ArrayList<>(); for (int i = 0; i < size; i++) { String s = stringRedisTemplate.opsForSet().pop(uidToAccessKeyStr); tokenInfoBoList.add(s); } if (tokenInfoBoList.size() > 0) { 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())); return tokenInfoBO; } private int getExpiresIn(int sysType) { // 3600秒 int expiresIn = 3600; // 普通用户token过期时间 1小时 if (Objects.equals(sysType, SysTypeEnum.ORDINARY.value())) { expiresIn = expiresIn * 24 * 30; } // 系统管理员的token过期时间 2小时 if (Objects.equals(sysType, SysTypeEnum.ADMIN.value())) { expiresIn = expiresIn * 24 * 30; } return expiresIn; } /** * 根据accessToken 获取用户信息 * @param accessToken accessToken * @param needDecrypt 是否需要解密 * @return 用户信息 */ public UserInfoInTokenBO getUserInfoByAccessToken(String accessToken, boolean needDecrypt) { if (StrUtil.isBlank(accessToken)) { throw new YamiShopBindException(YamiHttpStatus.UNAUTHORIZED,"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(YamiHttpStatus.UNAUTHORIZED,"accessToken 已过期"); } return userInfoInTokenBO; } /** * 刷新token,并返回新的token * @param refreshToken * @return */ public TokenInfoBO refreshToken(String refreshToken) { if (StrUtil.isBlank(refreshToken)) { throw new YamiShopBindException(YamiHttpStatus.UNAUTHORIZED,"refreshToken is blank"); } String realRefreshToken = decryptToken(refreshToken); String accessToken = stringRedisTemplate.opsForValue().get(getRefreshToAccessKey(realRefreshToken)); if (StrUtil.isBlank(accessToken)) { throw new YamiShopBindException(YamiHttpStatus.UNAUTHORIZED,"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<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(YamiHttpStatus.UNAUTHORIZED,"token error"); } } catch (Exception e) { throw new YamiShopBindException(YamiHttpStatus.UNAUTHORIZED,"token error"); } // 防止解密后的token是脚本,从而对redis进行攻击,uuid只能是数字和小写字母 if (!PrincipalUtil.isSimpleChar(decryptToken)) { throw new YamiShopBindException(YamiHttpStatus.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; } 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<String> tokenInfoBoList = stringRedisTemplate.opsForSet().pop(uidKey, size); List<String> tokenInfoBoList = new ArrayList<>(); for (int i = 0; i < size; i++) { String pop = stringRedisTemplate.opsForSet().pop(uidKey); tokenInfoBoList.add(pop); } 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; }); } } }
2.保存在token信息里面的用户信息
/** * 保存在token信息里面的用户信息 * * @author 菠萝凤梨 * @date 2022/3/25 17:33 */ @Data public class UserInfoInTokenBO { /** * 用户在自己系统的用户id */ private String userId; /** * 租户id (商家id) */ private Long shopId; /** * 昵称 */ private String nickName; /** * 系统类型 * @see com.yami.shop.security.common.enums.SysTypeEnum */ private Integer sysType; /** * 是否是管理员 */ private Integer isAdmin; private String bizUserId; /** * 权限列表 */ private Set<String> perms; /** * 状态 1 正常 0 无效 */ private Boolean enabled; /** * 其他Id */ private Long otherId; }
3.在系统上下文保存用户信息类AuthUserContext
import com.alibaba.ttl.TransmittableThreadLocal; import com.yami.shop.security.common.bo.UserInfoInTokenBO; /** * @author FrozenWatermelon * @date 2020/7/16 */ public class AuthUserContext { private static final ThreadLocal<UserInfoInTokenBO> USER_INFO_IN_TOKEN_HOLDER = new TransmittableThreadLocal<>(); public static UserInfoInTokenBO get() { return USER_INFO_IN_TOKEN_HOLDER.get(); } public static void set(UserInfoInTokenBO userInfoInTokenBo) { USER_INFO_IN_TOKEN_HOLDER.set(userInfoInTokenBo); } public static void clean() { if (USER_INFO_IN_TOKEN_HOLDER.get() != null) { USER_INFO_IN_TOKEN_HOLDER.remove(); } } }
4.授权过滤器AuthFilter
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.yami.shop.common.exception.YamiShopBindException; import com.yami.shop.common.handler.HttpHandler; import com.yami.shop.security.common.adapter.AuthConfigAdapter; import com.yami.shop.security.common.bo.UserInfoInTokenBO; import com.yami.shop.security.common.manager.TokenStore; 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.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; /** * 授权过滤,只要实现AuthConfigAdapter接口,添加对应路径即可: * * @author 菠萝凤梨 * @date 2022/3/25 17:33 */ @Component public class AuthFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class); @Autowired private AuthConfigAdapter authConfigAdapter; @Autowired private HttpHandler httpHandler; @Autowired private TokenStore tokenStore; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String requestUri = req.getRequestURI(); List<String> excludePathPatterns = authConfigAdapter.excludePathPatterns(); AntPathMatcher pathMatcher = new AntPathMatcher(); // 如果匹配不需要授权的路径,就不需要校验是否需要授权 if (CollectionUtil.isNotEmpty(excludePathPatterns)) { for (String excludePathPattern : excludePathPatterns) { if (pathMatcher.match(excludePathPattern, requestUri)) { chain.doFilter(req, resp); return; } } } String accessToken = req.getHeader("Authorization"); // 也许需要登录,不登陆也能用的uri boolean mayAuth = pathMatcher.match(AuthConfigAdapter.MAYBE_AUTH_URI, requestUri); UserInfoInTokenBO userInfoInToken = null; try { // 如果有token,就要获取token if (StrUtil.isNotBlank(accessToken)) { userInfoInToken = tokenStore.getUserInfoByAccessToken(accessToken, true); } else if (!mayAuth) { // 返回前端401 httpHandler.printServerResponseToWeb(HttpStatus.UNAUTHORIZED.getReasonPhrase(), HttpStatus.UNAUTHORIZED.value()); return; } // 保存上下文 AuthUserContext.set(userInfoInToken); chain.doFilter(req, resp); }catch (Exception e) { // 手动捕获下非controller异常 if (e instanceof YamiShopBindException) { httpHandler.printServerResponseToWeb(e.getMessage(), ((YamiShopBindException) e).getHttpStatusCode()); } else { throw e; } } finally { AuthUserContext.clean(); } } }
5.注册授权过滤器的配置
import cn.hutool.core.util.ArrayUtil; import com.yami.shop.security.common.adapter.AuthConfigAdapter; import com.yami.shop.security.common.adapter.DefaultAuthConfigAdapter; import com.yami.shop.security.common.filter.AuthFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import javax.servlet.DispatcherType; /** * 授权配置 * * @author 菠萝凤梨 * @date 2022/3/25 17:33 */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled=true) public class AuthConfig { @Autowired private AuthFilter authFilter; @Bean @ConditionalOnMissingBean public AuthConfigAdapter authConfigAdapter() { return new DefaultAuthConfigAdapter(); } @Bean @Lazy public FilterRegistrationBean<AuthFilter> filterRegistration(AuthConfigAdapter authConfigAdapter) { FilterRegistrationBean<AuthFilter> registration = new FilterRegistrationBean<>(); // 添加过滤器 registration.setFilter(authFilter); // 设置过滤路径,/*所有路径 registration.addUrlPatterns(ArrayUtil.toArray(authConfigAdapter.pathPatterns(), String.class)); registration.setName("authFilter"); // 设置优先级 registration.setOrder(0); registration.setDispatcherTypes(DispatcherType.REQUEST); return registration; } }
6.定义需要拦截和放行路径的配置接口(需自定义接口继承复写方法,实现自定义)
/** * 实现该接口之后,修改需要授权登陆的路径,不需要授权登陆的路径 * @author 菠萝凤梨 * @date 2022/3/25 17:31 */ public interface AuthConfigAdapter { /** * 也许需要登录才可用的url */ String MAYBE_AUTH_URI = "/**/ma/**"; /** * 需要授权登陆的路径 * @return 需要授权登陆的路径列表 */ List<String> pathPatterns(); /** * 不需要授权登陆的路径 * @return 不需要授权登陆的路径列表 */ List<String> excludePathPatterns(); }
7.用于在系统中快捷获取用户信息的工具类SecurityUtils
import com.yami.shop.common.util.HttpContextUtils; import com.yami.shop.security.api.model.YamiUser; import com.yami.shop.security.common.bo.UserInfoInTokenBO; import com.yami.shop.security.common.util.AuthUserContext; import lombok.experimental.UtilityClass; /** * @author LGH */ @UtilityClass public class SecurityUtils { private static final String USER_REQUEST = "/p/"; /** * 获取用户 */ public YamiUser getUser() { if (!HttpContextUtils.getHttpServletRequest().getRequestURI().startsWith(USER_REQUEST)) { // 用户相关的请求,应该以/p开头!!! throw new RuntimeException("yami.user.request.error"); } UserInfoInTokenBO userInfoInTokenBO = AuthUserContext.get(); YamiUser yamiUser = new YamiUser(); yamiUser.setUserId(userInfoInTokenBO.getUserId()); yamiUser.setBizUserId(userInfoInTokenBO.getBizUserId()); yamiUser.setEnabled(userInfoInTokenBO.getEnabled()); yamiUser.setShopId(userInfoInTokenBO.getShopId()); yamiUser.setStationId(userInfoInTokenBO.getOtherId()); return yamiUser; } }
8.登录接口代码
@PostMapping("/login") public ResponseEntity<TokenInfoVO> login( @Valid @RequestBody AuthenticationDTO authenticationDTO) { String mobileOrUserName = authenticationDTO.getUserName(); User user = getUser(mobileOrUserName); Integer type = authenticationDTO.getType(); String code = authenticationDTO.getCode(); String decryptPassword = passwordManager.decryptPassword(authenticationDTO.getPassWord()); // 半小时内密码输入错误十次,已限制登录30分钟 passwordCheckManager.checkPassword(SysTypeEnum.ORDINARY, authenticationDTO.getUserName(), decryptPassword, user.getLoginPassword()); UserInfoInTokenBO userInfoInToken = new UserInfoInTokenBO(); userInfoInToken.setUserId(user.getUserId()); userInfoInToken.setSysType(SysTypeEnum.ORDINARY.value()); userInfoInToken.setEnabled(user.getStatus() == 1); // 存储token返回vo TokenInfoVO tokenInfoVO = tokenStore.storeAndGetVo(userInfoInToken); return ResponseEntity.ok(tokenInfoVO); } private User getUser(String mobileOrUserName) { User user = null; // 手机验证码登陆,或传过来的账号很像手机号 if (PrincipalUtil.isMobile(mobileOrUserName)) { user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserMobile, mobileOrUserName)); } // 如果不是手机验证码登陆, 找不到手机号就找用户名 if (user == null) { // user = userMapper.selectOneByUserName(mobileOrUserName); user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getAccount, mobileOrUserName)); } if (user == null) { throw new YamiShopBindException("账号或密码不正确"); } return user; }
9.刷新token的代码
public class TokenController { @Autowired private TokenStore tokenStore; @Autowired private MapperFacade mapperFacade; @PostMapping("/token/refresh") public ResponseEntity<TokenInfoVO> refreshToken(@Valid @RequestBody RefreshTokenDTO refreshTokenDTO) { TokenInfoBO tokenInfoServerResponseEntity = tokenStore .refreshToken(refreshTokenDTO.getRefreshToken()); return ResponseEntity .ok(mapperFacade.map(tokenInfoServerResponseEntity, TokenInfoVO.class)); } }
10.退出登录的代码
public class LogoutController { @Autowired private TokenStore tokenStore; @PostMapping("/logOut") public ResponseEntity<Void> logOut(HttpServletRequest request) { String accessToken = request.getHeader("Authorization"); if (StrUtil.isBlank(accessToken)) { return ResponseEntity.ok().build(); } // 删除该用户在该系统当前的token tokenStore.deleteCurrentToken(accessToken); return ResponseEntity.ok().build(); } }
标签:return,springboot,accessToken,token,import,授权,Oauth2.0,public,String From: https://www.cnblogs.com/runwithraining/p/16858838.html