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 }
然后为了做统一校验,创建拦截器
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来捕获异常, 这种方式同时可以捕获程序运行时的各种错误,来做统一格式返回。
View Code
其中beforeBodyWrite就是用来修改响应内容,可以做到统一格式响应,需要注意的是,如果他的参数Object o是字符串,需要ObjectMapper做转换,否则在后续的序列化会失败返回500或者404错误。
至此,springboot使用jwt校验的方法说完了。另外需要说明的是,拦截器里抛出异常的话,虽然我们能捕获并修改他的响应,但是他会导致跨域处理失效,响应头中没有Control-Allowed-Oringin等响应头,目前我还没找到解决办法,只能在前端做代理来避免跨域