在微服务中,我们一般采用客户端模式进行微服务调用的安全保障,当我们获取到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,官网的意思就是这个繁琐的任务本不该由他们维护
那么我们在配置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信息了