在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕一个常见的开发话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


文章目录

Java 安全 07:Spring Security 认证授权(OAuth2.0)

🔒 认证授权漏洞有多致命? 2024年某企业内部系统因未做细粒度权限控制,普通员工通过URL直接访问管理员接口,下载300万用户数据;2023年某第三方应用滥用OAuth2.0授权,超范围获取用户通讯录信息,引发隐私泄露风波。在分布式系统和微服务架构中,“谁能访问”“能访问什么” 已成为核心安全问题,而Spring Security + OAuth2.0是解决这一问题的工业级方案。

本文将从Spring Security基础入手,逐步深入OAuth2.0的授权框架,结合完整Java代码示例、流程图和实战场景,带你掌握“用户认证”“权限控制”“第三方登录”全流程实现,最终构建符合企业级标准的认证授权体系。

一、Spring Security基础:认证与授权的“基石”

Spring Security是Spring生态中专注于认证(Authentication)和授权(Authorization)的安全框架,核心目标是确保“正确的人访问正确的资源”。它通过过滤器链(Filter Chain)拦截请求,完成身份验证和权限校验,无需手动编写重复的安全逻辑。

1.1 核心概念:先理清这3个关键术语

在学习Spring Security前,必须明确3个核心概念,避免混淆:

概念 含义 典型场景
认证(Authentication) 验证“用户是谁”(确认身份合法性) 登录时输入用户名密码,验证是否为系统用户
授权(Authorization) 验证“用户能做什么”(确认权限范围) 普通用户能否访问/admin/*接口
主体(Principal) 认证后的用户身份(如用户名、用户ID) 登录后通过SecurityContext获取用户信息

简单说:认证是“进门”,授权是“进门后能去哪”

1.2 核心组件:Spring Security的“骨架”

Spring Security通过一系列核心组件实现认证授权逻辑,关键组件如下:

1.2.1 UserDetailsService:用户信息来源

UserDetailsService是Spring Security获取用户信息的核心接口,作用是根据用户名加载用户详情(如密码、角色、权限)。默认实现是从内存加载(InMemoryUserDetailsManager),实际项目中需自定义实现,从数据库/Redis获取用户信息。

1.2.2 AuthenticationManager:认证管理器

AuthenticationManager是认证的“总指挥”,负责接收认证请求(如用户名密码),调用UserDetailsService获取用户信息,对比密码合法性,最终返回认证结果(Authentication对象)。

1.2.3 SecurityContext:安全上下文

SecurityContext用于存储当前登录用户的认证信息(Authentication对象),线程安全。认证通过后,Spring Security会自动将Authentication存入SecurityContext,后续可通过SecurityContextHolder随时获取:

// 获取当前登录用户信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // 获取用户名
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); // 获取用户权限
1.2.4 PasswordEncoder:密码加密器

PasswordEncoder负责密码的加密与比对,Spring Security推荐使用BCryptPasswordEncoder(前文《密码存储安全》中重点讲解的自适应哈希算法),避免明文存储密码。

1.3 基础实战:搭建第一个Spring Security项目

以Spring Boot 2.7.x为例,快速搭建一个包含“用户名密码登录”“接口权限控制”的基础项目。

✅ 步骤1:引入依赖(Maven)
<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Starter Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Spring Data JPA(用于从数据库获取用户,可选) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- H2数据库(内存数据库,方便测试) -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
✅ 步骤2:自定义UserDetailsService(从数据库加载用户)

首先定义用户实体类和Repository,再实现UserDetailsService

1. 用户实体类(User.java)

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;

@Data
@Entity
@Table(name = "sys_user")
@EntityListeners(AuditingEntityListener.class)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username; // 用户名(唯一)

    @Column(nullable = false)
    private String password; // 加密后的密码

    private String role; // 角色(如"ROLE_ADMIN"、"ROLE_USER")

    private boolean enabled = true; // 是否启用(禁用后无法登录)
}

2. UserRepository.java

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    // 根据用户名查询用户(登录时用)
    Optional<User> findByUsername(String username);
}

3. 自定义UserDetailsService(UserDetailsServiceImpl.java)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    /**
     * 根据用户名加载用户详情(Spring Security自动调用)
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 从数据库查询用户
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户名不存在:" + username));

        // 2. 转换角色为GrantedAuthority(Spring Security要求的权限格式)
        List<GrantedAuthority> authorities = Collections.singletonList(
                new SimpleGrantedAuthority(user.getRole())
        );

        // 3. 返回Spring Security的User对象(实现UserDetails接口)
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(), // 数据库中存储的是BCrypt加密后的密码
                user.isEnabled(),   // 是否启用
                true, true, true,   // 账户未过期、凭证未过期、账户未锁定
                authorities         // 用户权限
        );
    }
}
✅ 步骤3:配置Spring Security(SecurityConfig.java)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // 启用Spring Security
public class SecurityConfig {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    // 配置密码加密器(BCrypt)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 配置AuthenticationManager(认证管理器)
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        // 关联UserDetailsService和PasswordEncoder
        authBuilder.userDetailsService(userDetailsService)
                   .passwordEncoder(passwordEncoder());
        return authBuilder.build();
    }

    // 配置安全规则(接口权限、登录页面、退出逻辑等)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 1. 关闭CSRF(前后端分离项目常用,后续OAuth2.0会用令牌替代Session)
            .csrf().disable()
            // 2. 配置Session策略(无状态,不创建Session,适合API)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            // 3. 配置接口权限规则
            .authorizeRequests()
                // 公开接口(无需登录):登录接口、H2数据库控制台
                .antMatchers("/login", "/h2-console/**").permitAll()
                // 管理员接口:仅ROLE_ADMIN角色可访问
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 普通用户接口:ROLE_USER或ROLE_ADMIN可访问
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                // 其他所有接口:需登录(已认证)
                .anyRequest().authenticated()
            .and()
            // 4. 配置登录接口(使用Spring Security默认的登录逻辑,也可自定义)
            .formLogin()
                .loginProcessingUrl("/login") // 登录请求的URL(POST)
                .successHandler((request, response, auth) -> {
                    // 登录成功回调:返回用户信息(避免默认跳转)
                    response.setContentType("application/json;charset=UTF-8");
                    String json = "{\"code\":0,\"message\":\"登录成功\",\"data\":{\"username\":\"" + auth.getName() + "\"}}";
                    response.getWriter().write(json);
                })
                .failureHandler((request, response, exception) -> {
                    // 登录失败回调
                    response.setContentType("application/json;charset=UTF-8");
                    String json = "{\"code\":1,\"message\":\"登录失败:" + exception.getMessage() + "\"}";
                    response.getWriter().write(json);
                })
            .and()
            // 5. 配置退出接口
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler((request, response, auth) -> {
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write("{\"code\":0,\"message\":\"退出成功\"}");
                });

        // 6. 允许H2数据库控制台的iframe访问(测试用)
        http.headers().frameOptions().disable();

        return http.build();
    }
}
✅ 步骤4:初始化测试数据(DataInitializer.java)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DataInitializer implements CommandLineRunner {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 项目启动时初始化管理员和普通用户
    @Override
    public void run(String... args) throws Exception {
        // 1. 管理员用户(ROLE_ADMIN)
        User admin = new User();
        admin.setUsername("admin");
        admin.setPassword(passwordEncoder.encode("admin123")); // BCrypt加密密码
        admin.setRole("ROLE_ADMIN");
        userRepository.save(admin);

        // 2. 普通用户(ROLE_USER)
        User user = new User();
        user.setUsername("user");
        user.setPassword(passwordEncoder.encode("user123"));
        user.setRole("ROLE_USER");
        userRepository.save(user);

        System.out.println("初始化用户完成:admin/admin123,user/user123");
    }
}
✅ 步骤5:编写测试接口(TestController.java)
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    // 公开接口(无需登录)
    @GetMapping("/public/hello")
    public String publicHello() {
        return "Hello,这是公开接口,无需登录即可访问!";
    }

    // 普通用户接口(需ROLE_USER或ROLE_ADMIN)
    @GetMapping("/user/info")
    public String userInfo() {
        // 获取当前登录用户信息
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return "Hello " + auth.getName() + ",这是普通用户接口,你拥有的权限:" + auth.getAuthorities();
    }

    // 管理员接口(需ROLE_ADMIN)
    @GetMapping("/admin/dashboard")
    public String adminDashboard() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return "Hello " + auth.getName() + ",这是管理员控制台,你可以管理所有用户!";
    }
}
✅ 步骤6:测试验证
  1. 启动项目:访问H2数据库控制台(http://localhost:8080/h2-console),JDBC URL填写jdbc:h2:mem:testdb,登录后可看到sys_user表及初始化的用户数据。
  2. 登录测试:用Postman发送POST请求到http://localhost:8080/login,参数username=adminpassword=admin123,返回登录成功。
  3. 权限测试
    • 访问/admin/dashboard:admin用户可访问,user用户访问返回403。
    • 访问/user/info:admin和user用户均可访问。
    • 访问/public/hello:无需登录即可访问。

二、OAuth2.0核心:授权框架的“游戏规则”

Spring Security解决了单体应用的认证授权,但在分布式系统(如微服务)或第三方登录(如用GitHub账号登录)场景下,传统Session认证已不适用(Session无法跨服务共享)。此时需要OAuth2.0——一个专注于“授权”的开放标准框架。

2.1 为什么需要OAuth2.0?

先看一个真实场景:你用GitHub账号登录一个第三方代码托管平台(如Gitee),希望Gitee能读取你的GitHub仓库列表,但不希望Gitee知道你的GitHub密码。

OAuth2.0的核心价值就是:在不暴露用户密码的前提下,让第三方应用(Gitee)获得用户授权,访问用户在目标平台(GitHub)的资源

2.2 OAuth2.0的4个核心角色

OAuth2.0定义了4个角色,所有授权流程都围绕这4个角色展开:

角色 含义 示例
资源所有者(Resource Owner) 授权的主体(通常是用户),拥有资源的访问权限 你(用GitHub账号登录的用户)
客户端(Client) 第三方应用,需要获取用户授权才能访问资源 Gitee(需要访问你的GitHub仓库)
授权服务器(Authorization Server) 验证用户身份,发放授权令牌(Token) GitHub的OAuth2.0服务器
资源服务器(Resource Server) 存储用户资源,验证令牌合法性后提供资源访问 GitHub的API服务器(提供仓库列表)

📊 OAuth2.0角色交互流程图

flowchart TD
    A[资源所有者(用户)] --> B[客户端(Gitee)]:请求用GitHub登录
    B --> C[授权服务器(GitHub OAuth)]:请求授权
    C --> A:询问用户是否授权(如“允许Gitee访问你的仓库吗?”)
    A --> C:用户同意授权
    C --> B:发放授权令牌(Token)
    B --> D[资源服务器(GitHub API)]:携带Token请求资源
    D --> C:验证Token合法性
    C --> D:返回Token验证结果
    D --> B:返回用户仓库列表
    B --> A:展示仓库列表给用户

2.3 OAuth2.0的5种授权模式

OAuth2.0定义了5种授权模式,适用于不同场景,核心区别在于“令牌如何获取”:

2.3.1 授权码模式(Authorization Code)
  • 特点:最安全、最常用的模式,通过“授权码”间接获取令牌,避免令牌直接暴露在前端。
  • 适用场景:服务器端应用(如Spring Boot后端、Java Web应用)。
  • 流程:客户端请求授权 → 用户同意 → 授权服务器返回授权码 → 客户端用授权码换令牌 → 用令牌访问资源。
2.3.2 密码模式(Resource Owner Password Credentials)
  • 特点:用户直接向客户端提供用户名密码,客户端用密码换令牌。
  • 适用场景:信任度高的内部应用(如企业内部管理系统,不适合第三方应用)。
  • 风险:客户端需存储用户密码,安全性低,仅在必要时使用。
2.3.3 客户端模式(Client Credentials)
  • 特点:无用户参与,客户端用自身凭证(Client ID + Client Secret)换令牌,访问客户端自身的资源。
  • 适用场景:机器间通信(如微服务间调用,无需用户授权)。
2.3.4 简化模式(Implicit)
  • 特点:授权服务器直接返回令牌(无授权码步骤),令牌暴露在前端。
  • 适用场景:纯前端应用(如Vue/React单页应用),但安全性低,已逐步被授权码模式+PKCE替代。
2.3.5 刷新令牌模式(Refresh Token)
  • 特点:不是独立模式,而是补充——当访问令牌(Access Token)过期时,用刷新令牌(Refresh Token)换新的访问令牌,避免用户重新登录。
  • 适用场景:所有需要长期访问资源的场景。

2.4 OAuth2.0的2种核心令牌

OAuth2.0中有2种令牌,分工不同:

令牌类型 作用 有效期 特点
访问令牌(Access Token) 用于访问资源服务器的资源 短(如1小时) 包含用户权限,需保密,但可频繁使用
刷新令牌(Refresh Token) 用于访问令牌过期后,获取新的访问令牌 长(如7天、30天) 安全性高,仅在授权服务器使用,不传递给资源服务器

三、Spring Security OAuth2实战:搭建授权与资源服务器

Spring Security OAuth2是Spring官方对OAuth2.0的实现,分为授权服务器(发放令牌)和资源服务器(验证令牌、提供资源)两部分。下面基于Spring Boot 2.7.x,搭建完整的OAuth2.0认证授权体系。

3.1 环境准备:依赖与版本说明

Spring Boot 2.7.x后,Spring官方推荐使用spring-boot-starter-oauth2-authorization-server(替代旧的spring-security-oauth2-autoconfigure),需注意依赖版本匹配:

<!-- 授权服务器依赖(Spring Security OAuth2官方最新实现) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    <version>2.7.15</version> <!-- 与Spring Boot版本一致 -->
</dependency>

<!-- 资源服务器依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- JWT支持(用于生成JWT格式的令牌) -->
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>9.31</version>
</dependency>

3.2 实战1:搭建OAuth2.0授权服务器

授权服务器的核心功能是:验证用户身份、处理授权请求、发放令牌(Access Token/Refresh Token)

✅ 步骤1:配置授权服务器(AuthorizationServerConfig.java)
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    // 1. 配置授权服务器的SecurityFilterChain(优先级最高)
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 应用OAuth2.0授权服务器默认配置
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http
            // 配置授权服务器的端点安全规则
            .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 启用OpenID Connect(可选,用于第三方登录)
                .oidc(Customizer.withDefaults())
            .and()
            // 配置认证入口点:未登录时重定向到登录页面
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
            );

        return http.build();
    }

    // 2. 配置客户端信息(RegisteredClientRepository)
    // 客户端:第三方应用或资源服务器,需在授权服务器注册(Client ID + Client Secret)
    @Bean
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
        // 注册一个客户端(示例:资源服务器客户端)
        RegisteredClient resourceServerClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端ID(自定义,需保密)
                .clientId("resource-server-client")
                // 客户端密钥(BCrypt加密)
                .clientSecret(passwordEncoder.encode("client-secret-123"))
                // 客户端认证方式(客户端ID+密钥)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 支持的授权模式:授权码模式、密码模式、刷新令牌
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 授权码回调地址(需与客户端实际地址一致,防止恶意回调)
                .redirectUri("http://localhost:8081/login/oauth2/code/resource-server-client")
                // 授权范围(定义客户端可访问的资源范围)
                .scope(OidcScopes.OPENID) // OpenID范围(可选)
                .scope("read") // 只读权限
                .scope("write") // 写权限
                // 客户端设置:要求授权时用户确认(consent)
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        // 存储客户端信息(内存存储,生产环境建议用数据库)
        return new InMemoryRegisteredClientRepository(resourceServerClient);
    }

    // 3. 配置JWT令牌的密钥对(RSA非对称加密)
    // 用RSA私钥签名JWT,公钥验证JWT合法性
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        // 创建RSA JWK(JSON Web Key)
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();

        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    // 生成RSA密钥对(2048位)
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    // 4. 配置JWT解码器(用于验证JWT令牌)
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    // 5. 配置授权服务器设置(如授权端点URL)
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                // 授权端点的基础URL(默认是/oauth2/authorize)
                .authorizationEndpoint("/oauth2/authorize")
                // 令牌端点URL(默认是/oauth2/token)
                .tokenEndpoint("/oauth2/token")
                // JWT公钥端点URL(资源服务器获取公钥验证JWT)
                .jwksUri("/oauth2/jwks")
                .build();
    }

    // 6. 配置普通用户登录的SecurityFilterChain(优先级低于授权服务器)
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
            .and()
            // 配置表单登录(用户登录页面)
            .formLogin(Customizer.withDefaults());

        return http.build();
    }
}
✅ 步骤2:配置UserDetailsService(复用前文的实现)

授权服务器需要验证用户身份(如密码模式中验证用户名密码),直接复用前文的UserDetailsServiceImpl(从数据库获取用户)。

✅ 步骤3:测试授权服务器端点

授权服务器默认提供以下核心端点:

  • /oauth2/authorize:授权端点(获取授权码)。
  • /oauth2/token:令牌端点(用授权码/密码/刷新令牌换Access Token)。
  • /oauth2/jwks:JWT公钥端点(资源服务器获取公钥验证JWT)。
  • /login:用户登录端点。

用Postman测试密码模式(获取Access Token):

  1. 发送POST请求到http://localhost:8080/oauth2/token
  2. 设置请求头:Authorization: Basic cmVzb3VyY2Utc2VydmVyLWNsaWVudDpjbGllbnQtc2VjcmV0LTEyMw==(Base64编码,原文是clientId:clientSecret)。
  3. 设置请求参数(x-www-form-urlencoded):
    • grant_type=password(密码模式)。
    • username=admin
    • password=admin123
    • scope=read write(授权范围)。
  4. 响应结果(包含Access Token和Refresh Token):
    {
        "access_token": "eyJraWQiOiI...", // JWT格式的访问令牌
        "refresh_token": "8f6b8a...",    // 刷新令牌
        "token_type": "Bearer",
        "expires_in": 3600,              // 访问令牌有效期(1小时)
        "scope": "read write"
    }
    

3.3 实战2:搭建OAuth2.0资源服务器

资源服务器的核心功能是:验证Access Token的合法性,根据令牌中的权限控制资源访问(如微服务中的API接口)。

✅ 步骤1:创建资源服务器项目(独立Spring Boot项目)

资源服务器通常是独立的微服务,需引入资源服务器依赖(同前文),并配置application.yml

server:
  port: 8081 # 端口与授权服务器不同(避免冲突)

spring:
  application:
    name: resource-server

# OAuth2.0资源服务器配置
spring.security.oauth2.resourceserver:
  jwt:
    # JWT公钥端点(从授权服务器获取公钥,验证JWT签名)
    jwk-set-uri: http://localhost:8080/oauth2/jwks
✅ 步骤2:配置资源服务器(ResourceServerConfig.java)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 启用资源服务器
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    // 配置JWT转换器(将JWT中的声明转换为Spring Security的权限)
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )
            // 配置接口权限规则
            .authorizeRequests(authorize -> authorize
                // 公开接口:无需令牌
                .antMatchers("/public/**").permitAll()
                // 需read权限的接口:令牌中必须包含SCOPE_read
                .antMatchers("/api/read/**").hasAuthority("SCOPE_read")
                // 需write权限的接口:令牌中必须包含SCOPE_write
                .antMatchers("/api/write/**").hasAuthority("SCOPE_write")
                // 其他接口:需已认证(令牌有效)
                .anyRequest().authenticated()
            )
            // 关闭CSRF(API接口常用)
            .csrf().disable()
            // 无状态(不创建Session)
            .sessionManagement(session -> session
                .sessionCreationPolicy(org.springframework.security.config.http.SessionCreationPolicy.STATELESS)
            );

        return http.build();
    }

    /**
     * 配置JWT转换器:将JWT中的"scope"声明转换为Spring Security的GrantedAuthority
     * 例如:JWT中的"scope": "read" → 转换为"SCOPE_read"权限
     */
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // 设置JWT中存储权限的声明名称(默认是"scope")
        grantedAuthoritiesConverter.setAuthoritiesClaimName("scope");
        // 设置权限前缀(默认是"SCOPE_",最终权限为"SCOPE_read")
        grantedAuthoritiesConverter.setAuthorityPrefix("SCOPE_");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}
✅ 步骤3:编写资源服务器接口(ResourceController.java)
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ResourceController {

    // 需read权限的接口
    @GetMapping("/read/data")
    public String readData() {
        // 从JWT令牌中获取用户信息(Authentication已由资源服务器自动解析)
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return "用户 " + auth.getName() + " 访问read接口,获取数据成功!权限:" + auth.getAuthorities();
    }

    // 需write权限的接口
    @PostMapping("/write/data")
    public String writeData() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return "用户 " + auth.getName() + " 访问write接口,写入数据成功!";
    }

    // 公开接口(无需权限)
    @GetMapping("/public/info")
    public String publicInfo() {
        return "这是资源服务器的公开接口,无需令牌即可访问!";
    }
}
✅ 步骤4:测试资源服务器
  1. 获取Access Token:用前文密码模式的方法,从授权服务器获取access_token
  2. 访问资源接口:用Postman发送GET请求到http://localhost:8081/api/read/data,请求头添加Authorization: Bearer eyJraWQiOiI...(替换为实际的access_token)。
  3. 验证结果
    • 携带有效令牌且有read权限:返回200,显示用户信息。
    • 令牌过期或无read权限:返回401或403。
    • 不携带令牌访问/api/read/data:返回401。

三、OAuth2.0实战:授权码模式与第三方登录

前面实战了密码模式(适合内部应用),现在重点讲解授权码模式(适合第三方登录)和GitHub第三方登录(真实场景)。

3.1 授权码模式实战:第三方应用登录

授权码模式是最安全的OAuth2.0模式,流程如下:

📊 授权码模式流程图

flowchart TD
    A[用户(资源所有者)] --> B[第三方应用(客户端,如localhost:8082)]:点击“登录”
    B --> C[授权服务器(localhost:8080)]:重定向到/oauth2/authorize,携带client_id、redirect_uri、response_type=code
    C --> A:展示登录页面,用户输入用户名密码
    A --> C:用户登录成功,同意授权(如“允许第三方应用访问read权限”)
    C --> B:重定向到redirect_uri,携带授权码code
    B --> C:发送POST请求到/oauth2/token,携带code、client_id、client_secret、grant_type=authorization_code
    C --> B:返回access_token和refresh_token
    B --> D[资源服务器(localhost:8081)]:携带access_token请求资源
    D --> B:返回资源数据
    B --> A:展示资源数据给用户
✅ 步骤1:创建第三方应用项目(客户端)
  1. 引入依赖

    <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>
    
  2. 配置application.yml

    server:
      port: 8082 # 第三方应用端口
    
    spring:
      security:
        oauth2:
          client:
            registration:
              # 注册授权服务器的客户端(与授权服务器的registeredClient一致)
              resource-server-client:
                client-id: resource-server-client
                client-secret: client-secret-123
                authorization-grant-type: authorization_code
                redirect-uri: http://localhost:8082/login/oauth2/code/resource-server-client
                scope: read,write,openid
            provider:
              # 配置授权服务器提供者
              resource-server-client:
                authorization-uri: http://localhost:8080/oauth2/authorize
                token-uri: http://localhost:8080/oauth2/token
                jwk-set-uri: http://localhost:8080/oauth2/jwks
    
  3. 配置客户端Security(ClientSecurityConfig.java)

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
    import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
    import org.springframework.security.web.SecurityFilterChain;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    
    @Configuration
    @EnableWebSecurity
    public class ClientSecurityConfig {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().authenticated()
                .and()
                // 配置OAuth2.0登录(第三方登录)
                .oauth2Login(oauth2 -> oauth2
                    // 登录成功后的跳转页面
                    .defaultSuccessUrl("/home", true)
                )
                .logout(logout -> logout
                    // 配置OAuth2.0退出(通知授权服务器注销令牌)
                    .logoutSuccessHandler(oidcLogoutSuccessHandler(clientRegistrationRepository))
                );
    
            return http.build();
        }
    
        // 配置OIDC退出成功处理器(注销授权服务器的令牌)
        private LogoutSuccessHandler oidcLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) {
            OidcClientInitiatedLogoutSuccessHandler successHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
            // 退出后重定向到首页
            successHandler.setPostLogoutRedirectUri("http://localhost:8082/");
            return successHandler;
        }
    }
    
  4. 编写客户端接口(ClientController.java)

    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.oauth2.core.oidc.user.OidcUser;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class ClientController {
    
        // 首页(登录后访问)
        @GetMapping("/home")
        public String home() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            // 获取JWT中的用户信息(OidcUser包含完整的JWT声明)
            OidcUser oidcUser = (OidcUser) auth.getPrincipal();
            String username = oidcUser.getName();
            String accessToken = oidcUser.getAccessToken().getTokenValue();
    
            return "欢迎 " + username + "!<br>" +
                   "你的Access Token:" + accessToken + "<br>" +
                   "点击 <a href='/api/data'>访问资源服务器数据</a>";
        }
    
        // 访问资源服务器的接口(客户端作为中间层,转发请求)
        @GetMapping("/api/data")
        public String accessResourceServer() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            OidcUser oidcUser = (OidcUser) auth.getPrincipal();
            String accessToken = oidcUser.getAccessToken().getTokenValue();
    
            // 用RestTemplate转发请求到资源服务器
            org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
            org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
            headers.set("Authorization", "Bearer " + accessToken);
            org.springframework.http.HttpEntity<String> entity = new org.springframework.http.HttpEntity<>(headers);
    
            try {
                org.springframework.http.ResponseEntity<String> response = restTemplate.exchange(
                    "http://localhost:8081/api/read/data",
                    org.springframework.http.HttpMethod.GET,
                    entity,
                    String.class
                );
                return "资源服务器返回:<br>" + response.getBody();
            } catch (Exception e) {
                return "访问资源服务器失败:" + e.getMessage();
            }
        }
    
        // 公开首页(未登录时访问)
        @GetMapping("/")
        public String index() {
            return "欢迎访问第三方应用!<br>点击 <a href='/login/oauth2/code/resource-server-client'>用授权服务器账号登录</a>";
        }
    }
    
✅ 步骤2:测试授权码模式
  1. 启动项目:依次启动授权服务器(8080)、资源服务器(8081)、第三方应用(8082)。
  2. 访问第三方应用:打开http://localhost:8082,点击“用授权服务器账号登录”。
  3. 授权流程
    • 重定向到授权服务器登录页面,输入admin/admin123
    • 授权服务器询问是否同意授权,点击“同意”。
    • 重定向回第三方应用的/home页面,显示用户信息和Access Token。
  4. 访问资源:点击“访问资源服务器数据”,第三方应用用Access Token转发请求到资源服务器,返回资源数据。

3.2 第三方登录实战:集成GitHub登录

前面的授权服务器是自定义的,现在实战真实场景:用GitHub账号登录Spring Boot应用(GitHub作为OAuth2.0授权服务器)。

✅ 步骤1:在GitHub上注册OAuth应用
  1. 登录GitHub:访问https://github.com/settings/developers,点击“New OAuth App”。
  2. 填写应用信息
    • Application name:自定义(如“Spring Security OAuth2 Demo”)。
    • Homepage URL:http://localhost:8083(客户端项目地址)。
    • Authorization callback URL:http://localhost:8083/login/oauth2/code/github(回调地址,必须与客户端配置一致)。
  3. 注册成功:记录Client IDClient Secret(后续配置用)。
✅ 步骤2:创建GitHub登录项目
  1. 引入依赖:同前文第三方应用依赖。

  2. 配置application.yml

    server:
      port: 8083
    
    spring:
      security:
        oauth2:
          client:
            registration:
              # 注册GitHub客户端
              github:
                client-id: 你的GitHub Client ID
                client-secret: 你的GitHub Client Secret
                authorization-grant-type: authorization_code
                redirect-uri: http://localhost:8083/login/oauth2/code/github
                scope: user:email,read:user # 申请的权限范围(获取用户邮箱和基本信息)
            provider:
              # GitHub的OAuth2.0端点(Spring Boot已内置,无需手动配置)
              github:
                authorization-uri: https://github.com/login/oauth/authorize
                token-uri: https://github.com/login/oauth/access_token
                user-info-uri: https://api.github.com/user
                user-name-attribute: login # GitHub用户信息中用户名的字段名
    
  3. 配置Security(GitHubLoginConfig.java)

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    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 GitHubLoginConfig {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().authenticated()
                .and()
                // 配置GitHub OAuth2登录
                .oauth2Login(oauth2 -> oauth2
                    .loginPage("/login") // 自定义登录页面(可选)
                    .defaultSuccessUrl("/home", true) // 登录成功跳转
                )
                .logout(logout -> logout
                    .logoutSuccessUrl("/") // 退出后跳转首页
                );
    
            return http.build();
        }
    }
    
  4. 编写接口(GitHubController.java)

    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.oauth2.core.user.OAuth2User;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class GitHubController {
    
        @GetMapping("/")
        public String index() {
            return "欢迎访问!<br>点击 <a href='/login/oauth2/code/github'>用GitHub账号登录</a>";
        }
    
        @GetMapping("/home")
        public String home() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            OAuth2User oauth2User = (OAuth2User) auth.getPrincipal();
    
            // 从GitHub用户信息中获取字段(可通过GitHub API文档查看所有字段)
            String username = oauth2User.getAttribute("login"); // GitHub用户名
            String name = oauth2User.getAttribute("name"); // 真实姓名
            String email = oauth2User.getAttribute("email"); // 邮箱
            String avatarUrl = oauth2User.getAttribute("avatar_url"); // 头像URL
    
            return "登录成功!<br>" +
                   "GitHub用户名:" + username + "<br>" +
                   "姓名:" + name + "<br>" +
                   "邮箱:" + email + "<br>" +
                   "头像:<img src='" + avatarUrl + "' width='100'/><br>" +
                   "<a href='/logout'>退出登录</a>";
        }
    }
    
✅ 步骤3:测试GitHub登录
  1. 启动项目:访问http://localhost:8083,点击“用GitHub账号登录”。
  2. GitHub授权:重定向到GitHub登录页面,输入GitHub账号密码,同意授权。
  3. 登录成功:重定向回/home页面,显示GitHub用户的用户名、头像、邮箱等信息。

四、常见误区与最佳实践

4.1 这些OAuth2.0误区要避开!

误区1:混淆OAuth2.0和JWT

JWT是令牌的格式(一种轻量级的JSON令牌),OAuth2.0是授权框架(定义令牌的获取流程)。OAuth2.0的Access Token可以是JWT,也可以是随机字符串(如UUID),二者没有必然绑定。

误区2:Access Token长期有效

为降低令牌泄露风险,Access Token应设置短期有效期(如1小时),用Refresh Token获取新令牌。Refresh Token需存储在安全位置(如后端数据库,前端仅存储Access Token)。

误区3:客户端Secret暴露在前端

前端应用(如Vue/React)无法安全存储Client Secret,应使用授权码模式+PKCE(Proof Key for Code Exchange),避免传递Client Secret。

误区4:不验证令牌的签名和有效期

资源服务器必须验证Access Token的签名(确保未被篡改)和有效期(exp字段),不能仅验证令牌是否存在。

4.2 企业级最佳实践

1. 令牌安全
  • 使用JWT+RSA非对称加密:私钥签名JWT,公钥验证,避免对称加密密钥泄露风险。
  • Access Token短期化:有效期1小时内,Refresh Token有效期7-30天,且支持强制吊销。
  • 前端令牌存储:Access Token存储在sessionStorage(会话结束失效),避免存储在localStorage(易被XSS窃取)。
2. 授权粒度
  • 细粒度权限控制:用OAuth2.0的scope定义权限(如user:readuser:write),资源服务器根据scope控制接口访问。
  • 结合RBAC模型:在JWT中包含用户角色(如roles: ["ADMIN"]),资源服务器结合角色和scope做双重权限校验。
3. 安全配置
  • 强制HTTPS:所有OAuth2.0端点(授权、令牌、回调)必须使用HTTPS,防止令牌被拦截。
  • 限制回调地址:授权服务器仅允许注册过的redirect_uri,防止恶意回调(如redirect_uri=http://attacker.com)。
  • 日志审计:记录令牌的生成、使用、吊销日志,便于追踪异常访问。
4. 分布式场景
  • 授权服务器集群化:用Redis存储客户端信息和令牌,支持多节点部署。
  • 资源服务器无状态:资源服务器通过JWT公钥验证令牌,无需依赖授权服务器,支持水平扩展。

五、总结:认证授权的“技术选型指南”

场景 推荐方案 核心组件
单体应用认证授权 Spring Security + Session UserDetailsService、PasswordEncoder
微服务认证授权 Spring Security OAuth2.0 + JWT 授权服务器、资源服务器、JWT
第三方登录(如GitHub/微信) Spring Security OAuth2.0 Client + 授权码模式 OAuth2LoginConfig、ClientRegistration
前端应用(Vue/React) 授权码模式+PKCE + 短期Access Token PKCE、sessionStorage存储令牌

Spring Security + OAuth2.0是Java生态中认证授权的“标准答案”,它不仅支持传统单体应用,还能无缝对接微服务和第三方登录,满足从简单到复杂的各类安全需求。

通过本文的实战,你已掌握Spring Security的基础配置、OAuth2.0的核心流程、第三方登录的集成方法。在实际项目中,需根据业务场景选择合适的授权模式和令牌格式,始终将“安全”放在首位,平衡安全性与用户体验。


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐