簡介
手頭的新項目採用 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 配置,也類似,就不提了。
以上!