|
||
---|---|---|
.. | ||
official | ||
step-1 | ||
step-2 | ||
ReadMe.md | ||
pom.xml |
ReadMe.md
Spring Security 6 入门指南
基本配置
添加依赖
在Maven项目中添加Spring Security依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
基础安全配置
创建一个配置类启用Web安全:
@EnableWebSecurity
@Configuration
public class SecurityWebConfiguration {
}
自定义登录配置
重要提示
使用自定义页面时,必须在控制器中明确指定跳转地址,否则Security无法正确路由,即使URL正确也无法跳转。
启用与禁用选项
- 使用默认登录页:
.formLogin(Customizer.withDefaults())
- 禁用表单登录:
.formLogin(AbstractHttpConfigurer::disable)
配置示例
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityWebConfiguration {
private final DbUserDetailService dbUserDetailService;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorizeRequests ->
// 访问路径为 /api 时需要进行认证
authorizeRequests
// 只认证 /api/** 下的所有接口
.requestMatchers("/api/**").authenticated()
// 其余请求都放行
.anyRequest().permitAll()
)
.formLogin(loginPage -> loginPage
// 自定义登录页路径
.loginPage("/login-page")
// 处理登录的URL(默认就是/login)
.loginProcessingUrl("/login")
// 登录成功跳转
.defaultSuccessUrl("/")
// 登录失败跳转
.failureUrl("/login-page?error=true")
.permitAll()
)
// 使用默认的登录
// .formLogin(Customizer.withDefaults())
// 禁用表单登录
// .formLogin(AbstractHttpConfigurer::disable)
.logout(logout -> logout
.logoutSuccessUrl("/login-page?logout=true")
.permitAll()
)
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(configurer -> configurer
// 自定无权访问返回内容
.accessDeniedHandler(new SecurityAccessDeniedHandler())
// 自定义未授权返回内容
.authenticationEntryPoint(new SecurityAuthenticationEntryPoint())
)
.userDetailsService(dbUserDetailService)
;
return http.build();
}
}
认证与授权配置
URL访问控制
基本认证拦截
String[] permitAllUrls = {
"/", "/doc.html/**",
"/webjars/**", "/images/**", ".well-known/**", "favicon.ico", "/error/**",
"/v3/api-docs/**"
};
http.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/api/**").authenticated()
.requestMatchers(permitAllUrls).permitAll()
)
基于权限的拦截
[!WARNING]
内存模式下无法获取角色信息。
- 配置内存用户:
@Bean
@ConditionalOnMissingBean(UserDetailsService.class)
InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) {
String encodedPassword = passwordEncoder.encode("123456");
UserDetails user = User.builder()
.username("user")
.password(encodedPassword)
.roles("USER")
.authorities("read")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(encodedPassword)
.roles("ADMIN")
.authorities("all", "read")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
- 配置资源权限:
authorizeRequests
.requestMatchers(permitAllUrls).permitAll()
.requestMatchers("/api/security/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/anonymous/**").anonymous()
// 使用hasRole会自动添加ROLE_前缀
// .requestMatchers("/api/**").hasRole("ADMIN")
.requestMatchers("/api/**").hasAnyAuthority("all", "read")
完整配置示例
@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class SecurityWebConfiguration {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
String[] permitAllUrls = {
"/", "/doc.html/**",
"/webjars/**", "/images/**", ".well-known/**", "favicon.ico", "/error/**",
"/v3/api-docs/**"
};
http.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(permitAllUrls).permitAll()
.requestMatchers("/api/security/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/anonymous/**").anonymous()
.requestMatchers("/api/**").hasAnyAuthority("all", "read")
)
.formLogin(loginPage -> loginPage
.loginPage("/login-page")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login-page?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login-page?logout=true")
.permitAll()
);
return http.build();
}
}
密码校验器
/**
* 配置密码编码器Bean
*
* <p>Spring Security提供了多种密码编码器实现,推荐使用BCryptPasswordEncoder作为默认选择。</p>
*
* <p>特点:</p>
* <ul>
* <li>BCryptPasswordEncoder - 使用bcrypt强哈希算法,自动加盐,是当前最推荐的密码编码器</li>
* <li>Argon2PasswordEncoder - 使用Argon2算法,抗GPU/ASIC攻击,但需要更多内存</li>
* <li>SCryptPasswordEncoder - 使用scrypt算法,内存密集型,抗硬件攻击</li>
* <li>Pbkdf2PasswordEncoder - 使用PBKDF2算法,较老但广泛支持</li>
* </ul>
*
* <p>注意:不推荐使用MD5等弱哈希算法,Spring官方也不推荐自定义弱密码编码器。</p>
*
* @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中负责:
- 密码加密 - 将明文密码转换为不可逆的哈希值
- 密码验证 - 比较输入的密码与存储的哈希是否匹配
- 防止密码泄露 - 即使数据库泄露,攻击者也无法轻易获得原始密码
各编码器特点
- BCryptPasswordEncoder
- 使用bcrypt算法
- 自动加盐,防止彩虹表攻击
- 可配置强度参数(默认10)
- 目前最推荐的密码哈希方案
- Argon2PasswordEncoder
- 使用Argon2算法(2015年密码哈希比赛获胜者)
- 抗GPU/ASIC攻击
- 内存密集型,参数配置复杂
- 适合高安全需求场景
- SCryptPasswordEncoder
- 使用scrypt算法
- 内存密集型,抗硬件攻击
- 比bcrypt更抗ASIC攻击
- Pbkdf2PasswordEncoder
- 使用PBKDF2算法
- 较老的算法,但广泛支持
- 需要高迭代次数才安全
最佳实践
最佳实践是使用BCryptPasswordEncoder,原因包括:
- 它是Spring Security默认推荐的编码器
- 自动处理盐值,无需额外存储
- 经过充分的安全审查和实际验证
- 平衡了安全性和性能
- 广泛支持,易于配置
在Spring Security 5+版本中,BCryptPasswordEncoder是官方文档中首推的密码编码器实现。除非有特殊安全需求,否则应优先选择它。
实现自定义校验器
在Spring Security中,自定义密码编码器需要实现PasswordEncoder
接口。
以下是实现MD5示例及注意事项:
/**
* <h1>MD5密码编码器实现</h1>
*
* <strong>安全警告:</strong>此类使用MD5算法进行密码哈希,已不再安全,不推荐用于生产环境。
*
* <p>MD5算法因其计算速度快且易受彩虹表攻击而被认为不安全。即使密码哈希本身是单向的,
* 但现代计算能力使得暴力破解和预先计算的彩虹表攻击变得可行。</p>
*
* <p>Spring Security推荐使用BCrypt、PBKDF2、Argon2或Scrypt等自适应单向函数替代MD5。</p>
*
* @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
接口来完成。以下是正确实现方式:
标准实现示例
@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()
的上下文中。
/**
* 获取当前认证用户的基本信息
* 使用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资源认证配置
角色与权限配置
1. 基于角色的URL访问控制
单角色配置
配置/api/**
路径下的所有接口需要ADMIN
角色才能访问:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 注意:会自动添加"ROLE_"前缀,实际检查的是ROLE_ADMIN
.requestMatchers("/api/**").hasRole("ADMIN")
)
// 其他配置...
;
return http.build();
}
多角色配置(满足任一角色即可访问)
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 检查是否有ADMIN或USER角色(自动添加ROLE_前缀)
.requestMatchers("/api/**").hasAnyRole("ADMIN", "USER")
)
// 其他配置...
;
return http.build();
}
2. 基于权限的URL访问控制
需要所有指定权限
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 需要同时拥有"all"和"read"权限
.requestMatchers("/api/**").hasAuthority("all")
.requestMatchers("/api/**").hasAuthority("read")
)
// 其他配置...
;
return http.build();
}
满足任一权限即可
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 拥有"all"或"read"任一权限即可访问
.requestMatchers("/api/**").hasAnyAuthority("all", "read")
)
// 其他配置...
;
return http.build();
}
综合配置策略
1. 基本配置模式
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
// 特定路径需要认证
.requestMatchers("/api/**").authenticated()
// 其他请求全部放行
.anyRequest().permitAll()
)
// 其他配置...
;
return http.build();
}
2. 多路径匹配配置
@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();
}
重要说明
- 角色与权限的区别:
hasRole()
会自动添加"ROLE_"前缀hasAuthority()
直接使用指定的权限字符串
- 匹配顺序:
- Spring Security会按照配置的顺序进行匹配
- 更具体的路径应该放在前面,通用规则(如anyRequest)放在最后
- 方法选择建议:
hasRole()
/hasAnyRole()
:适合基于角色的访问控制hasAuthority()
/hasAnyAuthority()
:适合更细粒度的权限控制authenticated()
:只需认证通过,不检查具体角色/权限permitAll()
:完全开放访问
- 最佳实践:
- 对于REST API,通常使用
authenticated()
配合方法级权限控制 - 静态资源应明确配置
permitAll()
- 生产环境不建议使用
anyRequest().permitAll()
- 对于REST API,通常使用
关于UserDetailsService的深入解析
1. UserDetailsService的核心作用
UserDetailsService
是Spring Security的核心接口,负责提供用户认证数据。它只有一个核心方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
当用户尝试登录时,Spring Security会自动调用这个方法来获取用户详情。
2. 为什么不需要手动校验密码?
在标准的表单登录流程中,Spring Security的认证流程会自动处理密码校验,这是因为:
-
自动集成密码编码器:
Spring Security会自动使用配置的PasswordEncoder
来比对:- 用户提交的明文密码
- 数据库中存储的加密密码(通过
UserDetails
返回)
-
认证流程内部处理:
认证管理器(AuthenticationManager
)会自动处理以下流程:graph TD A[用户提交凭证] --> B[调用UserDetailsService] B --> C[获取UserDetails] C --> D[PasswordEncoder比对密码] D --> E[认证成功/失败]
3. 完整的安全配置示例
@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. 关键注意事项
-
必须提供PasswordEncoder:
如果没有配置,会出现There is no PasswordEncoder mapped
错误@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
-
UserDetails实现要求:
你的自定义UserDetails
实现必须包含:- 正确的用户名
- 加密后的密码
- 账号状态信息(是否过期/锁定等)
-
自动发现机制:
当以下条件满足时,Spring Boot会自动配置:- 容器中存在唯一的
UserDetailsService
实现 - 存在
PasswordEncoder
bean - 没有显式配置
AuthenticationManager
- 容器中存在唯一的
5. 扩展场景
如果需要自定义认证逻辑(如增加验证码校验),可以:
@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);
}
}
然后在配置中注册:
http.authenticationProvider(customAuthProvider);
总结对比表
场景 | 需要手动处理 | 自动处理 |
---|---|---|
用户查找 | 实现loadUserByUsername() |
✅ |
密码比对 | ❌ | 由PasswordEncoder 处理 |
账号状态检查 | 通过UserDetails 返回的状态 |
✅ |
权限加载 | 通过UserDetails.getAuthorities() |
✅ |
这样设计的好处是:开发者只需关注业务数据获取(用户信息查询),安全相关的校验逻辑由框架统一处理,既保证了安全性又减少了重复代码。