• 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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    在这里插入图片描述
    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 getAdditionalInformation();
    
        Set getScope();
    
        OAuth2RefreshToken getRefreshToken();
    
        String getTokenType();
    
        boolean isExpired();
    
        Date getExpiration();
    
        int getExpiresIn();
    
        String getValue();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    该接口定义了一些字段的名称,并且告诉我们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 scope;
        private Map 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 getScope() {
            return this.scope;
        }
    
        public void setScope(Set 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 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 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 getAdditionalInformation() {
            return this.additionalInformation;
        }
    
        public void setAdditionalInformation(Map additionalInformation) {
            this.additionalInformation = new LinkedHashMap(additionalInformation);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137

    (一)令牌解析

    令牌解析的目的是为了将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);
    ```
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    2.JWTAccessTokenConvert:enhance

    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
            Map 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 claims = this.objectMapper.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
                    if (claims.containsKey("jti")) {
                        encodedRefreshToken.setValue(claims.get("jti").toString());
                    }
                } catch (IllegalArgumentException var11) {
                }
    
                Map 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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.DefaultAccessTokenConverter:convertAccessToken

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

    public Map convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
            Map 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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    三、OAuth2Request、authentication以及token的关系

    在这里插入图片描述

  • 相关阅读:
    [区间dp]添加括号
    认识Spring
    小红书种草步骤:小红书种草怎么做?
    C++:类与对象(3)
    Qt窗口无标题栏拖动放大
    具有多孔光纤的偏振分束器
    DockerCompose安装、使用 及 微服务部署实操
    数据仓库数据分层详解
    Vue面试题-答案、例子
    [vxe-table] 合并行后滚动错位
  • 原文地址:https://blog.csdn.net/qq_42861526/article/details/126172290