Spring OAuth 简单实践
引言
最近在了解OAuth2.0,一直想搞一个自己的类似于SakuraFrp使用的OpenID授权站,就想自己写一个。找的很多国内教程用的包都是 spring-cloud
下的关于 oauth
的包,或是直接使用老版本的 security-oauth
包,由于 spring-security
最新版是 6.x
,教程的版本太老,且想使用 start.spring.io
中提供的 spring-boot-starter-oauth2-xxx
使用配置文件快速开发,写下本文记录。
环境:Jdk17
, Spring 3.1.4
实现
简单使用
使用 Client Credentials Grant
客户端模式对 Authorization-Server
进行简单使用
项目创建
新建一个空项目 oauth-demo
新建一个 authorization-server
模块
添加如下依赖:
代码语言:javascript复制implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
implementation("org.springframework.boot:spring-boot-starter-web")
修改配置文件
代码语言:javascript复制server:
port: 8080
spring:
application:
name: authorization-server
security:
oauth2:
authorization-server:
client:
demo-0:
registration:
client-id: demo-0
# {noop} 不加密,明文
client-secret: "{noop}passwd"
client-name: demo-0
client-authentication-methods:
- client_secret_basic
# 客户端授权模式
authorization-grant-types:
# 客户端模式
- client_credentials
logging:
level:
org.springframework.security: trace
启动项目
测试
使用 Insomnia
或 Postman
进行测试
向 http://127.0.0.1:8080/oauth2/token
发送 POST
请求
Basic Auth
- 设置请求体
- 发送请求
可以看到返回了 access_token
和过期时间,客户端模式的具体介绍请看参考2
授权码模式
使用 authorization-server 同时作为 授权服务和资源服务
使用 client-1 作为客户端
authorization-server
配置文件:
代码语言:javascript复制server:
port: 8080
spring:
application:
name: authorization-server
security:
# 提供的user,仅供测试使用
user:
name: user
password: "{noop}password"
roles: USER
oauth2:
authorization-server:
client:
# 客户端标识
client-1:
registration:
client-id: client-1
client-secret: "{noop}password2"
# 授权方式:授权码,或使用refresh_token拿新的access_token
authorization-grant-types:
- "authorization_code"
- "refresh_token"
# 客户端验证方式
client-authentication-methods:
- client_secret_basic
# 在同意授权后重定向的uri
redirect-uris:
- http://127.0.0.1:8081/login/oauth2/code/client-1
post-logout-redirect-uris:
- http://127.0.0.1:8081/logout
# 提供的权限
scopes:
- "openid"
- "profile"
- "read"
- "write"
require-authorization-consent: true
token:
access-token-time-to-live: 3600s
refresh-token-time-to-live: 7200s
logging:
level:
org.springframework.security: trace
client-1
项目创建
依赖
代码语言:javascript复制implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-web")
Config
代码语言:javascript复制@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(o -> o
.requestMatchers("/").permitAll()
.anyRequest().authenticated())
// 用于解决非https下 oauth Not injecting HSTS header since it did not match request to ... 问题
.headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(0)
.includeSubDomains(true)
)
)
.oauth2Login(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.build();
}
}
Controller
提供两个接口,一个用于测试是否启动成功,一个用于跳转OpenID登录
代码语言:javascript复制@RestController
@RequestMapping("/")
public class AppController {
private final AppService appService;
public AppController(AppService appService) {
this.appService = appService;
}
@GetMapping
public String getPublicData() {
return "Public data";
}
@GetMapping("/private-data")
public String getPrivateData() {
return appService.getJwtToken();
}
}
Service
getJwtToken()
需要权限 profile
@Service
public class AppService {
private final OAuth2AuthorizedClientService authorizedClientService;
public AppService(OAuth2AuthorizedClientService authorizedClientService) {
this.authorizedClientService = authorizedClientService;
}
@PreAuthorize("hasAuthority('SCOPE_profile')")
public String getJwtToken() {
var authentication = SecurityContextHolder.getContext().getAuthentication();
var accessToken = getAccessToken(authentication);
var refreshToken = getRefreshToken(authentication);
return String.format("Access Token = %s <br /><br /><br /> Refresh Token = %s",
accessToken.getTokenValue(), refreshToken.getTokenValue());
}
public OAuth2AccessToken getAccessToken(Authentication authentication) {
var authorizedClient = this.getAuthorizedClient(authentication);
return authorizedClient.getAccessToken();
}
public OAuth2RefreshToken getRefreshToken(Authentication authentication) {
var authorizedClient = this.getAuthorizedClient(authentication);
return authorizedClient.getRefreshToken();
}
private OAuth2AuthorizedClient getAuthorizedClient(Authentication authentication) {
if (authentication instanceof OAuth2AuthenticationToken oauthToken) {
String clientRegistrationId = oauthToken.getAuthorizedClientRegistrationId();
String principalName = oauthToken.getName();
return authorizedClientService.loadAuthorizedClient(clientRegistrationId, principalName);
}
return null;
}
}
配置文件
代码语言:javascript复制server:
port: 8081
spring:
application:
name: client-1
security:
oauth2:
client:
registration:
# 客户端标识,务必与授权服务注册的一致
client-1:
# 已在下面的provider中配置
provider: spring
# 标识自己
client-id: client-1
client-secret: password2
# 展示授权服务的名字
client-name: authorization-server
# 授权方式
authorization-grant-type: authorization_code
# redirect-uri 参数的内容
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
# 请求的权限,与授权服务注册的一致
scope:
- openid
- profile
- read
- write
# 验证方式
client-authentication-method: client_secret_basic
provider:
# 使用google,github等不需要配置,已内置
spring:
issuer-uri: http://localhost:8080
resource-server:
jwt:
issuer-uri: http://localhost:8080
流程图
测试
访问 http://127.0.0.1:8081
访问 http://127.0.0.1:8081/private-data
跳转到 http://127.0.0.1:8081/login
选择 Login with OAuth 2.0
中的 authorization-server
相当于访问 http://127.0.0.1:8081/oauth2/authorization/client-1,然后重定向到:
代码语言:javascript复制http://localhost:8080/oauth2/authorize?
response_type=code
&client_id=client-1
&scope=openid profile read write
&state=Za7PN3i8F-yLcxBUO0AkLFEm-Clg-s5RjJf46pDF6hE=
// 回调的重定向url
&redirect_uri=http://127.0.0.1:8081/login/oauth2/code/client-1
&nonce=1qN2mFKbEGaJb4jGFulwFDkR72dzEkgdZLrOSlRvfbk
登录并授权
授权后重定向到:
代码语言:javascript复制http://127.0.0.1:8081/login/oauth2/code/client-1?
// 授权码
code=CNL5dhSCyohdsRFVYkL4r9RNFhX-V-EWix8KIWd4jOYQW8gWiyihLLdAafnrswacxXaAZguuMphIbv_VLku50Q9LVehmkcpD0MbBgXlzABKhPU1X-FfkUZ8U0KqwMoxF
&state=Za7PN3i8F-yLcxBUO0AkLFEm-Clg-s5RjJf46pDF6hE=
然后重定向到:http://127.0.0.1:8081/private-data?continue
- 拿取access_token和refresh_token在Service中完成
<br />
<br />
参考资料:
- Syed Hasan's blog
- 阮一峰 - 理解OAuth 2.0