你是如何优雅的处理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.CLASSJVM加载时被丢弃,包含在类文件中,默认值
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滴!

感谢您赏脸收看,喜欢可以关注一下哦!一个分享java学习资源,技术文章,实战经验的公众号~

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值