本文采用JWT+注解+Cache(缓存)完成需求这里有条件的也可以吧缓存换成redis,对于大量用户登录redis更快而且可以记录更多token,缓存限制比较大
一、拦截器和Cache处理
1、AuthInterceptor拦截器(实现对所有请求的拦截和处理)
/**
* @Description: 请求拦截器
* @Author: zhang
* @Date: 2023/10/24 16:51
**/
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
public static final String TOKEN_KEY = "token";
/**
* 需要校验的请求接口
*/
public static final String[] SUPER_HANDLE_PREFIX = new String[] {
"/admin",
"/admin/stores",
"/management"
};
private static AntPathMatcher matcher = new AntPathMatcher();
@Autowired
private JwtUserService jwtUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (!method.isAnnotationPresent(NotLoginToken.class)) {
String token = getToken(request);
String uri = request.getRequestURI();//获取请求的url
//判断是否为需要校验电脑接口
for (int i = 0; i < SUPER_HANDLE_PREFIX.length; i++) {
if (uri.startsWith(SUPER_HANDLE_PREFIX[i])) {
if (StringUtils.isBlank(token)) throw new ApplicationException(ErrorCodes.EMPTY_TOKEN, "token不能为空");
//用户登录token校验
ActionResult<LoginDto> result = jwtUserService.verifyToken(token);
if (ErrorCodes.OK != result.getCode()) {
throw new ApplicationException(result.getCode(), result.getMessage());
}
LoginDto user = result.getData();
//校验用户权限
if (method.isAnnotationPresent(Permission.class)) {
//如果标记访问权限则验证权限
Permission permission = method.getAnnotation(Permission.class);
String permissionTag = permission.value(); //获取到标识
Integer userType = user.getUserType();
//如果不匹配(这里可以做查询数据库的操作,查询对应权限,我这里为演示方便定义了一个枚举进行简单的逻辑判断)
if (!AdminTypeEnum.valueOf(userType).getPermissionTag().equals(permissionTag)){
throw new ApplicationException(ErrorCodes.DENIED_UNAUTHORIZED, "该账号无此权限");
}
}
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
/**
* 获取到token
* @param request HttpServletRequest
* @return token
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader(TOKEN_KEY);
if (token == null) {
token = request.getParameter(TOKEN_KEY);
}
if (token == null) {
Cookie cookie = getTokenFromCookie(request);
if (cookie != null) {
token = cookie.getValue();
}
}
return token;
}
/**
* 拿到Cookie中的数据
* @param request HttpServletRequest
* @return cookie
*/
private Cookie getTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (AuthInterceptor.TOKEN_KEY.equals(cookie.getName())) {
return cookie;
}
}
}
return null;
}
private void writeResponse(HttpServletResponse response, String msg) {
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try {
writer = response.getWriter();
writer.print(ActionResult.error(msg));
} catch (IOException e) {
log.error("response error", e);
} finally {
if (writer != null) {
writer.close();
}
}
}
}
2、TokenCache缓存处理的核心类(这里有条件的可以用redis)
/**
* @Description: 缓存处理的核心
* @Author: zhang
* @Date: 2023/10/24 15:20
**/
@Getter
@Setter
@Component
public class TokenCache {
/**
* 最大缓存数量
*/
private static final int MAXIMUM_SIZE = 1000;
/**
* 缓存失效时间/分钟 TimeUnit.MINUTES
*/
private static final int EXPIRE_DURATION = 30;
private Cache<String, String> tokenCache;
public TokenCache() {
this.tokenCache = CacheBuilder.newBuilder()
.maximumSize(MAXIMUM_SIZE) //最大缓存数量
.expireAfterWrite(EXPIRE_DURATION, TimeUnit.MINUTES) //缓存失效时间/分钟
.build();
}
/**
* 获取缓存中对应的值,不存在则生成新的值并返回,实现了Callable接口
* @param key 键
* @return token
* @throws ExecutionException
*/
public String getTokenOrGenerateToken(String key) throws ExecutionException {
return tokenCache.get(key, new Callable<String>() {
@Override
public String call() throws Exception {
return RandomStringUtils.randomAlphanumeric(32);
}
});
}
/**
* 设置token
* @param key key
* @param token token
*/
public void setToken(String key, String token){
// 设置缓存值
tokenCache.put(key, token);
}
/**
* 获取缓存中的token
* @param key key
* @return token
*/
public String getToken(String key){
// 获取缓存中的token
return tokenCache.getIfPresent(key);
}
/**
* 删除缓存中的token
* @param key key
*/
public void deleteToken(String key){
// 删除缓存值
tokenCache.invalidate(key);
}
}
3、JWT的解析和处理(这里也可以使用其他技术)
1)JwtUserService JWT服务层
/**
* @Description: json-web-token解析类
* @Author: zhang
* @Date: 2023/10/24 16:51
**/
public interface JwtUserService {
/**
* 创建Jwt
* @param user User
* @return String
*/
String generateToken(LoginDto user);
/**
* 验证并解析Jwt
* @param token String
* @return ActionResult<User>
*/
ActionResult<LoginDto> verifyToken(String token);
/**
* 刷新Jwt
* @param token String
* @return String
*/
String refreshToken(String token);
/**
* 刷新Jwt
* @param user User
* @return String
*/
String refreshToken(LoginDto user);
/**
* 清除过期的token
* @param token
*/
void clearToken(String token);
/**
* 生成对应key
* @param user LoginDto对象
* @return key
*/
String getCacheKey(LoginDto user);
/**
* 验证并解析Jwt
* @param token String
* @return ActionResult<User>
*/
LoginDto analysisToken(String token);
}
2)JwtUserServiceImplJWT逻辑处理层
@Slf4j
@Service(value = "jwtUserService")
@RequiredArgsConstructor(onConstructor = @__({@Autowired, @Lazy}))
public class JwtUserServiceImpl implements JwtUserService {
/**
* 过期时间(7天)
*/
private static final int EXPIRE_TIME = 7;
/**
* 签名密钥
*/
private static final String SECRET = "ElkI99999I1NDUxNTUiLCJ1c2VySW";
/**
* 签名算法
*/
private static final Algorithm ALGORITHM = Algorithm.HMAC256(SECRET);
private final TokenCache tokenCache;
@Override
public String generateToken(LoginDto user) {
try{
Date futureDate = DateUtils.addDay(DateTime.now(), getLoginRememberExp());
//生成签名的时间
String token = JWT.build(user).withIssuedAt(new Date()).withExpiresAt(futureDate).sign(ALGORITHM);
String key = getCacheKey(user);
if (StringUtils.isNotBlank(tokenCache.getToken(key))){ //校验是否存在这个token
tokenCache.deleteToken(key);
}
tokenCache.setToken(key, token);
return token;
}catch (JWTCreationException | IllegalAccessException e){
e.printStackTrace();
throw new ApplicationException(ErrorCodes.ILLEGAL_OPERATE,e.getMessage());
}
}
@Override
public ActionResult<LoginDto> verifyToken(String token) {
try{
LoginDto user = JWT.decode(token, ALGORITHM, LoginDto.class);
String key = getCacheKey(user);
String userToken = tokenCache.getToken(key);
if(userToken == null){
return ActionResult.error(ErrorCodes.EXPIRED_TOKEN, "登录超时,请重新登录");
}
//生产进行验证token过期
// if(SpringContextHolder.getCurrentEnv() == Env.prod) {
if (!token.equals(userToken)) {
return ActionResult.error(ErrorCodes.EXPIRED_TOKEN, "登录超时,请重新登录");
}
// }
return ActionResult.ok(user);
} catch (IllegalAccessException | InstantiationException e) {
return ActionResult.error(ErrorCodes.INVALID_ARGUMENTS, "Token格式不正确");
} catch (TokenExpiredException e) {
return ActionResult.error(ErrorCodes.EXPIRED_TOKEN, "登录超时,请重新登录");
}catch (SignatureVerificationException e){
return ActionResult.error(ErrorCodes.INVALID_SIGN, "Token签名无效");
}catch (JWTDecodeException e){
return ActionResult.error(ErrorCodes.ILLEGAL_OPERATE, "非法Token,请重新登录");
}
}
@Override
public String refreshToken(String token) {
ActionResult<LoginDto> result = verifyToken(token);
if(ErrorCodes.OK == result.getCode()){
return generateToken(result.getData());
}
return null;
}
@Override
public String refreshToken(LoginDto user) {
return generateToken(user);
}
private int getLoginRememberExp(){
return EXPIRE_TIME;
}
@Override
public void clearToken(String token){
try{
LoginDto user = JWT.decode(token, ALGORITHM, LoginDto.class);
String key = getCacheKey(user);
tokenCache.deleteToken(key);
} catch (Exception e) {
throw new ApplicationException(ErrorCodes.EXPIRED_TOKEN,"登录超时");
}
}
/**
* 这里自定义key拼接就可以了
* @param user LoginDto对象
* @return
*/
@Override
public String getCacheKey(LoginDto user){
String key = null;
if(user.getUserType().intValue() == AdminTypeEnum.ADMINISTRATOR.getCode().intValue()) {
key = user.getUserType() +"_"+ user.getUserId() +"_"+ user.getStoreNo();
}else if(user.getUserType().intValue() == AdminTypeEnum.ADMIN_SUPPER.getCode().intValue()){
key = user.getUserType() +"_"+ user.getUserId() +"_"+ user.getStoreNo();
}else if(user.getUserType().intValue() == AdminTypeEnum.ADMIN.getCode().intValue()){
key = user.getUserType() +"_"+ user.getUserId() +"_"+ user.getStoreNo();
}
return key;
}
@Override
public LoginDto analysisToken(String token) {
LoginDto user = null;
if (StringUtils.isEmpty(token)){
return user;
}
try {
user = JWT.decode(token, ALGORITHM, LoginDto.class);
} catch (Exception e) {
log.error("JwtUserServiceImpl.analysisToken token fail exception:{}", e.getMessage(), e);
}
return user;
}
}
以上就是一整个对于token校验的逻辑
二、依赖项及各种工具类
1、导入JWT依赖项(这里使用Maven)
<dependencies>
<!-- Jwt依赖 -->
<dependency>
<groupId>com.github.xandb</groupId>
<artifactId>xandb-jwt</artifactId>
<version>2.0.1-RELEASE</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!-- org.apache.commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- SLF4J 日志门面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.5.4</version>
</dependency>
<!-- Logback 日志实现 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.5</version>
</dependency>
<!-- Hutool. -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
<!-- Validator. -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>7.0.2.Final</version>
</dependency>
</dependencies>
2、这里是我使用的各种工具类代码
1)@NotLoginToken注解,表示接口不用校验
/**
* @Description: 标记 @NotLoginToken 的接口将不会做登录token校验也不会做权限校验,直接放行
* @Author: zhang
* @Date: 2023/8/16 16:08
**/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotLoginToken {
// boolean required() default true;
}
2)@Permission注解,表示对接口进行权限校验
/**
* @Description: 权限校验注解,加在控制器的接口上,用于对指定的接口权限做校验
* @Author: zhang
* @Date: 2023/10/25 14:33
**/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
/**
* 权限的具体标识标识
* 用于细致化具体接口的权限
* @return 标识(String)
*/
String value();
}
3)LoginDto JWT解析对象
@Data
@Accessors(chain = true)
@NoArgsConstructor
public class LoginDto implements Serializable {
private static final long serialVersionUID = 7231832734440331591L;
@ApiModelProperty(value = "用户id")
private Integer userId;
@ApiModelProperty(value = "用户名")
private String userName;
@ApiModelProperty(value = "用户类型, 0-Administrator(唯一),1-超管,2-门店管理员")
private Integer userType;
@ApiModelProperty(value = "门店编号")
private String storeNo;
}
4)AdminTypeEnum 用户(类型)枚举
/**
* @Description: 用户类型
* @Author: zhang
* @Date: 2023/10/25 10:00
**/
@Getter
public enum AdminTypeEnum {
ADMINISTRATOR(0, "Administrator", "Administrator"),
ADMIN_SUPPER(1, "admin.supper", "超管"),
ADMIN(2, "store.admin", "门店管理员"),
;
private final Integer code;
/**
* 权限标识,为展示在这里设置,实际情况建议配置在数据库
*/
private final String permissionTag;
private final String name;
AdminTypeEnum(Integer code, String permissionTag, String name){
this.code = code;
this.permissionTag = permissionTag;
this.name = name;
}
public static AdminTypeEnum valueOf(Integer code) {
for (AdminTypeEnum c : AdminTypeEnum.values()) {
if (c.getCode().equals(code)) {
return c;
}
}
return null;
}
}
三、代码示例
1、对于无token用户使用@NotLoginToken注解
@ApiOperation("测试")
@PostMapping("/test")
@ApiOperationSupport(author = "author")
@NotLoginToken
public ActionResult<?> test(@RequestBody @Valid RequestForm form) {
//业务逻辑...
return ActionResult.ok();
}
这个接口使用了@NotLoginToken注解将不需要进行登录验证
2、使用@Permission注解进行权限校验
@ApiOperation("测试")
@PostMapping("/test")
@ApiOperationSupport(author = "author")
@NotLoginToken
@Permission("test.permission")
public ActionResult<?> test(@RequestBody @Valid RequestForm form) {
//业务逻辑...
return ActionResult.ok();
}
这个注解会对传入的值进行校验,如果权限标识不匹配,将返回异常信息
以上就是对于登录鉴权的整个过程了,欢迎交流学习~