简介
手头的新项目采用 jwt 做客户端验证,而不再使用 cookie,确实方便很多,起码跨域这事不用考虑了。
jwt 是什么之类的就不多说了,这玩意的介绍满大街都是,这儿只是简单介绍下我在使用过程中的一些处理方式。
目的
这个 API 接口项目中使用 jwt 达成如下效果:
- 每个用户的签名都不一样,而不是共用签名,这样即使某人的 jwt 信息泄露,也不会影响其他人
- 服务器有专门的表存储用户签名,这样也可以在服务端控制某用户 jwt 的无效化
- 定义一个 spring 的 annotation,在 controller 方法的参数里面使用,用于得到用户的 jwt 存储的信息。
实现
采用的 jwt 处理库是 io.jsonwebtoken:jjwt:0.8.0
,下面用伪码的方式介绍上述要求的实现过程。
签名方式
jjwt
组件支持自定义签名实现,只需要继承 SigningKeyResolverAdapter
即可:
public class SigningKeyResolverImpl extends SigningKeyResolverAdapter {
private byte[] decode(String secret) {
return TextCodec.BASE64URL.decode(secret);
}
/**
* 从数据库中返回相应的 hashId 用于加密或解密。
*
*/
public Optional<String> getHashId(UUID clientId) {
// 数据库读取过程略
return Optional.empty();
}
/**
* 根据不同的 clientId 对应的 {@link JwtHash} 的 id 生成不同的加密密钥。
*
* @param clientId 用户 id
* @return
*/
public byte[] resolveSigningKeyBytes(UUID clientId) {
Optional<String> hashIdOptional = getHashId(clientId);
if (hashIdOptional.isPresent()) {
String hashId = hashIdOptional.get();
return decode(hashId);
} else {
throw new IllegalArgumentException("不支持的参数格式");
}
}
/**
* 根据 claims 中 clientId 读取对应的 {@link JwtHash} 表中的 id 作为密钥来解密。
*
* @param header
* @param claims
* @return
*/
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
String id = claims.getSubject();
UUID clientId = UUID.fromString(id);
Optional<String> hashIdOptional = getHashId(clientId);
if (hashIdOptional.isPresent()) {
String hashId = hashIdOptional.get();
return decode(hashId);
}
return super.resolveSigningKeyBytes(header, claims);
}
}
然后在生成和解密 jwt 的方法中调用即可:
加密:
Jwts.builder().signWith(SignatureAlgorithm.HS512,
new SigningKeyResolverImpl
.resolveSigningKeyBytes(clientId))
解密:
Jwts.parser()
.setSigningKeyResolver(new SigningKeyResolverImpl)
定义 spring 的 annotation
其实在 spring 中获得请求头的 Authorization
信息的方法有多种,常用的有拦截器和自定义 annotation,我个人采用的是后者,因为更加清晰,达到的效果为:
@GetMapping("/auth")
public RestResponse authDemo(@JwtAuthHeader JwtAuth jwtAuth) {
return new RestResponse("auth success");
}
只要是方法中存在 @JwtAuthHeader
定义的参数,就解析 Authorization
头信息,用这种方式还有个好处就是直接对方法做了用户验证了,所以连 spring-security
都省了。
当然,有些时候某些方法虽然需要验证,但是方法体里面其实没有用到 JwtAuth
信息,这个也无所谓,定义此参数,不用就是了。
annotation 定义
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtAuthHeader {
}
HandlerMethodArgumentResolver 实现
public class JwtAuthHeaderHandlerMethodArgumentResolver implements
HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JwtAuthHeader.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
if (parameter.isOptional()) {
throw new IllegalArgumentException("@JwtAuthHeader 参数不支持 Optional");
}
if (!parameter.getParameterType().isAssignableFrom(JwtAuth.class)) {
throw new IllegalArgumentException("@JwtAuthHeader 参数必须是 JwtAuth");
}
String authorization = webRequest.getHeader('Authorization');
// Authorization 头不存在
if (StringUtils.isBlank(authorization)) {
throw new JwtAuthHeaderUnauthorizedException();
}
Optional<JwtAuth> jwtAuthOptional = JwtAuthUtil
.getJwtAuth(authorization);
// jwt 信息解析不匹配,表示没有权限
if (!jwtAuthOptional.isPresent()) {
throw new JwtAuthHeaderUnauthorizedException();
}
JwtAuth jwtAuth = jwtAuthOptional.get();
return jwtAuth;
}
}
上述代码抛出的异常,在 @ExceptionHandler
中捕获就可以了。
为使上述代码生效,如果是用的 spring java config,则增加如下代码:
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new JwtAuthHeaderHandlerMethodArgumentResolver());
}
}
如果是 xml 配置,也类似,就不提了。
以上!