Spring Security实战指南:从零开始构建安全的Java应用
目录导读
- Spring Security是什么?为什么需要它?
- 核心概念:认证与授权
- 快速入门:基础配置三步走
- 如何保护REST API?
- 实战问答:常见问题与解决
- 进阶优化:CSRF、CORS与密码加密
- 安全不止于配置
Spring Security是什么?为什么需要它?
在Web开发中,安全是不可回避的核心问题,Spring Security是Spring家族中专门负责安全控制的框架,它提供认证(你是谁?)和授权(你能做什么?)两大核心功能。

使用Spring Security的理由:
- 开箱即用:无需从零实现加密、会话管理、CSRF防护等底层机制。
- 高度可定制:支持表单登录、OAuth2、LDAP、JWT等多种认证方式。
- 社区成熟:作为Spring生态标准安全方案,文档丰富,问题反馈快。
一个例子:假设你有一个博客系统,未登录用户只能浏览文章,登录用户才能评论,管理员才能删除文章,Spring Security能让你通过声明式配置快速实现这套规则。
核心概念:认证与授权
认证 (Authentication)
验证用户身份,常见的认证方式包括:
- 表单登录:用户输入用户名密码
- HTTP Basic认证:请求头携带Base64编码的凭据
- JWT Token:无状态令牌验证
授权 (Authorization)
判断用户是否有权限执行操作,常见粒度:
- URL级别:如
/admin/**仅允许ADMIN角色访问 - 方法级别:如
@PreAuthorize("hasRole('USER')")标记在Service方法上
理解“安全上下文”:Spring Security通过 SecurityContextHolder 存储当前用户信息,就像给每个请求贴了一张“身份证”。
快速入门:基础配置三步走
第一步:引入依赖
如果你的项目是Maven,在 pom.xml 添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
一旦添加,Spring Boot会自动配置基础安全:所有端点需要登录,默认用户名 user,密码在启动日志中随机生成。
第二步:编写安全配置类
创建一个 SecurityConfig 类,使用 @EnableWebSecurity 和 @Configuration 注解:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
// 公开页面允许所有人访问
.requestMatchers("/", "/home", "/login", "/register").permitAll()
// 其他所有请求需要认证
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页
.defaultSuccessUrl("/dashboard", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/home")
.permitAll()
);
return http.build();
}
}
第三步:添加用户存储器
默认内存用户无法满足生产需求,推荐配置JDBC或JPA用户存储:
@Bean
public UserDetailsManager users(DataSource dataSource) {
// 使用数据库表存储用户(需预先建表)
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
return manager;
}
或者用内存用户做测试:
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}加密后的密码")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
关键注意:密码必须使用 PasswordEncoder 编码,推荐 BCryptPasswordEncoder。
如何保护REST API?
对于前后端分离项目,Spring Security通常配合JWT(JSON Web Token)工作。
核心思路:
- 用户登录时校验身份,生成JWT返回给前端
- 前端请求携带JWT到Header
- 自定义过滤器解析JWT并设置安全上下文
快速实现JWT过滤器示例:
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String token = extractToken(request);
if (token != null && validateToken(token)) {
String username = getUsernameFromToken(token);
// 创建认证对象
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
然后在配置中禁用csrf、不启用表单登录:
http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
实战问答:常见问题与解决
Q1:为什么配置了 permitAll() 但访问 /register 依然返回403?
解答:请检查你的配置顺序,Spring Security的规则生效顺序是“先匹配先生效”,若配置了 .anyRequest().authenticated() 在前面,后面permitAll将不生效,正确的顺序应把具体放行规则放在前面。
Q2:如何实现用户角色“管理员可以查看所有用户,普通用户只能查看自己”?
解答:在Controller或Service方法上使用 @PreAuthorize:
@GetMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public User getUser(@PathVariable Long id) { ... }
但这需要确保认证对象包含用户ID,通常在登录时设置到 UserDetails 实现类中。
Q3:为什么我的自定义登录页面不起作用?
解答:确认两点:1) loginPage() 路径正确,且该路径已被 permitAll() 放行;2) 登录页面表单的 action 路径必须是 /login(默认),或者通过 .loginProcessingUrl() 自定义提交地址。
Q4:使用 BCryptPasswordEncoder 加密后,登录时密码如何比对?
解答:Spring Security会自动处理,你只需在配置密码时使用 {bcrypt}前缀+加密后的密文,
User.withDefaultPasswordEncoder().username("user").password("密码明文").roles("USER")
但在生产环境不推荐硬编码,应使用 PasswordEncoder Bean自动编码。
进阶优化:CSRF、CORS与密码加密
CSRF防护
- 默认开启:对于非REST应用(表单提交)保持开启
- API场景关闭:如使用JWT无状态认证,则需通过
http.csrf(disable())关闭 - 提示:如果保留CSRF,前端每次POST/PUT/DELETE请求需携带
_csrftoken,可通过Cookie或Head传递
CORS配置
前后端分离时,需允许跨域请求:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000")); // 前端地址
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
然后在 http 配置中启用:.cors(cors -> cors.configurationSource(corsConfigurationSource()))
密码加密策略
- 推荐使用
BCryptPasswordEncoder,其内置随机盐值,抗彩虹表攻击 - 配置方式:在配置类中声明
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - 迁移旧系统时,可组合
DelegatingPasswordEncoder支持多种编码格式
Spring Security 的核心价值在于“通过配置将安全逻辑从业务代码中解耦”,从最简单的表单登录到复杂的OAuth2、LDAP集成,它都提供了一套可扩展的架构。
三个必须记住的原则:
- 认证在前,授权在后:先明确用户身份,再判断其权限
- 显式配置匹配顺序:精确路径总是优先,通配符规则放在最后
- 永远不要存储明文密码:使用强哈希算法加密,配合随机盐值
最后建议:在实际项目中,先明确安全需求(哪些资源需要保护?使用什么认证方式?),再动手配置,对于初学者,从表单登录+内存用户开始,逐步替换为数据库+JWT方案,能更平稳地掌握整个框架,安全不是一次性工作,而是迭代进化的过程。