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. 解释 Java 堆空间及 GC?

    当通过 Java 命令启动 Java 进程的时候,会为它分配内存.内存的一部分用于 创建堆空间,当程序中创建对象的时候,就从对空间中分配内存.GC 是 JVM 内 部的一个进程,回收无效对象的内存用于 ...

  2. synchronize、Lock、ReenTrantLock 的区别

    synchronize 和Lock: 1.synchronize 系java 内置关键字:而Lock 是一个类 2.synchronize 可以作用于变量.方法.代码块:而Lock 是显式地指定开始和 ...

  3. Tcp三次握手四次挥手个人学习

    最近想跳槽,学习了tcp中的三次握手与四次挥手,特意记录下,加深记忆 SYN 代表请求创建连接 FIN 表示请求关闭连接 ACK 代表确认接受,不管是三次握手还是四次分手,在回应的时候都会加上ACK= ...

  4. leetcode_两数相加

    给你两个 非空 的链表,表示两个非负的整数.它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字. 请你将两个数相加,并以相同形式返回一个表示和的链表. 你可以假设除了数字 0 ...

  5. Ubuntu 18.04 磁盘根目录在线扩容 & 修改分区 inode 数量

    Ubuntu 18.04 磁盘根目录在线扩容 & 修改分区 inode 数量   Ubuntu 作为服务器系统使用的时候,系统盘的空间可能并不是很充裕,apt apt 着,根目录就满了.诚然, ...

  6. 1108. IP 地址无效化

    给你一个有效的 IPv4 地址 address,返回这个 IP 地址的无效化版本. 所谓无效化 IP 地址,其实就是用 "[.]" 代替了每个 ".". 示例 ...

  7. 可想实现一个自己的简单jQuery库?(九)

    Lesson-8 事件机制 在讲事件机制之前呢,我们有一个很重要的东西要先讲,那就是如何实现事件委托(代理). 只有必须先明白了如何实现一个事件委托,我们才能更好的去实现on和off.在我看来,on和 ...

  8. python大佬养成计划----基于flask_sqlalchemy的网页显示数据库信息

    网页显示数据库信息 使用我们刚学习的flask_sqlalchemy,在网页中显示数据库表中的数据.在开始运行程序前,确保数据库中执行过创建表和创建用户的操作,详见链接描述. # 模板文件templa ...

  9. 动态代理-JDK

    代理模式:假设一个场景,你的公司是一位软件公司,你是一位软件工程师,显然客户带着需求不会去找你谈,而是去找商务谈,此时商务就代表公司. 商务的作用:商务可以谈判:也有可能在开发软件之前就谈失败,此时商 ...

  10. Java学习day41

    在力扣刷了几个算法题,对比了自己和优解的差距