优雅,意味着优美雅致,用猿话讲就是这代码看得舒服,用得也舒服。登录认证方式有很多,有的是用cookie,有的是用session,有的是用token认证。而本文主要讲述基于jwt以自定义注解方式优雅地处理token认证,此处的优雅只是作者个人口味,萝卜青菜各有所爱,还拦着你的重口味不成?
首先,我们得先了解一下什么是自定义注解,当然,这里只是简单的说明一下,本文的重点不是它。
声明一个注解要用到的元素
修饰符
访问修饰符必须为public,不写默认为pubic;
关键字
关键字为@interface;
注解名称
注解名称为自定义注解的名称;
注解类型元素
注解类型元素是注解中内容,可以理解成自定义接口的实现部分;
public @interface LoginUser {
//String name() default "hello";
}
JDK中有一些元注解,主要有@Target,@Retention,@Document,@Inherited用来修饰注解。
表该注解使用于哪里,如方法,字段,类。它有如下部分类型:
类型 | 描述 |
---|---|
ElementType.TYPE | 应用于类、接口(包括注解类型)、枚举 |
ElementType.FIELD | 应用于属性(包括枚举中的常量) |
ElementType.METHOD | 应用于方法 |
ElementType.PARAMETER | 应用于方法的形参 |
ElementType.CONSTRUCTOR | 应用于构造函数 |
ElementType.LOCAL_VARIABLE | 应用于局部变量 |
ElementType.ANNOTATION_TYPE | 应用于注解类型 |
ElementType.PACKAGE | 应用于包 |
表明该注解的生命周期
类型 | 描述 |
---|---|
RetentionPolicy.SOURCE | 编译时被丢弃,不包含在类文件中 |
RetentionPolicy.CLASS | JVM加载时被丢弃,包含在类文件中,默认值 |
RetentionPolicy.RUNTIME | 由JVM 加载,包含在类文件中,在运行时可以被获取到 |
表明该注解标记的元素可以被Javadoc 或类似的工具文档化
表明使用了@Inherited注解的注解,所标记的类的子类也会拥有这个注解
知识储备已到位,接下来开始实现自定义注解的方式解决登录认证
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>
package com.ao.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//定义注解使用于参数上
@Target(ElementType.PARAMETER)
//定义注解在运行时生效
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
在这里说明一下HandlerMethodArgumentResolver是用来处理方法参数的解析器,包含以下2个方法:
package com.ao.demo.annotation.support;
import com.ao.demo.annotation.LoginUser;
import com.ao.demo.utils.UserTokenManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Slf4j
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
public static final String LOGIN_TOKEN_KEY = "X-My-Token";
/**
* 判断是否支持要转换的参数类型
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("进来supportsParameter啦,我要判断是否支持要转换的参数类型");
//这里是判断参数的类型是否是Integer类型及是否拥有LoginUse这个注解,如果都满足的话进入resolveArgument方法
return parameter.getParameterType().isAssignableFrom(Integer.class) && parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
/*
* 每一次请求都会检测是否存在HTTP头部域`X-My-Token`。
如果存在,则内部查询转换成LoginUser,然后作为请求参数。
如果不存在,则作为null请求参数。
*/
String token = request.getHeader(LOGIN_TOKEN_KEY);
log.info("进来resolveArgument啦,拿到的token是" + token);
Integer userId = JwtHelper.verifyTokenAndGetUserId(token);
log.info("登录的用户id是:"+ userId);
if (userId == null){
return null;
}
return userId;
}
}
package com.ao.demo.config;
import com.ao.demo.annotation.support.LoginUserHandlerMethodArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WxWebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new LoginUserHandlerMethodArgumentResolver());
}
}
package com.ao.demo.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang3.time.DateUtils;
import java.util.*;
public class JwtHelper {
// 秘钥
static final String SECRET = "X-My-Token";
// 签名是有谁生成
static final String ISSUSER = "me";
// 签名的主题
static final String SUBJECT = "this is my token";
// 签名的观众
static final String AUDIENCE = "MY-USER";
public String createToken(Integer userId){
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Map<String, Object> map = new HashMap<String, Object>();
map.put("alg", "HS256");
map.put("typ", "JWT");
String token = JWT.create()
// 设置头部信息 Header
.withHeader(map)
// 设置 载荷 Payload
.withClaim("userId", userId)
.withIssuer(ISSUSER)
.withSubject(SUBJECT)
.withAudience(AUDIENCE)
// 生成签名的时间
.withIssuedAt(new Date())
// 签名过期的时间
.withExpiresAt(DateUtils.addHours(new Date(), 1))
// 签名 Signature
.sign(algorithm);
return token;
} catch (JWTCreationException exception){
exception.printStackTrace();
}
return null;
}
public Integer verifyTokenAndGetUserId(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUSER)
.build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> claims = jwt.getClaims();
Claim claim = claims.get("userId");
return claim.asInt();
} catch (JWTVerificationException exception){
return null;
}
}
}
在需要认证登录的接口添加@LoginUser注解即可
package com.ao.demo.web;
import com.ao.demo.annotation.LoginUser;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
@RestController
public class TestController {
@GetMapping("/test")
public String tt(@LoginUser Integer userId){
if (userId == null){
return "请先登录";
}
return "登录成功";
}
}
首先用main方法生成了用户id为1的token,值为:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aGlzIGlzIG15IHRva2VuIiwiYXVkIjoiTVktVVNFUiIsImlzcyI6Im1lIiwiZXhwIjoxNTk0MTc1Nzg4LCJ1c2VySWQiOjEsImlhdCI6MTU5NDE3MjE4OH0.eBsFzFPHjtjoL3yF2LvHFkFfNH2–XkJhbXBOz5hKBo
至此,这算是比较优雅的写法啦,直接在需要认证的接口添加自定义的注解然后进行判断即可。看到这里,可能会有这样的疑问,每个认证的接口都去判断一下userId是否为null会不会有点繁琐呢?那有什么解决办法呢?其实我们可以用全局异常去处理,这样就不用每个认证接口都去判断一下。本来是想单独写一篇优雅的处理返回结果的,但是觉得内容少,然后就与这篇合并啦_,接下来继续往下看。
主要用来记录用户相关异常的信息
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum UserExceptionEnum {
UNLOGIN(500,"请先登录吧!!")
//.....定义异常信息
;
private int code;
private String msg;
}
@Getter
public class UserException extends RuntimeException {
private UserExceptionEnum userExceptionEnum;
public UserException(UserExceptionEnum userExceptionEnum) {
this.userExceptionEnum = userExceptionEnum;
}
}
@Data
public class ExceptionResult {
private int status;
private String message;
private long timestamp;
public ExceptionResult(ExceptionEnum em) {
this.status = em.getCode();
this.message = em.getMsg();
this.timestamp = System.currentTimeMillis();
}
}
它比较常用的场景有如下,这里不一一道说,可以自己去了解一下。
ResponseEntity标识整个http相应:状态码、头部信息以及相应体内容。
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(UserException.class)
public ResponseEntity<ExceptionResult> handleException(UserException e){
return ResponseEntity.status(e.getUserExceptionEnum().getCode()).body(new ExceptionResult(e.getUserExceptionEnum()));
}
/*这里可以定义多个来处理不同的业务,如用户相关异常,商品订单异常*/
}
这样的返回结果是不是优雅一点,每种业务定义一个异常类和异常枚举类,然后再交给全局异常处理,让代码更直观,业务更清晰点。
如果不想给每个需要登录认证的接口写一个判断,那么可以交给全局异常处理,只需要在LoginUserHandlerMethodArgumentResolver改造一下便可,如下:
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
/*
* 每一次请求都会检测是否存在HTTP头部域`X-My-Token`。
如果存在,则内部查询转换成LoginUser,然后作为请求参数。
如果不存在,则作为null请求参数。
*/
String token = request.getHeader(LOGIN_TOKEN_KEY);
log.info("进来resolveArgument啦,拿到的token是" + token);
Integer userId = JwtHelper.verifyTokenAndGetUserId(token);
log.info("登录的用户id是:"+ userId);
if (userId == null){
throw new UserException(UserExceptionEnum.UNLOGIN);
}
return userId;
}
如果userId为null的话,那么就抛出自定义的异常,是不是又优雅了一点~
这样需要登录认证的接口就不用每个去判断userId是否为空啦,okok滴!