JWT是什么我就不說了,這里只說名SpringBoot中怎么用。
首先在pom中天際依賴
1 <dependency> 2 <groupId>org.bitbucket.b_c</groupId> 3 <artifactId>jose4j</artifactId> 4 <version>0.6.5</version> 5 </dependency>
這里我用的jose4j,他與其他幾個庫的對比可以參考各類JWT庫的對比
之后新建一個工具類,方便token生成和校驗
1 import com.example.demo.domain.User; 2 import org.jose4j.jwk.RsaJsonWebKey; 3 import org.jose4j.jwk.RsaJwkGenerator; 4 import org.jose4j.jws.AlgorithmIdentifiers; 5 import org.jose4j.jws.JsonWebSignature; 6 import org.jose4j.jwt.JwtClaims; 7 import org.jose4j.jwt.consumer.InvalidJwtException; 8 import org.jose4j.jwt.consumer.JwtConsumer; 9 import org.jose4j.jwt.consumer.JwtConsumerBuilder; 10 import org.jose4j.lang.JoseException; 11 12 import java.util.Random; 13 14 public class JWTManager { 15 /** 16 * RsaJsonWebKeyBuilder 采用單例模式獲取rsaJsonWebKey, 這樣任何時候都可以得到同樣的公鑰/私鑰對 17 */ 18 private static class RsaJsonWebKeyBuilder { 19 private static volatile RsaJsonWebKey rsaJsonWebKey; 20 private RsaJsonWebKeyBuilder(){} 21 public static RsaJsonWebKey getRasJsonWebKeyInstance() { 22 if(rsaJsonWebKey == null) { 23 synchronized (RsaJsonWebKey.class) { 24 if(rsaJsonWebKey == null){ 25 try { 26 rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); 27 rsaJsonWebKey.setKeyId(String.valueOf(new Random().nextLong())); 28 } catch(Exception e){ 29 return null; 30 } 31 } 32 } 33 } 34 return rsaJsonWebKey; 35 } 36 } 37 38 public static String generateToken(User user, int expiration) throws Exception{ 39 JwtClaims jwtClaims = new JwtClaims(); 40 jwtClaims.setIssuer(user.getEmail()); 41 jwtClaims.setAudience(System.getProperty("os.name")); 42 jwtClaims.setExpirationTimeMinutesInTheFuture(expiration); 43 jwtClaims.setGeneratedJwtId(); 44 jwtClaims.setIssuedAtToNow(); 45 jwtClaims.setNotBeforeMinutesInThePast(2); 46 jwtClaims.setSubject("Bearer"); 47 48 JsonWebSignature jsonWebSignature = new JsonWebSignature(); 49 jsonWebSignature.setPayload(jwtClaims.toJson()); 50 jsonWebSignature.setKey(RsaJsonWebKeyBuilder.getRasJsonWebKeyInstance().getPrivateKey()); 51 jsonWebSignature.setKeyIdHeaderValue(RsaJsonWebKeyBuilder.getRasJsonWebKeyInstance().getKeyId()); 52 jsonWebSignature.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_PSS_USING_SHA256); 53 54 String jwt = jsonWebSignature.getCompactSerialization(); 55 56 return "Bearer " + jwt; 57 } 58 public static boolean verifyToken(String token, String email) { // 由於生成token時使用了用戶的email作為issuer,故這里需要傳入email來做校驗,這樣做可以防止對不同用戶的修改操作 59 String tokenContent = token.substring(7); 60 JwtConsumer consumer = new JwtConsumerBuilder() 61 .setRequireExpirationTime() 62 .setMaxFutureValidityInMinutes(5256000) 63 .setAllowedClockSkewInSeconds(30) 64 .setRequireSubject() 65 .setExpectedIssuer(email) 66 .setExpectedAudience(System.getProperty("os.name")) 67 .setVerificationKey(RsaJsonWebKeyBuilder.getRasJsonWebKeyInstance().getPublicKey()) 68 .build(); 69 try { 70 JwtClaims claims = consumer.processToClaims(tokenContent); 71 return true; 72 } catch (InvalidJwtException e) { 73 return false; 74 } 75 } 76 }
然后為了做統一校驗,創建攔截器

1 import com.example.demo.exceptions.ResponseException; 2 import com.example.demo.service.UserService; 3 import com.example.demo.utils.JWTManager; 4 import com.example.demo.utils.annotaion.LoginRequired; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.web.method.HandlerMethod; 7 import org.springframework.web.servlet.HandlerInterceptor; 8 import org.springframework.web.servlet.ModelAndView; 9 10 import javax.servlet.http.HttpServletRequest; 11 import javax.servlet.http.HttpServletResponse; 12 import java.lang.reflect.Method; 13 14 public class AuthenticationInterceptor implements HandlerInterceptor { 15 @Autowired 16 UserService userService; 17 18 @Override 19 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 20 String token = request.getHeader("Authorization"); 21 if (!(handler instanceof HandlerMethod)) 22 return true; 23 Method method = ((HandlerMethod) handler).getMethod(); 24 if(method.isAnnotationPresent(LoginRequired.class)) { 25 LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); 26 if(loginRequired.required()) { 27 if(token == null) { 28 throw ResponseException.UNAUTHORIZED; 29 } 30 // 校驗token 31 if (JWTManager.verifyToken(token, request.getParameter("email"))){ 32 return true; 33 } 34 else 35 throw ResponseException.UNAUTHORIZED; 36 } 37 } 38 return true; 39 } 40 41 @Override 42 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 43 } 44 45 @Override 46 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 47 48 } 49 }
注意24行, 他的目的使檢驗方法是否被LoginRequired裝飾。對於沒有被裝飾和LoginRequired的value是false的情況全部放行, 否則則校驗token, 對於沒有token, 或者校驗不同過的情況,拋出ResponseException異常。
再來看LoginRequired裝飾器,他的定義很簡單
1 import java.lang.annotation.ElementType; 2 import java.lang.annotation.Retention; 3 import java.lang.annotation.RetentionPolicy; 4 import java.lang.annotation.Target; 5 6 @Target({ElementType.METHOD, ElementType.TYPE}) 7 @Retention(RetentionPolicy.RUNTIME) 8 public @interface LoginRequired { 9 boolean required() default true; 10 }
使用時,支取要在需要登錄驗證的方法上添加@LoginRequired修飾即可
ResponseException繼承自RuntimeException, 只有RuntimeException的子類才能被spingboot處理
1 public class ResponseException extends RuntimeException { 2 public static ResponseException UNAUTHORIZED = new ResponseException(401, "請先登錄"); 3 4 private int code; 5 private String message; 6 7 public ResponseException(int code, String message) { 8 super(message); 9 this.code = code; 10 this.message = message; 11 } 12 13 @Override 14 public String getMessage() { 15 return message; 16 } 17 18 public void setMessage(String message) { 19 this.message = message; 20 } 21 22 public int getCode() { 23 return code; 24 } 25 26 public void setCode(int code) { 27 this.code = code; 28 } 29 }
另外我們需要添加一個異常捕獲,來捕獲校驗失敗拋出的異常。這里才用@ConrollerAdvice + @ExceptionHandler來捕獲異常, 這種方式同時可以捕獲程序運行時的各種錯誤,來做統一格式返回。

1 @RestControllerAdvice 2 public class ResponseAdvice implements ResponseBodyAdvice { 3 private Logger logger = LoggerFactory.getLogger(ResponseAdvice.class); 4 private ThreadLocal<ObjectMapper> threadLocal = ThreadLocal.withInitial(ObjectMapper::new); 5 6 @Override 7 public boolean supports(MethodParameter methodParameter, Class aClass) { 8 return true; 9 } 10 11 @Override 12 public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { 13 ApiResult body; 14 ObjectMapper mapper = threadLocal.get(); 15 16 if (o instanceof ResultMessage) { 17 body = new ApiResult(((ResultMessage) o).getCode(), ((ResultMessage) o).getMessage(), null); 18 } else if (o instanceof ApiResult) { 19 body = (ApiResult) o; 20 } else if (o instanceof String) { 21 body = new ApiResult(ResultMessage.SUCEESS, o); 22 try { 23 return mapper.writeValueAsString(body); 24 } catch (JsonProcessingException e) { 25 body = new ApiResult(ResultMessage.JSON_PARSE_ERROR, null); 26 } 27 } else { 28 body = new ApiResult(ResultMessage.SUCEESS, o); 29 } 30 31 return body; 32 } 33 34 /** 35 * 401 - Unauthorized Exception 36 */ 37 @ExceptionHandler(value = ResponseException.class) 38 @ResponseBody 39 public ApiResult unAuthorizedExceptionHandler(ResponseException e) { 40 logger.trace(e.getMessage()); 41 return new ApiResult(e.getCode(), e.getMessage(), null); 42 } 43 44 /** 45 * 500 - Internal Server Error 46 */ 47 @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) 48 @ExceptionHandler(value = Exception.class) 49 @ResponseBody 50 public ApiResult internalServerErrorHandler(Exception e) { 51 logger.trace(e.getStackTrace()[0].toString()); 52 return new ApiResult(500, e.getStackTrace()[0].toString(), null); 53 } 54 55 56 }
其中beforeBodyWrite就是用來修改響應內容,可以做到統一格式響應,需要注意的是,如果他的參數Object o是字符串,需要ObjectMapper做轉換,否則在后續的序列化會失敗返回500或者404錯誤。
至此,springboot使用jwt校驗的方法說完了。另外需要說明的是,攔截器里拋出異常的話,雖然我們能捕獲並修改他的響應,但是他會導致跨域處理失效,響應頭中沒有Control-Allowed-Oringin等響應頭,目前我還沒找到解決辦法,只能在前端做代理來避免跨域