SpringBoot中使用JWT


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 }
AuthenticationInterceptor

 注意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 }
View Code

其中beforeBodyWrite就是用來修改響應內容,可以做到統一格式響應,需要注意的是,如果他的參數Object o是字符串,需要ObjectMapper做轉換,否則在后續的序列化會失敗返回500或者404錯誤。

至此,springboot使用jwt校驗的方法說完了。另外需要說明的是,攔截器里拋出異常的話,雖然我們能捕獲並修改他的響應,但是他會導致跨域處理失效,響應頭中沒有Control-Allowed-Oringin等響應頭,目前我還沒找到解決辦法,只能在前端做代理來避免跨域


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM