優雅,意味着優美雅緻,用猿話講就是這代碼看得舒服,用得也舒服。登陸認證方式有不少,有的是用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 排版