JWT 在 Spring 上的實踐

簡介

手頭的新項目採用 jwt 作客戶端驗證,而再也不使用 cookie,確實方便不少,起碼跨域這事不用考慮了。java

jwt 是什麼之類的就很少說了,這玩意的介紹滿大街都是,這兒只是簡單介紹下我在使用過程當中的一些處理方式。web

目的

這個 API 接口項目中使用 jwt 達成以下效果:spring

  1. 每一個用戶的簽名都不同,而不是共用簽名,這樣即便某人的 jwt 信息泄露,也不會影響其餘人
  2. 服務器有專門的表存儲用戶簽名,這樣也能夠在服務端控制某用戶 jwt 的無效化
  3. 定義一個 spring 的 annotation,在 controller 方法的參數裏面使用,用於獲得用戶的 jwt 存儲的信息。

實現

採用的 jwt 處理庫是 io.jsonwebtoken:jjwt:0.8.0,下面用僞碼的方式介紹上述要求的實現過程。數據庫

簽名方式

jjwt 組件支持自定義簽名實現,只須要繼承 SigningKeyResolverAdapter 便可:json

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))

解密:cookie

Jwts.parser()
                .setSigningKeyResolver(new SigningKeyResolverImpl)

定義 spring 的 annotation

其實在 spring 中得到請求頭的 Authorization 信息的方法有多種,經常使用的有攔截器和自定義 annotation,我我的採用的是後者,由於更加清晰,達到的效果爲:app

@GetMapping("/auth")
public RestResponse authDemo(@JwtAuthHeader JwtAuth jwtAuth) {
    return new RestResponse("auth success");
}

只要是方法中存在 @JwtAuthHeader 定義的參數,就解析 Authorization 頭信息,用這種方式還有個好處就是直接對方法作了用戶驗證了,因此連 spring-security 都省了。ide

固然,有些時候某些方法雖然須要驗證,可是方法體裏面其實沒有用到 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 配置,也相似,就不提了。

以上!

相關文章
相關標籤/搜索