From 5516fdddf9666435c32194a79cdb418645fc7686 Mon Sep 17 00:00:00 2001 From: bunny <1319900154@qq.com> Date: Mon, 14 Jul 2025 20:17:06 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=B7=BB=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=9A=84=E8=A7=92=E8=89=B2=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring-security/ReadMe.md | 206 +++++++++++++++++- .../step2/controller/RoleController.java | 1 + .../com/spring/step2/mapper/UserMapper.java | 9 + .../config/SecurityWebConfiguration.java | 4 - .../security/service/DbUserDetailService.java | 39 +++- .../src/main/resources/mapper/UserMapper.xml | 7 + 6 files changed, 257 insertions(+), 9 deletions(-) diff --git a/spring-security/ReadMe.md b/spring-security/ReadMe.md index d8ed209..3f2ea70 100644 --- a/spring-security/ReadMe.md +++ b/spring-security/ReadMe.md @@ -29,6 +29,63 @@ public class SecurityWebConfiguration { - 使用默认登录页:`.formLogin(Customizer.withDefaults())` - 禁用表单登录:`.formLogin(AbstractHttpConfigurer::disable)` +#### 配置示例 + +```java +@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访问控制 @@ -476,4 +533,151 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 4. **最佳实践**: - 对于REST API,通常使用`authenticated()`配合方法级权限控制 - 静态资源应明确配置`permitAll()` - - 生产环境不建议使用`anyRequest().permitAll()` \ No newline at end of file + - 生产环境不建议使用`anyRequest().permitAll()` + +## 关于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()` | ✅ | + +这样设计的好处是:开发者只需关注业务数据获取(用户信息查询),安全相关的校验逻辑由框架统一处理,既保证了安全性又减少了重复代码。 \ No newline at end of file diff --git a/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java b/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java index 8a2a9d1..1981074 100644 --- a/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java +++ b/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java @@ -29,6 +29,7 @@ import java.util.List; @RestController @RequestMapping("/api/role") @RequiredArgsConstructor + public class RoleController { private final RoleService roleService; diff --git a/spring-security/step-2/src/main/java/com/spring/step2/mapper/UserMapper.java b/spring-security/step-2/src/main/java/com/spring/step2/mapper/UserMapper.java index ddd8099..9d0425b 100644 --- a/spring-security/step-2/src/main/java/com/spring/step2/mapper/UserMapper.java +++ b/spring-security/step-2/src/main/java/com/spring/step2/mapper/UserMapper.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.spring.step2.domain.dto.user.UserDto; import com.spring.step2.domain.entity.PermissionEntity; +import com.spring.step2.domain.entity.RoleEntity; import com.spring.step2.domain.entity.UserEntity; import com.spring.step2.domain.vo.UserVo; import org.apache.ibatis.annotations.Mapper; @@ -47,4 +48,12 @@ public interface UserMapper extends BaseMapper { * @return 用户 {@link UserEntity} */ UserEntity selectByUsername(String username); + + /** + * 根据用户id查找该用户的角色内容 + * + * @param userId 用户id + * @return 当前用户的角色信息 + */ + List selectRolesByUserId(Long userId); } diff --git a/spring-security/step-2/src/main/java/com/spring/step2/security/config/SecurityWebConfiguration.java b/spring-security/step-2/src/main/java/com/spring/step2/security/config/SecurityWebConfiguration.java index b68aad1..d7ef03c 100644 --- a/spring-security/step-2/src/main/java/com/spring/step2/security/config/SecurityWebConfiguration.java +++ b/spring-security/step-2/src/main/java/com/spring/step2/security/config/SecurityWebConfiguration.java @@ -2,7 +2,6 @@ package com.spring.step2.security.config; import com.spring.step2.security.handler.SecurityAccessDeniedHandler; import com.spring.step2.security.handler.SecurityAuthenticationEntryPoint; -import com.spring.step2.security.service.DbUserDetailService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,8 +17,6 @@ import org.springframework.security.web.SecurityFilterChain; @RequiredArgsConstructor public class SecurityWebConfiguration { - private final DbUserDetailService dbUserDetailService; - @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -57,7 +54,6 @@ public class SecurityWebConfiguration { // 自定义未授权返回内容 .authenticationEntryPoint(new SecurityAuthenticationEntryPoint()) ) - .userDetailsService(dbUserDetailService) ; return http.build(); diff --git a/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java b/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java index 8927b0a..8914ea6 100644 --- a/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java +++ b/spring-security/step-2/src/main/java/com/spring/step2/security/service/DbUserDetailService.java @@ -2,20 +2,22 @@ package com.spring.step2.security.service; import com.baomidou.dynamic.datasource.annotation.DS; import com.spring.step2.domain.entity.PermissionEntity; +import com.spring.step2.domain.entity.RoleEntity; import com.spring.step2.domain.entity.UserEntity; import com.spring.step2.mapper.UserMapper; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @DS("testJwt") @Service +@Transactional @RequiredArgsConstructor public class DbUserDetailService implements UserDetailsService { @@ -31,18 +33,47 @@ public class DbUserDetailService implements UserDetailsService { throw new UsernameNotFoundException("用户不存在"); } + Long userId = userEntity.getId(); + + // 设置用户角色 + String[] roles = findUserRolesByUserId(userId); + // 设置用户权限 - List authorities = findPermissionByUserId(userEntity.getId()).stream() - .map(SimpleGrantedAuthority::new) - .toList(); + List permissionsByUserId = findPermissionByUserId(userId); + String[] authorities = permissionsByUserId.toArray(String[]::new); + + // 也可以转成下面的形式 + // authorities = permissionsByUserId.stream() + // .map(SimpleGrantedAuthority::new) + // .toList(); return User.builder() .username(userEntity.getUsername()) .password(userEntity.getPassword()) + // 设置用户角色 + .roles(roles) + // 设置用户权限 .authorities(authorities) .build(); } + /** + * 根据用户id查找该用户的角色内容 + * + * @param userId 用户id + * @return 当前用户的角色信息 + */ + public String[] findUserRolesByUserId(Long userId) { + List roleList = userMapper.selectRolesByUserId(userId); + return roleList.stream().map(RoleEntity::getRoleCode).toArray(String[]::new); + } + + /** + * 根据用户id查找该用户的权限内容 + * + * @param userId 用户id + * @return 当前用户的权限信息 + */ public List findPermissionByUserId(Long userId) { List permissionList = userMapper.selectPermissionByUserId(userId); return permissionList.stream().map(PermissionEntity::getPermissionCode).toList(); diff --git a/spring-security/step-2/src/main/resources/mapper/UserMapper.xml b/spring-security/step-2/src/main/resources/mapper/UserMapper.xml index d19c6b0..9517d49 100644 --- a/spring-security/step-2/src/main/resources/mapper/UserMapper.xml +++ b/spring-security/step-2/src/main/resources/mapper/UserMapper.xml @@ -57,4 +57,11 @@ + + +