Spring Authorization Server实战
Spring Authorizatin Server
Spring Authorizatin Server是一个框架,它提供了OAuth2.1和OpenID Connect 1.0规范以及其它相关规范的实现,它是基于Spring Security构建的
OAuth2.0协议介绍
OAuth是一个开放标准的授权协议,允许用户授权第三方应用访问其在某个服务提供者上的受保护资源,而无需将其实际的凭证(比如用户和密码)分享给第三方应用。
OAuth2.0协议的流程如下:
这个流程翻译后如下:
- 用户打开客户端后,客户端要求用户给与授权;
- 用户同意给客户端授权;
- 客户端使用上一步获得授权,向服务器申请令牌;
- 授权服务器对客户端认证后,向客户端发放令牌;
- 客户端使用令牌,向资源服务器申请获取资源;
- 资源服务器确认令牌有效后,向客户端开发资源;
授权流程案例
我们以阿里云盘使用支付宝授权登录来说明这个流程:
-
在阿里云盘登录界面,选择支付宝登录;
-
接着会跳转到支付宝授权的页面,让用户给阿里云盘授权获取用户在支付宝中的信息;
-
当用户点击同意后,会返回阿里云盘登录界面;
-
阿里云盘会使用获得的授权,向支付宝申请token,拿到token后,使用token去支付宝服务器获取用户信息;如果是第一次登录,会根据获取到的信息给用户创建一个阿里云盘的账号,并把支付宝上用户的信息和阿里云盘中创建的账号做一个映射,这样用户下次再登录,直接找到已有的账号登录即可;(这个步骤不需要用户做任何操作,只会在登录界面停留很短的时间,接着就跳转到阿里云盘主界面了)
OAuth2协议的应用场景
OAuth2协议的应用场景非常广泛,只要是涉及到第三方应用程序访问用户数据和资源的场景都可以使用
授权模式
OAuth2有四种授权模式,每种授权模式有不同的应用场景。四种模式如下:
- 授权码模式:使用最为广泛的模式;比如上面的阿里云盘登录使用的就是授权码模式;
- 密码模式(Password):密码模式是一种安全级别较低且要求资源拥有者(用户)完全信任服务器的模式。在OAuth2.1中该模式已经被废除,在第三方平台上使用密码模式是一种非常不安全的行为,假设某平台支持QQ登录,如果用户直接使用QQ账号、密码登录,则该平台就获取了用户的QQ账号、密码;
密码模式的流程如下:
- 客户端模式(Client Credentials Grant):客户端模式是指客户端以自己的名义进行授权,而不经过用户授权。客户端模式是安全级别最低且要求授权服务器对客户端高度信任的模式。因为客户端向服务器请求认证授权的过程中没有用户的参与,客户端通过提供自己在授权服务器上的信息即可在授权服务器完成认证授权,而客户端获取授权后即可向服务器请求资源。这种模式一般用于对接接口,比如对接北森之类系统的接口;
客户端模式的流程如下:
- 令牌刷新模式:如果令牌的有效期到了,如果让客户端重复之前的流程再去获取token,比较麻烦;OAuth2允许用户自动更新令牌。令牌刷新模式是对access_token过期的一种补办操作,OAuth2在给客户端颁发令牌的时候同时给客户端颁发了refresh_token,当access_token过期后,客户端拿refresh_token再去申请新的access_token即可
令牌刷新模式流程如下:
OAuth2.1协议
OAuth2.1协议主要是对OAuth2.0的改进,OAuth 2.1去掉了OAuth2.0中的密码模式、简化模式,增加了设备授权码模式,同时也对授权码模式增加了PKCE扩展。
点击查看OAuth2.1的内容
OpenID Connect 1.0协议
OpenID Connect 1.0 是 OAuth 2.0 协议之上的一个简单的身份层。其实就是客户端向认证服务器请求认证授权的时候,多返回一个 id_token,该 id_token 是一串使用 jwt 加密过的字符串。
OpenID Connect 1.0协议详细内容
OAuth实战
授权服务器
引入依赖
xml
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.1.4</spring-boot.version>
</properties>
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
相关配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* 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的缩写)
.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 过滤链配置(此处是纯Spring Security相关配置)
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
//设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
.anyRequest().authenticated()
)
// 由Spring Security过滤链中UsernamePasswordAuthenticationFilter过滤器拦截处理"login"页面提交的登录信息。
.formLogin(Customizer.withDefaults());
return http.build();
}
/**
* 设置用户信息,校验用户名、密码
*/
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("test")
.password("123456")
.roles("USER")
.build();
//基于内存的用户数据校验
return new InMemoryUserDetailsManager(userDetails);
}
/**
* 注册客户端信息
*
* 查询认证服务器信息
* http://127.0.0.1:9000/.well-known/openid-configuration
*
* 获取授权码
* http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile openid&redirect_uri=http://www.baidu.com
*
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oidc-client")
//{noop}开头,表示"secret"以明文存储
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置授权码模式,刷新令牌,客户端模式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//认证回调地址,接收认证服务器回传的code,需要和客户端配置的一致
.redirectUri("http://spring-oauth-client:9001/login/oauth2/code/messaging-client-oidc")
//没有客户端时使用
.redirectUri("http://www.baidu.com")
.postLogoutRedirectUri("http://127.0.0.1:8080/")
.clientName("web-client")
//设置客户端权限范围
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
//客户端设置用户需要确认授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
//配置基于内存的客户端信息
return new InMemoryRegisteredClientRepository(oidcClient);
}
/**
* 配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
*/
@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();
}
}
客户端
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
yml文件配置
yml
logging:
level:
org.springframework.security: trace
spring:
application:
name: auth-client
security:
oauth2:
client:
provider:
#认证服务器信息
oauth-server:
issuer-uri: http://spring-oauth-server:9000 #授权地址
authorizationUri: ${spring.security.oauth2.client.provider.oauth-server.issuer-uri}/oauth2/authorize #OAuth2认证服务器的授权端点地址
tokenUri: ${spring.security.oauth2.client.provider.oauth-server.issuer-uri}/oauth2/token #令牌获取地址
registration:
messaging-client-oidc:
provider: oauth-server #认证提供者,标识由哪个认证服务器进行认证,和上面的oauth-server进行关联
client-name: web-client #客户端名称
client-id: oidc-client #客户端id,从认证平台申请的客户端id
client-secret: secret #客户端秘钥
client-authentication-method: client_secret_basic #客户端认证方式,除了client_secret_basic还有client_secret_post、client_secret_jwt、private_key_jwt等
authorization-grant-type: authorization_code #使用授权码模式获取令牌(token)
redirect-uri: http://spring-oauth-client:9001/login/oauth2/code/messaging-client-oidc #回调地址,接收认证服务器回传code的接口地址
scope: #OAuth2客户端的权限范围
- profile #允许客户端获取用户的个人资料信息
- openid #penid 是一个特定的 scope 值,它用于 OpenID Connect 协议中。当客户端应用请求 openid 范围时,它表明应用想要验证用户的身份
资源服务器
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
配置
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
//所有的访问都需要通过身份认证
.anyRequest().authenticated())
.oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
}
配置文件
yml
server:
port: 9002
logging:
level:
org.springframework.security: trace
spring:
application:
name: spring-oauth-resource
security:
oauth2:
resource-server:
jwt:
issuer-uri: http://spring-oauth-server:9000
测试
直接访问资源服务器中的资源,不代码token
使用http://spring-oauth-client:9001/token地址,向客户端发起请求,获取token
携带token访问资源服务器上的资源