SpringSecurity-Oauth2 之JWT令牌详解

一、JWT的执行时机和底层逻辑

JWT有两个执行时机,一个是在用户认证成功时,对authentication进行加密,得到token发给用户,一个是当用户携带加密后的token访问服务时,JWT将token进行解码,再把token提取为authentication

这两个时机的执行都是依靠JwtAccessTokenConverter来完成的

(一)JWT配置

JWT组件主要包括:sigingKey(对策密钥)、accessTokenConvert(accessToken转化器)、tokenStore(密钥策略:包含token解析、存储等)、authenticationTokenConvert(authentication转化器,包含再accessTokenConvert中)

注意:上面说的只是组件,JWT暴露给SpringSecurity的服务是tokenService,SpringSecurity也是通过tokenService来完成token的生成、解析以及存储的

@Configuration
public class JWTConfig {

    @Value("${siging-key}")
    private String SIGNING_KEY;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来解密
        ClientDefaultAccessTokenConverter accessTokenConverter = new ClientDefaultAccessTokenConverter()
        accessTokenConverter.setUserTokenConverter(new UnifiedUserAuthenticationConverter()); //设置自定义的authentication转化器
        converter.setAccessTokenConverter(accessTokenConverter);
        return converter;
    }

}

(二) 组件之间的依赖关系

在这里插入图片描述
JWT暴露给tokenService使用的类是tokenStore,而tokenStore又依赖于accessTokenConvert和authenticationConvert完成token的生成和解析
注意:SpringSecurity是通过tokenService来解析和生成token的,tokenStore依赖于tokenService!!!

二、令牌生成、解析过程

首先我们来看看JWT令牌在代码中的样子,先来看它的接口
OAuth2AccessToken

public interface OAuth2AccessToken {
    String BEARER_TYPE = "Bearer";
    String OAUTH2_TYPE = "OAuth2";
    String ACCESS_TOKEN = "access_token";
    String TOKEN_TYPE = "token_type";
    String EXPIRES_IN = "expires_in";
    String REFRESH_TOKEN = "refresh_token";
    String SCOPE = "scope";

    Map<String, Object> getAdditionalInformation();

    Set<String> getScope();

    OAuth2RefreshToken getRefreshToken();

    String getTokenType();

    boolean isExpired();

    Date getExpiration();

    int getExpiresIn();

    String getValue();
}

该接口定义了一些字段的名称,并且告诉我们token需要具备value(编码后的hash值),scope(范围)、expire(过期时间)以及一些额外信息additionalInformation。接下来我们再来看框架提供给我们的默认实现类

DefaultOAuth2AccessToken

public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {
    private static final long serialVersionUID = 914967629530462926L;
    private String value;
    private Date expiration;
    private String tokenType;
    private OAuth2RefreshToken refreshToken;
    private Set<String> scope;
    private Map<String, Object> additionalInformation;

    public DefaultOAuth2AccessToken(String value) {
        this.tokenType = "Bearer".toLowerCase();
        this.additionalInformation = Collections.emptyMap();
        this.value = value;
    }

    private DefaultOAuth2AccessToken() {
        this((String)null);
    }

    public DefaultOAuth2AccessToken(OAuth2AccessToken accessToken) {
        this(accessToken.getValue());
        this.setAdditionalInformation(accessToken.getAdditionalInformation());
        this.setRefreshToken(accessToken.getRefreshToken());
        this.setExpiration(accessToken.getExpiration());
        this.setScope(accessToken.getScope());
        this.setTokenType(accessToken.getTokenType());
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getValue() {
        return this.value;
    }

    public int getExpiresIn() {
        return this.expiration != null ? Long.valueOf((this.expiration.getTime() - System.currentTimeMillis()) / 1000L).intValue() : 0;
    }

    protected void setExpiresIn(int delta) {
        this.setExpiration(new Date(System.currentTimeMillis() + (long)delta));
    }

    public Date getExpiration() {
        return this.expiration;
    }

    public void setExpiration(Date expiration) {
        this.expiration = expiration;
    }

    public boolean isExpired() {
        return this.expiration != null && this.expiration.before(new Date());
    }

    public String getTokenType() {
        return this.tokenType;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public OAuth2RefreshToken getRefreshToken() {
        return this.refreshToken;
    }

    public void setRefreshToken(OAuth2RefreshToken refreshToken) {
        this.refreshToken = refreshToken;
    }

    public Set<String> getScope() {
        return this.scope;
    }

    public void setScope(Set<String> scope) {
        this.scope = scope;
    }

    public boolean equals(Object obj) {
        return obj != null && this.toString().equals(obj.toString());
    }

    public int hashCode() {
        return this.toString().hashCode();
    }

    public String toString() {
        return String.valueOf(this.getValue());
    }

    public static OAuth2AccessToken valueOf(Map<String, String> tokenParams) {
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken((String)tokenParams.get("access_token"));
        if (tokenParams.containsKey("expires_in")) {
            long expiration = 0L;

            try {
                expiration = Long.parseLong(String.valueOf(tokenParams.get("expires_in")));
            } catch (NumberFormatException var5) {
            }

            token.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000L));
        }

        if (tokenParams.containsKey("refresh_token")) {
            String refresh = (String)tokenParams.get("refresh_token");
            DefaultOAuth2RefreshToken refreshToken = new DefaultOAuth2RefreshToken(refresh);
            token.setRefreshToken(refreshToken);
        }

        if (tokenParams.containsKey("scope")) {
            Set<String> scope = new TreeSet();
            StringTokenizer tokenizer = new StringTokenizer((String)tokenParams.get("scope"), " ,");

            while(tokenizer.hasMoreTokens()) {
                scope.add(tokenizer.nextToken());
            }

            token.setScope(scope);
        }

        if (tokenParams.containsKey("token_type")) {
            token.setTokenType((String)tokenParams.get("token_type"));
        }

        return token;
    }

    public Map<String, Object> getAdditionalInformation() {
        return this.additionalInformation;
    }

    public void setAdditionalInformation(Map<String, Object> additionalInformation) {
        this.additionalInformation = new LinkedHashMap(additionalInformation);
    }
}

(一)令牌解析

令牌解析的目的是为了将token的编码转化为AccessToken,从而提取出authentication等信息

在这里插入图片描述

(二)令牌生成

使用通过认证的authentication来生成令牌

1. DefaultTokenServices:createAccessToken(OAuth2Authentication authentication)

```
 @Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
    OAuth2RefreshToken refreshToken = null;
    if (existingAccessToken != null) {
        if (!existingAccessToken.isExpired()) {
            this.tokenStore.storeAccessToken(existingAccessToken, authentication);
            return existingAccessToken;
        }

        if (existingAccessToken.getRefreshToken() != null) {
            refreshToken = existingAccessToken.getRefreshToken();
            this.tokenStore.removeRefreshToken(refreshToken);
        }

        this.tokenStore.removeAccessToken(existingAccessToken);
    }

    if (refreshToken == null) {
        refreshToken = this.createRefreshToken(authentication);
    } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
        ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
        if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
            refreshToken = this.createRefreshToken(authentication);
        }
    }

    OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
    this.tokenStore.storeAccessToken(accessToken, authentication);
    refreshToken = accessToken.getRefreshToken();
    if (refreshToken != null) {
        this.tokenStore.storeRefreshToken(refreshToken, authentication);
    }

    return accessToken;
}
```
**createAccessToken**
```
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L));
        }

        token.setRefreshToken(refreshToken);
        token.setScope(authentication.getOAuth2Request().getScope());
        return (OAuth2AccessToken)(this.accessTokenEnhancer != null ? this.accessTokenEnhancer.enhance(token, authentication) : token);
```

2.JWTAccessTokenConvert:enhance

public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
        Map<String, Object> info = new LinkedHashMap(accessToken.getAdditionalInformation());
        String tokenId = result.getValue();
        if (!info.containsKey("jti")) {
            info.put("jti", tokenId);
        } else {
            tokenId = (String)info.get("jti");
        }

        result.setAdditionalInformation(info);
        //注意这一行
        //将authentication的信息提取到result中,再对result编码
        result.setValue(this.encode(result, authentication));
        OAuth2RefreshToken refreshToken = result.getRefreshToken();
        if (refreshToken != null) {
            DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
            encodedRefreshToken.setValue(refreshToken.getValue());
            encodedRefreshToken.setExpiration((Date)null);

            try {
                Map<String, Object> claims = this.objectMapper.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
                if (claims.containsKey("jti")) {
                    encodedRefreshToken.setValue(claims.get("jti").toString());
                }
            } catch (IllegalArgumentException var11) {
            }

            Map<String, Object> refreshTokenInfo = new LinkedHashMap(accessToken.getAdditionalInformation());
            refreshTokenInfo.put("jti", encodedRefreshToken.getValue());
            refreshTokenInfo.put("ati", tokenId);
            encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
            DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(this.encode(encodedRefreshToken, authentication));
            if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                Date expiration = ((ExpiringOAuth2RefreshToken)refreshToken).getExpiration();
                encodedRefreshToken.setExpiration(expiration);
                token = new DefaultExpiringOAuth2RefreshToken(this.encode(encodedRefreshToken, authentication), expiration);
            }

            result.setRefreshToken((OAuth2RefreshToken)token);
        }

        return result;
    }

encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication)

protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        String content;
        try {
            content = this.objectMapper.formatMap(this.tokenConverter.convertAccessToken(accessToken, authentication));
        } catch (Exception var5) {
            throw new IllegalStateException("Cannot convert access token to JSON", var5);
        }

        String token = JwtHelper.encode(content, this.signer).getEncoded();
        return token;
    }

3.DefaultAccessTokenConverter:convertAccessToken

该方法将authentication中需要被提取的信息封装成map返回,我们也可以重写该方法自定义返回的map来扩充JWT令牌

public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        Map<String, Object> response = new HashMap();
        OAuth2Request clientToken = authentication.getOAuth2Request();
        if (!authentication.isClientOnly()) {
            response.putAll(this.userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));
        } else if (clientToken.getAuthorities() != null && !clientToken.getAuthorities().isEmpty()) {
            response.put("authorities", AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));
        }

        if (token.getScope() != null) {
            response.put(this.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() / 1000L);
        }

        if (this.includeGrantType && authentication.getOAuth2Request().getGrantType() != null) {
            response.put("grant_type", authentication.getOAuth2Request().getGrantType());
        }

        response.putAll(token.getAdditionalInformation());
        response.put(this.clientIdAttribute, clientToken.getClientId());
        if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) {
            response.put("aud", clientToken.getResourceIds());
        }

        return response;
    }

三、OAuth2Request、authentication以及token的关系

在这里插入图片描述

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值