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等响应头,目前我还没找到解决办法,只能在前端做代理来避免跨域

posted @ 2019-03-17 15:40  不想取名字所以就随便写了  阅读(2922)  评论(2编辑  收藏  举报