SpringSecurity OAuth2 生成JWT

        在《SpringSecurity之OAuth2 令牌accessToken的生成过程_clonetx的博客-CSDN博客》中我们分析了access_token生成整体流程的源码,最终提到了accessTokenEnhancer.enhance(token, authentication)方法。

而说到转换token,最普遍的就是转成 jwt 了。

1 jwt

        jwt共分为三部分组成,header、payload、signature,即头、体、签名;其结构如下,使用英文句号连接:

Base64(header).Base64(payload).HMACSHA256(Base64(header)+"."+Base64(payload),secret)

下面分别看下每部分的内容。

header:{
    "alg":"HS256",//签名算法
	"typ":"JWT" //类型
}
payload: {
	"iss": "jwt签发者",
	"sub": "jwt的主体,如用户标识",
	"aud": "jwt的接收者,即resource_ids",
	"exp": "jwt的过期时间",//不能为空
	"nbf": "定义在指定时间之前,该jwt不可用",
	"iat": "jwt签发时间",
	"jti": "jwt的唯一身份标识,用来作为一次性token时可以防止重放攻击",
    "自定义key1":"自定义value1",
    "自定义key2":"自定义value2"
}

        这里注意一下,jwt中,仅仅对 payload 进行了一次 base64,并非什么加密手段,因此,千万不要自定义敏感信息进去,比如用户的身份证号,密码,手机号等信息的明文数据。

signature:HMACSHA256(Base64(header)+"."+Base64(payload),secret)

        签名的作用就是为了防止窜改 header 和 payload ,验证 jwt 的时候会对signature进行解密,根据上面的加密公式看,显然解密得到的就是 header 和 payload 的 base64 串,只要与签发时的header 和 payload 一对比就可以知道 jwt是否被修改过。

        加密方式可以使用对称加密也可以使用非对称加密,这是因为,加解密都是在服务端进行的,密钥不泄露就不用担心被伪造。个人推荐非对称方式,即私钥签名,公钥验证签名,这样更安全,也可以允许第三方验证我们的 jwt。

//客户端授权模式产生的 jwt 示例
{
    "access_token": "eyJhbGciOiJIUzI1NiIsIn5cCI6IkpXVCJ9.eyJzY29wZSI6Wy0ZXN0Il0sImV4cCI6MTY1MzUzNDgyNCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9DTElFTlQiXSwianRpIjoiYjFjNjRmN2YtMmMxYS00YjljLTlmMTgtNGYxN2Q0YmY2OTE2IiwiY2xpZW50X2lkIjoidGVzdEFwcElkIn0.F2ZZq4rg9BDw2oCU_qDs9fulSKbhD53xnmTmYH57L6U",
    "token_type": "bearer",
    "expires_in": 1473,
    "scope": "test",
    "jti": "b1c64f7f-2c1a-4b9c-9f18-4f17d4bf6916"
}

2 TokenEnhancer

        在上一篇源码分析中,我们提到TokenEnhancer的对象是在 AuthorizationServerEndpointsConfigurer 类中设置的。

private DefaultTokenServices createDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(reuseRefreshToken);
        tokenServices.setClientDetailsService(clientDetailsService());
        tokenServices.setTokenEnhancer(tokenEnhancer());//token增强器
        addUserDetailsService(tokenServices, this.userDetailsService);
        return tokenServices;
    }

private TokenEnhancer tokenEnhancer() {
    //如果授权服务器的配置中endpoints没有指定 tokenEnhancer,
    //并且设置了JwtAccessTokenConverter类型的token转换器,就返回转换器
    if (this.tokenEnhancer == null && accessTokenConverter() instanceof JwtAccessTokenConverter) {
            tokenEnhancer = (TokenEnhancer) accessTokenConverter;
    }
    //N 如果endpoints 指定了tokenEnhancer,就返回指定的,或者token转换器类型不是jwt的就只能返回null了
    return this.tokenEnhancer;
}

private AccessTokenConverter accessTokenConverter() {
    if (this.accessTokenConverter == null) {
       accessTokenConverter = new DefaultAccessTokenConverter();//非JwtAccessTokenConverter类型
    }
    return this.accessTokenConverter;
}

        结合授权服务器的配置,如果我们既没有指定 tokenEnhancer(),也没有指定accessTokenConverter()配置,那么tokenEnhancer() 的返回值就一定是null了。下例我们设置一个 JwtAccessTokenConverter 的对象。

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
            ......
            //.tokenEnhancer(null)
            .accessTokenConverter(new JwtAccessTokenConverter())
    ;
}

其实,我们能动手脚的也就是endpoints的这两个配置了。

3 JwtAccessTokenConverter

        在 DefaultTokenServices 类里面创建accessToken的最终实现中,我们看到有一个 accessTokenEnhancer.enhance(token, authentication)方法,在配置了accessTokenConverter()后,其 enhance() 方法的具体实现代码如下:

        这段代码逻辑分别处理了access_token和refresh_token,逻辑比较简单,主要工作就是在原本的token中加了一个jti,也就是原本的uuid不再作为access_token的值,而变成了 jti 的值,然后就是从 authentication 对象中取了一些值构造出一个content,即payload,并使用JwtHelper工具类基于content生成了一个jwt作为最终的access_token。

public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
    //info 中只放了一个jti
    Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
    String tokenId = result.getValue();
    if (!info.containsKey(TOKEN_ID)) {
        //TOKEN_ID 就是 jti 也就是原始token的值,即uuid
        info.put(TOKEN_ID, tokenId);
    } else {
        tokenId = (String) info.get(TOKEN_ID);
    }
    result.setAdditionalInformation(info);
    //N 这里的value就是我们最终看到的access_token
    result.setValue(encode(result, authentication));
    OAuth2RefreshToken refreshToken = result.getRefreshToken();
    if (refreshToken != null) {
        //N 开始处理refresh_token
        DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
        encodedRefreshToken.setValue(refreshToken.getValue());
        // Refresh tokens do not expire unless explicitly of the right type
        encodedRefreshToken.setExpiration(null);//默认refreshToken 不过期
        try {
            //claims 就是refreshToken中的 payload
            Map<String, Object> claims = objectMapper
                    .parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
            if (claims.containsKey(TOKEN_ID)) {
                //这里只保留了 refreshToken 的 uuid
                encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
            }
        } catch (IllegalArgumentException e) {
        }
        Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
                accessToken.getAdditionalInformation());
        refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
        //refreshToken 的info 同时存储了jti 和 ati
        refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
        encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
        DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
                encode(encodedRefreshToken, authentication));
        if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            // refreshToken 的类型是在创建时,根据数据库表中是否设置了过期时间决定的
            Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
            encodedRefreshToken.setExpiration(expiration);
            token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
        }
        result.setRefreshToken(token);
    }
    return result;
}

        encode()方法的作用就是基于payload生成 jwt,而 payload 的生成则是由 tokenConverter 完成的,在创建JwtAccessTokenConverter 对象时,tokenConverter 默认是 DefaultAccessTokenConverter类型,当然我们也可以通过set方法改变 tokenConverter 为自定义的。

protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    String content;
    try {
        //N tokenConverter 默认是 DefaultAccessTokenConverter
        content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
    } catch (Exception e) {
        throw new IllegalStateException("Cannot convert access token to JSON", e);
    }
    //N content 就是 payload
    String token = JwtHelper.encode(content, signer).getEncoded();
    return token;
}

4 DefaultAccessTokenConverter

        下面代码我们通过Map<String, Object> response中put的值就可以知道最终生成的 jwt 中 payload 会有哪些内容了,想要拓展 payload,只要 set 一个自定义的 tokenConverter 取代默认的 DefaultAccessTokenConverter就可以。

/**
 * 这里就是在构造 payload 了,如果我们配置JwtAccessTokenConverter 时,set 一个自定义的tokenConverter,
 * 就可以添加一下自定义的内容了,比如 authentication 中的用户标识 account 等
 */
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
    Map<String, Object> response = new HashMap<String, Object>();
    OAuth2Request clientToken = authentication.getOAuth2Request();

    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()));
        }
    }

    if (token.getScope()!=null) {
        response.put(SCOPE, 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());

    response.put(CLIENT_ID, clientToken.getClientId());
    if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) {
        response.put(AUD, clientToken.getResourceIds());
    }
    return response;
}

至此,如何将一个 uuid 的 access_token 转成 jwt 的主要源码就分析完了。

5 授权服务器配置

对 token 的额外处理,可以说完全是围绕下面的授权服务器的配置了。

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
            ......
            //.tokenEnhancer()
            //.accessTokenConverter(new JwtAccessTokenConverter())
    ;
}
  • 如果我们想拓展payload的内容,在 new JwtAccessTokenConverter() 之后,set一个自定义的AccessTokenConverter对象即可

public void setAccessTokenConverter(AccessTokenConverter tokenConverter) {
   this.tokenConverter = tokenConverter;
}
  • 如果我们不想用 JwtHelper 的加密方式,RSA、HMACSHA256等。比如想换成SM2啥的,可以继承JwtAccessTokenConverter,子类重写JWT的生成和验证方法,然后将new JwtAccessTokenConverter() 替换成子类对象就可以了。

  • 如果我们不想用jwt,想自定义一种token,可以给 endpoints 设置一个自定义的 tokenEnhancer 对象,按需求实现enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) 方法。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值