🔥 系列导读:本系列将带你从零开始,逐步掌握Spring Security的各项核心技能,从基础入门到高级应用,最终成为安全框架专家。本文是系列第二篇,将带你快速上手Spring Security,实现基本的认证和授权功能。
📚 前言
在上一篇文章中,我们介绍了Spring Security的基础概念和架构。今天,我们将通过实际操作,带你快速入门Spring Security,实现一个具备基本安全功能的Web应用。
本文将通过一个渐进式的示例,从最简单的配置开始,逐步添加更多功能,帮助你理解Spring Security的工作方式和配置方法。
🛠️ 环境准备
首先,我们需要创建一个Spring Boot项目,并添加必要的依赖。
项目依赖
使用Spring Initializr或手动创建一个包含以下依赖的项目:
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Thymeleaf (可选,用于视图渲染) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
</dependencies>
基础控制器
创建一些简单的控制器来测试不同的访问权限:
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home"; // 首页,所有人可访问
}
@GetMapping("/user")
public String userPage() {
return "user"; // 用户页面,需要USER角色
}
@GetMapping("/admin")
public String adminPage() {
return "admin"; // 管理页面,需要ADMIN角色
}
}
🔒 最小化配置
添加Spring Security依赖后,应用已经具备了基本的安全防护,无需任何额外配置!这是因为Spring Boot的自动配置机制。
默认行为
- 所有HTTP请求都需要认证
- 生成随机密码(控制台输出)和默认用户名"user"
- 提供基本的登录表单
- CSRF防护已启用
- Session Fixation防护已启用
- 安全Headers已配置(XSS防护等)
自定义基础配置
虽然默认配置已经提供了基本安全性,但在实际应用中我们通常需要自定义配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/", "/home", "/css/**", "/js/**").permitAll() // 公开资源
.antMatchers("/user/**").hasRole("USER") // 用户资源
.antMatchers("/admin/**").hasRole("ADMIN") // 管理资源
.anyRequest().authenticated() // 其他资源需认证
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页
.defaultSuccessUrl("/home") // 登录成功跳转
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout") // 注销后跳转
.permitAll()
);
return http.build();
}
}
💡 注意:Spring Security 6.0以上版本使用了新的Lambda DSL风格配置,与旧版本有所不同。
👤 用户认证配置
Spring Security提供了多种方式来配置用户信息,从最简单的内存用户到与数据库集成的方式。
内存用户配置
适合开发测试或简单应用:
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG") // "password"
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG") // "password"
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
密码编码器
永远不要存储明文密码!Spring Security要求配置一个密码编码器:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
使用密码编码器创建用户:
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
// 更多用户...
return new InMemoryUserDetailsManager(user);
}
数据库认证
实际应用中,用户信息通常存储在数据库中:
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
默认情况下,JdbcUserDetailsManager期望以下表结构:
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users (username)
);
自定义UserDetailsService
更常见的做法是实现自己的UserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
🔐 自定义登录页面
默认的登录页面非常简单,在实际应用中我们通常需要自定义登录页面:
登录页面控制器
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
Thymeleaf登录页面模板
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<div class="login-container">
<h2>欢迎登录</h2>
<div th:if="${param.error}" class="alert alert-error">
用户名或密码错误
</div>
<div th:if="${param.logout}" class="alert alert-success">
您已成功退出
</div>
<form th:action="@{/login}" method="post">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required autofocus />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" required />
</div>
<div class="form-group remember-me">
<input type="checkbox" id="remember-me" name="remember-me" />
<label for="remember-me">记住我</label>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
</body>
</html>
💡 提示:表单字段名必须是"username"和"password",除非你自定义了UsernamePasswordAuthenticationFilter。
🔄 会话管理
会话管理是Web应用安全的重要部分,Spring Security提供了丰富的会话管理功能:
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 会话创建策略
.invalidSessionUrl("/login?invalid") // 无效会话跳转
.maximumSessions(1) // 最大会话数
.maxSessionsPreventsLogin(false) // 是否阻止新登录
.expiredUrl("/login?expired") // 会话过期跳转
);
记住我功能
"记住我"功能允许用户在会话过期后仍然保持登录状态:
http
.rememberMe(remember -> remember
.key("uniqueAndSecretKey") // 加密密钥
.tokenValiditySeconds(86400) // 有效期(秒)
);
🛡️ CSRF防护
跨站请求伪造(CSRF)是一种常见的Web攻击,Spring Security默认启用CSRF防护:
http
.csrf(csrf -> csrf
.ignoringAntMatchers("/api/**") // 忽略API请求的CSRF验证
);
在表单中添加CSRF令牌:
<form th:action="@{/login}" method="post">
<!-- Thymeleaf自动添加CSRF令牌 -->
<!-- 或手动添加: -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- 表单字段... -->
</form>
🔍 实际应用示例
下面是一个更完整的配置示例,整合了我们讨论的各个部分:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 请求授权
.authorizeRequests(authorize -> authorize
.antMatchers("/", "/home", "/css/**", "/js/**", "/images/**").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 表单登录
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/perform_login")
.defaultSuccessUrl("/home")
.failureUrl("/login?error=true")
.permitAll()
)
// 注销
.logout(logout -> logout
.logoutUrl("/perform_logout")
.logoutSuccessUrl("/login?logout=true")
.deleteCookies("JSESSIONID")
.permitAll()
)
// 记住我
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400)
)
// 会话管理
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/login?invalid")
.maximumSessions(1)
.expiredUrl("/login?expired")
)
// 异常处理
.exceptionHandling(exceptions -> exceptions
.accessDeniedPage("/access-denied")
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
🧪 测试安全配置
Spring Security提供了强大的测试支持,使我们能够轻松测试安全配置:
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityConfigTest {
@Autowired
private MockMvc mockMvc;
@Test
public void accessUnsecuredEndpointsWithoutLogin() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk());
}
@Test
public void accessSecuredEndpointRedirectsToLogin() throws Exception {
mockMvc.perform(get("/user"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
@Test
@WithMockUser(roles = "USER")
public void userCanAccessUserEndpoint() throws Exception {
mockMvc.perform(get("/user"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
public void userCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = {"USER", "ADMIN"})
public void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isOk());
}
}
🚀 最佳实践与常见问题
最佳实践
- 永远不要存储明文密码,始终使用强密码编码器
- 遵循最小权限原则,只授予必要的权限
- 保持依赖更新,及时修复安全漏洞
- 启用HTTPS,保护传输中的数据
- 合理配置会话管理,防止会话固定攻击
- 定制错误消息,避免泄露敏感信息
常见问题
-
登录失败但没有错误消息
- 检查用户名/密码是否正确
- 确认密码编码器配置正确
- 查看日志获取详细错误信息
-
无法访问静态资源
- 确保静态资源路径已在安全配置中允许公开访问
-
CSRF导致表单提交失败
- 确保表单包含CSRF令牌
- 检查是否正确配置了CSRF排除项
-
记住我功能不工作
- 确认表单中包含remember-me字段
- 检查rememberMe配置是否正确
📝 小结
在本文中,我们通过实际示例学习了Spring Security的基本配置和使用方法,包括:
- 基本安全配置
- 用户认证配置
- 自定义登录页面
- 会话管理
- CSRF防护
- 安全测试
这些知识为我们构建安全的Spring应用奠定了基础。在下一篇文章中,我们将深入探讨Spring Security的认证机制,包括表单认证、OAuth2和JWT等高级认证方式。
📚 参考资源
🎯 作者简介:资深Java开发工程师,专注于Spring生态技术栈,拥有多年企业应用安全架构经验。
📢 声明:本系列文章将持续更新,欢迎关注、收藏、点赞、评论,与我一起探索Spring Security的奥秘!