你是如何優雅的處理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 排版

相關文章
相關標籤/搜索