您现在的位置是:首页 >其他 >Spring Security Oauth2.1 最新版 1.1.0 整合 gateway 完成授权认证(基于 springboot 3.1)网站首页其他
Spring Security Oauth2.1 最新版 1.1.0 整合 gateway 完成授权认证(基于 springboot 3.1)
目录
Spring Authorization Server 1.1.0官方文档
授权服务AuthorizationServerConfig配置
背景
基于 Spring Cloud Alibaba 架构下,需要一个统一授权中心,与 gateway 配合使用实现微服务的授权与认证,下面主要介绍整个集成过程,基于springboot3.1最新版
版本
Spring Boot 3.1
最新发布的springboot3.1版本对 oauth2 提供了默认的支持,可以引用下面的依赖来快速构建,为了体验新版本特性,我这边切换到了 3.1版本
Spring Boot 3.1 提供了一个 spring-boot-starter-oauth2-authorization-server 启动器,可以支持 Spring Authorization Server 的自动配置,轻松配置基于 Servlet 的 OAuth2 授权服务器,同时@EnableAuthorizationServer这些注解也早已废弃
Spring Authorization Server 1.1.0官方文档
基础
spring security
关于springsecurity的基础知识,之前写过一篇 springboot 与 Spring Security 集成的基于 jwt的授权的,可以看下面的
(296条消息) springboot 2.7整合spring security 5.7整合jwt实现用户登录注册与鉴权全记录_ricardo.M.Yu的博客-CSDN博客
OAuth2.0
OAuth2.0可以提供一个统一的认证服务。主要模块如下:
模块构成
- Resource owner(资源拥有者):拥有该资源的服务或用户,如我们自己或者资源网站
- Authorization server(认证服务器):即用来认证与颁发令牌(如token)的服务
- Resource server(资源服务器):拥有资源的服务,如我们要访问的网站
- Client(客户端):即访问的客户端,如我们自己用的访问网站
授权方式
- 授权码模式(authorization_code):最正规的模式,客户端先将用户导向认证服务器,登录后获取授权码,然后进行授权,最后根据授权码获取访问令牌
- 刷新模式(refresh_token):用刷新码获取
- 客户端模式(client_credentials):第三方应用自己本身需要获取资源
详见 AuthorizationGrantType 这个类
下面的密码模式已经被废弃
- 密码模式(resource owner password credentials):直接带用户名和密码去向认证服务器申请令牌
集成过程
我下面会分为三个阶段逐次递进改造,
- 第一阶段:官方demo演示与组件讲解测试
- 第二阶段:个性化改造
- 第三阶段:集成 springcloud gateway 完成分布式授权改造
官方demo
代码集成
依赖
只需要下面的这一个依赖,前提springboot 版本为 3.1
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
授权服务AuthorizationServerConfig配置
spring 官方在快速开始里面给出了下面的默认最小配置,
我先粘下来再介绍,代码结构大概这样,一共两个配置类
AuthorizationServerConfig
@Configuration
public class AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
//针对 Spring Authorization Server 最佳实践配置
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oidc-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://www.baidu.com")
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/oidc-client")
.postLogoutRedirectUri("http://127.0.0.1:8080/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.scope("all")
// 设置 Client 需要页面审核授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
/**
* 默认发放令牌
* @return
*/
@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);
}
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);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
DefaultSecurityConfig
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(new AntPathRequestMatcher("/actuator/**"),
new AntPathRequestMatcher("/oauth2/**"),
new AntPathRequestMatcher("/**/*.json"),
new AntPathRequestMatcher("/**/*.html")).permitAll()
.anyRequest().authenticated()
)
.cors(Customizer.withDefaults())
.csrf((csrf) -> csrf.disable())
// .httpBasic(Customizer.withDefaults())
// // Form login handles the redirect to the login page from the
// // authorization server filter chain
.formLogin(Customizer.withDefaults())
;
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
分别介绍下这几个@Bean配置,也是 AuthorizationServer 的几个重要概念。
重要组件
SecurityFilterChain -> authorizationServerSecurityFilterChain: Spring Security的过滤器链,用于协议端点的。
SecurityFilterChain -> defaultSecurityFilterChain: Spring Security的过滤器链,用于Spring Security的身份认证
UserDetailsService :主要进行用户身份验证
RegisteredClientRepository:主要用于管理客户端
JWKSource:用于签名访问令牌
KeyPair: 启动时生成的带有密钥的KeyPair实例,用于创建上面的JWKSource
JwtDecoder:JwtDecoder的一个实例,用于解码已签名的访问令牌
AuthorizationServerSettings:用于配置Spring Authorization Server的AuthorizationServerSettings实例。
测试
为了方便测试,上面的配置中,客户端的回调地址我已经改成了 百度的,授权方式用授权码模式,认证方式用client_secret_basic
服务启动,端口为9000
查看授权服务配置
地址:
调用 http://127.0.0.1:9000/.well-known/openid-configuration
后,查看地址配置如下:其实就是每个请求的url
详细的是下面
{
"issuer": "http://127.0.0.1:9000",
"authorization_endpoint": "http://127.0.0.1:9000/oauth2/authorize",
"device_authorization_endpoint": "http://127.0.0.1:9000/oauth2/device_authorization",
"token_endpoint": "http://127.0.0.1:9000/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"jwks_uri": "http://127.0.0.1:9000/oauth2/jwks",
"userinfo_endpoint": "http://127.0.0.1:9000/userinfo",
"end_session_endpoint": "http://127.0.0.1:9000/connect/logout",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"revocation_endpoint": "http://127.0.0.1:9000/oauth2/revoke",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"introspection_endpoint": "http://127.0.0.1:9000/oauth2/introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}
访问授权服务
浏览器地址栏输入
http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=message.read openid&redirect_uri=http://www.baidu.com
用这个请求来模拟客户端,实际开发中,其实是先访问资源服务,由资源服务来拼接这几个参数来重定向到授权服务的,参数意义如下,这些参数都是需要再上面RegisteredClientRepository配置过的
- response_type:这个意思是相应的方式为code码
- client_id:即客户端的id,即上面配置中在 RegisteredClientRepository 配置的
- scope:请求授权范围,也需要在上面的配置中
- redirect_uri:授权通过后,重定向回来的地址
输入完上面的地址后,会重定向到下面这个登录页面,
我们输入上面配置好的用户名密码:
user
password
点击登录
授权
登录过后,会到下面这个授权页面,点击授权范围,然后点击 submit
回调
授权通过后,授权服务回调到了百度的地址,然后附带这我们的授权码,如下图
获取 access_token
拿到授权码之后,可以用postman测试来获取 access_token
测试接口参数
Header
请求体
curl命令如下
curl --location --request POST 'http://localhost:9000/oauth2/token?grant_type=authorization_code&code=a_lOQegEwElR09Sj6auVpBdYGgnhhK0uz1Uks286ei_zkbyDFKII2uf7gMIF7CU4cLN8ZEY3EsSq9jMAZ-Rmtmlq5pI6KPB95LMQg9fFirFg2wWjdd5PEwQLMEogY9B6&redirect_uri=http%3A%2F%2Fwww.baidu.com'
--header 'Authorization: Basic b2lkYy1jbGllbnQ6c2VjcmV0'
参数说明:
- grant_type:即授权方式,authorization_code即授权码模式
- code:即授权码,上面重定向到百度给我们的授权码
- redirect_uri:重定向的url
- header中的 Authorization参数:因为我们用的客户端认证方式 为 client_secret_basic ,这个需要传参,还有一些其他的认证方式,具体参数说明如下
- client_secret_basic: 将 clientId 和 clientSecret 通过 ‘:’ 号拼接,( clientId 和 clientSecret 都在上面配置中,)并使用 Base64 进行编码得到一串字符,再在前面加个 注意有个 Basic 前缀(Basic后有一个空格), 即得到上面参数中的 Basic b2lkYy1jbGllbnQ6c2VjcmV0
- client_secret_post :clientId 和 clientSecret 放到表单去发送请求。如下图:
使用我们的 client_secret_basic 方式传参,接口调用结果:
已经正常拿到了 access_token。
完整的过滤器执行顺序,控制台输出
获取用户信息
获取用户信息接口为 /userinfo,注意需要有 opid 的授权范围,需要传参的值为 上面获取到的access_token,并在前面拼上 Bearer
参数说明
Authorization:值格式为 Bearer + ${access_token}, 注意 Bearer 后面附带空格
curl命令
curl --location --request POST 'http://127.0.0.1:9000/userinfo'
--header 'Authorization: Bearer eyJraWQiOiI4ZDc5YTIwNi1kOWZhLTQ5NWQtODJkMi1iMzk2MjQwNGQ4YmIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJuYmYiOjE2ODY3MzM4MTYsInNjb3BlIjpbIm9wZW5pZCIsIm1lc3NhZ2UucmVhZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJleHAiOjE2ODY3MzQxMTYsImlhdCI6MTY4NjczMzgxNn0.AiGV5LIl8a4_7a7L2gbR61sjvHVLW4dZ6cElAwsWZnp-P7ocQT119KIASTPv138MU6ZK2_aF_-ER5FKaFQVSOj10Fy_Gv9PXa2ExrzTajfkPtA_t63jCcazzllaVWY4QIVD4fU8hPe6zDwjNOOX8R7hJFu2qtZ8V3bhzTlC0M4XWDAQ0goymYrAnVq8BR6hRm5-pY4nMCUZPFCeEFqGnl68EGRzosdSQeuRd-PtzB837i-C7lxqIjs4Y5hZ9mQw3R1zfa0WoP2KeN8K3WjyTIYd9PvrLIFCB5Zhj54sdNpZTy7wwC-oCVzwFFCEkgY-vprfgk4e4sZ10Lx60j--fHA'
--header 'Cookie: JSESSIONID=7B10DA37A285902E4AEE4586AC181343'
效果如下:
默认返回的只有用户名,其他的数据,需要我们来重写一些东西获取
过滤器执行链
BearerTokenAuthenticationFilter: 检验token
AuthenticationEntryPointFailureHandler
AuthenticationFailureHandler
OidcUserInfoEndpointFilter
个性化改造
正在改造中。。。
集成GateWay
正在集成中。。。
关于 Spring Cloud Alibaba 的基础环境搭建,可以看下面的文章,很详细,本文主要介绍集成OAuth2的过程
Spring Cloud Alibaba 最新版本整合完整使用及与各中间件集成(基于Spring Boot 3.0.x)_ricardo.M.Yu的博客-CSDN博客
代办事项
现在需要做的改造如下:
1、新建授权服务,集成 oauth2-authorization-server ,即auth模块做授权中心
2、修改业务模块,集成 oauth2-client,即做资源中心与客户端
3、修改网关模块,即 gateway 做相关修改
Oauth2主要结构
OAuth2AuthorizationEndpointFilter: 针对 /login 或自行请求 授权码的处理器
OAuth2TokenEndpointFilter:针对获取 token 时的处理器
ProviderManager:
OAuth2ClientAuthenticationFilter
OAuth2TokenEndpointFilter
ClientSecretAuthenticationProvider
DelegatingAuthenticationConverter
OAuth2AuthorizationCodeAuthenticationProvider
OAuth2AuthorizationEndpointFilter
UsernamePasswordAuthenticationFilter
sql脚本
我直接整理好了
/*
IMPORTANT:
If using PostgreSQL, update ALL columns defined with 'blob' to 'text',
as PostgreSQL does not support the 'blob' data type.
*/
CREATE TABLE oauth2_authorization (
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
authorized_scopes varchar(1000) DEFAULT NULL,
attributes blob DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at timestamp DEFAULT NULL,
authorization_code_expires_at timestamp DEFAULT NULL,
authorization_code_metadata blob DEFAULT NULL,
access_token_value blob DEFAULT NULL,
access_token_issued_at timestamp DEFAULT NULL,
access_token_expires_at timestamp DEFAULT NULL,
access_token_metadata blob DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value blob DEFAULT NULL,
oidc_id_token_issued_at timestamp DEFAULT NULL,
oidc_id_token_expires_at timestamp DEFAULT NULL,
oidc_id_token_metadata blob DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
refresh_token_expires_at timestamp DEFAULT NULL,
refresh_token_metadata blob DEFAULT NULL,
user_code_value blob DEFAULT NULL,
user_code_issued_at timestamp DEFAULT NULL,
user_code_expires_at timestamp DEFAULT NULL,
user_code_metadata blob DEFAULT NULL,
device_code_value blob DEFAULT NULL,
device_code_issued_at timestamp DEFAULT NULL,
device_code_expires_at timestamp DEFAULT NULL,
device_code_metadata blob DEFAULT NULL,
PRIMARY KEY (id)
);
CREATE TABLE oauth2_authorization_consent (
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorities varchar(1000) NOT NULL,
PRIMARY KEY (registered_client_id, principal_name)
);
CREATE TABLE oauth2_registered_client (
id varchar(100) NOT NULL,
client_id varchar(100) NOT NULL,
client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
client_secret varchar(200) DEFAULT NULL,
client_secret_expires_at timestamp DEFAULT NULL,
client_name varchar(200) NOT NULL,
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000) DEFAULT NULL,
post_logout_redirect_uris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id)
);