Spring OAuth 简单实践

2023-10-18 15:42:45 浏览数 (2)

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 进行简单使用

项目创建

新建一个空项目 oauth-demo

新建一个 authorization-server 模块

添加如下依赖:

代码语言:javascript复制
implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
implementation("org.springframework.boot:spring-boot-starter-web")

修改配置文件

代码语言:javascript复制
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
  1. 设置请求体
  1. 发送请求

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

授权码模式

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

使用 client-1 作为客户端

authorization-server

配置文件:

代码语言:javascript复制
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
项目创建

依赖

代码语言:javascript复制
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-web")

Config

代码语言:javascript复制
@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();
    }
}

Controller

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

代码语言:javascript复制
@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();
    }
}

Service

getJwtToken() 需要权限 profile

代码语言:javascript复制
@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;
    }
}

配置文件

代码语言:javascript复制
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
流程图
测试

访问 http://127.0.0.1:8081

访问 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,然后重定向到:

代码语言:javascript复制
http://localhost:8080/oauth2/authorize?
    response_type=code
    &client_id=client-1
    &scope=openid profile read write
    &state=Za7PN3i8F-yLcxBUO0AkLFEm-Clg-s5RjJf46pDF6hE=
    // 回调的重定向url
    &redirect_uri=http://127.0.0.1:8081/login/oauth2/code/client-1
    &nonce=1qN2mFKbEGaJb4jGFulwFDkR72dzEkgdZLrOSlRvfbk

登录并授权

授权后重定向到:

代码语言:javascript复制
http://127.0.0.1:8081/login/oauth2/code/client-1?
    // 授权码
    code=CNL5dhSCyohdsRFVYkL4r9RNFhX-V-EWix8KIWd4jOYQW8gWiyihLLdAafnrswacxXaAZguuMphIbv_VLku50Q9LVehmkcpD0MbBgXlzABKhPU1X-FfkUZ8U0KqwMoxF
    &state=Za7PN3i8F-yLcxBUO0AkLFEm-Clg-s5RjJf46pDF6hE=

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

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

<br />

<br />

参考资料:

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

0 人点赞