一、为什么要用 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,需要在项目依赖中引入:
java
代码解读
复制代码
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
这样就能自动拉取所有与 Security 相关的组件。
3.2 SecurityConfig
这是最核心的配置类,主要做两件事:1) 设置过滤链规则,2) 告知 Spring Security 怎么做认证和授权。
示例配置(逐句解析):
java
代码解读
复制代码
@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 实体类
用来在数据库里存放用户信息,一般至少包含:用户名、邮箱、加密后的密码、角色 等属性。
java
代码解读
复制代码
@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 数据:
java
代码解读
复制代码
public interface UserRepository extends JpaRepository<User, Long> { // 根据用户名查找 Optional<User> findByUsername(String username); // 根据邮箱查找 Optional<User> findByEmail(String email); }
3.5 UserService(业务逻辑,列如注册)
在这里我们一般会做两件事:
- 注册:检查用户名、邮箱是否已存在,若不存在则加密密码后保存到数据库。
- 查询:提供给其他地方调用,列如查用户信息。
java
代码解读
复制代码
@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 进行下一步的密码验证和权限判断。
java
代码解读
复制代码
@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 里,你可以写各种接口,列如:
java
代码解读
复制代码
@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’)”) 做权限控制,需要启用全局方法安全:
- java
- 代码解读
- 复制代码
- @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 等),让系统更加健壮。
作者:WanderInk
链接:
https://juejin.cn/post/7455573449798139942



