Spring OAuth 简单实践

引言

最近在了解OAuth2.0,一直想搞一个自己的类似于SakuraFrp使用的OpenID授权站,就想自己写一个。找的很多国内教程用的包都是 spring-cloud 下的关于 oauth 的包,或是直接使用老版本的 security-oauth 包,由于 spring-security 最新版是 6.x ,教程的版本太老,且想使用 start.spring.io 中提供的 spring-boot-starter-oauth2-xxx 使用配置文件快速开发,写下本文记录。

环境:Jdk17, Spring 3.1.4

实现

简单使用

使用 Client Credentials Grant 客户端模式对 Authorization-Server 进行简单使用

项目创建

  1. 新建一个空项目 oauth-demo

    oauth 1-1.png

  2. 新建一个 authorization-server 模块

    oauth 1-2.png

    添加如下依赖:

    oauth 1-3.png

    implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
    implementation("org.springframework.boot:spring-boot-starter-web")
    
  3. 修改配置文件

    server:
      port: 8080
    spring:
      application:
        name: authorization-server
      security:
        oauth2:
          authorization-server:
            client:
              demo-0:
                registration:
                  client-id: demo-0
                  # {noop} 不加密,明文
                  client-secret: "{noop}passwd"
                  client-name: demo-0
                  client-authentication-methods:
                    - client_secret_basic
                  # 客户端授权模式
                  authorization-grant-types:
                    # 客户端模式
                    - client_credentials
    logging:
      level:
        org.springframework.security: trace
    

    启动项目

    测试

    使用 InsomniaPostman 进行测试

    http://127.0.0.1:8080/oauth2/token 发送 POST 请求

    1. Basic Auth

      oauth 1-4.png

    2. 设置请求体

      oauth 1-5.png

      oauth 1-6.png

    3. 发送请求

      oauth 1-7.png

      可以看到返回了 access_token 和过期时间,客户端模式的具体介绍请看参考2

授权码模式

使用 authorization-server 同时作为 授权服务和资源服务

使用 client-1 作为客户端

authorization-server

配置文件:

server:
  port: 8080
spring:
  application:
    name: authorization-server
  security:
    # 提供的user,仅供测试使用
    user:
      name: user
      password: "{noop}password"
      roles: USER
    oauth2:
      authorization-server:
        client:
          # 客户端标识
          client-1:
            registration:
              client-id: client-1
              client-secret: "{noop}password2"
              # 授权方式:授权码,或使用refresh_token拿新的access_token
              authorization-grant-types:
                - "authorization_code"
                - "refresh_token"
              # 客户端验证方式
              client-authentication-methods:
                - client_secret_basic
              # 在同意授权后重定向的uri
              redirect-uris:
                - http://127.0.0.1:8081/login/oauth2/code/client-1
              post-logout-redirect-uris:
                - http://127.0.0.1:8081/logout
              # 提供的权限
              scopes:
                - "openid"
                - "profile"
                - "read"
                - "write"
            require-authorization-consent: true
            token:
              access-token-time-to-live: 3600s
              refresh-token-time-to-live: 7200s
logging:
  level:
    org.springframework.security: trace

client-1

项目创建
  1. 依赖

    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
    implementation("org.springframework.boot:spring-boot-starter-web")
    
  2. Config

    @Configuration
    public class SecurityConfig {
        @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            return http
                    .authorizeHttpRequests(o -> o
                            .requestMatchers("/").permitAll()
                            .anyRequest().authenticated())
                	// 用于解决非https下 oauth Not injecting HSTS header since it did not match request to ... 问题
                    .headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer
                            .httpStrictTransportSecurity(hstsConfig -> hstsConfig
                                    .maxAgeInSeconds(0)
                                    .includeSubDomains(true)
                            )
                    )
                    .oauth2Login(Customizer.withDefaults())
                    .formLogin(Customizer.withDefaults())
                    .build();
        }
    }
    
  3. Controller

    提供两个接口,一个用于测试是否启动成功,一个用于跳转OpenID登录

    @RestController
    @RequestMapping("/")
    public class AppController {
        private final AppService appService;
    
        public AppController(AppService appService) {
            this.appService = appService;
        }
    
        @GetMapping
        public String getPublicData() {
            return "Public data";
        }
    
        @GetMapping("/private-data")
        public String getPrivateData() {
            return appService.getJwtToken();
        }
    }
    
  4. Service

    getJwtToken() 需要权限 profile

    @Service
    public class AppService {
        private final OAuth2AuthorizedClientService authorizedClientService;
    
        public AppService(OAuth2AuthorizedClientService authorizedClientService) {
            this.authorizedClientService = authorizedClientService;
        }
    
        @PreAuthorize("hasAuthority('SCOPE_profile')")
        public String getJwtToken() {
            var authentication = SecurityContextHolder.getContext().getAuthentication();
            var accessToken = getAccessToken(authentication);
            var refreshToken = getRefreshToken(authentication);
            return String.format("Access Token = %s <br /><br /><br /> Refresh Token = %s",
                    accessToken.getTokenValue(), refreshToken.getTokenValue());
        }
    
        public OAuth2AccessToken getAccessToken(Authentication authentication) {
            var authorizedClient = this.getAuthorizedClient(authentication);
            return authorizedClient.getAccessToken();
        }
    
        public OAuth2RefreshToken getRefreshToken(Authentication authentication) {
            var authorizedClient = this.getAuthorizedClient(authentication);
            return authorizedClient.getRefreshToken();
        }
    
        private OAuth2AuthorizedClient getAuthorizedClient(Authentication authentication) {
            if (authentication instanceof OAuth2AuthenticationToken oauthToken) {
                String clientRegistrationId = oauthToken.getAuthorizedClientRegistrationId();
                String principalName = oauthToken.getName();
                return authorizedClientService.loadAuthorizedClient(clientRegistrationId, principalName);
            }
            return null;
        }
    }
    
  5. 配置文件

    server:
      port: 8081
    spring:
      application:
        name: client-1
      security:
        oauth2:
          client:
            registration:
              # 客户端标识,务必与授权服务注册的一致
              client-1:
                # 已在下面的provider中配置
                provider: spring
                # 标识自己
                client-id: client-1
                client-secret: password2
                # 展示授权服务的名字
                client-name: authorization-server
                # 授权方式
                authorization-grant-type: authorization_code
                # redirect-uri 参数的内容
                redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
                # 请求的权限,与授权服务注册的一致
                scope:
                  - openid
                  - profile
                  - read
                  - write
                # 验证方式
                client-authentication-method: client_secret_basic
            provider:
              # 使用google,github等不需要配置,已内置
              spring:
                issuer-uri: http://localhost:8080
          resource-server:
            jwt:
              issuer-uri: http://localhost:8080
    
流程图

oauth 2-1.png

测试
  1. 访问 http://127.0.0.1:8081

  2. 访问 http://127.0.0.1:8081/private-data

    跳转到 http://127.0.0.1:8081/login

    选择 Login with OAuth 2.0 中的 authorization-server

    相当于访问 http://127.0.0.1:8081/oauth2/authorization/client-1,然后重定向到:

    http://localhost:8080/oauth2/authorize?
    	response_type=code
    	&client_id=client-1
    	&scope=openid%20profile%20read%20write
    	&state=Za7PN3i8F-yLcxBUO0AkLFEm-Clg-s5RjJf46pDF6hE%3D
    	// 回调的重定向url
    	&redirect_uri=http://127.0.0.1:8081/login/oauth2/code/client-1
    	&nonce=1qN2mFKbEGaJb4jGFulwFDkR72dzEkgdZLrOSlRvfbk
    
  3. 登录并授权

    授权后重定向到:

    http://127.0.0.1:8081/login/oauth2/code/client-1?
    	// 授权码
    	code=CNL5dhSCyohdsRFVYkL4r9RNFhX-V-EWix8KIWd4jOYQW8gWiyihLLdAafnrswacxXaAZguuMphIbv_VLku50Q9LVehmkcpD0MbBgXlzABKhPU1X-FfkUZ8U0KqwMoxF
    	&state=Za7PN3i8F-yLcxBUO0AkLFEm-Clg-s5RjJf46pDF6hE%3D
    

​ 然后重定向到:http://127.0.0.1:8081/private-data?continue

  1. 拿取access_token和refresh_token在Service中完成


参考资料:

  1. Syed Hasan's blog
  2. 阮一峰 - 理解OAuth 2.0

ねぇ,あなたは何色になりたい