• 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
  • 相关阅读:
    Vite搭建Vue3 + TypeScript 项目(Yarn)
    C++面试 -操作系统-架构能力:系统网络性能评估与优化
    PDF转成图片并识别成文字输出记录java代码
    网络是怎样链接的--向DNS服务器查询Web服务器的IP地址
    Git Cherry Pick的使用
    LeetCode --- 1486. XOR Operation in an Array 解题报告
    1/2.数据库集簇、元组,进程、内存架构
    30. 有一个人前来买瓜
    【无标题】
    ubuntu安装Docker
  • 原文地址:https://www.cnblogs.com/Zhang-Xiang/p/16254474.html