JWT是什么我就不说了,这里只说名SpringBoot中怎么用。
首先在pom中天际依赖
12 org.bitbucket.b_c 3jose4j 40.6.5 5
这里我用的jose4j,他与其他几个库的对比可以参考
之后新建一个工具类,方便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 }
然后为了做统一校验,创建拦截器
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
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 @Autowired16 UserService userService;17 18 @Override19 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 // 校验token31 if (JWTManager.verifyToken(token, request.getParameter("email"))){32 return true;33 }34 else35 throw ResponseException.UNAUTHORIZED;36 }37 }38 return true;39 }40 41 @Override42 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {43 }44 45 @Override46 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 @Override14 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来捕获异常, 这种方式同时可以捕获程序运行时的各种错误,来做统一格式返回。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
1 @RestControllerAdvice 2 public class ResponseAdvice implements ResponseBodyAdvice { 3 private Logger logger = LoggerFactory.getLogger(ResponseAdvice.class); 4 private ThreadLocalthreadLocal = ThreadLocal.withInitial(ObjectMapper::new); 5 6 @Override 7 public boolean supports(MethodParameter methodParameter, Class aClass) { 8 return true; 9 }10 11 @Override12 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 Exception36 */37 @ExceptionHandler(value = ResponseException.class)38 @ResponseBody39 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 Error46 */47 @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)48 @ExceptionHandler(value = Exception.class)49 @ResponseBody50 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等响应头,目前我还没找到解决办法,只能在前端做代理来避免跨域