OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无需将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。
采用令牌(token)的方式可以让用户灵活的对第三方应用授权或者收回权限。
互联网应用中最常见的 OAuth2 就是各种第三方登录,例如:QQ 授权登录、微信授权登录等。它们允许用户通过 QQ 或微信账号来登录其他网站或应用,而不需要为这些网站或应用创建新的账户。这些流程帮助开发者利用 QQ 和微信的大量用户基础,简化登录过程,同时为用户提供便利。
1. OAuth2 常用授权模式
1. 授权码模式
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
参与角色:客户端、认证中心,客户端负责拿令牌,认证中心负责发放令牌。
不是所有客户端都有权限请求令牌,需要事先在认证中心申请,比如微信并不是所有网站都能直接接入,而是要去微信后台开通这个权限。
至少要提前向认证中心申请的几个参数如下:
client_id:客户端唯一id,认证中心颁发的唯一标识
client_secret:客户端的秘钥,相当于密码
scope:客户端的权限
redirect_uri:授权码模式使用的跳转uri,需要事先告知认证中心。
1. 请求授权码
/oauth/authorize?client_id=&response_type=code&scope=&redirect_uri=
上述url中携带的参数如下:
- client_id:客户端的id,这个由认证中心分配,并不是所有的客户端都能随意接入认证中心。
- response_type:固定值为code,表示要求返回授权码。
- scope:表示要求的授权范围,客户端的权限。
- redirect_uri:跳转的uri,认证中心同意或者拒绝授权跳转的地址,如果同意会在uri后面携带一个code=xxx,这就是授权码。
2. 返回授权码
第1步请求之后,认证中心会要求登录、是否同意授权,用户同意授权之后直接跳转到redirect_uri(这个需要事先在认证中心申请配置),授权码会携带在这个地址后面,如下:
http://xxxx?code=NMoj5y
上述链接中的NMoj5y就是授权码了。
3. 请求令牌
客户端拿到授权码之后,直接携带授权码发送请求给认证中心获取令牌,请求的url如下:
/oauth/token?
client_id=&
client_secret=&
grant_type=authorization_code&
code=NMoj5y&
redirect_uri=
相同的参数同上,不同参数解析如下:
- grant_type:授权类型,授权码固定的值为authorization_code。
- code:这个就是上一步获取的授权码。
4. 返回令牌
认证中心收到令牌请求之后,通过之后,会返回一段JSON数据,其中包含了令牌access_token,如下:
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101
}
access_token则是颁发的令牌,refresh_token是刷新令牌,一旦令牌失效则携带这个令牌进行刷新。
2. 密码模式
密码模式比较简单,直接通过用户名、密码获取令牌,流程如下:
1. 请求令牌
认证中心要求客户端输入用户名、密码,认证成功则颁发令牌,请求的url如下:
/oauth/token?
grant_type=password&
username=&
password=&
client_id=&
client_secret=
参数解析如下:
- grant_type:授权类型,密码模式固定值为password。
- username:用户名。
- password:密码。
- client_id:客户端id。
- client_secret:客户端的秘钥。
2. 返回令牌
上述认证通过,直接返回JSON数据,不需要跳转,如下:
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101
}
access_token则是颁发的令牌,refresh_token是刷新令牌,一旦令牌失效则携带这个令牌进行刷新。
2. OAuth2.0 认证中心搭建
1. 配置 SecurityConfig 类
新版本的Spring Security 不需要继承WebSecurityConfigurerAdapter 类,亦不需要配置@EnableWebSecurity注解。
@Configuration
public class SecurityConfig {
// 配置 HTTP 安全管理
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.antMatchers("/public/**").permitAll() // 允许访问 "/public/**"
.anyRequest().authenticated() // 其他请求需要认证
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页面
.permitAll()
)
.logout(logout -> logout
.permitAll() // 登出设置
);
return http.build();
}
// 定义 AuthenticationManager Bean
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2. 配置令牌存储
1. 配置令牌存储在redis中
1. Redis 配置类
@Configuration
public class RedisAuthorizationConfig {
private final RedisTemplate<String, Object> redisTemplate;
public RedisAuthorizationConfig(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Bean
public OAuth2AuthorizationService authorizationService() {
return new RedisOAuth2AuthorizationService(redisTemplate);
}
@Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30)) // 设置访问令牌的有效期
.refreshTokenTimeToLive(Duration.ofDays(30)) // 设置刷新令牌的有效期
.build();
}
// 其他必要的配置,例如 JWT 签名等
}
2. 配置令牌存储在JWT中
1. JWT 配置类
@Configuration
public class JwtAuthorizationConfig {
@Bean
public JwtEncoder jwtEncoder() {
KeyPair keyPair = ... // 生成或加载密钥对
return new JwtEncoder() {
@Override
public Jwt encode(JwtEncoderParameters parameters) {
// 实现 JWT 的编码逻辑
return ...;
}
};
}
@Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30)) // 设置访问令牌的有效期
.refreshTokenTimeToLive(Duration.ofDays(30)) // 设置刷新令牌的有效期
.build();
}
// 其他必要的配置,例如客户端详情等
}
2. 生成密钥对
3. 客户端配置
开发和测试环境使用内存存储,可以快速迭代和调试,生产环境使用Mysql和redis存储。
1. 使用mysql数据库存储
1. 创建实体类
创建一个 RegisteredClient 实体类,用于映射数据库中的客户端信息:
@Entity
@Table(name = "registered_clients")
public class RegisteredClientEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 客户端唯一ID
private String clientId;
// 指定秘钥,使用加密算法加密了
private String clientSecret;
private String clientName;
// 给客户端分配的资源权限,对应的是资源服务
private String resourceIds;
// 认证中心支持的授权类型: authorization_code、password
private String authorizationGrantType;
// 跳转的uri
private String redirectUri;
// 定义客户端的权限,这里只是一个标识,资源服务可以根据这个权限进行鉴权
private String scope;
// 是否需要授权,设置为false则不需要用户点击确认授权直接返回授权码
private boolean autoApprove;
// Getters and Setters
}
2. 创建 Repository
创建一个 Spring Data JPA Repository 来管理 RegisteredClientEntity 实体:
public interface RegisteredClientRepository extends JpaRepository<RegisteredClientEntity, Long> {
RegisteredClientEntity findByClientId(String clientId);
}
3. 实现 Custom RegisteredClientRepository
创建一个自定义的 RegisteredClientRepository,用于将数据库中的信息转换为 Spring Security 所需的 RegisteredClient 对象:
@Repository
public class CustomRegisteredClientRepository implements RegisteredClientRepository {
private final RegisteredClientRepository registeredClientRepository;
public CustomRegisteredClientRepository(RegisteredClientRepository registeredClientRepository) {
this.registeredClientRepository = registeredClientRepository;
}
@Override
public void save(RegisteredClient registeredClient) {
// 将 RegisteredClient 保存到数据库
// ...
}
@Override
public RegisteredClient findById(String id) {
// 从数据库中根据 ID 查找 RegisteredClient
// ...
}
@Override
public RegisteredClient findByClientId(String clientId) {
// 从数据库中根据 clientId 查找 RegisteredClient
// ...
}
@Override
public List<RegisteredClient> findAll() {
// 从数据库中获取所有 RegisteredClient
// ...
}
}
4. 配置 Client Repository
在 OAuth2 客户端配置类中,使用自定义的 RegisteredClientRepository:
@Configuration
public class OAuth2ClientConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
return new CustomRegisteredClientRepository();
}
}
5. 总结
- 创建数据库实体、JPA Repository 和自定义的 RegisteredClientRepository以支持客户端信息的存储和检索。
- 在实际实现中,需要实现 save、findById 和 findByClientId 等方法,确保它们与数据库交互并返回正确的 RegisteredClient 实例。
- 通过这种方式,可以将客户端信息持久化存储在 MySQL 数据库中,使其在应用重启后仍然可用。
2. 使用redis进行存储
1. 实现 RedisRegisteredClientRepository
创建一个自定义的 RegisteredClientRepository,以便使用 Redis 存储客户端信息:
@Repository
public class RedisRegisteredClientRepository implements RegisteredClientRepository {
private final RedisTemplate<String, RegisteredClient> redisTemplate;
public RedisRegisteredClientRepository(RedisTemplate<String, RegisteredClient> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void save(RegisteredClient registeredClient) {
redisTemplate.opsForValue().set(registeredClient.getId(), registeredClient, 30, TimeUnit.DAYS);
}
@Override
public RegisteredClient findById(String id) {
return redisTemplate.opsForValue().get(id);
}
@Override
public RegisteredClient findByClientId(String clientId) {
return redisTemplate.opsForValue().get(clientId);
}
}
2. 配置客户端信息
创建一个配置类来初始化客户端信息并将其保存到 Redis 中:
@Configuration
public class OAuth2ClientConfig {
@Bean
public RegisteredClientRepository registeredClientRepository(RedisRegisteredClientRepository repository) {
// 创建客户端信息
RegisteredClient registeredClient = RegisteredClient.withId("client-id")
.clientId("your-client-id")
.clientSecret("{noop}your-client-secret") // {noop}表示不加密
.clientName("Your Client Name")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://localhost:8080/login/oauth2/code/{registrationId}")
.scope("read")
.build();
// 保存客户端信息到 Redis
repository.save(registeredClient);
return repository;
}
}
4. 授权码服务配置
1. 使用Mysql存储授权码
1. 创建授权码实体类
创建一个 OAuth2Authorization 实体类,用于映射数据库中的授权码信息:
@Entity
@Table(name = "oauth2_authorizations")
public class OAuth2AuthorizationEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String authorizationId; // 授权码 ID
private String clientId; // 客户端 ID
private String principalName; // 用户名
private String authorizationCode; // 授权码
private Instant createdAt; // 创建时间
private Instant expiresAt; // 过期时间
// Getters and Setters
}
2. 创建 Repository
创建一个 Spring Data JPA Repository 来管理 OAuth2AuthorizationEntity 实体:
public interface OAuth2AuthorizationRepository extends JpaRepository<OAuth2AuthorizationEntity, Long> {
OAuth2AuthorizationEntity findByAuthorizationId(String authorizationId);
}
3. 实现自定义授权服务
创建一个自定义的 OAuth2AuthorizationService 来处理授权码的存储和检索:
@Service
public class CustomOAuth2AuthorizationService implements OAuth2AuthorizationService {
private final OAuth2AuthorizationRepository authorizationRepository;
public CustomOAuth2AuthorizationService(OAuth2AuthorizationRepository authorizationRepository) {
this.authorizationRepository = authorizationRepository;
}
@Override
@Transactional
public void save(OAuth2Authorization authorization) {
OAuth2AuthorizationEntity entity = new OAuth2AuthorizationEntity();
entity.setAuthorizationId(UUID.randomUUID().toString());
entity.setClientId(authorization.getClientId());
entity.setPrincipalName(authorization.getPrincipalName());
entity.setAuthorizationCode(authorization.getAuthorizationCode().getTokenValue());
entity.setCreatedAt(authorization.getCreatedAt());
entity.setExpiresAt(authorization.getExpiresAt());
authorizationRepository.save(entity);
}
@Override
public OAuth2Authorization findById(String id) {
OAuth2AuthorizationEntity entity = authorizationRepository.findByAuthorizationId(id);
if (entity == null) {
return null; // 或者抛出异常
}
// 将实体转换为 OAuth2Authorization
return OAuth2Authorization.withId(entity.getAuthorizationId())
.clientId(entity.getClientId())
.principalName(entity.getPrincipalName())
.authorizationCode(entity.getAuthorizationCode())
.createdAt(entity.getCreatedAt())
.expiresAt(entity.getExpiresAt())
.build();
}
}
4. 配置授权服务
@Configuration
public class OAuth2AuthorizationConfig {
@Bean
public OAuth2AuthorizationService oauth2AuthorizationService(OAuth2AuthorizationRepository repository) {
return new CustomOAuth2AuthorizationService(repository);
}
}
5. 总结
- 创建数据库实体、JPA Repository 和自定义的 OAuth2AuthorizationService 以支持授权码的存储和检索。
- 确保实现 save 和 findById 方法,确保它们与数据库交互并返回正确的 OAuth2Authorization 实例。
- 通过这种方式,可以将授权码持久化存储在 MySQL 数据库中,确保其在应用重启后仍然可用。
2. 使用redis存储授权码
1. 配置 RedisConnectionFactory
@Configuration
public class RedisConfig {
// Configure the RedisConnectionFactory
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// Configuring Redis in standalone mode with default settings
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName("localhost"); // Redis server host
redisConfig.setPort(6379); // Redis server port
// If Redis requires password authentication
// redisConfig.setPassword("your-redis-password");
// Using LettuceConnectionFactory to create connection to Redis
return new LettuceConnectionFactory(redisConfig);
}
// RedisTemplate to interact with Redis
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// Setting key serializer
template.setKeySerializer(new StringRedisSerializer());
// Setting value serializer
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
2. Redis Token Store 配置
在 Spring Security 的 OAuth2 认证中心配置类中,配置 Redis 来存储授权码。可以通过配置 OAuth2AuthorizationService 和 OAuth2AuthorizationConsentService 来使用 Redis 进行存储。
@Configuration
public class OAuth2AuthorizationServerConfig {
// Redis Template for working with Redis
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
// OAuth2AuthorizationService using Redis to store authorization codes
@Bean
public OAuth2AuthorizationService authorizationService(RedisTemplate<String, Object> redisTemplate, RegisteredClientRepository registeredClientRepository) {
return new RedisOAuth2AuthorizationService(redisTemplate, registeredClientRepository);
}
// OAuth2AuthorizationConsentService using Redis to store consent information
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(RedisTemplate<String, Object> redisTemplate, RegisteredClientRepository registeredClientRepository) {
return new RedisOAuth2AuthorizationConsentService(redisTemplate, registeredClientRepository);
}
}
3. 授权码存储和操作
在这个配置中,RedisOAuth2AuthorizationService 用来存储授权码、刷新令牌等授权信息,而 RedisOAuth2AuthorizationConsentService 用来存储用户的授权同意信息。
- RedisOAuth2AuthorizationService:用于将授权信息(包括授权码)存储在 Redis 中。
- RedisOAuth2AuthorizationConsentService:用于将用户的同意记录存储在 Redis 中。
RedisOAuth2AuthorizationService 内部通过 RedisTemplate 实现授权码和令牌的存储、查找和删除。每次生成新的授权码时,它都会将其存储在 Redis 中,客户端通过授权码来换取访问令牌。
5. 令牌服务的配置
令牌服务配置主要包括 JwtEncoder、OAuth2TokenCustomizer(可选)和 OAuth2TokenIssuer 等部分。可以选择使用 JWT(JSON Web Token)作为令牌格式,或者选择其他存储和格式。
这里选择使用 JWT 来生成和验证令牌,并且配置了 JwtEncoder 和OAuth2TokenCustomizer。
@Configuration
public class AuthorizationServerConfig extends OAuth2AuthorizationServerConfigurerAdapter {
// Configure the JWT encoder
// JwtEncoder: 负责将令牌生成为 JWT 格式。这里使用 NimbusJwtEncoder,它是 Spring Security OAuth2 认证服务器的默认实现之一。
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
// Configure the JWK source (JSON Web Key Set) for JWT signing
// 配置 JSON Web Key (JWK) 来进行 JWT 签名和验证。这里使用 RSA 密钥对来签名 JWT。
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsaKey();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
// Generate RSA Key Pair for JWT signing
private static RSAKey generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();
}
// Configure the token services (including token customizer, if needed)
// 用于定制令牌的声明(claims)。在这里,我们可以为 JWT 添加自定义的声明。
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
return context -> {
if (OAuth2TokenClaimsContext.class.isAssignableFrom(context.getClass())) {
OAuth2TokenClaimsContext claimsContext = (OAuth2TokenClaimsContext) context;
claimsContext.getClaims().claim("custom-claim", "custom-value");
}
};
}
// Token settings for managing expiration times, reuse, etc.
// 用于设置令牌的过期时间、刷新令牌的使用策略等。你可以在这里自定义访问令牌和刷新令牌的有效期。
@Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1)) // Access token valid for 1 hour
.refreshTokenTimeToLive(Duration.ofDays(30)) // Refresh token valid for 30 days
.reuseRefreshTokens(true) // Allow refresh token reuse
.build();
}
}
在 Spring Security 新版本中,通过配置 JwtEncoder、OAuth2TokenCustomizer 以及 TokenSettings,可以轻松实现令牌的生成、签名和管理。基于 JWT 的无状态访问令牌非常适合分布式系统,且可以通过 RSA 密钥进行安全签名。如果你选择使用其他令牌类型(如不透明令牌),则需要结合存储机制,如 Redis 或数据库。
6. 令牌访问端点的配置
令牌访问端点(token endpoint)负责处理客户端的令牌请求。在 OAuth2 中,客户端通过令牌端点获取访问令牌(Access Token),这包括授权码模式、客户端凭据模式、刷新令牌等。
令牌端点的配置一般会涉及以下几个方面:
- 定义授权服务(Authorization Service)
- 定义令牌服务(Token Service)
- 配置安全约束,以确保只有经过认证的客户端才能访问端点
- 配置端点的路径和访问权限
在 Spring Security 新版本中,不需要显式地配置很多细节,因为 OAuth2AuthorizationServerConfiguration 类自动配置了很多端点。但是,仍然可以自定义端点的安全配置和相关的路径。
以下是配置令牌访问端点的完整示例代码:
1. 配置授权码数据库实现
这里是mysql的实现,redis同理。
@Service
public class JpaAuthorizationCodeServices implements AuthorizationCodeServices {
@Autowired
private EntityManager entityManager;
@Override
@Transactional
public OAuth2AuthorizationCode createAuthorizationCode(OAuth2Authorization authorization) {
OAuth2AuthorizationCode code = new OAuth2AuthorizationCode(UUID.randomUUID().toString());
entityManager.persist(code);
return code;
}
@Override
@Transactional
public OAuth2AuthorizationCode removeAuthorizationCode(String code) {
OAuth2AuthorizationCode authCode = findAuthorizationCode(code);
if (authCode != null) {
entityManager.remove(authCode);
}
return authCode;
}
@Override
public OAuth2AuthorizationCode findAuthorizationCode(String code) {
return entityManager.find(OAuth2AuthorizationCode.class, code);
}
}
@Configuration
public class SecurityConfig {
// Configuring the SecurityFilterChain to include the OAuth2 Authorization Server
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Apply the default OAuth2 Authorization Server security configuration
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// Custom security for OAuth2 endpoints
http
.exceptionHandling(exceptions ->
exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); // For resource server JWT validation
return http.build();
}
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JpaAuthorizationCodeServices();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean(); // 返回 AuthenticationManager
}
}
在上面的 SecurityFilterChain 配置中,OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) 方法会自动配置 OAuth2 的默认端点,包括 /oauth2/token 端点。
可以通过 HttpSecurity 对象自定义该端点的访问权限,或更改端点的路径。
如果需要对令牌端点进行更多自定义,比如修改令牌端点路径,或增加额外的安全性,可以通过如下方式进行配置:
@Configuration
public class OAuth2TokenEndpointConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer();
// Customize the token endpoint
authorizationServerConfigurer
.tokenEndpoint(tokenEndpoint -> {
tokenEndpoint
.accessTokenRequestConverter(new CustomAccessTokenRequestConverter()) // Custom token request converter (optional)
.accessTokenResponseHandler(new CustomAccessTokenResponseHandler()) // Custom token response handler (optional)
.errorResponseHandler(new CustomErrorResponseHandler()); // Custom error response handler (optional)
});
http.apply(authorizationServerConfigurer);
return http.build();
}
}
7. 资源服务器配置
配置资源服务器是确保 API 能够验证访问令牌并保护资源,其中包括OAuth2.0 令牌校验服务,通常是为了验证客户端请求所带的访问令牌是否合法。Spring Security 提供了内置的支持来验证 OAuth2.0 令牌(包括 JWT 令牌和自定义的令牌)。相关配置类如下:
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.antMatchers("/public/**").permitAll() // 公开路径无需认证
.antMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色的权限
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 需要 USER 或 ADMIN 角色的权限
.anyRequest().authenticated() // 其他请求需认证
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter()) // 自定义 JWT 权限转换器
)
);
return http.build();
}
// 自定义 JWT 权限转换器,将令牌中的 scope 或其他字段映射到权限
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); // 角色前缀
grantedAuthoritiesConverter.setAuthoritiesClaimName("scope"); // 令牌中的权限字段
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
配置说明
- SecurityFilterChain:定义了安全过滤器链,配置了哪些路径是公开的,哪些路径需要认证。
- JWT 验证:配置了 OAuth2 资源服务器使用 JWT 进行认证 (oauth2.jwt()),这意味着OAuth2 认证服务器会发出 JWT 令牌。
- JwtAuthenticationConverter:自定义 JWT 权限转换器,将 JWT 中的 scope 字段转换为 Spring Security 中的权限。
1. 配置应用程序属性
在 application.yml 中,需要配置 JWT 的相关属性,比如授权服务器的公钥或 JWK 地址:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://your-auth-server/.well-known/jwks.json
2. 令牌的验证流程
当资源服务器收到请求时,Spring Security 会自动从请求的 Authorization 头中解析出 Bearer Token,并通过配置的 JWK 或公钥验证令牌的合法性。验证成功后,将解析出的权限应用到当前用户的上下文中,以便后续的请求授权。
8. 配置AuthorizationServerConfig类
新版本的Spring Security不需要再继承AuthorizationServerConfigurerAdapter类了,亦不再需要@EnableAuthorizationServer注解。
@Configuration
public class AuthorizationServerConfig {
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http
.exceptionHandling(exceptions ->
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults()))
.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/login/oauth2/code/custom-client")
.scope(OidcScopes.OPENID)
.scope("read")
.scope("write")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("http://localhost:9000") // 设置 OAuth2 提供者的 URL
.build();
}
}
3. OAuth2.0 模式测试
启动认证中心和资源服务。
1. 授权码模式
1. 获取授权码
http://localhost:2003/auth-server/oauth/authorize?client_id=myjszl&response_type=code&scope=all&redirect_uri=http://www.baidu.com
浏览器访问,security需要登录,如下:
输入用户名user,密码123,成功登录。
此时来到了确认授权的页面,如下:
选择Apporove、确认授权,成功跳转到了百度页面,并且携带了授权码,如下:
这里的6yV2bF就是获取到的授权码。
2. 获取token
http://localhost:2003/auth-server/oauth/token?code=jvMH5U&client_id=myjszl&client_secret=123&redirect_uri=http://www.baidu.com&grant_type=authorization_code
注意:/oauth/token获取token的接口请求允许的方式要配置在授权服务器中,比如配置POST方式,代码如下:
.allowedTokenEndpointRequestMethods(HttpMethod.POST)
3. 访问资源服务
拿着令牌访问资源服务的/hello接口,请求如下:
请求头需要添加Authorization,并且值为Bearer+" "+access_token的形式。
2. 密码模式
密码模式比较简单,不用先获取授权码,直接使用用户名、密码获取token。
4. OAuth2.0 其他端点的测试
1. 刷新令牌
OAuth2.0提供了令牌刷新机制,一旦access_token过期,客户端可以拿着refresh_token去请求认证中心进行令牌的续期。请求的url如下:
http://localhost:2003/auth-server/oauth/token?client_id=myjszl&client_secret=123&grant_type=refresh_token&refresh_token=
2. 校验令牌
OAuth2.0还提供了校验令牌的端点,请求的url如下:
http://localhost:2003/auth-server/oauth/check_token?toke=