1、简介 {#1简介}
OAuth 是一种描述授权过程的开放标准。它可用于授权用户访问 API。例如,REST API 可以限制只有具有适当角色的注册用户才能访问。
OAuth 授权服务器负责认证用户身份,并签发包含用户数据和适当访问策略的访问令牌(Access Token)。
本将带你了解如何使用 Spring Security OAuth 授权服务器 实现一个简单的 OAuth 应用。
我们要创建一个 CS 应用,通过 REST API 获取资源服务器上的文章列表。客户端服务和服务器服务都需要 OAuth 身份认证。
2、授权服务器实现 {#2授权服务器实现}
先来看看 OAuth 授权服务器的配置。它作为文章资源和客户端服务器的身份认证源。
2.1、依赖 {#21依赖}
首先,在 pom.xml
中添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.0</version>
</dependency>
2.2、配置 {#22配置}
在 application.yml
文件中,设置 server.port
属性来配置认证服务器的运行端口:
server:
port: 9000
然后,就可以开始配置 Spring Bean 了。首先,需要一个 @Configuration
类,在该类中创建一些 OAuth 特有的 Bean。
第一个是客户端服务的 Repository。在本例中,使用 RegisteredClient
Builder 类创建一个客户端:
@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("articles-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope("articles.read")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
}
配置的属性如下:
- 客户端 ID(Client ID )- Spring 将用它来识别哪个客户端正在尝试访问资源
- 客户端 Secret Code(Client secret code) - 客户端和服务器都知道的 Secret Code,提供双方之间的信任
- 认证方法(Authentication method) - 使用 Basic Authentication,即用户名和密码
- 授权模式(Authorization Grant Type) - 允许客户端同时生成授权码(Authorization Code)和刷新令牌(Refresh Token)
- 重定向 URI(Redirect URI) - 客户端在基于重定向的模式中使用它
- 范围(Scope)- 该参数定义了客户端可能拥有的授权。在本例中,拥有
OidcScopes.OPENID
和自定义的articles.read
接下来,配置一个 Bean 来应用默认的 OAuth Security,并生成一个默认的表单登录页面:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
每个授权服务器都需要自己的 Token 签名密钥,以保持安全域之间的适当边界。
生成一个 2048 字节的 RSA 密钥:
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
除了签名密钥,每个授权服务器还需要有一个唯一的 issuer URL。
创建 ProviderSettings
Bean 将其设置为 localhost
的别名:http://auth-server
,端口为 9000
:
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("http://auth-server:9000")
.build();
}
此外,还需要在 /etc/hosts
文件中添加一个 127.0.0.1 auth-server
条目。这样,就可以在本地计算机上运行客户端和授权服务器,避免了 Session Cookie 在两者之间覆盖的问题。
然后,使用 @EnableWebSecurity
注解配置类启用 Spring Web Security 模块:
@EnableWebSecurity
public class DefaultSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
// ...
}
这里调用 authorizeRequests.anyRequest().authenticated()
来要求对所有请求进行身份认证。还通过调用 formLogin(defaults())
方法提供了基于表单的身份认证。
最后,定义一组用于测试的示例用户。在本例中,创建一个只有一个 admin 用户的 Repository:
@Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.build();
return new InMemoryUserDetailsManager(user);
}
3、资源服务器 {#3资源服务器}
现在,创建一个资源服务器,通过 GET 端点返回文章列表。这些端点应该只允许经过了授权服务器进行身份认证的请求。
3.1、依赖 {#31依赖}
首先,添加所有必要的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.5.4</version>
</dependency>
3.2、配置 {#32配置}
在 application.yml
文件中配置一些属性。首先是服务器端口:
server:
port: 8090
接下来是 Security 配置。需要使用之前在 ProviderSettings
Bean 中配置的主机和端口为身份认证服务器设置正确的 URL:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://auth-server:9000
现在,可以设置 Web 安全配置了。再次说明,对文章资源的每个请求都必须经过授权,并拥有适当的 articles.read
权限:
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.mvcMatcher("/articles/**")
.authorizeRequests()
.mvcMatchers("/articles/**")
.access("hasAuthority('SCOPE_articles.read')")
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
}
如上所示,调用了 oauth2ResourceServer()
方法,该方法将根据 application.yml
配置来配置与授权服务器的连接。
3.3、ArticlesController {#33articlescontroller}
最后,创建一个 REST Controller,通过 GET /articles
端点返回文章列表:
@RestController
@RequestMapping
public class ArticlesController {
@GetMapping("/articles")
public String[] getArticles() {
return new String[] { "Article 1", "Article 2", "Article 3" };
}
}
4、API 客户端 {#4api-客户端}
最后,创建一个 REST API 客户端,从资源服务器获取文章列表。
4.1、依赖 {#41依赖}
添加所有必要的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>1.0.9</version>
</dependency>
4.2、配置 {#42配置}
和前面一样,定义一些用于身份认证的配置属性:
server:
port: 8080
spring:
security:
oauth2:
client:
registration:
articles-client-oidc:
provider: spring
client-id: articles-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}"
scope: openid
client-name: articles-client-oidc
articles-client-authorization-code:
provider: spring
client-id: articles-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:8080/authorized"
scope: articles.read
client-name: articles-client-authorization-code
provider:
spring:
issuer-uri: http://auth-server:9000
现在,创建一个 WebClient
实例,以执行对资源服务器的 HTTP 请求。
使用标准实现,只增加一个 OAuth Authorization Filter:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
WebClient
需要依赖 OAuth2AuthorizedClientManager
。
创建一个默认实现:
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
最后,配置 Web Security:
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/articles-client-oidc"))
.oauth2Client(withDefaults());
return http.build();
}
}
如上,需要对每个请求进行身份认证。此外,还需要配置登录页面的 URL(在 .yml
配置文件中定义)和 OAuth 客户端。
4.3、ArticlesController {#43articlescontroller}
最后,创建 Controller 访问数据。使用之前配置的 WebClient
向资源服务器发送 HTTP 请求:
@RestController
public class ArticlesController {
private WebClient webClient;
@GetMapping(value = "/articles")
public String[] getArticles(
@RegisteredOAuth2AuthorizedClient("articles-client-authorization-code") OAuth2AuthorizedClient authorizedClient
) {
return this.webClient
.get()
.uri("http://127.0.0.1:8090/articles")
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String[].class)
.block();
}
}
在上例中,通过 OAuth2AuthorizedClient
类从请求中获取 OAuth Authorization Token。Spring 会使用 @RegisterdOAuth2AuthorizedClient
注解自动将其绑定,并进行适当的标识。在本例中,它来自之前在 .yml
文件中配置的 article-client-authorizaiton-code
。
该 Authorization Token 会进一步传递给 HTTP 请求。
4.4、访问文章列表 {#44访问文章列表}
打开浏览器并尝试访问 http://127.0.0.1:8080/articles
页面时,会被自动重定向到 http://auth-server:9000/login
URL 下的授权服务器登录页面:
提供正确的用户名和密码后,授权服务器会将我们重定向到请求的 URL,即文章列表。
进一步对文章端点的请求无需登录,因为 Access Token 将保存在 cookie 中。
5、总结 {#5总结}
本文介绍了如何设置、配置和使用 Spring Security OAuth 授权服务器。
Ref:https://www.baeldung.com/spring-security-oauth-auth-server