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

前言

优雅,意味着优美雅致,用猿话讲就是这代码看得舒服,用得也舒服。登录认证方式有很多,有的是用cookie,有的是用session,有的是用token认证。而本文主要讲述基于jwt以自定义注解方式优雅地处理token认证,此处的优雅只是作者个人口味,萝卜青菜各有所爱,还拦着你的重口味不成?

首先,我们得先了解一下什么是自定义注解,当然,这里只是简单的说明一下,本文的重点不是它。

注解的基本元素

声明一个注解要用到的元素

  • 修饰符
    访问修饰符必须为public,不写默认为pubic;

  • 关键字
    关键字为@interface;

  • 注解名称
    注解名称为自定义注解的名称;

  • 注解类型元素
    注解类型元素是注解中内容,可以理解成自定义接口的实现部分;

public @interface LoginUser {
   //String name() default "hello";
}
复制代码

元注解修饰注解

JDK中有一些元注解,主要有@Target,@Retention,@Document,@Inherited用来修饰注解。

@Target

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

类型 描述
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
  • 登录不成功案例

你是如何优雅的处理token认证登录?
你是如何优雅的处理token认证登录?
  • 登录成功案例

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

至此,这算是比较优雅的写法啦,直接在需要认证的接口添加自定义的注解然后进行判断即可。看到这里,可能会有这样的疑问,每个认证的接口都去判断一下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的话,那么就抛出自定义的异常,是不是又优雅了一点~

测试一波

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

这样需要登录认证的接口就不用每个去判断userId是否为空啦,okok滴!

原创不易,可以关注一下哦。

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

本文使用 mdnice
排版

原文 

https://juejin.im/post/5f056c046fb9a07ebf2b3044

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 你是如何优雅的处理token认证登录?

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址