Spring Authorization Server 介绍
Spring Authorization Server 是一个提供OAuth 2.1和OpenID Connect 1.0规范以及其他相关规范的实现的框架。它构建在Spring Security之上,为构建 OpenID Connect 1.0 身份提供商和 OAuth2 授权服务器产品提供安全、轻量级和可定制的基础。
框架提供五种授权模式
- Authorization Code(授权码模式)
- Client Credentials(客户端模式)
- Refresh Token(令牌刷新)
- Device Code(设备码模式)
- Token Exchange(token交换)
本篇文章主要讲解常用的授权码模式、客户端模式、令牌刷新这三种模式,以及如何自己扩展授权模式(以账号密码模式为例)
框架提供两种token格式
- Self-contained (JWT) (信息透明)
- Reference (Opaque) (信息不透明)
本篇文章会讲解两种token的原理,如何使用不同格式的token,如何自定义token内容的扩展,以及如何自己自定义token生成器(以用短字符串自定义不透明token为例)
入门
创建一个新项目 添加依赖
使用目前最新版Spring Boot 3.4.0 要求JDK版本大于等于17
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
</dependencies>
创建配置类,添加基本配置
package chick.authorization.security;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, Customizer.withDefaults())
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
)
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);
http
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
// 用于检索用户进行身份验证
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withUsername("admin")
.password(passwordEncoder().encode("123123"))
.roles("admin")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
// 用于密码加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 用于管理客户端
@Bean
public RegisteredClientRepository registeredClientRepository() {
TokenSettings tokenSettings = TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1)) // 设置访问令牌有效期为1小时
.refreshTokenTimeToLive(Duration.ofDays(30)) // 设置刷新令牌有效期为30天
//.accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 这个设置是开启不透明token
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 使用透明token
.build();
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("chick")
.clientSecret(passwordEncoder().encode("123456"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("https://www.baidu.com")
.postLogoutRedirectUri("http://127.0.0.1:8000/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.tokenSettings(tokenSettings)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
// 用于签署访问令牌
@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);
}
// 启动时生成的密钥,用于创建上面的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;
}
// 用于解码签名访问令牌
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
// 用于配置 Spring Authorization Server
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
目前已解锁
- Authorization Code(授权码模式)
- Client Credentials(客户端模式)
- Refresh Token(令牌刷新)
测试授权码模式
浏览器访问:http://127.0.0.1:8000/oauth2/authorizeresponse_type=code&client_id=chick&scope=openid&redirect_uri=https://www.baidu.com
会重定向到登录页
输入上面UserDetailsService中设置的用户名密码 登录会重定向到百度并 获取到code
使用code获取token等信息
POST /oauth2/token HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&redirect_uri=https://www.baidu.com&code=9ZAclrji.....
测试令牌刷新模式
使用上一步返回的refresh_token
POST /oauth2/token HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&code=9ZAclrji.....
测试客户端授权模式
有多种方式,这里介绍常用的client_secret_basic和client_secret_post
POST /oauth2/token HTTP/1.1
Authorization: Basic Base64Encode(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
POST /oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=