优雅,意味着优美雅致,用猿话讲就是这代码看得舒服,用得也舒服。登陆认证方式有不少,有的是用cookie,有的是用session,有的是用token认证。而本文主要讲述基于jwt以自定义注解方式优雅地处理token认证,此处的优雅只是做者我的口味,萝卜青菜各有所爱,还拦着你的重口味不成?java
首先,咱们得先了解一下什么是自定义注解,固然,这里只是简单的说明一下,本文的重点不是它。web
声明一个注解要用到的元素spring
修饰符 访问修饰符必须为public,不写默认为pubic;apache
关键字 关键字为@interface;cookie
注解名称 注解名称为自定义注解的名称;session
注解类型元素 注解类型元素是注解中内容,能够理解成自定义接口的实现部分;app
public @interface LoginUser {
//String name() default "hello"; } 复制代码
JDK中有一些元注解,主要有@Target,@Retention,@Document,@Inherited用来修饰注解。编辑器
表该注解使用于哪里,如方法,字段,类。它有以下部分类型:ide
类型 | 描述 |
---|---|
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滴!
本文使用 mdnice 排版