Java框架快速入门:Spring Security+OAuth2之认证与授权全场景实战

纲要

  • Spring SecurityOAuth2 核心概念:认证与授权
  • 主流安全框架对比与选型依据
  • 单体应用安全架构:过滤器链、安全上下文、密码编码器
  • 多因子认证与 TOTP 实战
  • JWT 令牌设计:访问令牌与刷新令牌
  • OAuth2 授权模式与微服务角色(授权服务器、资源服务器、客户端)
  • 社交登录与单点登录实现思路
  • 授权模型:角色、权限、自定义表达式
  • 前端集成注意事项:CORS、CSRF
  • 示例代码

认证与授权的基础概念

在任何一个需要保护资源的系统中,核心的安全问题都可以归纳为两点:认证授权

  • 认证 回答“你是谁”的问题。例如通过用户名密码、短信验证码、指纹等方式证明用户身份。
  • 授权 回答“你能做什么”的问题。在认证通过后,系统根据角色或权限决定用户是否能访问某个接口、执行某个操作或查看某部分数据。

打个比方:进入小区大门需要刷门禁卡,这是认证;进入小区后,能否打开某一栋楼或某一户的门,就属于授权。几乎所有的企业级应用、电商系统、后台管理平台都离不开这套机制。同时,认证与授权是整个系统安全基座的一部分,基础不稳会带来严重安全风险。而且攻防技术不断演进,安全策略也需要持续升级,因此这一技术栈需要长期学习。

为什么选择 Spring Security + OAuth2

在 Java 生态中,安全框架主要有 Apache ShiroSpring Security 以及标准 OAuth2 实现等。根据 Google Trends 的数据,Spring Security 的社区热度和关注度长期远超其他竞品,这反映了开发者社区的广泛认可。

选择 Spring Security 的几点理由:

  • 它是 Spring 全家桶的核心成员,与 Spring Boot、Spring Cloud 等其他组件无缝集成。
  • 功能覆盖全面:支持认证、授权、CSRF 防御、会话管理、方法级安全等,几乎可以覆盖所有企业安全需求。
  • 架构设计良好,业务代码与安全逻辑高度解耦,通过过滤器链和注解即可完成非侵入式的安全配置。

当系统从单体演进到微服务架构时,OAuth2 成为分布式场景下授权的标准协议。它定义了资源所有者、客户端、授权服务器和资源服务器四种角色,支持授权码模式、密码模式、客户端模式、简化模式等多种授权流程,能够优雅地解决跨服务认证与授权难题。

单体架构下的安全基础

在单体应用中,Spring Security 基于 过滤器链 工作。每个请求都会经过一系列过滤器,分别完成安全上下文构建、身份认证、授权决策等。核心组件包括:

  • SecurityContextHolder:存放当前认证通过的安全上下文,包含用户主体及其权限信息。
  • UserDetailsService:从数据库或其他数据源加载用户信息的抽象接口。
  • GrantedAuthority:表示授予用户的权限或角色。
  • 过滤器链中的 UsernamePasswordAuthenticationFilterBasicAuthenticationFilter 等负责处理不同类型的认证请求。

一个典型的单体安全配置如下:

package com.example.security.config;

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout.permitAll());
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        var user = User.withUsername("user")
                .password(passwordEncoder().encode("password"))
                .roles("USER")
                .build();
        var admin = User.withUsername("admin")
                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

在这个配置中:

  • 使用 SecurityFilterChain 自定义了路由授权规则:公开路径 /public/** 无需认证,/admin/** 需要 ADMIN 角色,其他请求必须认证。
  • 开启了表单登录并指定了自定义登录页。
  • 通过内存用户演示了 UserDetailsService 和密码编码器的配置。

密码进化与验证

密码存储必须使用不可逆的加密算法。Spring Security 推荐使用 BCryptPasswordEncoder,它可以自适应计算强度。此外,还支持 DelegatingPasswordEncoder 以便兼容多种历史编码格式。对于字段校验,可以使用 Bean Validation 注解,并结合自定义注解实现复杂的密码策略。

多因子认证 (MFA)

简单的“用户名+密码”已不足以应对现代安全需求。多因子认证要求用户提供两种或以上不同类型的凭证,比如:

  1. 用户输入用户名和密码(知识因素)
  2. 系统向用户手机或邮箱发送一次性验证码(所有权因素)

常见实现方案是基于 TOTP(基于时间的一次性密码)或短信、邮件验证码。后端可以设计如下接口:

  • 选择接收验证码的方式(短信 / 邮件)
  • 发送验证码
  • 验证验证码并完成登录

同时可以利用 Redis 缓存验证码,设置合理的过期时间,并结合 Spring Security 的自定义过滤器来串联这一认证流程。

JWT 令牌与前后端分离

在前后端分离或微服务架构中,传统基于 Session 的认证机制会带来状态共享和扩展性问题。JSON Web Token (JWT) 作为一种自包含的令牌格式,成为主流选择。

认证成功后,服务器签发两个令牌:

  • Access Token:短期有效,携带用户身份和权限信息,用于访问资源。
  • Refresh Token:长期有效,仅用于换取新的 Access Token,降低泄露风险。

JWT 的结构和签发流程可以用下图表示:

资源服务认证服务客户端资源服务认证服务客户端alt[令牌有效][令牌过期]登录 (用户名/密码)返回 Access Token + Refresh Token请求资源 (携带 Access Token)验证令牌返回资源刷新令牌 (携带 Refresh Token)返回新 Access Token重试请求 (新 Access Token)返回资源

自定义 JWT 过滤器需要继承 OncePerRequestFilter,在过滤器中解析 Token、验证签名和有效期,然后将用户信息设置到 SecurityContextHolder 中。

package com.example.security.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final String secret = "YourSecretKey";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Claims claims = Jwts.parser()
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
                String username = claims.getSubject();
                if (username != null) {
                    UsernamePasswordAuthenticationToken auth =
                            new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (Exception e) {
                // Token 无效或过期
            }
        }
        filterChain.doFilter(request, response);
    }

    // 生成 Token 的工具方法可在服务类中实现
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24小时
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
}

同时需要提供登录接口和刷新令牌接口,实现完整的认证流程。

授权模型:从角色到权限

认证决定用户是谁,授权则决定用户能做什么。Spring Security 中授权可以通过两种维度控制:

  • 角色:粗粒度,例如 ROLE_USERROLE_ADMIN
  • 权限:细粒度,例如 READ_PRIVILEGEWRITE_PRIVILEGE

当简单的角色不满足需求时,可以引入权限资源表,并在方法或 URL 级别使用表达式进行控制:

@PreAuthorize("hasRole('ADMIN') or hasAuthority('DOCUMENT_READ')")
public Document getDocument(Long id) {
    // ...
}

还可以自定义权限表达式,例如:

@PreAuthorize("@documentPermissionEvaluator.hasPermission(#id, authentication)")
public Document getDocument(Long id) {
    // ...
}

这允许将授权逻辑从硬编码中剥离,方便动态配置。

角色之间也可以建立继承关系,例如 ROLE_ADMIN 自动拥有 ROLE_USER 的所有权限。可以通过 RoleHierarchy 实现。

方法级与 URL 级安全配置对比

安全粒度优点缺点
URL 级(WebSecurity 配置)集中管理,适合简单的路径权限与业务逻辑耦合度较低,无法细化到数据级
方法级(@PreAuthorize 等)紧密集成业务代码,支持复杂表达式,可到方法参数级控制分散在代码各处,需注意一致性

微服务中的 OAuth2 与授权服务器

当应用拆分为多个微服务时,采用 OAuth2 可以统一完成认证和授权。

典型角色划分:

  • 授权服务器:负责认证用户,并向客户端颁发令牌。
  • 资源服务器:持有受保护资源,验证令牌后提供服务。
  • 客户端:需要访问资源的应用,可以是前端 SPA 或另一个微服务。

实现自己的授权服务器可以使用 Spring Security OAuth2 或 Spring Authorization Server。以下展示资源服务器如何配置 JWT 验证:

package com.example.resourceserver.config;

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.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain resourceFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/public").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.decoder(jwtDecoder()))
            );
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri("http://auth-server/oauth2/jwks").build();
    }
}

OAuth2 授权模式

  • 授权码模式:最安全,适合有后端的 Web 应用。
  • 密码模式:已不推荐,但遗留系统可能还在使用。
  • 客户端模式:用于服务间通信,无用户参与。
  • 简化模式:已过时,不推荐使用。

在实际开发中,推荐使用授权码模式 + PKCE 增强安全性。

单点登录与社交登录

在统一授权服务器的基础上,单点登录(SSO)变得自然而然:用户只需登录一次,就可以访问多个信任的应用。

社交登录(如 GitHub、微信、微博)可以集成 OAuth2 登录模块,只需配置对应的 ClientRegistration

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: your-client-id
            client-secret: your-client-secret
            scope: read:user,user:email
        provider:
          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

若需与已有的第三方授权服务器(如 Keycloak)集成,只需将 spring.security.oauth2.resourceserver.jwt.issuer-urijwk-set-uri 指向其端点即可,几乎零代码侵入。

项目结构示例

一个完整的单体应用结合 JWT 认证与 OAuth2 客户端、角色权限管理的典型结构如下:

src/main/java/com/example/demo/
├── config
│   ├── SecurityConfig.java
│   ├── JwtConfig.java
│   └── ResourceServerConfig.java
├── controller
│   ├── AuthController.java
│   └── UserController.java
├── filter
│   └── JwtAuthenticationFilter.java
├── model
│   ├── User.java
│   └── Role.java
├── repository
│   └── UserRepository.java
├── service
│   ├── UserService.java
│   └── AuthService.java
└── DemoApplication.java

前后端协作注意事项

  • CORS(跨域资源共享):后端需配置允许的前端源、方法和头信息。Spring Security 中可通过 cors() 配置,并注册 CorsConfigurationSource
  • CSRF 防护:对于前后端分离场景,若使用 JWT 并存储在 Authorization 头中而非 Cookie,可以关闭 CSRF,因为 CSRF 攻击依赖于浏览器的自动 Cookie 发送。但在涉及 cookie 存储令牌时,仍需开启 CSRF。
  • 错误响应统一:认证失败或权限不足时应返回清晰的 JSON 格式,便于前端统一处理。

总结

从单体应用的过滤器链和用户详情服务,到 JWT 设计、多因子认证,再到微服务下的 OAuth2 授权服务器架构,Spring SecurityOAuth2 共同构成了 Java 企业级安全的核心技术栈。

掌握这些知识不仅能够独立设计安全的单体应用,也有能力为分布式系统构建统一的认证授权中心。通过持续学习与实践,可以构建出既坚固又灵活的安全体系。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值