theme: cyanosis
一、为什么要用 Spring Security?
市面上常见的 Java Web 项目,大多需要“登录”、“权限控制”、“安全防护”等功能。如果你手写这些功能,可能会面临:
- 密码存储方式:如何保证密码不被明文泄露?如何应对彩虹表攻击?
- 登录流程:如何验证用户名/密码?如何处理会话(session)或 token?
- 权限控制:如何区分管理员和普通用户?如何管理访问 URL、页面的权限?
- 防护措施:如何防止 CSRF 攻击、XSS 攻击、Session Fixation 攻击等?
Spring Security 正是为了解决以上安全问题而生的。它提供了开箱即用的认证和授权机制,还能灵活定制,让你把更多精力放在业务逻辑上。
二、Spring Security 的整体架构
要理解 Spring Security 的工作原理,最重要的是理解它的 Filter Chain(过滤器链) 以及 AuthenticationProvider、UserDetailsService 等核心概念。
2.1 Filter Chain(过滤器链)
当一个请求进来时,会首先进入到一系列由 Spring Security 管理的过滤器( Filter )。它们按照特定的顺序执行,每一个过滤器都可能对请求进行拦截、认证或授权等处理。
常见过滤器包括:
UsernamePasswordAuthenticationFilter
:处理表单登录的用户名、密码提交。BasicAuthenticationFilter
:处理 HTTP Basic 认证。SecurityContextPersistenceFilter
:负责在SecurityContextHolder
中持久化用户信息。CsrfFilter
:处理跨站请求伪造 (CSRF) 防护。
以及其他若干过滤器。在 Spring Boot 中,一般我们只需要通过 HttpSecurity
配置这些过滤器的顺序和条件即可,无需手动管理所有类。
2.2 认证(Authentication) 与 授权(Authorization)
- 认证:是谁在登录?需要校验用户名、密码,或者 token 是否有效。
- 授权:这个用户是否有权限访问特定功能?常见的方式是基于角色 (Role) 或权限 (Authority) 来做控制。
在 Spring Security 中,认证和授权大都通过过滤器链 + AuthenticationProvider
+ UserDetailsService
来完成:
- UserDetailsService:告诉 Spring Security,如何根据用户名(或其他标识)加载用户信息,比如密码、角色。
- PasswordEncoder:用什么算法加密、验证密码,如 BCrypt、PBKDF2 等。
- AuthenticationProvider:将“用户信息 + 用户输入的凭证”进行验证,验证通过后把角色信息等存入
SecurityContextHolder
。 - AccessDecisionManager +
SecurityInterceptor
:检查用户是否有访问某个 URL 的权限(授权)。
三、项目结构与关键文件
一个典型的 Spring Boot + Spring Security 项目中,跟 Security 相关的常见文件有:
SecurityConfig
(核心配置类)User
实体类(存放用户信息)UserRepository
(与数据库交互)UserService
(业务逻辑,比如注册、加密密码)UserDetailsServiceImpl
(告诉 Spring Security 如何加载用户信息)- 登录 / 注册 / 权限相关的 Controller(处理前端请求)
下面让我们一个个来看看。
3.1 pom.xml
/ build.gradle
要使用 Spring Security,需要在项目依赖中引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这样就能自动拉取所有与 Security 相关的组件。
3.2 SecurityConfig
这是最核心的配置类,主要做两件事:1) 设置过滤链规则,2) 告诉 Spring Security 怎么做认证和授权。
示例配置(逐句解析):
@Configuration
@EnableWebSecurity // 启用Spring Security
public class SecurityConfig {
// 构造函数注入一个自定义的 UserDetailsService,实现类会在后面介绍
private final UserDetailsServiceImpl userDetailsService;
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* 1. PasswordEncoder Bean
* 这里选用BCrypt加密算法,它会在保存密码和校验密码时发挥作用
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 2. DaoAuthenticationProvider
* 这是Spring Security用于做用户名/密码验证的提供者(Provider)
* 告诉它:需要从userDetailsService加载用户信息,并用BCrypt做密码匹配
*/
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 3. 配置过滤链
* - 禁用CSRF(仅开发环境方便测试,生产视情况开启)
* - 配置授权规则,哪些路径开放、哪些需角色
* - 定义登录/注销的页面与处理URL
* - Session管理(限制单点登录等)
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 3.1 临时禁用CSRF
.csrf(csrf -> csrf.disable())
// 3.2 路径权限
.authorizeHttpRequests(auth -> auth
// 下列路径不需要登录即可访问(静态资源、登录页、注册接口等)
.requestMatchers("/login", "/register", "/api/users/register", "/css/**", "/js/**").permitAll()
// 只有ADMIN角色能访问 /admin/** 下的资源
.requestMatchers("/admin/**").hasRole("ADMIN")
// 其他所有请求,登录后可访问
.anyRequest().authenticated()
)
// 3.3 登录表单配置
.formLogin(form -> form
.loginPage("/login") // 登录页面(自定义)
.loginProcessingUrl("/perform_login") // Spring Security会拦截此URL执行登录逻辑
.defaultSuccessUrl("/home", true) // 登录成功后跳转页面
.failureUrl("/login?error") // 登录失败后跳转
.permitAll()
)
// 3.4 登出配置
.logout(logout -> logout
.logoutUrl("/logout") // 处理登出请求的URL
.logoutSuccessUrl("/login?logout") // 登出成功后跳转
.permitAll()
)
// 3.5 指定自定义的AuthenticationProvider
.authenticationProvider(authenticationProvider())
// 3.6 Session管理(限制一个账号只能登录一次,可选)
.sessionManagement(session -> session
.maximumSessions(1)
.expiredUrl("/login?expired")
);
return http.build();
}
}
代码逐句解读:
@EnableWebSecurity
:让 Spring Security 的自动配置生效。@Bean
PasswordEncoder
:BCrypt 会在注册时把明文密码加密存库,在登录时把用户输入的明文密码和数据库里的密文匹配。DaoAuthenticationProvider
:Spring Security 内置的“用户名/密码验证”实现,会调用你自定义的UserDetailsService
来加载用户信息。.requestMatchers(...)
:用来做授权规则匹配,比如某些 URL 只有特定角色才能访问。.formLogin()
:配置表单登录的具体跳转页面、请求 URL 等。.logout()
:登出逻辑,比如点击 /logout 就会清理 session 并重定向到登录页。.sessionManagement()
:可配置单点登录等。
3.3 User
实体类
用来在数据库里存放用户信息,一般至少包含:用户名、邮箱、加密后的密码、角色 等属性。
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username; // 用户名
@Column(nullable = false, unique = true)
private String email; // 邮箱
@Column(nullable = false)
private String password; // 密码(BCrypt加密后)
// 使用ElementCollection或多对多表存储角色集
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles; // 存放用户的角色, e.g. ADMIN, USER
// Getter/Setter省略
}
为什么用 ElementCollection
?\
因为有些时候一个用户会有多个角色(比如 ADMIN+USER),这样会在数据库自动生成一张 user_roles
表来保存关系。
3.4 UserRepository
用来和数据库进行交互,查询或保存 User 数据:
public interface UserRepository extends JpaRepository<User, Long> {
// 根据用户名查找
Optional<User> findByUsername(String username);
// 根据邮箱查找
Optional<User> findByEmail(String email);
}
3.5 UserService
(业务逻辑,比如注册)
在这里我们一般会做两件事:
- 注册:检查用户名、邮箱是否已存在,若不存在则加密密码后保存到数据库。
- 查询:提供给其他地方调用,比如查用户信息。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public User registerUser(User user) {
// 1. 检查是否已存在
if (userRepository.findByUsername(user.getUsername()).isPresent()) {
throw new RuntimeException("用户名已存在");
}
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
throw new RuntimeException("邮箱已被注册");
}
// 2. 加密密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
// 3. 如果角色为空, 给个默认USER角色
if (user.getRoles() == null || user.getRoles().isEmpty()) {
user.setRoles(Set.of("USER"));
}
// 4. 保存到数据库
return userRepository.save(user);
}
public Optional<User> getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
}
注意:之所以在这里用 passwordEncoder.encode(...)
,就是为了让数据库里只保存 BCrypt 哈希之后的密码,避免明文泄露。
3.6 UserDetailsServiceImpl
这是 Spring Security“加载用户信息”的关键类。当用户在登录页面提交了用户名 + 密码时,DaoAuthenticationProvider
会调用这个类的 loadUserByUsername()
来获取用户的完整信息(包括密码、角色)。
如果找不到,抛出 UsernameNotFoundException
,登录就会失败;如果找到,就把角色信息一并返回给 Security 进行下一步的密码验证和权限判断。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
* 根据用户名加载用户信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查找数据库中的 User
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
// 转换成 Spring Security 自带的 UserDetails 对象
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
// roles() 会自动给每个角色加 "ROLE_" 前缀
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
代码逐句解读:
UserDetailsService
:Spring Security 官方规定的“用户信息读取”接口,必须实现loadUserByUsername()
。UserRepository.findByUsername(...)
:从数据库查用户,如果没有就抛异常。org.springframework.security.core.userdetails.User.builder()
:这是一个工具类,把数据库里取到的用户信息(密码+角色)转换成 Spring Security 需要的UserDetails
。.roles(...)
:接收角色数组,会自动拼上ROLE_
前缀,比如ADMIN
->ROLE_ADMIN
,这是 Spring Security 区分角色的惯例。
3.7 前端或 Controller
上面我们已经写完了所有核心的后端安全逻辑。在实际项目中,前端会访问这些接口,比如:
POST /api/users/register
:注册用户GET /login
:登录页面POST /perform_login
:提交登录表单GET /admin/**
:只有管理员能访问
在 Controller 里,你可以写各种接口,比如:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody User user) {
try {
User saved = userService.registerUser(user);
return ResponseEntity.ok(saved);
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@GetMapping("/{username}")
public ResponseEntity<?> getUserInfo(@PathVariable String username) {
return userService.getUserByUsername(username)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
与 Security 关联:
- 如果在
SecurityConfig
里限制了/api/users/**
只能由登录用户访问,那么未登录访问就会被重定向到登录页面。 - 如果只是注册开放,则要把
/api/users/register
配置成permitAll()
。
四、Spring Security 运行流程(从请求到响应的全过程)
- 客户端发起登录请求:访问
/login
,提交用户名、密码到/perform_login
。 - Spring Security 拦截请求:
UsernamePasswordAuthenticationFilter
读取表单中的 username、password。 - 调用
UserDetailsServiceImpl
:根据 username 去数据库查用户。 - 密码校验:用
PasswordEncoder
(BCrypt) 对前端提交的明文密码做哈希,与数据库里的哈希做匹配。 - 认证成功:如果匹配成功,Spring Security 会生成
Authentication
对象放进SecurityContextHolder
。 - 跳转成功页面:
/sales
或者/home
,这时用户已登录。 - 访问其他受保护资源:当用户带着会话(session) 或者 SecurityContext 再次请求受保护的 URL 时,会检查是否有
ROLE_ADMIN
/ROLE_USER
等。若权限足够就访问成功,否则403拒绝。
五、常见进阶操作
Method-Level Security:在方法上用注解
@PreAuthorize("hasRole('ADMIN')")
做权限控制,需要启用全局方法安全:@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { ... }
然后在 Service 或 Controller 方法上写注解即可。
CSRF Protection:生产环境可能需要开启 CSRF 并配置白名单(对于 AJAX / API 调用如何携带 CSRF Token 等)。
Remember-Me:当用户勾选“记住我”,下次不登录也能保持身份,会在 Cookie 中存储一个持久化令牌。
JWT:在分布式微服务中,可能用 JWT 替代 Session 做无状态认证,需要自己配置
OncePerRequestFilter
等。OAuth2:集成社交登录(如 GitHub、Google),需要使用
spring-boot-starter-oauth2-client
并做相应配置。
六、总结
Spring Security 的核心离不开:
- FilterChain:把请求一级级过滤,进行认证、授权、CSRF 防护等。
- AuthenticationProvider + UserDetailsService:决定了“如何加载用户”以及“如何校验密码”。
常见配置都在
SecurityConfig
里:区分哪些请求开放、哪些请求需登录、哪些需 ADMIN 角色等。数据库表设计:关键是存储用户的角色、密码。Spring Security 不需要复杂的表结构,但要保证有一个唯一标识(用户名或邮箱)和一个加密密码。
自定义扩展也很灵活**:你可以改用手机号码登录、加多重验证(MFA)、编写自定义过滤器等,都是在 Spring Security 提供的框架之上扩展。
拥有了以上这套设计,一个基本的用户认证和权限管理系统就搭建完毕啦。之后你可以根据业务需求,深挖细节和安全策略(比如 XSS 防护、Remember-Me、单点登录 SSO 等),让系统更加健壮。
祝你开发愉快!(๑•̀ㅂ•́)و✧