【OAuth2.0】二、OAuth2.0怎么用?

573 阅读12分钟

要么忙着活,要么忙着死。

《肖申克的救赎》

在上篇文章【OAuth2.0】一、OAuth2.0是什么?中,我们主要介绍了OAuth2.0是什么、为什么我们要使用OAuth2.0以及OAuth2.0中的四种授权模式。这篇文章我们就来一起学习下怎么在实际项目中接入并使用OAuth2.0,Let‘s get it

项目创建

首先确定我们的开发环境

  1. JDK 17
  2. Spring Boot 3.1.12
  3. spring-security-oauth2-authorization-server 1.2.7

接下来我们创建3个Spring Boot项目工程文件,分别对应OAuth2.0中的第三方应用程序(Third - Party Application)、认证服务器(Authorization Server)、资源服务器(Resource Server)。3个项目(3个应用)之间的交互流程是:第三方应用程序(client)访问认证服务器(authorization)获取token,然后第三方应用程序(client)携带该token访问资源服务器(resource)获取需要的资源。

所以我们3个工程的命名,自然而然的就应该是client、authorization、resource。当然,你说你偏不这么命名,你就要按照你自己的想法来命名这也是可以的。

项目配置

创建完项目,接下来就是做项目配置。

首先,我们确定下3个项目的端口号。我这里配置的是client:8081、authorization:8083、resource:8082。端口号在合理的范围内,大家凭自己喜好配置即可。

接下来需要分别为3个项目添加依赖、配置yml文件、安全配置。

client

添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>

    <!--
        thymeleaf主要是方便我们开发,实际项目可以不用
    -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

配置yml文件

spring:
  security:
    oauth2:
      client:
        registration:
          client-app:
            # 指定OAuth2的提供者
            provider: authorization-app
            # 客户端ID
            client-id: client-app
            # 客户端密钥
            client-secret: client-secret
            # 客户端名称
            client-name: Client App
            # 客户端认证方式
            client-authentication-method: client_secret_basic
            # 授权类型
            authorization-grant-type: authorization_code
            # 重定向URI模板
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            # 请求的权限范围
            scope:
              - openid
              - profile
              - read
        provider:
          authorization-app:
            # 授权服务器的授权URI
            authorization-uri: http://localhost:8083/oauth2/authorize
            # 授权服务器的令牌URI
            token-uri: http://localhost:8083/oauth2/token
            # JWKS集合的URI,用于验证JWT令牌
            jwk-set-uri: http://localhost:8083/oauth2/jwks
            # 发行人URI,通常用于OpenID Connect
#            issuer-uri: http://localhost:8083

安全配置

/**
 * 配置Security过滤链
 * <p>
 * 该方法用于配置Web安全,包括请求的授权和OAuth2登录的设置
 * 它定义了哪些请求可以被所有人访问,哪些请求需要身份验证
 * 同时,它还配置了OAuth2登录的_success_url_
 *
 * @param http 用于配置Web安全的HttpSecurity对象
 * @return 返回配置好的SecurityFilterChain对象
 * @throws Exception 配置过程中可能抛出的异常
 */
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(auth -> auth
                    // 配置哪些请求可以被所有人访问
                    .requestMatchers("/", "/index", "/index.html", "/css/**", "/js/**", "/login.html").permitAll()
                    // 其他所有请求都需要身份验证
                    .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                    // 配置OAuth2登录成功后的默认跳转页面
                    .defaultSuccessUrl("/index", false)
            );
    return http.build();
}

authorization

添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-authorization-server</artifactId>
        <version>1.2.7</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

配置yml文件

无~

安全配置

/**
 * 配置Spring Security过滤链的Bean
 * 该方法定义了默认的安全过滤链,用于处理HTTP请求的安全配置
 *
 * @param http HttpSecurity实例,用于配置Web安全
 * @return 返回配置好的SecurityFilterChain实例
 * <p>
 * 优先级设置为2,以确保该过滤链按照预期的顺序被考虑
 */
@Bean
@Order(2)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http
            // 配置请求授权规则
            .authorizeHttpRequests(authorize ->
                    authorize
                            .anyRequest().authenticated()
            )
            // 使用默认的表单登录配置
            .formLogin(Customizer.withDefaults());
    return http.build();
}


/**
 * 配置用户服务的Bean
 * 该方法在Spring框架中定义一个Bean,类型为UserDetailsService,用于提供用户详细信息的服务
 * 主要用于内存中存储用户信息,适用于开发或测试环境下的简单认证
 *
 * @return UserDetailsService 一个配置了用户详情的服务实例,用于处理用户认证相关请求
 */
@Bean
UserDetailsService users() {
    // 创建一个用户详情实例,用于配置用户信息
    // 这里使用了Spring Security的内置方式配置用户密码
    // 用户名配置为"user",密码为"123456",并赋予"USER"角色
    UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("123456")
            .roles("USER")
            .build();

    // 返回一个内存中的用户详情管理器实例,初始化时包含之前配置的用户信息
    // 该管理器主要用于在内存中管理用户详情,适用于简单的认证场景
    return new InMemoryUserDetailsManager(user);
}

/**
 * 配置授权服务器的安全过滤链
 * <p>
 * 此方法主要用于构建和配置授权服务器的安全过滤链它定义了如何对传入的HTTP请求进行安全检查,
 * 以确保只有经过身份验证和授权的用户才能访问授权服务器的功能
 *
 * @param http HttpSecurity实例,用于配置Web安全设置
 * @return 返回配置好的SecurityFilterChain实例,用于应用安全过滤
 * @throws Exception 配置过程中可能抛出的异常
 */
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    // 应用OAuth2授权服务器的默认安全配置
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    // 开启oidc
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults());
    // 配置表单登录的默认设置
    return http.formLogin(Customizer.withDefaults()).build();
}


/**
 * 配置并返回一个客户端注册存储的Bean
 * 该Bean用于在内存中存储已注册的OAuth2客户端详细信息
 * 主要包括客户端ID、客户端密钥、认证方法、授权类型、重定向URI和作用域等信息
 *
 * @return RegisteredClientRepository 客户端注册存储的实例
 */
@Bean
public RegisteredClientRepository registeredClientRepository() {
    // 创建并配置一个RegisteredClient实例
    RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("client-app")
            .clientSecret("{noop}client-secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .redirectUri("http://127.0.0.1:8081/login/oauth2/code/client-app")
            .redirectUri("http://127.0.0.1:8081/authorized")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .scope("read")
            .clientSettings(ClientSettings.builder()
                    .requireAuthorizationConsent(true)
                    .build())
            .build();

    // 返回一个内存中的客户端注册存储实例,用于存储配置好的客户端信息
    return new InMemoryRegisteredClientRepository(client);
}


/**
 * 配置JWKSource Bean,用于提供JWT的公钥和私钥
 * 此方法主要用于初始化和配置一个JWKSource对象,该对象包含了一对RSA密钥(公钥和私钥)
 * 这对于JWT的签名和验证过程至关重要
 *
 * @param keyPair 一个包含RSA公钥和私钥的密钥对
 * @return 返回一个ImmutableJWKSet对象,包含生成的RSA密钥对
 */
@Bean
public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
    // 从密钥对中提取RSA公钥和私钥
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

    // 创建一个RSAKey对象,包含公钥、私钥,并为其分配一个唯一的键ID
    // @formatter:off
    RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();
    // @formatter:on

    // 将RSAKey对象封装到一个JWKSet中,然后创建一个ImmutableJWKSet对象
    JWKSet jwkSet = new JWKSet(rsaKey);
    return new ImmutableJWKSet<>(jwkSet);
}


/**
 * 配置JWT解码器
 * <p>
 * 该方法用于创建和配置一个JWT解码器,该解码器使用提供的密钥对中的公钥进行JWT的验证和解码
 * 主要解决如何验证和解析JWT令牌的问题,确保令牌合法并能提取出其中的信息
 *
 * @param keyPair 密钥对,包含公钥和私钥,这里使用其公钥来构建JWT解码器
 * @return 返回配置好的JwtDecoder实例,用于后续的JWT验证和解码操作
 */
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
    // 使用提供的密钥对中的公钥来构建一个NimbusJwtDecoder实例
    // 这里选择RSAPublicKey是因为在JWT验证中通常使用RSA算法的公钥来验证令牌的签名
    return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
}


/**
 * 生成RSA密钥对
 * <p>
 * 本方法使用RSA算法生成一个密钥对,包含公钥和私钥主要用于加密和解密 purposes.
 * 选择RSA算法是因为它在当前加密标准下既安全又高效.
 * 密钥长度设定为2048位,以确保安全性同时保持性能的可接受性.
 *
 * @return KeyPair对象,包含生成的RSA公钥和私钥
 * @throws IllegalStateException 如果密钥对生成过程中出现异常,抛出此异常
 */
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
KeyPair generateRsaKey() {
    KeyPair keyPair;
    try {
        // 获取RSA算法的密钥对生成器实例
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        // 初始化密钥对生成器,指定密钥长度为2048位
        keyPairGenerator.initialize(2048);
        // 生成密钥对
        keyPair = keyPairGenerator.generateKeyPair();
    } catch (Exception ex) {
        // 如果生成密钥对过程中出现异常,抛出IllegalStateException
        throw new IllegalStateException(ex);
    }
    // 返回生成的密钥对
    return keyPair;
}


/**
 * 配置授权服务器的设置
 * <p>
 * 此方法定义了OAuth2授权服务器的配置参数
 * 主要包括设置授权服务器的Issuer URI,即标识授权服务器的统一资源定位符
 * <p>
 * http://localhost:8083/.well-known/openid-configuration 请求后
 * <p>
 * {
 * "issuer": "http://localhost:9000",
 * "authorization_endpoint": "http://localhost:9000/oauth2/authorize",
 * "device_authorization_endpoint": "http://localhost:9000/oauth2/device_authorization",
 * "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",
 * "end_session_endpoint": "http://localhost:9000/connect/logout",
 * "response_types_supported": [
 * "code"
 * ],
 * "grant_types_supported": [
 * "authorization_code",
 * "client_credentials",
 * "refresh_token",
 * "urn:ietf:params:oauth:grant-type:device_code"
 * ],
 * "revocation_endpoint": "http://localhost:9000/oauth2/revoke",
 * "revocation_endpoint_auth_methods_supported": [
 * "client_secret_basic",
 * "client_secret_post",
 * "client_secret_jwt",
 * "private_key_jwt"
 * ],
 * "introspection_endpoint": "http://localhost:9000/oauth2/introspect",
 * "introspection_endpoint_auth_methods_supported": [
 * "client_secret_basic",
 * "client_secret_post",
 * "client_secret_jwt",
 * "private_key_jwt"
 * ],
 * "code_challenge_methods_supported": [
 * "S256"
 * ],
 * "subject_types_supported": [
 * "public"
 * ],
 * "id_token_signing_alg_values_supported": [
 * "RS256"
 * ],
 * "scopes_supported": [
 * "openid"
 * ],
 * "registration_endpoint": "http://localhost:9000/connect/register"
 * }
 *
 * @return AuthorizationServerSettings对象,包含了授权服务器的配置设置
 */
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder()
            .issuer("http://localhost:8083") // 设置授权服务器的Issuer URI
            .build();
}

resource

添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
</dependencies>

配置yml文件

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083

安全配置

/**
 * 配置Security过滤链
 *
 * 此方法用于配置和构建Spring Security的过滤链它定义了如何处理 incoming请求的认证和授权
 * 特别地,这段代码配置了对所有请求都需要进行认证,并且设置了使用OAuth 2资源服务器的JWT认证方式
 *
 * @param http HttpSecurity实例,用于配置Web安全设置
 * @return SecurityFilterChain对象,代表配置好的安全过滤链
 * @throws Exception 配置过程中可能抛出的异常
 */
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            // 配置请求授权规则
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            // 配置OAuth 2资源服务器的JWT认证
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

至此,OAuth2.0实战配置基本结束。

项目完善

client

对于client项目,我希望实现的效果是:需要一个登录页(login.html),页面上有一个按钮,点击按钮做第三方登录(OAuth2.0的认证服务器),登录完成后,跳转到client的index页面(index.html)。同时在index页面中,请求资源服务器获取资源。

所以client中还需要2个html页面和一个controller,用来配合实现上述效果。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<h2>Login Page</h2>
<!--
这里的/oauth2/authorization/client-app路径是Spring Security OAuth2提供的一个默认路径
-->
<a href="/oauth2/authorization/client-app">
    <button>Login with OAuth2</button>
</a>
</body>
</html>
<!--
thymeleaf 模板建议放在resources/templates 目录下
-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
<h2>Index Page</h2>
<div th:text="${data}"></div>
</body>
</html>
@GetMapping("/")
public String loginPage() {
    // 重定向到登录页面
    return "redirect:/login.html";
}

@GetMapping("/index")
public String indexPage(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient client) {
    HttpHeaders headers = new HttpHeaders();
    headers.setBearerAuth(client.getAccessToken().getTokenValue());

    HttpEntity<Void> request = new HttpEntity<>(headers);

    try {
        ResponseEntity<Map> response = restTemplate.exchange(
                "http://localhost:8082/api/data", HttpMethod.GET, request, Map.class);
        Map<String, Object> data = response.getBody();
        model.addAttribute("data", data);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return "index"; // index.html
}

resource

对于resource项目,既然是资源服务器,必然需要提供资源的请求路径、方式、参数等等。

所以resource中至少需要1个controller。

@GetMapping("/api/data")
public Map<String, String> getData() {
    return Map.of("message", "This is protected data from resource server.");
}

authorization

认证服务仅提供认证功能,也就是登录——>授权——>返回token。这些功能默认已经在spring-security-oauth2-authorization-server中,所以authorization项目我们无需完善。

实战效果展示

首先启动3个项目。

浏览器访问http://localhost:8081,跳转到login.html页面。

图片

点击Login with OAuth2,跳转到localhost:8083/login。这是一个内置页面,也可自定配置成你想要的登录页。

图片

输入在authorization项目里面配置的用户名和密码,点击Sign in,跳转到确认授权页。这也是一个内置页面,你也可以修改成你自己的确认授权页。

图片

勾选你需要的权限,点击Submit Consent,页面跳转回127.0.0.1:8081/index页面,并且在页面上展示了从resource服务中获取到的资源信息。

图片

OK,OAuth2.0实战全流程跑通🆒~

总结

至此,OAuth2.0 第二篇结束。看到这里,想必大家已经了解并掌握如何在我们的项目中接入OAuth2。如果没太理解也没关系,后面我会贴上Github源码地址,对照文章结合源码,大家跑一遍流程加深下印象。

细心的朋友应该注意到,我这里仅演示了授权码模式(Authorization Code),还有3个模式没演示。

对于客户端凭证模式(Client Credentials),朋友们有需要的话,可以点赞留言,后面我看情况贴一下Github源码。

对于密码模式(Resource Owner Password Credentials),在OAuth2.1中已经被移除,最终被移除是必然,我这边建议是没必要实战,了解下即可。

对于简化模式(Implicit),在OAuth2.1中已经被移除,最终被移除是必然,我这边建议是没必要实战,了解下即可。

源码地址:github.com/JeffrayZ/OA…

如果能帮我点个免费的关注,那就是对我个人的最大的肯定。如果觉得写的还行,分享一下也是我生活的小确幸~

Peace Guys,我们下篇文章再见。