引言
当企业用微服务搭建起灵活的业务中台,却发现“城门大开”——服务间调用可能被截获、用户密码明文传输、越权操作屡见不鲜…微服务的“松耦合”特性,反而让安全防线变得支离破碎。这时候,Spring Security就像一位“安全管家”,能为微服务集群织起一张密不透风的防护网。本文将从原理到实战,带你拆解Spring Security的“十八般武艺”,无论是表单登录、角色权限控制,还是OAuth2第三方登录、JWT无状态认证,都能让你轻松上手!
一、Spring Security的“安检中心”:架构原理深度解析
如果把微服务系统比作一个大型商场,Spring Security就是商场的“智能安检系统”。每个进入商场的请求(顾客)都要经过一系列安检通道(过滤器链),只有通过所有检查(身份验证+权限校验),才能到达目标店铺(业务接口)。
1.1 过滤器链:请求的“必经之路”
Spring Security的核心是过滤器链(FilterChain),就像商场的安检通道,每个过滤器负责不同的检查任务:
UsernamePasswordAuthenticationFilter
:检查是否携带表单登录信息(相当于检查是否有纸质门票)BasicAuthenticationFilter
:验证HTTP Basic的Authorization头(相当于检查电子门票二维码)FilterSecurityInterceptor
:最终的权限校验(相当于检查门票是否能进入VIP区域)
当一个请求进入系统时,会按顺序经过这些过滤器。如果某个过滤器发现异常(如密码错误),会直接拦截请求;只有通过所有过滤器,请求才能到达Controller层。这种“链式检查”设计,让我们可以灵活插拔安全功能——比如不需要表单登录时,直接移除对应的过滤器即可。
1.2 核心组件:安检系统的“四大部门”
要理解Spring Security的运行机制,必须认识这几个关键组件,它们就像安检系统中的登记处、验证处、权限库和广播中心:
(1)AuthenticationManager:安检总调度
AuthenticationManager
是身份验证的总接口,最常用的实现类是ProviderManager
。它就像安检调度中心,管理着多个AuthenticationProvider
(具体安检员)。比如:
- 当用户通过表单登录时,
ProviderManager
会调用DaoAuthenticationProvider
(数据库验证员) - 当用户使用LDAP登录时,会调用
LdapAuthenticationProvider
(LDAP验证员)
(2)UserDetailsService:用户信息的“数据库桥梁”
UserDetailsService
是Spring Security与业务系统的“数据翻译官”,它的loadUserByUsername
方法负责从数据库/缓存中加载用户信息(用户名、密码、权限)。例如:
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库查询用户
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 将业务用户对象转换为Spring Security需要的UserDetails
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
// 转换权限:数据库中的“ROLE_ADMIN”转为SimpleGrantedAuthority
user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList())
);
}
}
注意:这里返回的UserDetails
包含密码(会被自动与用户输入的密码比对)和权限列表(决定能访问哪些资源)。
(3)SecurityContextHolder:安全信息的“全局广播”
SecurityContextHolder
就像商场的广播系统,存储着当前用户的安全上下文(SecurityContext
)。在Controller、Service甚至工具类中,我们可以随时获取当前用户信息:
// 获取当前登录用户的用户名
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentUsername = authentication.getName();
// 获取当前用户的权限列表
List<String> authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
这个设计非常巧妙——不需要在每个方法参数中传递用户信息,真正实现了“安全无感知”。
(4)GrantedAuthority:权限的“身份证”
GrantedAuthority
是用户权限的具体表示,通常用SimpleGrantedAuthority
实现。比如:
- 角色权限:
ROLE_ADMIN
(表示管理员角色) - 功能权限:
USER_DELETE
(表示可以删除用户) - 数据权限:
DEPT_1001
(表示可以访问1001部门的数据)
在授权时,Spring Security会检查用户的GrantedAuthority
是否包含目标资源所需的权限。
二、配置Spring Security:从Java代码到XML的“两种姿势”
Spring Security的配置方式有两种:Java配置(Spring Boot推荐)和XML配置(传统项目兼容)。我们先看更现代的Java配置。
2.1 Java配置:用代码定义安全策略(Spring Boot主流)
在Spring Boot中,通过@Configuration
+@EnableWebSecurity
注解定义配置类,核心是构建SecurityFilterChain
Bean。
示例1:基础权限控制(允许/public路径公开访问)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 注入自定义的UserDetailsService
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置请求授权规则
.authorizeHttpRequests(auth -> auth
// /public路径下的所有请求无需认证
.requestMatchers("/public/**").permitAll()
// /admin路径需要ADMIN角色
.requestMatchers("/admin/**").hasRole("ADMIN")
// /user/edit需要USER_EDIT权限
.requestMatchers("/user/edit").hasAuthority("USER_EDIT")
// 其他所有请求需要认证
.anyRequest().authenticated()
)
// 启用表单登录
.formLogin(form -> form
// 自定义登录页面(默认是Spring Security提供的)
.loginPage("/login")
// 登录处理接口(默认是/login)
.loginProcessingUrl("/doLogin")
// 登录成功后跳转的页面
.defaultSuccessUrl("/home", true)
// 登录失败跳转的页面
.failureUrl("/login?error")
// 允许所有用户访问登录页面
.permitAll()
)
// 启用HTTP Basic认证(用于接口调用)
.httpBasic()
// 禁用CSRF保护(前后端分离项目常用,表单提交需启用)
.csrf().disable();
return http.build();
}
// 配置密码编码器(必须!否则密码无法正确比对)
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt强哈希算法(自动生成盐值)
return new BCryptPasswordEncoder();
}
// 配置用户存储(这里使用自定义的UserDetailsService)
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
}
关键配置说明:
authorizeHttpRequests
:定义请求的授权规则,支持路径匹配(requestMatchers
)、角色校验(hasRole
)、权限校验(hasAuthority
)等。formLogin
:配置表单登录的页面、处理接口、跳转逻辑。passwordEncoder
:必须配置!Spring Security不会明文存储密码,而是存储哈希值(如BCrypt)。authenticationProvider
:将UserDetailsService
和PasswordEncoder
绑定,告诉Spring Security如何验证用户。
示例2:基于方法的权限控制(@PreAuthorize)
除了路径级别的权限控制,Spring Security还支持方法级别的控制。需要启用@EnableMethodSecurity
:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用方法安全注解
public class MethodSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
// 其他配置...
}
// 在Service方法中使用注解
@Service
public class UserService {
// 只有ADMIN角色可以调用
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// 删除用户逻辑...
}
// 只有拥有USER_EDIT权限且用户名是当前登录用户才能调用
@PreAuthorize("hasAuthority('USER_EDIT') && principal.username == #username")
public void editUser(String username, UserDTO userDTO) {
// 编辑用户逻辑...
}
}
@PreAuthorize
在方法执行前校验权限,支持SpEL表达式(如principal
获取当前用户,#username
获取方法参数)。
2.2 XML配置:传统项目的“怀旧选择”
在传统Spring项目中(非Spring Boot),可以通过XML配置Spring Security。虽然不如Java配置灵活,但对于老系统迁移很有用。
示例:基础权限+内存用户(适合演示)
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 配置HTTP请求安全规则 -->
<http>
<!-- /public路径公开访问 -->
<intercept-url pattern="/public/**" access="permitAll"/>
<!-- /admin路径需要ADMIN角色 -->
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/>
<!-- 其他路径需要认证 -->
<intercept-url pattern="/**" access="authenticated"/>
<!-- 启用表单登录 -->
<form-login
login-page="/login"
default-target-url="/home"
authentication-failure-url="/login?error"/>
<!-- 启用HTTP Basic认证 -->
<http-basic/>
<!-- 禁用CSRF(前后端分离项目) -->
<csrf disabled="true"/>
</http>
<!-- 配置认证管理器 -->
<authentication-manager>
<authentication-provider>
<!-- 内存用户(实际项目应替换为jdbc-user-service或自定义UserDetailsService) -->
<user-service>
<user name="admin" password="{bcrypt}$2a$10$X5z4...(BCrypt哈希值)" authorities="ROLE_ADMIN,USER_EDIT"/>
<user name="user" password="{bcrypt}$2a$10$Y7w3...(BCrypt哈希值)" authorities="ROLE_USER"/>
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
注意:XML配置中的密码必须使用{encoder}
前缀指定编码器(如{bcrypt}
),否则Spring Security无法正确解析。
三、实战:用Spring Security实现微服务的“三重防护”
微服务安全的核心是身份认证(Who are you?)和授权(What can you do?)。我们通过三个典型场景,演示Spring Security的实战应用。
3.1 场景1:表单登录(最常见的用户认证)
对于面向C端的Web应用,表单登录是最常用的认证方式。我们需要:
- 自定义登录页面(/login)
- 处理登录请求(/doLogin)
- 登录成功/失败的跳转逻辑
步骤1:创建登录页面(Thymeleaf示例)
<!-- src/main/resources/templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
</head>
<body>
<h1>用户登录</h1>
<!-- 显示登录错误信息 -->
<div th:if="${param.error}" style="color: red;">用户名或密码错误</div>
<form th:action="@{/doLogin}" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required>
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required>
</div>
<button type="submit">登录</button>
</form>
</body>
</html>
步骤2:配置Spring Security(前文示例1的扩展)
@Configuration
@EnableWebSecurity
public class FormLoginConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页面
.loginProcessingUrl("/doLogin") // 登录请求处理路径(与表单action一致)
.defaultSuccessUrl("/home") // 登录成功跳转
.failureUrl("/login?error") // 登录失败跳转
.permitAll() // 允许所有用户访问登录相关路径
)
// 配置记住我功能(30天有效)
.rememberMe(remember -> remember
.key("uniqueAndSecret") // 加密密钥
.tokenValiditySeconds(2592000) // 30天(秒)
.userDetailsService(userDetailsService) // 指定UserDetailsService
);
return http.build();
}
// 其他Bean(passwordEncoder、authenticationProvider)...
}
扩展功能:rememberMe
配置“记住我”功能,会生成一个加密的Cookie,用户30天内无需重复登录。
3.2 场景2:HTTP Basic认证(接口的轻量级认证)
对于微服务间的接口调用(如A服务调用B服务的API),HTTP Basic认证是更简单的选择。它通过请求头传递Authorization: Basic base64(username:password)
,适合无界面的服务间通信。
配置示例:
@Configuration
@EnableWebSecurity
public class HttpBasicConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/private/**").authenticated()
.anyRequest().denyAll() // 其他路径拒绝访问
)
.httpBasic() // 启用HTTP Basic认证
.csrf().disable(); // 前后端分离项目通常禁用CSRF
return http.build();
}
// 配置内存用户(实际项目用数据库)
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.builder()
.username("serviceA")
.password(passwordEncoder().encode("secret123"))
.roles("SERVICE")
.build();
return new InMemoryUserDetailsManager(admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
测试接口:
使用Postman发送请求,在Headers中添加:
Authorization: Basic c2VydmljZUE6c2VjcmV0MTIz(即base64("serviceA:secret123"))
注意:HTTP Basic的密码是明文传输的(Base64可解码),必须配合HTTPS使用!