0

0

Spring Boot应用中的用户认证策略:JWT与OAuth2的协同

碧海醫心

碧海醫心

发布时间:2025-09-29 13:27:13

|

839人浏览过

|

来源于php中文网

原创

Spring Boot应用中的用户认证策略:JWT与OAuth2的协同

在Spring Boot应用中整合用户注册与多种认证方式(如自定义凭证和社交媒体登录)时,最佳实践是利用成熟的OAuth2/OpenID Connect授权服务器(如Keycloak、Auth0),而非自行实现JWT认证。本文将阐述如何将REST API作为资源服务器,UI作为客户端,并探讨OAuth2客户端库的使用及BFF模式,以构建安全、可扩展的认证系统。

核心理念:授权服务器与角色划分

在构建现代web应用,特别是需要支持多种认证方式(如传统用户名/密码和社交媒体登录)的spring boot应用时,一个常见的误区是试图从零开始实现一个完整的用户管理和认证系统。最佳实践是采纳行业标准oauth2和openid connect,并利用成熟的授权服务器

为何不应自建授权服务器? 自行构建授权服务器不仅复杂,而且极易引入安全漏洞。一个完整的授权服务器需要处理用户注册、密码管理、多因素认证、社交媒体身份联邦、令牌颁发与刷新、会话管理等诸多功能,这些都需要深厚的安全专业知识。

推荐的授权服务器 市面上已有许多功能强大、安全可靠的OAuth2/OpenID Connect授权服务器可供选择:

  • 自托管解决方案: 如Keycloak,提供丰富的用户管理、身份联邦、MFA等功能,可部署在自己的基础设施上。
  • 云服务: 如Auth0、Amazon Cognito、Okta等,提供开箱即用的认证和授权服务,大大简化了集成难度。

这些授权服务器通常内置了社交媒体登录(Google、Facebook等)的集成能力,极大地简化了多认证源的管理。

OAuth2体系中的角色 在OAuth2/OpenID Connect框架下,您的Spring Boot应用的不同部分将扮演不同的角色:

  1. 授权服务器 (Authorization Server):负责用户身份验证、颁发访问令牌(Access Token)和刷新令牌(Refresh Token)。这是您应该选择现有解决方案的部分。
  2. 资源服务器 (Resource Server):您的Spring Boot REST API。它不处理用户凭证,只负责验证传入请求中的访问令牌,并根据令牌的有效性和权限来决定是否允许访问受保护的资源。
  3. 客户端 (Client):您的用户界面(UI),无论是单页应用(SPA)、移动应用还是传统的服务器端渲染应用。它代表用户向授权服务器请求令牌,并使用这些令牌访问资源服务器。

资源服务器的实现:保护您的Spring Boot REST API

您的Spring Boot REST API作为资源服务器,其主要职责是验证来自客户端的请求中包含的访问令牌。Spring Security提供了强大的支持来简化这一过程。

配置Spring Security作为资源服务器 使用spring-boot-starter-oauth2-resource-server依赖可以轻松地将您的Spring Boot应用配置为OAuth2资源服务器。它能够解析JWT(JSON Web Token)格式的访问令牌,并根据授权服务器提供的公钥进行验证。

示例配置:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                // 允许特定路径公开访问,例如健康检查、文档等
                .requestMatchers("/api/public/**").permitAll()
                // 所有其他请求都需要认证
                .anyRequest().authenticated()
            )
            // 配置OAuth2资源服务器,默认使用JWT验证
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        // 如果需要,可以禁用CSRF,因为JWT是无状态的
        // http.csrf(csrf -> csrf.disable());
        return http.build();
    }

    // Spring Boot会自动配置JwtDecoder,通过 'spring.security.oauth2.resourceserver.jwt.issuer-uri' 或 'jwk-set-uri'
    // 如果需要更复杂的JWT解析,可以自定义JwtDecoder Bean
    // @Bean
    // public JwtDecoder jwtDecoder() {
    //     // 例如,从特定的JWK Set URI加载
    //     return NimbusJwtDecoder.withJwkSetUri("YOUR_AUTHORIZATION_SERVER_JWKS_URI").build();
    // }
}

在application.properties或application.yml中,您需要指定授权服务器的URI,以便资源服务器能够获取验证JWT所需的公钥(JWK Set URI):

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/your-realm # Keycloak示例
# 或者
# spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/realms/your-realm/protocol/openid-connect/certs

通过上述配置,您的Spring Boot REST API将能够自动验证传入请求的Authorization: Bearer 头中的JWT,并根据令牌的有效性(签名、过期时间、颁发者等)来决定是否允许访问。

客户端的实现:用户界面与OAuth2流程

客户端(UI)负责引导用户完成认证流程,并获取访问令牌以访问资源服务器。根据UI的类型,实现方式有所不同。

1. 单页应用 (SPA) 或移动应用 对于Angular、React、Vue等前端框架构建的SPA或原生移动应用,它们通常直接与授权服务器交互,采用授权码流 (Authorization Code Flow) 配合PKCE (Proof Key for Code Exchange)

  • 流程:
    1. 用户在SPA中点击登录。
    2. SPA将用户重定向到授权服务器的登录页面。
    3. 用户在授权服务器完成认证(用户名/密码或社交登录)。
    4. 授权服务器将用户重定向回SPA,并带上一个授权码。
    5. SPA使用授权码和PKCE密钥向授权服务器的令牌端点交换访问令牌和刷新令牌。
    6. SPA将访问令牌存储在本地(如LocalStorage),并在后续请求中将其添加到Authorization头中发送给资源服务器。
  • 客户端库: 建议使用成熟的OAuth2/OpenID Connect客户端库,如oidc-client-js (JavaScript/TypeScript)、AppAuth-JS、或特定框架的库。
  • 注意事项: 令牌存储在浏览器端存在XSS攻击风险,需谨慎处理。

2. 服务器端渲染 (SSR) 或后端服务前端 (BFF) 如果您的UI是由Spring Boot应用本身渲染(如使用Thymeleaf)或者您采用了后端服务前端(BFF)模式,那么您的Spring Boot应用将扮演OAuth2客户端的角色。

使用spring-boot-starter-oauth2-client Spring Security提供了spring-boot-starter-oauth2-client,用于简化Spring Boot应用作为OAuth2客户端的实现。

示例配置 (application.yml):

Veggie AI
Veggie AI

Veggie AI 是一款利用AI技术生成可控视频的在线工具

下载
spring:
  security:
    oauth2:
      client:
        registration:
          google: # 注册一个名为 'google' 的OAuth2客户端
            client-id: your-google-client-id
            client-secret: your-google-client-secret
            scope: openid,profile,email # 请求的权限范围
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" # 回调URI
            client-name: Google
            authorization-grant-type: authorization_code # 授权码流
          # 您也可以配置自定义授权服务器
          my-auth-server:
            client-id: my-client-id
            client-secret: my-client-secret
            scope: openid,api.read,api.write
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            client-name: My Auth Server
            authorization-grant-type: authorization_code
        provider:
          google: # 配置Google授权服务器的端点
            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
            token-uri: https://oauth2.googleapis.com/token
            user-info-uri: https://openidconnect.googleapis.com/v1/userinfo
            jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
            user-name-attribute: sub # 用于获取用户名的属性
          my-auth-server: # 配置自定义授权服务器的端点
            issuer-uri: http://localhost:8080/realms/your-realm # OpenID Connect Issuer URI

通过上述配置,Spring Security会自动处理OAuth2授权码流。用户访问受保护的页面时,会被重定向到授权服务器进行登录。成功登录后,Spring Boot客户端会获取访问令牌,并将其存储在会话中。

获取用户信息: 在控制器中,您可以轻松获取已认证的用户信息:

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Map;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(@AuthenticationPrincipal OAuth2User oauth2User, Model model) {
        if (oauth2User != null) {
            model.addAttribute("userName", oauth2User.getAttribute("name"));
            model.addAttribute("userAttributes", oauth2User.getAttributes());
        } else {
            model.addAttribute("userName", "Guest");
        }
        return "index"; // 返回一个Thymeleaf模板
    }

    @GetMapping("/userinfo")
    public Map getUserInfo(@AuthenticationPrincipal OAuth2User oauth2User) {
        return oauth2User.getAttributes();
    }
}

后端服务前端(BFF)模式:提升安全性与体验

当您拥有一个SPA前端和一个Spring Boot后端API时,BFF模式是一个非常推荐的架构选择,尤其是在安全性方面。

什么是BFF模式? BFF(Backend For Frontend)模式是指为特定前端应用(如SPA)提供定制化API服务的后端层。在这个场景中,BFF充当了OAuth2客户端,代表浏览器与授权服务器和资源服务器进行交互。

BFF模式的优势:

  • 隐藏令牌: 访问令牌和刷新令牌存储在BFF的服务器端,不会暴露给浏览器端的JavaScript。这大大降低了跨站脚本(XSS)攻击获取令牌的风险。
  • 会话管理: BFF可以与浏览器建立传统的、基于会话的认证机制(如HTTP Only Cookie),简化前端的认证逻辑,前端无需直接处理OAuth2令牌。
  • 协议转换: BFF将浏览器端的会话请求转换为带有访问令牌的请求,转发给资源服务器。
  • 简化前端: 前端无需关心复杂的OAuth2流程和令牌管理,只需与BFF进行简单的会话通信。

BFF的实现 您可以构建一个独立的Spring Boot应用作为BFF。这个BFF应用将同时扮演OAuth2客户端的角色(使用spring-boot-starter-oauth2-client与授权服务器交互)和代理的角色(将请求转发给资源服务器)。

示例架构:

  1. 浏览器 (SPA) BFF (Spring Boot)
  2. BFF (Spring Boot) 授权服务器
  3. BFF (Spring Boot) 资源服务器 (Spring Boot REST API)

BFF中的配置与转发逻辑: BFF应用需要配置为OAuth2客户端,如上文所示。当用户通过BFF登录后,BFF会持有用户的访问令牌。在转发请求到资源服务器时,BFF需要将这个访问令牌添加到请求的Authorization头中。

您可以使用Spring Cloud Gateway或自定义的WebClient来实现请求转发:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        return WebClient.builder()
                .filter(oauth2ClientFilter(authorizedClientManager)) // 添加OAuth2客户端过滤器
                .build();
    }

    // 配置OAuth2AuthorizedClientManager以获取和刷新令牌
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {
        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken()
                        .clientCredentials()
                        .password()
                        .build();
        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        return authorizedClientManager;
    }

    // WebClient过滤器,用于将访问令牌添加到请求头
    private org.springframework.web.reactive.function.client.ExchangeFilterFunction oauth2ClientFilter(
            OAuth2AuthorizedClientManager authorizedClientManager) {
        return (request, next) -> {
            // 这里需要根据实际情况获取当前用户的OAuth2AuthorizedClient
            // 通常可以通过SecurityContextHolder获取当前认证用户的Principal
            // 并使用authorizedClientManager.authorize(OAuth2AuthorizeRequest)来获取或刷新令牌
            // 简化示例:假设我们总是使用某个注册ID的令牌
            // 实际应用中需要更复杂的逻辑来关联用户和其OAuth2AuthorizedClient
            // 例如,从SecurityContext中获取OAuth2AuthenticationToken
            // String clientRegistrationId = "google"; // 或从请求中动态获取
            // OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(
            //     OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId)
            //         .principal(SecurityContextHolder.getContext().getAuthentication())
            //         .build());
            // if (authorizedClient != null && authorizedClient.getAccessToken() != null) {
            //     request = WebClient.RequestHeadersSpec::header(HttpHeaders.AUTHORIZATION,
            //         "Bearer " + authorizedClient.getAccessToken().getTokenValue());
            // }
            return next.exchange(request);
        };
    }
}

注意: 上述BFF的oauth2ClientFilter是一个概念性示例,实际实现中,如何将当前用户的OAuth2AuthorizedClient与WebClient请求关联,需要更精细的逻辑,通常涉及从SecurityContextHolder中获取OAuth2AuthenticationToken并构建OAuth2AuthorizeRequest。

注意事项与最佳实践

  • 选择合适的授权服务器: 根据项目规模、安全性要求、团队熟悉度及预算,选择最适合的授权服务器。
  • 始终使用HTTPS: 所有的认证和授权通信都必须通过HTTPS进行,以防止中间

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

557

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

754

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

478

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

434

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1031

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

553

2023.09.20

excel表格操作技巧大全 表格制作excel教程
excel表格操作技巧大全 表格制作excel教程

Excel表格操作的核心技巧在于 熟练使用快捷键、数据处理函数及视图工具,如Ctrl+C/V(复制粘贴)、Alt+=(自动求和)、条件格式、数据验证及数据透视表。掌握这些可大幅提升数据分析与办公效率,实现快速录入、查找、筛选和汇总。

0

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Vue 教程
Vue 教程

共42课时 | 6.8万人学习

Vue3.x 工具篇--十天技能课堂
Vue3.x 工具篇--十天技能课堂

共26课时 | 1.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号