惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - Zhang_Xiang

代码是 AI 写的,生产事故谁背锅? AI Agent 走出 Demo 幻觉的唯一解药:Harness Engineering 从 page、page_size 到游标:深入解析C端产品的两种主流分页技术 Apache Kafka 的基本概念 Apache Kafka 移除 ZK Proposals webRTC demo Spring Authorization Server(AS)从 Mysql 中读取客户端、用户 Java 对象实现 Serializable 的原因 Spring Data JPA 使用 OAuth 2.1 框架 Spring Security dapr 本地环境升级 BuildPack 打包 spring-boot 2.5.4,nacos 作为配置、服务发现中心,Cloud Native Buildpacks 打包镜像,GitLab CI/CD 如何拆分大型单体系统为微服务 高可用 Keycloak,K8s Keycloak 13 自定义用户身份认证流程(User Storage SPI) - Zhang_Xiang OAuth 2.0、OIDC 讲不清楚? Mokito 单元测试与 Spring-Boot 集成测试 关于 JMeter 5.4.1 的一点记录
Spring Authorization Server 实现授权中心
Zhang_Xiang · 2022-05-10 · via 博客园 - Zhang_Xiang

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
组件 版本

本文的目的:

  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 路径下的访问权限。

@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