你是如何优雅的处理token认证登陆?

前言

优雅,意味着优美雅致,用猿话讲就是这代码看得舒服,用得也舒服。登陆认证方式有不少,有的是用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用来修饰注解。编辑器

@Target

表该注解使用于哪里,如方法,字段,类。它有以下部分类型:ide

类型 描述
ElementType.TYPE 应用于类、接口(包括注解类型)、枚举
ElementType.FIELD 应用于属性(包括枚举中的常量)
ElementType.METHOD 应用于方法
ElementType.PARAMETER 应用于方法的形参
ElementType.CONSTRUCTOR 应用于构造函数
ElementType.LOCAL_VARIABLE 应用于局部变量
ElementType.ANNOTATION_TYPE 应用于注解类型
ElementType.PACKAGE 应用于包

@Retention

代表该注解的生命周期函数

类型 描述
RetentionPolicy.SOURCE 编译时被丢弃,不包含在类文件中
RetentionPolicy.CLASS JVM加载时被丢弃,包含在类文件中,默认值
RetentionPolicy.RUNTIME 由JVM 加载,包含在类文件中,在运行时能够被获取到

@Document

代表该注解标记的元素能够被Javadoc 或相似的工具文档化

@Inherited

代表使用了@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个方法:

  • supportsParameter(知足某种要求,返回true,方可进入resolveArgument作参数处理)
  • resolveArgument
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());  }  }  复制代码

JwtHelper

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();  } } 复制代码

全局异常处理

@ControllerAdvice

它比较经常使用的场景有以下,这里不一一道说,能够本身去了解一下。

  • 全局异常处理
  • 全局数据绑定
  • 全局数据预处理
ResponseEntity

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 排版

相关文章
相关标签/搜索