Spring Security 是什么
Spring Security 是 Spring 生态中专注于认证(Authentication)、授权(Authorization)与攻击防护的强大的可定制安全框架。它通过整合 Servlet 过滤器机制,为 Spring 应用提供了行业标准的安全保护能力。本教程默认读者已具备独立搭建 Spring Boot 或 SSM 项目的能力,以下内容聚焦于 Spring Security 本身,不再展开 Spring Core 或 Spring Boot 的基础知识。
定义与作用:为什么需要 Spring Security
在没有安全框架时,开发者通常需要为每个 Web 应用手写大量安全过滤器:检查登录状态、校验密码、判断权限、防御 CSRF、处理登出逻辑等。这些手写代码不仅重复劳动量大,而且极易因疏忽留下安全漏洞。
手写过滤器的典型痛点:
| 痛点 | 说明 |
|---|---|
| 重复造轮子 | 每个项目都要重写登录校验、权限判断、密码比对 |
| 安全漏洞难防 | CSRF、Session Fixation、Clickjacking 等攻击需要专业的防御实现 |
| 扩展困难 | 新增 OAuth2、JWT 等认证方式时需重写整套逻辑 |
| 与 Spring 容器割裂 | 手写 javax.servlet.Filter 无法享受 Spring 的依赖注入(DI)和 AOP 能力 |
Spring Security 解决了上述所有问题:它将安全能力抽象为一组可配置的过滤器链,开发者只需通过声明式配置(如 HttpSecurity)即可启用企业级安全防护,同时保留高度扩展性。
核心原理
Spring Security 的骨架可以概括为一句话:通过一条过滤器链,在请求到达 Controller 之前完成认证与授权决策,并自动附加各类攻击防护。
其核心能力围绕三大支柱展开:
- 认证(Authentication):验证用户身份("你是谁")。支持表单登录、HTTP Basic、JWT、OAuth2、LDAP 等多种方式。
- 授权(Authorization):控制用户可访问的资源("你能做什么")。支持 URL 级别、方法级别、实例级别的细粒度控制。
- 攻击防护:内置 CSRF 防御、Session Fixation 防护、安全响应头(防 Clickjacking、XSS 等)。
在内部实现上,Spring Security 将请求委托给 DelegatingFilterProxy,再由 FilterChainProxy 调度到匹配当前请求的 SecurityFilterChain。链中的每个过滤器各司其职,按固定顺序执行。
示例一:从手写过滤器到 Spring Security 配置
场景说明
某后台管理系统需要保护 /admin/** 路径,仅允许 ADMIN 角色的用户访问。其余路径对外开放。
操作前配置:手写过滤器方案
在没有 Spring Security 时,开发者通常手写一个过滤器:
public class AdminAuthFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String uri = request.getRequestURI();
if (uri.startsWith("/admin/")) {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.sendRedirect("/login");
return;
}
// 还要手动检查角色... 代码继续膨胀
}
chain.doFilter(req, res);
}
}
然后在 web.xml 中注册:
<filter>
<filter-name>adminAuthFilter</filter-name>
<filter-class>com.example.AdminAuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>adminAuthFilter</filter-name>
<url-pattern>/admin/*</url-pattern>
</filter-mapping>
痛点:角色判断、密码校验、Session 管理、CSRF 防护全部缺失;过滤器无法注入 Spring 的 Service 层查询用户。
操作后配置:Spring Security 方案
引入 Spring Security 依赖后,仅需一个配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/**").permitAll()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin")
.password("{noop}admin123") // 5.x 支持 {noop} 前缀
.roles("ADMIN")
.build());
return manager;
}
}
结果分析
| 维度 | 手写过滤器 | Spring Security |
|---|---|---|
| 代码量 | 50+ 行,且需自行处理 Session、角色、重定向 | 约 15 行声明式配置 |
| 安全性 | 无 CSRF 防护,无密码编码,无 Session Fixation 防护 | 全部默认开启 |
| 可扩展性 | 新增 OAuth2 或 JWT 需重写整套代码 | 替换配置即可接入新协议 |
| Spring 集成 | 无法注入 Service | 完整享受 DI、AOP、事件机制 |
示例二:快速启用方法级安全控制
场景说明
URL 级别的授权只能控制到路径维度。如果 /user/profile 下的"修改密码"和"查看信息"需要区分权限,就必须下沉到方法级别控制。
操作前配置
手写 AOP 切面来拦截 Service 方法,手动从 Session 中读取用户身份,再比对权限字符串:
@Aspect
@Component
public class MethodSecurityAspect {
@Around("@annotation(com.example.RequiresRole)")
public Object checkRole(ProceedingJoinPoint pjp) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
RequiresRole anno = ((MethodSignature) pjp.getSignature())
.getMethod().getAnnotation(RequiresRole.class);
if (user == null || !Arrays.asList(anno.value()).contains(user.getRole())) {
throw new RuntimeException("无权限");
}
return pjp.proceed();
}
}
这要求自定义注解、硬编码权限判断逻辑,且无法复用 Spring Security 的 AccessDecisionManager 投票体系。
操作后配置
开启 Spring Security 的方法级安全:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 5.x 开启方法级安全
public class SecurityConfig {
// ... 其他配置
}
在 Service 层直接加注解:
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public UserProfile getProfile(Long userId) {
return userDao.findById(userId);
}
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
userDao.delete(userId);
}
}
结果分析
- 使用
@PreAuthorize后,权限表达式直接支持 SpEL,可以引用方法参数(#userId)和当前认证对象(authentication.principal)。 - Spring Security 自动在方法调用前完成权限校验,失败抛出
AccessDeniedException,并由ExceptionTranslationFilter统一转译为 403 响应。 - 开发者无需手写 AOP 切面,无需手动操作 Session,无需处理异常转译。
易错场景与面试考点
易错场景:混淆 hasRole 与 hasAuthority
在 Spring Security 5.x 中,hasRole("ADMIN") 会自动在内部补上前缀,实际检查的是 ROLE_ADMIN;而 hasAuthority("ADMIN") 则不做任何前缀补全,直接检查权限字符串是否精确等于 ADMIN。
错误写法:
// 如果 UserDetails 中授予的是 ROLE_ADMIN,以下写法会永久返回 403
.antMatchers("/admin/**").hasAuthority("ADMIN")
正确写法:
// 方式一:使用 hasRole,框架自动补全 ROLE_ 前缀
.antMatchers("/admin/**").hasRole("ADMIN")
// 方式二:使用 hasAuthority,手动写全权限字符串
.antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
面试考点:面试官常问
hasRole和hasAuthority的区别。记住一句话:hasRole会自动追加ROLE_前缀,hasAuthority是精确匹配。在企业代码中,建议统一使用hasAuthority并显式写出ROLE_XXX,避免隐性前缀导致维护困惑。
版本说明:本章节示例基于 Spring Security 5.x 语法,使用
antMatchers与@EnableGlobalMethodSecurity(prePostEnabled=true)等 5.x 典型配置风格。