添加用户的角色信息

This commit is contained in:
bunny 2025-07-14 20:17:06 +08:00
parent 84a053fdb6
commit 5516fdddf9
6 changed files with 257 additions and 9 deletions

View File

@ -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()`
- 生产环境不建议使用`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()` | ✅ |
这样设计的好处是:开发者只需关注业务数据获取(用户信息查询),安全相关的校验逻辑由框架统一处理,既保证了安全性又减少了重复代码。

View File

@ -29,6 +29,7 @@ import java.util.List;
@RestController
@RequestMapping("/api/role")
@RequiredArgsConstructor
public class RoleController {
private final RoleService roleService;

View File

@ -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<UserEntity> {
* @return 用户 {@link UserEntity}
*/
UserEntity selectByUsername(String username);
/**
* 根据用户id查找该用户的角色内容
*
* @param userId 用户id
* @return 当前用户的角色信息
*/
List<RoleEntity> selectRolesByUserId(Long userId);
}

View File

@ -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();

View File

@ -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<SimpleGrantedAuthority> authorities = findPermissionByUserId(userEntity.getId()).stream()
.map(SimpleGrantedAuthority::new)
.toList();
List<String> 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<RoleEntity> roleList = userMapper.selectRolesByUserId(userId);
return roleList.stream().map(RoleEntity::getRoleCode).toArray(String[]::new);
}
/**
* 根据用户id查找该用户的权限内容
*
* @param userId 用户id
* @return 当前用户的权限信息
*/
public List<String> findPermissionByUserId(Long userId) {
List<PermissionEntity> permissionList = userMapper.selectPermissionByUserId(userId);
return permissionList.stream().map(PermissionEntity::getPermissionCode).toList();

View File

@ -57,4 +57,11 @@
</where>
</select>
<!-- 根据用户id查找该用户的角色内容 -->
<select id="selectRolesByUserId" resultType="com.spring.step2.domain.entity.RoleEntity">
select *
from t_user_role tur
join t_role tr on tur.role_id = tr.id
</select>
</mapper>