spring authorization server使用不透明令牌

浮生半日闲 发布于 2024-07-20 13 次阅读


spring authorization server默认使用的是jwt令牌,但由于jwt令牌的缺点:

  • 服务器不保存会话状态,一旦JWT被颁发,除非更改密钥或等待令牌过期,否则无法在有效期内撤销
  • 尽管JWT包含了签名,但如果密钥管理不当,例如密钥泄露或密钥强度不足,令牌仍可能被伪造
  • JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限
  • WT令牌的长度可能较长,占用较多的存储空间,这可能导致每个请求的额外网络开销,尤其是在移动设备或低带宽环境下

因此,由于上述相关的缺点,现将令牌改为不透明令牌模式。

1、启用不透明令牌配置

RegisteredClient的accessTokenFormat设置为OAuth2TokenFormat.REFERENCE即可启用不透明令牌。

配置完成后,默认生成的key的长度为96位,为了节省存储空间和传输带宽,现将位数进行缩短。

2、配置TokenGenerator(非必须)

创建token生成类,实现OAuth2TokenGenerator接口,来生成AccessToken和RefreshToken。

如果需要更多的信息,可以对OAuth2Token进行重写,添加更多的属性。

@Component
public class RedisTokenGenerator implements OAuth2TokenGenerator<OAuth2Token> {
    @Override
    public OAuth2Token generate(OAuth2TokenContext context) {
        // accessToken和refreshToken都会通过该方法进行生成,需要独立进行判断
        OAuth2TokenType tokenType = context.getTokenType();
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) &&
                OAuth2TokenFormat.REFERENCE.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
            // accessToken 生成
            return buildAccessToken(context);
        } else if (OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType()) && !isPublicClientForAuthorizationCodeGrant(context)) {
            // refreshToken 生成
            return buildRefreshToken(context);
        }

        return null;
    }

    /**
     * 判断是不是public client
     * @param context
     * @return
     */
    private boolean isPublicClientForAuthorizationCodeGrant(OAuth2TokenContext context) {
        if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&
                (context.getAuthorizationGrant().getPrincipal() instanceof OAuth2ClientAuthenticationToken clientPrincipal)) {
            return clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE);
        }
        return false;
    }

    /**
     * 构建access token {@link OAuth2AccessTokenGenerator}
     * @param context
     */
    private OAuth2Token buildAccessToken(OAuth2TokenContext context) {
        RegisteredClient registeredClient = context.getRegisteredClient();

        Instant issuedAt = Instant.now();
        Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());

        return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, buildUUID(), issuedAt, expiresAt, context.getAuthorizedScopes());
    }

    /**
     * 构建刷新密钥 {@link OAuth2RefreshTokenGenerator}
     * @param context
     */
    private OAuth2RefreshToken buildRefreshToken(OAuth2TokenContext context) {
        Instant issuedAt = Instant.now();
        Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive());
        return new OAuth2RefreshToken(buildUUID(), issuedAt, expiresAt);
    }

    private String buildUUID() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

使用了@Component注解后,spring security会自动使用在容器内的OAuth2TokenGenerator类,不会再创建默认配置(DelegatingOAuth2TokenGenerator)。也可以在SecurityConfig中通过OAuth2AuthorizationServerConfigurer.tokenGenerator()方法配置对应的OAuth2TokenGenerator类。

3、使用redis保存令牌信息

为了使用redis来保存令牌信息,我们需要实现OAuth2AuthorizationService接口。

3.1 管理token类型

使用枚举的方式来管理spring authorization server中所有的token类型,防止遗漏。

@Getter
public enum OAuth2AuthorizationTokenType {

    STATE(OAuth2ParameterNames.STATE, null) {
        @Override
        public String getTokenValue(OAuth2Authorization authorization) {
            String authorizationState = authorization.getAttribute(OAuth2ParameterNames.STATE);
            if (StringUtils.hasText(authorizationState)) {
                return authorizationState;
            }

            return null;
        }

        @Override
        public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
            return null;
        }
    },
    AUTHORIZATION_CODE(OAuth2ParameterNames.CODE, OAuth2AuthorizationCode.class) {
        @Override
        public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
            return new OAuth2AuthorizationCode(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
        }
    },
    ACCESS_TOKEN(OAuth2TokenType.ACCESS_TOKEN.getValue(), OAuth2AccessToken.class) {
        @Override
        public OAuth2AuthorizationTokenValue getTokenValue(OAuth2Authorization.Token<? extends OAuth2Token> token) {
            OAuth2AuthorizationTokenValue auth2AuthorizationTokenValue = super.getTokenValue(token);
            OAuth2AccessToken oAuth2AccessToken = (OAuth2AccessToken) token.getToken();
            auth2AuthorizationTokenValue.setTokenType(oAuth2AccessToken.getTokenType().getValue());
            auth2AuthorizationTokenValue.setScopes(oAuth2AccessToken.getScopes());

            return auth2AuthorizationTokenValue;
        }

        @Override
        public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
            OAuth2AccessToken.TokenType tokenType = null;
            if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(tokenValue.getTokenType())) {
                tokenType = OAuth2AccessToken.TokenType.BEARER;
            }

            return new OAuth2AccessToken(tokenType, tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt(), tokenValue.getScopes());
        }
    },
    ID_TOKEN(OidcParameterNames.ID_TOKEN, OidcIdToken.class) {
        @Override
        public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
            return new OidcIdToken(tokenValue.getValue(),
                    tokenValue.getIssuedAt(),
                    tokenValue.getExpiresAt(),
                    (Map<String, Object>) tokenValue.getMetaData().get(OAuth2Authorization.Token.CLAIMS_METADATA_NAME)
            );
        }
    },
    REFRESH_TOKEN(OAuth2TokenType.REFRESH_TOKEN.getValue(), OAuth2RefreshToken.class) {
        @Override
        public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
            return new OAuth2RefreshToken(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
        }
    },
    DEVICE_CODE(OAuth2ParameterNames.DEVICE_CODE, OAuth2DeviceCode.class) {
        @Override
        public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
            return new OAuth2DeviceCode(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
        }
    },
    USER_CODE(OAuth2ParameterNames.USER_CODE, OAuth2UserCode.class) {
        @Override
        public OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue) {
            return new OAuth2UserCode(tokenValue.getValue(), tokenValue.getIssuedAt(), tokenValue.getExpiresAt());
        }
    }

    ;

    /**
     * 类型
     */
    String tokenType;

    /**
     * 对应的对象类型
     */
    Class<? extends OAuth2Token> oAuth2Token;

    OAuth2AuthorizationTokenType(String tokenType, Class<? extends OAuth2Token> oAuth2Token) {
        this.tokenType = tokenType;
        this.oAuth2Token = oAuth2Token;
    }

    /**
     * 获取
     * @param authorization
     */
    public OAuth2Authorization.Token<? extends OAuth2Token> getToken(OAuth2Authorization authorization) {
        if (oAuth2Token == null) {
            return null;
        }

        return authorization.getToken(oAuth2Token);
    }

    /**
     * 获取token value对象
     * @return
     */
    public OAuth2AuthorizationTokenValue getTokenValue(OAuth2Authorization.Token<? extends OAuth2Token> token) {
        OAuth2AuthorizationTokenValue oAuth2AuthorizationTokenValue = new OAuth2AuthorizationTokenValue();
        oAuth2AuthorizationTokenValue.setValue(token.getToken().getTokenValue());
        oAuth2AuthorizationTokenValue.setExpiresAt(token.getToken().getExpiresAt());
        oAuth2AuthorizationTokenValue.setIssuedAt(token.getToken().getIssuedAt());
        oAuth2AuthorizationTokenValue.setMetaData(token.getMetadata());

        return oAuth2AuthorizationTokenValue;
    }

    /**
     * 获取token的值
     * @param authorization
     * @return
     */
    public String getTokenValue(OAuth2Authorization authorization) {
        OAuth2Authorization.Token<? extends OAuth2Token> token = getToken(authorization);
        if (token != null) {
            return token.getToken().getTokenValue();
        }

        return null;
    }

    /**
     * 反序列化构建对象
     * @param tokenValue
     * @return
     */
    public abstract OAuth2Token getOAuth2Token(OAuth2AuthorizationTokenValue tokenValue);

    public static final Map<String, OAuth2AuthorizationTokenType> INSTANCE = new HashMap<>();

    static {
        for (OAuth2AuthorizationTokenType item : OAuth2AuthorizationTokenType.values()) {
            INSTANCE.put(item.getTokenType(), item);
        }
    }

    public static OAuth2AuthorizationTokenType getInstance(String type) {
        return INSTANCE.get(type);
    }
}

3.2 Oauth2Authorization管理

Oauth2Authorization内部有很多的类没有默认构造函数,我们这里使用的是jackson的方式存储到redis,当反序列化时,没有默认构造函数的类就会反序列化错误,因此,这里我们定义自己的类来管理令牌的信息。

@Setter
@Getter
@NoArgsConstructor
public class RedisOAuth2Authorization {

    private String id;

    /**
     * 客户端key
     */
    private String registeredClientId;

    /**
     * 对象名称
     */
    private String principalName;

    /**
     * 授权类型
     */
    private String authorizationGrantType;

    /**
     * 授权范围
     */
    private Set<String> authorizedScopes;

    /**
     * 参数
     */
    private Map<String, Object> attributes;

    /**
     * 状态
     */
    private String state;

    /**
     * 最小过期时间
     */
    private Instant minExpiresAt;

    /**
     *
     */
    private Map<OAuth2AuthorizationTokenType, OAuth2AuthorizationTokenValue> tokenValuesMap = new HashMap<>();

    public RedisOAuth2Authorization(OAuth2Authorization authorization) {
        this.id = authorization.getId();
        this.registeredClientId = authorization.getRegisteredClientId();
        this.principalName = authorization.getPrincipalName();
        this.authorizationGrantType = authorization.getAuthorizationGrantType().getValue();
        this.authorizedScopes = authorization.getAuthorizedScopes();
        this.attributes = authorization.getAttributes();
        this.state = OAuth2AuthorizationTokenType.STATE.getTokenValue(authorization);

        Instant minExpiresAt = null;
        for (OAuth2AuthorizationTokenType tokenType : OAuth2AuthorizationTokenType.values()) {
            OAuth2Authorization.Token<? extends OAuth2Token> token = tokenType.getToken(authorization);
            if (token != null) {
                tokenValuesMap.put(tokenType, tokenType.getTokenValue(token));

                minExpiresAt = getMinExpiresAt(minExpiresAt, token.getToken().getExpiresAt());
            }
        }

        this.minExpiresAt = minExpiresAt;
    }

    /**
     * 获取最小的到期时间
     * @param lastExpiresAt
     * @param currentExpiresAt
     */
    private Instant getMinExpiresAt(Instant lastExpiresAt, Instant currentExpiresAt) {
        if (currentExpiresAt == null) {
            return lastExpiresAt;
        }

        if (lastExpiresAt == null || currentExpiresAt.isBefore(lastExpiresAt)) {
            return currentExpiresAt;
        }

        return lastExpiresAt;
    }
}

3.3 存储令牌到redis内

创建类,实现OAuth2AuthorizationService接口相关方法即可。

@Component
@AllArgsConstructor
public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {

    public static final String KEY = "authorization" + VariableConstant.SEPARATOR;

    private RedisTemplate redisTemplate;

    private RegisteredClientRepository registeredClientRepository;

    @Override
    public void save(OAuth2Authorization authorization) {
        /**
         *  通过id存储实际的内容,
         *  然后其他的token对应存储id,后续查找时通过二次查询获取
         */
        RedisOAuth2Authorization redisOAuth2Authorization = new RedisOAuth2Authorization(authorization);

        // 存储到redis中
        Duration cacheExpireTime = Duration.ofSeconds(redisOAuth2Authorization.getMinExpiresAt() == null ? 60*30L :
                redisOAuth2Authorization.getMinExpiresAt().getEpochSecond() - Instant.now().getEpochSecond());
        redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                /**
                 * 查询token时,会通过token的类型和token的值的方式来查询
                 */
                if (!redisOAuth2Authorization.getTokenValuesMap().isEmpty()) {
                    // token类型的对应
                    redisOAuth2Authorization.getTokenValuesMap().forEach((key, value) -> {
                        operations.opsForValue().set(KEY + key.name() + VariableConstant.SEPARATOR + value.getValue(),
                                redisOAuth2Authorization.getId(),
                                cacheExpireTime);
                    });
                }

                /**
                 * state类型也是TokenType的一种,也需要存储
                 */
                if (StringUtils.hasText(redisOAuth2Authorization.getState())) {
                    operations.opsForValue().set(KEY + OAuth2AuthorizationTokenType.STATE.name() + VariableConstant.SEPARATOR + redisOAuth2Authorization.getState(),
                            redisOAuth2Authorization.getId(),
                            cacheExpireTime);
                }

                // 具体信息
                operations.opsForValue().set(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + redisOAuth2Authorization.getId(),
                        redisOAuth2Authorization,
                        cacheExpireTime);
                return null;
            }
        });
    }

    @Override
    public void remove(OAuth2Authorization authorization) {
        List<String> tokens = new ArrayList<>();
        for (OAuth2AuthorizationTokenType tokenType : OAuth2AuthorizationTokenType.values()) {
            String token = tokenType.getTokenValue(authorization);
            if (!StringUtils.hasText(token)) {
                continue;
            }

            tokens.add(KEY + tokenType.name() + VariableConstant.SEPARATOR + token);
        }

        // id的映射
        tokens.add(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + authorization.getId());

        redisTemplate.delete(tokens);
    }

    @Override
    public OAuth2Authorization findById(String id) {
        Assert.hasText(id, "id 不能为空");

        return getOAuth2Authorization((RedisOAuth2Authorization) redisTemplate.opsForValue().get(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + id));
    }

    @Override
    public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
        Assert.hasText(token, "token 不能为空");
        OAuth2AuthorizationTokenType oAuth2AuthorizationTokenType = OAuth2AuthorizationTokenType.getInstance(tokenType.getValue());
        Assert.isTrue(oAuth2AuthorizationTokenType != null, "token类型不支持");

        String oAuth2AuthorizationId = (String) redisTemplate.opsForValue().get(KEY + oAuth2AuthorizationTokenType.name() + VariableConstant.SEPARATOR + token);
        if (StringUtils.hasText(oAuth2AuthorizationId)) {
            return getOAuth2Authorization((RedisOAuth2Authorization) redisTemplate.opsForValue().get(KEY + VariableConstant.ID + VariableConstant.SEPARATOR + oAuth2AuthorizationId));
        }

        return null;
    }

    /**
     * 转成OAuth2Authorization对象
     * @param authorization redis中存储的对象
     */
    private OAuth2Authorization getOAuth2Authorization(RedisOAuth2Authorization authorization) {
        if (authorization == null) {
            return null;
        }

        // 使用构造器生成OAuth2Authorization
        OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(RegisteredClient.withId(authorization.getRegisteredClientId()).build());
        builder.id(authorization.getId())
                .principalName(authorization.getPrincipalName())
                .authorizationGrantType(new AuthorizationGrantType(authorization.getAuthorizationGrantType()))
                .authorizedScopes(authorization.getAuthorizedScopes())
                .attributes((attrs) -> attrs.putAll(authorization.getAttributes()));

        if (StringUtils.hasText(authorization.getState())) {
            builder.attribute(OAuth2ParameterNames.STATE, authorization.getState());
        }

        // token
        authorization.getTokenValuesMap().forEach((key, value) -> {
            builder.token(key.getOAuth2Token(value), (metadata) -> metadata.putAll(value.getMetaData()));
        });

        return builder.build();

    }
}

4、解析token

当接口请求时,对token进行解析成为用户信息。

4.1 自定义用户类信息

自定义用户类信息,实现OAuth2AuthenticatedPrincipal 接口。可自行进行扩充。

public record OAuth2IntrospectionAuthenticatedPrincipal (String name,
                                                         String clientId,
                                                         Map<String, Object> attributes,
                                                         Collection<GrantedAuthority> authorities,
                                                         Principal principal) implements OAuth2AuthenticatedPrincipal {

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getName() {
        return name;
    }
}

4.2 将token解析成用户类

添加类,实现OpaqueTokenIntrospector 接口,在接口中根据token转换成用户类信息。

@Component
public class OpaqueTokenIntrospectorHandler implements OpaqueTokenIntrospector {

    private OAuth2AuthorizationService oAuth2AuthorizationService;

    public OpaqueTokenIntrospectorHandler(OAuth2AuthorizationService oAuth2AuthorizationService) {
        this.oAuth2AuthorizationService = oAuth2AuthorizationService;
    }

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2Authorization oAuth2Authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
        if (oAuth2Authorization == null) {
            throw new BadOpaqueTokenException("授权已经过期,请重新进行登录");
        }

        return new OAuth2IntrospectionAuthenticatedPrincipal(oAuth2Authorization.getPrincipalName(),
                oAuth2Authorization.getRegisteredClientId(),
                new HashMap<>(),
                new ArrayList<>(),
                oAuth2Authorization.getAttribute(Principal.class.getName()));
    }
}

4.3 将解析类添加到配置

在SecurityConfig中,添加oauth2ResourceServer配置信息。