springboot中使用jwt

JWT是什么我就不说了,这里只说名SpringBoot中怎么用。
首先在pom中添加依赖

pom.xml
1
2
3
4
5
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.6.5</version>
</dependency>

这里我用的jose4j,他与其他几个库的对比可以参考各类JWT库的对比
之后新建一个工具类,方便token生成和校验
JWTManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import com.example.demo.domain.User;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.lang.JoseException;

import java.util.Random;

public class {

* RsaJsonWebKeyBuilder 采用单例模式获取rsaJsonWebKey, 这样任何时候都可以得到同样的公钥/私钥对
*/
private static class RsaJsonWebKeyBuilder {
private static volatile RsaJsonWebKey rsaJsonWebKey;
private RsaJsonWebKeyBuilder(){}
public static RsaJsonWebKey getRasJsonWebKeyInstance() {
if(rsaJsonWebKey == null) {
synchronized (RsaJsonWebKey.class) {
if(rsaJsonWebKey == null){
try {
rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId(String.valueOf(new Random().nextLong()));
} catch(Exception e){
return null;
}
}
}
}
return rsaJsonWebKey;
}
}

public static String generateToken(User user, int expiration) throws Exception{
JwtClaims jwtClaims = new JwtClaims();
jwtClaims.setIssuer(user.getEmail());
jwtClaims.setAudience(System.getProperty("os.name"));
jwtClaims.setExpirationTimeMinutesInTheFuture(expiration);
jwtClaims.setGeneratedJwtId();
jwtClaims.setIssuedAtToNow();
jwtClaims.setNotBeforeMinutesInThePast(2);
jwtClaims.setSubject("Bearer");

JsonWebSignature jsonWebSignature = new JsonWebSignature();
jsonWebSignature.setPayload(jwtClaims.toJson());
jsonWebSignature.setKey(RsaJsonWebKeyBuilder.getRasJsonWebKeyInstance().getPrivateKey());
jsonWebSignature.setKeyIdHeaderValue(RsaJsonWebKeyBuilder.getRasJsonWebKeyInstance().getKeyId());
jsonWebSignature.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_PSS_USING_SHA256);

String jwt = jsonWebSignature.getCompactSerialization();

return "Bearer " + jwt;
}
public static boolean verifyToken(String token, String email) { // 由于生成token时使用了用户的email作为issuer,故这里需要传入email来做校验,这样做可以防止对不同用户的修改操作
String tokenContent = token.substring(7);
JwtConsumer consumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setMaxFutureValidityInMinutes(5256000)
.setAllowedClockSkewInSeconds(30)
.setRequireSubject()
.setExpectedIssuer(email)
.setExpectedAudience(System.getProperty("os.name"))
.setVerificationKey(RsaJsonWebKeyBuilder.getRasJsonWebKeyInstance().getPublicKey())
.build();
try {
JwtClaims claims = consumer.processToClaims(tokenContent);
return true;
} catch (InvalidJwtException e) {
return false;
}
}
}

然后为了做统一校验,创建拦截器
AuthenticationInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import com.example.demo.exceptions.ResponseException;
import com.example.demo.service.UserService;
import com.example.demo.utils.JWTManager;
import com.example.demo.utils.annotaion.LoginRequired;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class AuthenticationInterceptor implements HandlerInterceptor {

UserService userService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (!(handler instanceof HandlerMethod))
return true;
Method method = ((HandlerMethod) handler).getMethod();
if(method.isAnnotationPresent(LoginRequired.class)) {
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if(loginRequired.required()) {
if(token == null) {
throw ResponseException.UNAUTHORIZED;
}
// 校验token
if (JWTManager.verifyToken(token, request.getParameter("email"))){
return true;
}
else
throw ResponseException.UNAUTHORIZED;
}
}
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}
}

注意24行, 他的目的使检验方法是否被LoginRequired装饰。对于没有被装饰和LoginRequired的value是false的情况全部放行, 否则则校验token, 对于没有token, 或者校验不同过的情况,抛出ResponseException异常。

再来看LoginRequired装饰器,他的定义很简单

LoginRequired
1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
boolean required() default true;
}

使用时,支取要在需要登录验证的方法上添加@LoginRequired修饰即可

ResponseException继承自RuntimeException, 只有RuntimeException的子类才能被spingboot处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ResponseException extends RuntimeException {
public static ResponseException UNAUTHORIZED = new ResponseException(401, "请先登录");

private int code;
private String message;

public ResponseException(int code, String message) {
super(message);
this.code = code;
this.message = message;
}

@Override
public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}
}

另外我们需要添加一个异常捕获,来捕获校验失败抛出的异常。这里采用@ConrollerAdvice + @ExceptionHandler来捕获异常, 这种方式同时可以捕获程序运行时的各种错误,来做统一格式返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
private Logger logger = LoggerFactory.getLogger(ResponseAdvice.class);
private ThreadLocal<ObjectMapper> threadLocal = ThreadLocal.withInitial(ObjectMapper::new);

@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}

@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
ApiResult body;
ObjectMapper mapper = threadLocal.get();

if (o instanceof ResultMessage) {
body = new ApiResult(((ResultMessage) o).getCode(), ((ResultMessage) o).getMessage(), null);
} else if (o instanceof ApiResult) {
body = (ApiResult) o;
} else if (o instanceof String) {
body = new ApiResult(ResultMessage.SUCEESS, o);
try {
return mapper.writeValueAsString(body);
} catch (JsonProcessingException e) {
body = new ApiResult(ResultMessage.JSON_PARSE_ERROR, null);
}
} else {
body = new ApiResult(ResultMessage.SUCEESS, o);
}

return body;
}


* 401 - Unauthorized Exception
*/
@ExceptionHandler(value = ResponseException.class)
@ResponseBody
public ApiResult unAuthorizedExceptionHandler(ResponseException e) {
logger.trace(e.getMessage());
return new ApiResult(e.getCode(), e.getMessage(), null);
}


* 500 - Internal Server Error
*/
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ApiResult internalServerErrorHandler(Exception e) {
logger.trace(e.getStackTrace()[0].toString());
return new ApiResult(500, e.getStackTrace()[0].toString(), null);
}
}

其中beforeBodyWrite就是用来修改响应内容,可以做到统一格式响应,需要注意的是,如果他的参数Object o是字符串,需要ObjectMapper做转换,否则在后续的序列化会失败返回500或者404错误。

至此,springboot使用jwt校验的方法说完了。另外需要说明的是,拦截器里抛出异常的话,虽然我们能捕获并修改他的响应,但是他会导致跨域处理失效,响应头中没有Control-Allowed-Oringin等响应头,目前我还没找到解决办法,只能在前端做代理来避免跨域