使用 spring-authorization-server 时如何将 token 存储在 redis 中

494 阅读6分钟

在微服务中,我们一般采用客户端模式进行微服务调用的安全保障,当我们获取到token的时候是存在内存中的,但是在授权服务器集群化的情况下就无法满足要求,这时候我们要把token存入redis

1. spring-security-oauth2

spring-security-oauth2中我们可以定义TokenStore:

@Bean
public TokenStore redisTokenStore() {
    RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
    redisTokenStore.setPrefix(redisTokenPrefix);
    return redisTokenStore;
}

在spring-security-oauth2 中有内置的与redis集成的支持,以上这个配置可以很方便的把token存入redis中,但是该包spring-security-oauth2已经被废弃了,在springboot3中无法使用

2.spring authorization server

在spring authorization server中并没有与redis集成的支持,具体可参考#558,官网的意思就是这个繁琐的任务本不该由他们维护

image.png

那么我们在配置spring authorization server时如何将 token 存储在 redis 中呢?

授权服务器如下

/**
 * 该类是授权服务器的配置类
 */

@Configuration
@EnableWebSecurity

public class AuthorizationServerConfig{


    /**
     * 配置密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10);
    }


    /**
     * 配置内存中的用户存储
     */
    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        // 定义一个用户 "admin",角色为 "ROLE_ADMIN"
        UserDetails userDetails = User.builder()
                .username("admin")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN")
                .build();

        // 使用 InMemoryUserDetailsManager 管理用户
        return new InMemoryUserDetailsManager(userDetails);
    }

    /**
     * 配置认证管理器(AuthenticationManager)
     */
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, UserDetailsService userDetailsService) throws Exception {
        return http.getSharedObject(AuthenticationManager.class);
    }

    /*
    配置注册客户端
     */
    @Bean                                                            //这里会自动注入我自定义的配置
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
        RegisteredClient build = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client") //  第三方客户端的名称
                .clientSecret(passwordEncoder.encode("coin-secret"))// 客户端秘钥
                .scope("all") //第三方客户端额度授权范围  指定客户端的权限范围,例如允许访问用户的只读数据或完全控制
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)//授权码模式
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) //刷新token模式
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) //客户端模式 不涉及用户的授权流程,仅基于客户端的 client_id 和 client_secret 验证。通常用于服务之间的授权。
                .tokenSettings(TokenSettings.builder()
                        .accessTokenTimeToLive(Duration.ofHours(1))
                        .refreshTokenTimeToLive(Duration.ofDays(7)).build())
//                .redirectUri("http://localhost:9999/login/oauth2/code/demo-client") ////认证回调地址,接收认证服务器回传的code,需要和客户端配置的一致
                .redirectUri("https://www.baidu.com")
                //客户端设置用户需要确认授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
//                .tokenSettings(TokenSettings.builder().build())
                //客户端的权限范围
//                .scope(OidcScopes.OPENID)
                .build();

        return new InMemoryRegisteredClientRepository(build);
    }


    /**
     *  Spring Authorization Server 相关配置
     *  主要配置OAuth 2.1和OpenID Connect 1.0
     */
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                //开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)   就会返回一个ID token
                .oidc(Customizer.withDefaults());
        http
                //将需要认证的请求,重定向到login进行登录认证。
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // 使用jwt处理接收到的access token
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));



        return http.build();
    }


    /**
     * 该方法是spring security的配置
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    @Order(2)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.csrf(customize->customize.disable())
                .authorizeHttpRequests(authorizeRequests ->authorizeRequests.anyRequest().authenticated())
                .formLogin(Customizer.withDefaults()) // 使用表单登录(适合手动测试)
                .build();
    }



    @Bean  //用于签署访问令牌。
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); //公钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); //私钥
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    /*
           深沉RSA密钥对,给上面jwkSource() 方法提供密钥对
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /*
        配置JWT解析器
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 配置授权服务器请求地址
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        //什么都不配置,则使用默认地址
        return AuthorizationServerSettings.builder().build();
    }

以上配置完成的情况下,token是存在内存中的,这时候我们要要创建一个自定义类实现OAuth2AuthorizationService

/**
 * 它用于管理 OAuth2 授权信息。OAuth2AuthorizationService 是 Spring Authorization Server 提供的接口,
 * 负责存储、检索和删除 OAuth2 授权相关的信息(例如授权码、访问令牌、刷新令牌等)
 */
@Component
@Slf4j
public class MyTokenAuthService implements OAuth2AuthorizationService {

    @Autowired
    private  RedisTemplate<String, String> redisTemplate;
    @Autowired
    @Lazy
    private  ObjectMapper objectMapper;


    /**
     * 该方法用于将一个 OAuth2 授权对象 (OAuth2Authorization) 持久化存储。通常,
     * 在 OAuth2 授权服务器中,当用户授权客户端应用程序访问资源时,会生成一个授权对象。
     * 这个对象通常包含访问令牌、刷新令牌以及相关的客户端信息、授权范围等信息
     * @param authorization the {@link OAuth2Authorization}
     */
    @Override
    public void save(OAuth2Authorization authorization) {

        try {
            // 将 OAuth2Authorization 对象转为 JSON 字符串
            String json = objectMapper.writeValueAsString(authorization);
            // 使用授权 ID 作为 Redis 键
            redisTemplate.opsForValue().set(authorization.getId(), json);
            log.info("OAuth2Authorization 被存入 Redis 授权ID为: {}", authorization.getId());
        } catch (JsonProcessingException e) {
            log.error("序列化失败或存入redis失败:", e);
        }


    }

    /**
     * 该方法用于移除指定的 OAuth2 授权对象。一般来说,这意味着删除某个授权信息(例如,当令牌过期或用户撤销授权时)
     * @param authorization the {@link OAuth2Authorization}
     */
    @Override
    public void remove(OAuth2Authorization authorization) {
        // 使用授权 ID 删除 Redis 中的授权信息
        redisTemplate.delete(authorization.getId());
        log.info("redis中的{},token被删除",authorization.getId());
    }

    /**
     * 该方法用于根据授权 ID 查询并返回 OAuth2 授权对象。授权 ID 是每个授权对象唯一的标识符,可以用来查找特定的授权信息
     * @param id the authorization identifier
     * @return
     */
    @Override
    public OAuth2Authorization findById(String id) {
        String json = redisTemplate.opsForValue().get(id);
        if (json != null) {
            //如果读取的json不为空则转换为对应的OAuth2Authorization 信息返回
            return objectMapper.convertValue(json, OAuth2Authorization.class);
        }
        //否则返回null
        return null;
    }

    /**
     * 该方法用于根据令牌和令牌类型查询并返回相应的 OAuth2 授权对象。例如,访问令牌(Access Token)和刷新令牌(Refresh Token)是两种常见的令牌类型,你可以根据令牌的值来查找关联的授权信息
     * @param token the token credential
     * @param tokenType the {@link OAuth2TokenType token type}
     * @return
     */
    @Override
    public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
        // 在 Redis 中根据令牌和令牌类型查找授权信息
        String key = token + ":" + tokenType.toString();
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            // 反序列化为 OAuth2Authorization 对象
            try {
                return objectMapper.readValue(json, OAuth2Authorization.class);
            } catch (JsonProcessingException e) {
                log.error("序列化失败",e);
            }
        }
        return null;
    }



    /*
       这个方法是用于配置 RedisTemplate 的 Spring Bean
    */
    @Bean
    public RedisTemplate<String, String> getRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, String> stringObjectRedisTemplate = new RedisTemplate<>();
        stringObjectRedisTemplate.setConnectionFactory(redisConnectionFactory); //给该模板设置redis连接仓库
        stringObjectRedisTemplate.setKeySerializer(new StringRedisSerializer()); //设置键的序列化方式
        stringObjectRedisTemplate.setValueSerializer(new StringRedisSerializer()); ////设置值的序列化方式
        return stringObjectRedisTemplate;
    }

    /*
        ObjectMapper 是 Jackson 提供的一个类,用于在 Java 对象与 JSON 之间进行转换(序列化和反序列化)
     */
    @Bean
    public ObjectMapper getObjectMapper() {
        ObjectMapper objectMapper1 = new ObjectMapper();
        objectMapper1.registerModule(new JavaTimeModule());
        objectMapper1.findAndRegisterModules();
        return objectMapper1;
    }

同时在application.yml中配置redis相关信息

server:
  port: 9999
spring:
  application:
    name: authorization-server
  cloud:
    nacos:
      discovery:
        server-addr: nacos-server:8848
 # 用于控制台输出过滤器链路
  data:
    redis:
      host: ip
      port: 6379
      password: password
logging:
  level:
    org.springframework.security: trace

pom.xml配置如下

<!--     服务发现依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--   这里使用的是Spring Authorization Server 来做授权服务器     -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
<!--    web层的依赖    -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--    redis依赖    -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

这样就可以在redis中存入相关token信息了

image.png