Spring Authorization Server 实现授权中心

源码地址

当前,Spring Security 对 OAuth 2.0 框架提供了全面的支持。Spring Authorization Server 出现的含义在于替换 Spring Security OAuth,交付 OAuth 2.1 授权框架。 Spring 官方已弃用 Spring Security OAuth。

本文涉及的组件版本如下:

组件 版本
JDK 17
org.springframework.boot 2.6.7
Gradle 7.4.1
spring-security-oauth2-authorization-server 0.2.3
spring-security-oauth2-authorization-server 项目由 Spring Security 团队领导,**社区驱动**。

本文的目的:

  1. 搭建授权中心示例
  2. fork 当前项目从而免去一些工作

本 demo 的结构

  • root

    • [[#auth-center|授权中心]]
    • [[#user-service|用户服务]]
    • [[#client-gateway|移动端网关]]

OAuth 2.1 支持三种许可类型,[[OAuth 2.1 授权框架#授权码许可]]、[[OAuth 2.1 授权框架#客户端证书许可]]、[[OAuth 2.1 授权框架#刷新令牌许可]]。

auth-center

build.gradle

plugins {
id 'org.springframework.boot' version '2.6.7'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
} group = 'com.insight.into.life'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17' configurations {
compileOnly {
extendsFrom annotationProcessor
}
} repositories {
mavenCentral()
} dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.2.3'
implementation 'org.springframework.boot:spring-boot-starter-actuator' compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// runtimeOnly 'mysql:mysql-connector-java'
runtimeOnly "com.h2database:h2" annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
} tasks.named('test') {
useJUnitPlatform()
}

config

...

@EnableWebSecurity
@Slf4j
public class DefaultSecurityConfig { @Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.formLogin(withDefaults());
return http.build();
} @Bean
public UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user1")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
...
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig { @Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(withDefaults()).build();
} @Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("mobile-gateway-client")
.clientSecret("{noop}123456")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:9100/login/oauth2/code/mobile-gateway-client-oidc")
.redirectUri("http://127.0.0.1:9100/authorized")
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")
.build(); JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient); return registeredClientRepository;
} @Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
} @Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
} @Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://localhost:9000").build();
} @Bean
public EmbeddedDatabase embeddedDatabase() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
  1. 这里的两个 config 中有两个 SecurityFilterChain 类。调用顺序是 authorizationServerSecurityFilterChain、defaultSecurityFilterChain。
  2. registeredClientRepository 用于注册 client。这里的两个 redirectUri 中地址来自于[[#mobile-gateway|移动端网关]]。

application.yml

server:
port: 9000 logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO

启动服务

在浏览器中输入:http://localhost:9000/.well-known/openid-configuration,得到以下内容。

// 20220510135753
// http://localhost:9000/.well-known/openid-configuration {
"issuer": "http://localhost:9000",
"authorization_endpoint": "http://localhost:9000/oauth2/authorize",
"token_endpoint": "http://localhost:9000/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"jwks_uri": "http://localhost:9000/oauth2/jwks",
"userinfo_endpoint": "http://localhost:9000/userinfo",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid"
]
}

user-service

用户服务在 demo 中的角色是资源服务器。

build.gradle

plugins {
id 'org.springframework.boot' version '2.6.7'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
} group = 'com.insight.into.life'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17' configurations {
compileOnly {
extendsFrom annotationProcessor
}
} repositories {
mavenCentral()
} dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test'
} tasks.named('test') {
useJUnitPlatform()
}

config

...
@EnableWebSecurity
public class ResourceServerConfig { @Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.mvcMatcher("/menu/**")
.authorizeRequests()
.mvcMatchers("/menu/**").access("hasAuthority('SCOPE_message.read')")
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
}

定义 menu 路径下的访问权限。

MenuController

@RestController
@RequestMapping("/menu")
public class MenuController { @GetMapping("/list")
public List<String> list() {
return List.of("menu1", "menu2", "menu3");
}
}

application.yml

server:
port: 9001 spring:
application:
name: user-service
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000

启动服务

资源服务器目前不需要做额外配置,只需要启动即可。

client-gateway

build.gradle

plugins {
id 'org.springframework.boot' version '2.6.7'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
} group = 'com.insight.into.life'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17' configurations {
compileOnly {
extendsFrom annotationProcessor
}
} repositories {
mavenCentral()
} dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework:spring-webflux"
implementation "io.projectreactor.netty:reactor-netty"
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.2' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
} tasks.named('test') {
useJUnitPlatform()
}

这里引入 org.springframework:spring-webfluxio.projectreactor.netty:reactor-netty 的原因在于使用了 WebClient。

config

...
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LoopbackIpRedirectFilter extends OncePerRequestFilter { @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getServerName().equals("localhost") && request.getHeader("host") != null) {
UriComponents uri = UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(request))
.host("127.0.0.1").build();
response.sendRedirect(uri.toUriString());
return;
}
filterChain.doFilter(request, response);
} }

该配置用于转换地址。将 localhost 转换为 127.0.0.1

...

@EnableWebSecurity
@Slf4j
public class SecurityConfig { @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.oauth2Login(oauth2Login -> oauth2Login.loginPage("/oauth2/authorization/mobile-gateway-client-oidc"))
.oauth2Client(withDefaults());
return http.build();
}
}
...
@Configuration
public class WebClientConfig { @Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder().apply(oauth2Client.oauth2Configuration()).build();
} @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;
}
}

AuthController

@RestController
@Slf4j
@RequiredArgsConstructor
public class AuthController { private final WebClient webClient;
@Value("${user-service.base-uri}")
private String userServiceBaseUri; @GetMapping("/menus")
public String menus(@RegisteredOAuth2AuthorizedClient("client-gateway-authorization-code") OAuth2AuthorizedClient authorizedClient) {
return this.webClient
.get()
.uri(userServiceBaseUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class)
.block();
} }

application.yml

server:
port: 9100 spring:
application:
name: client-gateway
security:
oauth2:
client:
registration:
mobile-gateway-client-oidc:
provider: spring
client-id: mobile-gateway-client
client-secret: 123456
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:9100/login/oauth2/code/{registrationId}"
scope: openid
client-gateway-authorization-code:
provider: spring
client-id: mobile-gateway-client
client-secret: 123456
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:9100/authorized"
scope: message.read,message.write
provider:
spring:
issuer-uri: http://localhost:9000 user-service:
base-uri: http://127.0.0.1:9001/menu/list

启动服务

在浏览器中输入:http://127.0.0.1:9100

输入账号密码:user1/password,这里的用户在 [[#auth-center#config]] 中配置。得到以下内容:

总结

  1. spring-authorization-server 目前还没有正式发布。文档较少。
  2. 还有一些需要完善的点。比如用户持久化、client 持久化。
  3. 此 demo 还要继续更新,为了能和本文对应,所以对应的 git tag 为 primitive-man

Spring Authorization Server 实现授权中心的更多相关文章

  1. Spring Authorization Server 全新授权服务器整合使用

    前言 Spring Authorization Server 是 Spring 团队最新开发适配 OAuth 协议的授权服务器项目,旨在替代原有的 Spring Security OAuth 经过半年 ...

  2. Spring Authorization Server(AS)从 Mysql 中读取客户端、用户

    Spring AS 持久化 jdk version: 17 spring boot version: 2.7.0 spring authorization server:0.3.0 mysql ver ...

  3. Spring Authorization Server授权服务器入门

    11月8日Spring官方已经强烈建议使用Spring Authorization Server替换已经过时的Spring Security OAuth2.0,距离Spring Security OA ...

  4. Spring Authorization Server的使用

    Spring Authorization Server的使用 一.背景 二.前置知识 三.需求 四.核心代码编写 1.引入授权服务器依赖 2.创建授权服务器用户 3.创建授权服务器和客户端 五.测试 ...

  5. Spring Authorization Server 0.2.3发布,放出联合身份DEMO

    很快啊Spring Authorization Server又发新版本了,现在的版本是0.2.3.本次都有什么改动呢?我们来了解一下. 0.2.3版本特性 本次更新的新特性不少. 为公开客户端提供默认 ...

  6. Spring Authorization Server 0.3.0 发布,官方文档正式上线

    基于OAuth2.1的授权服务器Spring Authorization Server 0.3.0今天正式发布,在本次更新中有几大亮点. 文档正式上线 Spring Authorization Ser ...

  7. Spring Cloud(6.1):搭建OAuth2 Authorization Server

    配置web.xml 添加spring-cloud-starter-security和spring-security-oauth2-autoconfigure两个依赖. </dependency& ...

  8. spring cloud 2.x版本 Eureka Server服务注册中心教程

    本文采用Spring cloud本文为2.1.8RELEASE,version=Greenwich.SR3 1.创建服务注册中心 1.1 新建Spring boot工程:eureka-server 1 ...

  9. Spring Security OAuth2 Demo —— 授权码模式

    本文可以转载,但请注明出处https://www.cnblogs.com/hellxz/p/oauth2_oauthcode_pattern.html 写在前边 在文章OAuth 2.0 概念及授权流 ...

随机推荐

  1. idea-spring-boot打包jar/var

    下面的插件配置的里面需要加上具体的main类 <groupId>org.springframework.boot</groupId> <artifactId>spr ...

  2. Eclipse建立Web项目,手动生成web.xml文件

    相关文章:https://blog.csdn.net/ys_code/article/details/79156188(Web项目建立,手动生成web.xml文件

  3. 什么是 UML?

    UML 是统一建模语言(Unified Modeling Language)的缩写,它发表于 1997 年,综合了当时已经存在的面向对象的建模语言.方法和过程,是一个支持模型 化和软件系统开发的图形化 ...

  4. 解释 Spring 框架中 bean 的生命周期?

    Spring 容器 从 XML 文件中读取 bean 的定义,并实例化 bean. Spring 根据 bean 的定义填充所有的属性. 如果 bean 实现了 BeanNameAware 接口,Sp ...

  5. flash的TotalFrames显示undefined

    通过js来操作flash的时候,获取到总帧数的是属性.TotalFrames,而不是属性TotalFrames().在asp.net中,js放在最后可以在一定程度上避免当前flash没有加载完,导致获 ...

  6. github 编写README时常用的写法

    参考:https://github.com/HeTingwei/ReadmeLearn#%E7%BC%96%E5%86%99readme%E6%97%B6%E5%B8%B8%E7%94%A8%E7%9 ...

  7. 若没有任何实例包含Class Body 则enum被隐式声明为final

    本文参考 今天在Java Language Specification上偶然看到一条关于枚举的语法特点说明 An enum declaration is implicitly final unless ...

  8. 如何使用Flannel搭建跨主机互联的容器网络

    当您将多台服务器节点组成一个Docker集群时,需要对集群网络进行设置,否则默认情况下,无法跨主机容器互联,接下来我们首先分析一下原因. 跨主机容器互联 下图描述了一个简单的集群网络,在该集群内,有两 ...

  9. TextView显示html样式的文字

    项目需求: TextView显示一段文字,格式为:白雪公主(姓名,字数不确定)向您发来了2(消息个数,不确定)条消息 这段文字中名字和数字的长度是不确定的,还要求名字和数字各自有各自的颜色. 一开始我 ...

  10. add jars、add external jars、add library、add class folder的区别

    add external jars = 增加工程外部的包add jars = 增加工程内包add library = 增加一个库add class folder = 增加一个类文件夹add jar是表 ...