美文网首页
Spring Security 源码分析(五):JWT 实现

Spring Security 源码分析(五):JWT 实现

作者: wch853 | 来源:发表于2019-03-24 19:39 被阅读0次

    JWT

    JWT(Json Web Token) 是一个开放标准,它定义了一种紧凑和自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。

    • 紧凑:token 值是一个很小的 Base64 编码的字符串,可以通过 http 请求参数或者 header 传递。
    • 自包含:token 可以包含很多信息,包括用户名、权限、过期时间等,支持开发者自定义。

    通常一个 JWT 字符串的解析结果如下:

    JWT编码解码

    JWT 串由 3 部分组成:

    • header:头部,用于标识 tokenJWT 类型和使用的签名算法。

    • payload:有效数据,JWT 自包含的信息。

    • signature:对头部和有效信息的签名。

    因此 JWT 能够安全地传输安全信息。

    使用 JWT 替换默认 token 实现

    Spring Security 提供了诸多的 TokenStore 实现,如存在内存中的 InMemoryTokenStore 、存在数据库中的 JdbcTokenStore、存在 Redis 中的 RedisTokenStore,这些都是通过将生成的 token 存储下来,当第三方应用请求受保护资源部时,会去 TokenStore 查询是否有相应的令牌。仅将令牌存储在内存中不支持分布式环境;存储在数据库或 Redis 中,每次请求都去查询又会增加后端的负担;一旦服务器宕机,势必又要影响用户访问。而 JWT 对于令牌的实现由于自包含的特性,能有效解决上述问题。

    JwtTokenStore

    无论是 4 种授权方式的哪一种,在授权认证完成后,都是通过在 AbstractTokenGranter 中调用 AuthorizationServerTokenServices#createAccessToken 方法颁发令牌的,JWT 由于其自包含的特性,是不会存储在后端应用中的,因此每次都需要申请授权都会直接创建新的令牌。普通令牌中只有 scoperefresh_token 等基本信息,JWT 如何实现其自包含特性呢?在创建令牌时,DefaultTokenServices#createAccessToken 方法使用了 TokenEnhancerJWT 中添加附加信息:

        private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
            // ...
            return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
        }
    

    因此需要配置 JwtAccessTokenConverter 来增强 JWT 的构成。

    JwtAccessTokenConverter

    在授权端点配置 AuthorizationServerEndpointsConfigurer 中,我们可以配置 JwtAccessTokenConverter

      public AuthorizationServerEndpointsConfigurer accessTokenConverter(AccessTokenConverter accessTokenConverter) {
        // 配置 JwtAccessTokenConverter
        this.accessTokenConverter = accessTokenConverter;
        return this;
      }
      private TokenEnhancer tokenEnhancer() {
        if (this.tokenEnhancer == null && accessTokenConverter() instanceof JwtAccessTokenConverter) {
          // JwtAccessTokenConverter 也实现了 TokenEnhancer 接口
            tokenEnhancer = (TokenEnhancer) accessTokenConverter;
        }
        return this.tokenEnhancer;
      }
    
      private TokenStore tokenStore() {
        if (tokenStore == null) {
          if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
            // 如果配置了 JwtAccessTokenConverter,那么配置 JwtTokenStore
            this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
          } else {
            this.tokenStore = new InMemoryTokenStore();
            }
        }
        return this.tokenStore;
      }
    

    一旦设置了 JwtAccessTokenConverter 就可以默认配置 tokenEnhancer,并将 tokenStore 设置为 JwtTokenStoreJwtAccessTokenConverter#enhance 方法中对于增加 JWT 附加信息的逻辑如下:

      public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
        Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
        String tokenId = result.getValue();
        if (!info.containsKey(TOKEN_ID)) {
          // 增加 jti,即授权服务器生成的原始访问令牌字符串
          info.put(TOKEN_ID, tokenId);
        } else {
          tokenId = (String) info.get(TOKEN_ID);
        }
        result.setAdditionalInformation(info);
        // 按照 JWT 生成算法拼装 JWT
        result.setValue(encode(result, authentication));
        OAuth2RefreshToken refreshToken = result.getRefreshToken();
        if (refreshToken != null) {
          // 拼接刷新令牌的 JWT
          // ...
        }
        return result;
      }
    

    在对 payload 部分编码时调用了 DefaultAccessTokenConverter#convertAccessToken 方法:

      public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        // ...
            // 增加用户名及其权限信息
        if (!authentication.isClientOnly()) {
          response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));
        } else {
          if (clientToken.getAuthorities()!=null && !clientToken.getAuthorities().isEmpty()) {
            response.put(UserAuthenticationConverter.AUTHORITIES,
                         AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));
          }
        }
            // 增加 scope 信息
        if (token.getScope()!=null) {
          response.put(scopeAttribute, token.getScope());
        }
        // 增加原始访问令牌
        if (token.getAdditionalInformation().containsKey(JTI)) {
          response.put(JTI, token.getAdditionalInformation().get(JTI));
        }
            // 增加令牌过期时间
        if (token.getExpiration() != null) {
          response.put(EXP, token.getExpiration().getTime() / 1000);
        }
        // 增加授权类型
        if (includeGrantType && authentication.getOAuth2Request().getGrantType()!=null) {
          response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType());
        }
            // 增加其他附加信息
        response.putAll(token.getAdditionalInformation());
            // ...
        return response;
      }
    

    JWT 的加密、解密是通过 Spring Security 提供的工具 JwtHelper 实现的,开发者可以自定义秘钥。

    TokenKeyEndpoint

    关于 JWTSpring Security 还留有一个彩蛋:在配置授权端点时,引入了 TokenKeyEndpointRegistrar 配置,当 Spring 容器中有 JwtAccessTokenConverter 实例时会注册 TokenKeyEndpoint,此配置提供了 /oauth/token_key 接口用于查询生成 JWT 签名的算法以及用于验证的密钥。

    /oauth/token_key 接口默认拒绝任何访问请求,通过授权服务器安全配置扩展设置接口的访问权限:

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
      security.tokenKeyAccess("authenticated");
    }
    

    小结

    • JWT 是一种紧凑和自包含的 token 实现方式,其有效数据包含了用户的认证信息,并通过加密签名来保证安全性。
    • JWT 可以存储在客户端,由于其无状态特性,天然支持分布式。由于自包含有效数据,避免了每次访问资源服务器都需要查询后端数据。
    • Spring Security 中通过配置 JwtAccessTokenConverter 来使用 JWT。开发者可以干预 JWT 中的附加信息和加密方式等。

    相关文章

      网友评论

          本文标题:Spring Security 源码分析(五):JWT 实现

          本文链接:https://www.haomeiwen.com/subject/uuqpvqtx.html