# Spring Security 6 入门指南

## 基本配置
### 添加依赖
在Maven项目中添加Spring Security依赖:
```xml
Spring Security提供了多种密码编码器实现,推荐使用BCryptPasswordEncoder作为默认选择。
* *特点:
*注意:不推荐使用MD5等弱哈希算法,Spring官方也不推荐自定义弱密码编码器。
* * @return PasswordEncoder 密码编码器实例 * @see BCryptPasswordEncoder */ @Bean public PasswordEncoder passwordEncoder() { // 实际项目中只需返回一个密码编码器 return new BCryptPasswordEncoder(); // 其他编码器示例(根据需求选择一种): // return new Argon2PasswordEncoder(16, 32, 1, 1 << 14, 2); // return new SCryptPasswordEncoder(); // return new Pbkdf2PasswordEncoder("secret", 185000, 256); } ``` ### 密码校验器的作用和特点 #### 作用 密码校验器(PasswordEncoder)在Spring Security中负责: 1. 密码加密 - 将明文密码转换为不可逆的哈希值 2. 密码验证 - 比较输入的密码与存储的哈希是否匹配 3. 防止密码泄露 - 即使数据库泄露,攻击者也无法轻易获得原始密码 #### 各编码器特点 1. **BCryptPasswordEncoder** - 使用bcrypt算法 - 自动加盐,防止彩虹表攻击 - 可配置强度参数(默认10) - 目前最推荐的密码哈希方案 2. **Argon2PasswordEncoder** - 使用Argon2算法(2015年密码哈希比赛获胜者) - 抗GPU/ASIC攻击 - 内存密集型,参数配置复杂 - 适合高安全需求场景 3. **SCryptPasswordEncoder** - 使用scrypt算法 - 内存密集型,抗硬件攻击 - 比bcrypt更抗ASIC攻击 4. **Pbkdf2PasswordEncoder** - 使用PBKDF2算法 - 较老的算法,但广泛支持 - 需要高迭代次数才安全 #### 最佳实践 **最佳实践是使用BCryptPasswordEncoder**,原因包括: 1. 它是Spring Security默认推荐的编码器 2. 自动处理盐值,无需额外存储 3. 经过充分的安全审查和实际验证 4. 平衡了安全性和性能 5. 广泛支持,易于配置 在Spring Security 5+版本中,BCryptPasswordEncoder是官方文档中首推的密码编码器实现。除非有特殊安全需求,否则应优先选择它。 ### 实现自定义校验器 在Spring Security中,自定义密码编码器需要实现`PasswordEncoder`接口。 以下是实现MD5示例及注意事项: ```java /** *MD5算法因其计算速度快且易受彩虹表攻击而被认为不安全。即使密码哈希本身是单向的, * 但现代计算能力使得暴力破解和预先计算的彩虹表攻击变得可行。
* *Spring Security推荐使用BCrypt、PBKDF2、Argon2或Scrypt等自适应单向函数替代MD5。
* * @see PasswordEncoder * 一般仅用于遗留系统兼容,新系统应使用更安全的密码编码器 */ public class MD5PasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { if (rawPassword == null) { throw new IllegalArgumentException("原始密码不能为null"); } byte[] md5Digest = DigestUtils.md5Digest(rawPassword.toString().getBytes()); return HexFormat.of().formatHex(md5Digest); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { if (rawPassword == null) { throw new IllegalArgumentException("原始密码不能为null"); } if (!StringUtils.hasText(encodedPassword)) { return false; } return encodedPassword.equalsIgnoreCase(encode(rawPassword)); } @Override public boolean upgradeEncoding(String encodedPassword) { // MD5已不安全,始终返回true建议升级到更安全的算法 return true; } } ``` ## 自定义UserDetailsService 在Spring Security中,如果需要自定义用户认证逻辑,可以通过实现`UserDetailsService`接口来完成。以下是正确实现方式: ### 标准实现示例 ```java @Service public class CustomUserDetailsService implements UserDetailsService { private final PasswordEncoder passwordEncoder; // 推荐使用构造器注入 public CustomUserDetailsService(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 这里应该根据username从数据库或其他存储中查询用户信息 // 以下是模拟数据,实际应用中应从数据库查询 // 2. 如果用户不存在,抛出UsernameNotFoundException if (!"bunny".equalsIgnoreCase(username)) { throw new UsernameNotFoundException("User not found: " + username); } // 3. 构建UserDetails对象返回 return User.builder() .username(username) // 使用传入的用户名 .password(passwordEncoder.encode("123456")) // 密码应该已经加密存储,这里仅为示例 .roles("USER") // 角色会自动添加ROLE_前缀 .authorities("read", "write") // 添加具体权限 .build(); } } ``` ## 当前用户登录信息 用户的信息都保存在`SecurityContextHolder.getContext()`的上下文中。 ```java /** * 获取当前认证用户的基本信息 * 使用Spring Security的SecurityContextHolder获取当前认证信息 */ @Operation(summary = "当前用户的信息", description = "当前用户的信息") @GetMapping("/current-user") public Authentication getCurrentUser() { // 从SecurityContextHolder获取当前认证对象 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 打印当前用户名和权限信息到控制台(用于调试) System.out.println("Current user: " + auth.getName()); System.out.println("Authorities: " + auth.getAuthorities()); // 返回完整的认证对象 return auth; } /** * 获取当前用户的详细信息 * 从认证主体中提取UserDetails信息 */ @Operation(summary = "获取用户详情", description = "获取用户详情") @GetMapping("user-detail") public UserDetails getCurrentUserDetail() { // 从SecurityContextHolder获取当前认证对象 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 获取认证主体(principal) Object principal = auth.getPrincipal(); // 检查主体是否是UserDetails实例 if (principal instanceof UserDetails) { // 如果是,则转换为UserDetails并返回 return (UserDetails) principal; } else { // 如果不是UserDetails类型,返回null return null; } } ``` ## URL资源认证配置 ### 角色与权限配置 > [!IMPORTANT] > > 1. **角色与权限的区别**: > - `hasRole()`会自动添加"ROLE_"前缀 > - `hasAuthority()`直接使用指定的权限字符串 > 2. **匹配顺序**: > - Spring Security会按照配置的顺序进行匹配 > - 更具体的路径应该放在前面,通用规则(如anyRequest)放在最后 > 3. **方法选择建议**: > - `hasRole()`/`hasAnyRole()`:适合基于角色的访问控制 > - `hasAuthority()`/`hasAnyAuthority()`:适合更细粒度的权限控制 > - `authenticated()`:只需认证通过,不检查具体角色/权限 > - `permitAll()`:完全开放访问 > 4. **最佳实践**: > - 对于REST API,通常使用`authenticated()`配合方法级权限控制 > - 静态资源应明确配置`permitAll()` > - 生产环境不建议使用`anyRequest().permitAll()` #### 1. 基于角色的URL访问控制 ##### 单角色配置 配置`/api/**`路径下的所有接口需要`ADMIN`角色才能访问: ```java @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize // 注意:会自动添加"ROLE_"前缀,实际检查的是ROLE_ADMIN .requestMatchers("/api/**").hasRole("ADMIN") ) // 其他配置... ; return http.build(); } ``` ##### 多角色配置(满足任一角色即可访问) ```java @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize // 检查是否有ADMIN或USER角色(自动添加ROLE_前缀) .requestMatchers("/api/**").hasAnyRole("ADMIN", "USER") ) // 其他配置... ; return http.build(); } ``` #### 2. 基于权限的URL访问控制 ##### 需要所有指定权限 ```java @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize // 需要同时拥有"all"和"read"权限 .requestMatchers("/api/**").hasAuthority("all") .requestMatchers("/api/**").hasAuthority("read") ) // 其他配置... ; return http.build(); } ``` ##### 满足任一权限即可 ```java @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize // 拥有"all"或"read"任一权限即可访问 .requestMatchers("/api/**").hasAnyAuthority("all", "read") ) // 其他配置... ; return http.build(); } ``` ### 综合配置策略 #### 1. 基本配置模式 ```java @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize // 特定路径需要认证 .requestMatchers("/api/**").authenticated() // 其他请求全部放行 .anyRequest().permitAll() ) // 其他配置... ; return http.build(); } ``` #### 2. 多路径匹配配置 ```java @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 定义无需认证的白名单路径 String[] permitAllUrls = { "/", "/doc.html/**", "/webjars/**", "/images/**", "/.well-known/**", "/favicon.ico", "/error/**", "/swagger-ui/**", "/v3/api-docs/**" }; http.authorizeHttpRequests(authorize -> authorize // API路径需要认证 .requestMatchers("/api/**").authenticated() // 白名单路径直接放行 .requestMatchers(permitAllUrls).permitAll() // 其他请求需要登录(非匿名访问) .anyRequest().authenticated() ) // 其他配置... ; return http.build(); } ``` ### 基于方法的授权 > [!NOTE] > > 通过在任何 `@Configuration` 类上添加 `@EnableMethodSecurity` 注解。 > > Spring Boot Starter Security 默认情况下不会激活方法级别的授权。 #### 提供的注解 1. @PreAuthorize 2. @PostAuthorize 3. @PreFilter 4. @PostFilter ## 关于UserDetailsService的深入解析 ### 简单阐述 #### 1. UserDetailsService的核心作用 `UserDetailsService`是Spring Security的核心接口,负责提供用户认证数据。它只有一个核心方法: ```java UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; ``` 当用户尝试登录时,Spring Security会自动调用这个方法来获取用户详情。 #### 2. 为什么不需要手动校验密码? 在标准的表单登录流程中,Spring Security的认证流程会自动处理密码校验,这是因为: 1. **自动集成密码编码器**: Spring Security会自动使用配置的`PasswordEncoder`来比对: - 用户提交的明文密码 - 数据库中存储的加密密码(通过`UserDetails`返回) 2. **认证流程内部处理**: 认证管理器(`AuthenticationManager`)会自动处理以下流程: ```mermaid graph TD A[用户提交凭证] --> B[调用UserDetailsService] B --> C[获取UserDetails] C --> D[PasswordEncoder比对密码] D --> E[认证成功/失败] ``` #### 3. 完整的安全配置示例 ```java @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final DbUserDetailService dbUserDetailService; private final PasswordEncoder passwordEncoder; @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/**").authenticated() .anyRequest().permitAll() ) .formLogin(form -> form .loginProcessingUrl("/login") .permitAll() ) // 即使不显式设置也会自动生效 .userDetailsService(dbUserDetailService) // 必须配置PasswordEncoder .authenticationManager(authenticationManager(http)); return http.build(); } @Bean AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { return http.getSharedObject(AuthenticationManagerBuilder.class) .userDetailsService(dbUserDetailService) .passwordEncoder(passwordEncoder) .and() .build(); } } ``` #### 4. 关键注意事项 1. **必须提供PasswordEncoder**: 如果没有配置,会出现`There is no PasswordEncoder mapped`错误 ```java @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } ``` 2. **UserDetails实现要求**: 你的自定义`UserDetails`实现必须包含: - 正确的用户名 - 加密后的密码 - 账号状态信息(是否过期/锁定等) 3. **自动发现机制**: 当以下条件满足时,Spring Boot会自动配置: - 容器中存在唯一的`UserDetailsService`实现 - 存在`PasswordEncoder` bean - 没有显式配置`AuthenticationManager` #### 5. 扩展场景 如果需要自定义认证逻辑(如增加验证码校验),可以: ```java @Component @RequiredArgsConstructor public class CustomAuthProvider implements AuthenticationProvider { private final UserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; @Override public Authentication authenticate(Authentication auth) { // 自定义逻辑 UserDetails user = userDetailsService.loadUserByUsername(auth.getName()); // 手动密码比对 if (!passwordEncoder.matches(auth.getCredentials().toString(), user.getPassword())) { throw new BadCredentialsException("密码错误"); } return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); } @Override public boolean supports(Class> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } } ``` 然后在配置中注册: ```java http.authenticationProvider(customAuthProvider); ``` #### 总结对比表 | 场景 | 需要手动处理 | 自动处理 | | ------------ | ---------------------------------- | ----------------------- | | 用户查找 | 实现`loadUserByUsername()` | ✅ | | 密码比对 | ❌ | 由`PasswordEncoder`处理 | | 账号状态检查 | 通过`UserDetails`返回的状态 | ✅ | | 权限加载 | 通过`UserDetails.getAuthorities()` | ✅ | 这样设计的好处是:开发者只需关注业务数据获取(用户信息查询),安全相关的校验逻辑由框架统一处理,既保证了安全性又减少了重复代码。 ### 获取角色与权限 #### 1. 角色信息处理 在`UserDetailsService`实现中获取并设置用户角色: ```java @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 查询用户基本信息 UserEntity userEntity = userMapper.selectByUsername(username); if (userEntity == null) { throw new UsernameNotFoundException("用户不存在"); } // 2. 获取角色信息(自动添加ROLE_前缀) String[] roles = findUserRolesByUserId(userEntity.getId()); // 3. 获取权限信息 List