Spring Authorization Server 实现授权中心

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

@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
posted @ 2022-05-10 17:27  Zhang_Xiang  阅读(3681)  评论(1编辑  收藏  举报