纲要
Spring Security与OAuth2核心概念:认证与授权- 主流安全框架对比与选型依据
- 单体应用安全架构:过滤器链、安全上下文、密码编码器
- 多因子认证与 TOTP 实战
- JWT 令牌设计:访问令牌与刷新令牌
- OAuth2 授权模式与微服务角色(授权服务器、资源服务器、客户端)
- 社交登录与单点登录实现思路
- 授权模型:角色、权限、自定义表达式
- 前端集成注意事项:CORS、CSRF
- 示例代码
认证与授权的基础概念
在任何一个需要保护资源的系统中,核心的安全问题都可以归纳为两点:认证和授权。
- 认证 回答“你是谁”的问题。例如通过用户名密码、短信验证码、指纹等方式证明用户身份。
- 授权 回答“你能做什么”的问题。在认证通过后,系统根据角色或权限决定用户是否能访问某个接口、执行某个操作或查看某部分数据。
打个比方:进入小区大门需要刷门禁卡,这是认证;进入小区后,能否打开某一栋楼或某一户的门,就属于授权。几乎所有的企业级应用、电商系统、后台管理平台都离不开这套机制。同时,认证与授权是整个系统安全基座的一部分,基础不稳会带来严重安全风险。而且攻防技术不断演进,安全策略也需要持续升级,因此这一技术栈需要长期学习。
为什么选择 Spring Security + OAuth2
在 Java 生态中,安全框架主要有 Apache Shiro、Spring Security 以及标准 OAuth2 实现等。根据 Google Trends 的数据,Spring Security 的社区热度和关注度长期远超其他竞品,这反映了开发者社区的广泛认可。
选择 Spring Security 的几点理由:
- 它是 Spring 全家桶的核心成员,与 Spring Boot、Spring Cloud 等其他组件无缝集成。
- 功能覆盖全面:支持认证、授权、CSRF 防御、会话管理、方法级安全等,几乎可以覆盖所有企业安全需求。
- 架构设计良好,业务代码与安全逻辑高度解耦,通过过滤器链和注解即可完成非侵入式的安全配置。
当系统从单体演进到微服务架构时,OAuth2 成为分布式场景下授权的标准协议。它定义了资源所有者、客户端、授权服务器和资源服务器四种角色,支持授权码模式、密码模式、客户端模式、简化模式等多种授权流程,能够优雅地解决跨服务认证与授权难题。
单体架构下的安全基础
在单体应用中,Spring Security 基于 过滤器链 工作。每个请求都会经过一系列过滤器,分别完成安全上下文构建、身份认证、授权决策等。核心组件包括:
SecurityContextHolder:存放当前认证通过的安全上下文,包含用户主体及其权限信息。UserDetailsService:从数据库或其他数据源加载用户信息的抽象接口。GrantedAuthority:表示授予用户的权限或角色。- 过滤器链中的
UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter等负责处理不同类型的认证请求。
一个典型的单体安全配置如下:
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)
简单的“用户名+密码”已不足以应对现代安全需求。多因子认证要求用户提供两种或以上不同类型的凭证,比如:
- 用户输入用户名和密码(知识因素)
- 系统向用户手机或邮箱发送一次性验证码(所有权因素)
常见实现方案是基于 TOTP(基于时间的一次性密码)或短信、邮件验证码。后端可以设计如下接口:
- 选择接收验证码的方式(短信 / 邮件)
- 发送验证码
- 验证验证码并完成登录
同时可以利用 Redis 缓存验证码,设置合理的过期时间,并结合 Spring Security 的自定义过滤器来串联这一认证流程。
JWT 令牌与前后端分离
在前后端分离或微服务架构中,传统基于 Session 的认证机制会带来状态共享和扩展性问题。JSON Web Token (JWT) 作为一种自包含的令牌格式,成为主流选择。
认证成功后,服务器签发两个令牌:
- Access Token:短期有效,携带用户身份和权限信息,用于访问资源。
- Refresh Token:长期有效,仅用于换取新的 Access Token,降低泄露风险。
JWT 的结构和签发流程可以用下图表示:
自定义 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_USER、ROLE_ADMIN。 - 权限:细粒度,例如
READ_PRIVILEGE、WRITE_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-uri 或 jwk-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 Security 与 OAuth2 共同构成了 Java 企业级安全的核心技术栈。
掌握这些知识不仅能够独立设计安全的单体应用,也有能力为分布式系统构建统一的认证授权中心。通过持续学习与实践,可以构建出既坚固又灵活的安全体系。

5854

被折叠的 条评论
为什么被折叠?



