Spring Security+RBAC的权限验证示例
参考资料:【SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-挑战黑马&尚硅谷】 https://www.bilibili.com/video/BV1mm4y1X7Hc/?p=33&share_source=copy_web&vd_source=bccce5410c11fef6dbb58270c065e8c8
重写实现UserDetails
示例
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
//重新实现UserDetails接口,用于Spring Security的登录验证
@Data
@AllArgsConstructor
public class LoginStaff implements UserDetails {
private final Staff staff;
private final List<String> permissions; // 权限集合变量
@JSONField(serialize = false) //不序列化到redis中,节省内存空间
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//不为空就返回,提高效率
if(authorities != null) {
return authorities;
}
//将List<String> permissions转换为Collection<? extends GrantedAuthority>对象返回
//通过查找GrantedAuthority的实现类SimpleGrantedAuthority发现可以将List<String>中的String封装为SimpleGrantedAuthority
// List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
// for (String permission : permissions) {
// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
// simpleGrantedAuthorities.add(simpleGrantedAuthority);
// }
//使用stream实现
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;// 返回权限集合
}
@Override
public String getPassword() {
//这儿返回密码会用到:DaoAuthenticationProvider 使用 PasswordEncoder 来比较用户提交的密码和数据库中存储的密码。
return staff.getPasswordHash();
}
@Override
public String getUsername() {
return staff.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return !staff.getIsBanned();
}
}
重写实现UserDetailsService
示例:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cxy.cbms.manage.entity.LoginStaff;
import com.cxy.cbms.manage.entity.Staff;
import com.cxy.cbms.manage.mapper.StaffMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private StaffMapper staffMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<Staff> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Staff::getUsername, username);
Staff staff = staffMapper.selectOne(queryWrapper);
if(Objects.isNull(staff))
{
throw new UsernameNotFoundException("用户名或密码错误");
}
// 查询用户的权限
List<String> authorities = staffMapper.selectStaffAuthorities(staff.getId());
//把数据封装成UserDetails返回
return new LoginStaff(staff, authorities, null);
}
}
Spring Sceurity配置
当 Spring Security 启动时,以下顺序会被执行:
- Spring 容器初始化
SecurityConfig
类。 - 创建
PasswordEncoder
Bean。 - 创建
AuthenticationManager
Bean。 - 调用
configureGlobal
方法,完成UserDetailsService
和PasswordEncoder
的绑定。 - 创建
SecurityFilterChain
Bean,配置 HTTP 请求的安全规则。
SecurityConfig.java
import com.cxy.cbms.common.filter.JwtFilter;
import com.cxy.cbms.manage.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
@Autowired
private JwtFilter jwtFilter; // 注入 JwtFilter
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 使用 BCrypt 加密
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
// 配置用户认证逻辑(例如从数据库中加载用户)
authenticationManagerBuilder
.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用 CSRF
.authorizeHttpRequests()
.requestMatchers(PermitAllPaths.getPaths().toArray(new String[0])).permitAll() // 允许任何人访问接口
.anyRequest().authenticated() // 其他请求需要认证
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 关闭 session
// 将 JwtFilter 添加到过滤器链中,放在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public UserDetailsServiceImpl userDetailsService() {
return new UserDetailsServiceImpl(); // 自定义用户详情服务
}
}
JwtFilter.java
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.cxy.cbms.common.config.PermitAllPaths;
import com.cxy.cbms.common.entity.Result;
import com.cxy.cbms.common.utils.RedisService;
import com.cxy.cbms.common.utils.TokenUtil;
import com.cxy.cbms.manage.entity.LoginStaff;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Objects;
@Slf4j
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private RedisService redisService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获取当前请求路径
String path = request.getServletPath();
// 检查token是否存在且是否是需要放行的路径
if (PermitAllPaths.isPermitAllPath(path)) {
filterChain.doFilter(request, response); // 放行请求
return;
}
//获取请求头中的token
String token = extractTokenFromRequest(request);
if (TokenUtil.validateToken(token)) {
Long userid = TokenUtil.getIdFromToken(token);
//从redis中获取员工信息
LoginStaff loginStaff = (LoginStaff) redisService.getValue("Staff:" + userid);
//如果redis中员工信息消失,新登录,如下原因会导致
//1. token过期时间长于redis存储数据时间
//2. 员工信息(如权限等)被修改后redis中员工信息被删除
if(Objects.isNull(loginStaff))
{
log.warn("Redis 中未找到员工信息,用户 ID: {}", userid);
// 返回 401 未授权错误
returnResult(response, Result.unauthorized("请重新登录", null));
return; // 不放行请求
}
log.info("员工信息:"+loginStaff);
//可以将 loginStaff 存储在 SecurityContext 中,供后续流程使用
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginStaff, null, loginStaff.getAuthorities())
);
filterChain.doFilter(request, response);
}
else {
// Token 无效或不存在
returnResult(response, Result.unauthorized("无效的 Token", null));
}
}
/**
* 从请求头中提取 Bearer Token
*
* @param request HTTP 请求对象
* @return 返回提取的 token,如果未找到或格式错误则返回 null
*/
public static String extractTokenFromRequest(HttpServletRequest request) {
// 获取请求头中的 Authorization 头
String tokenHeader = request.getHeader("Authorization");
// 判断是否有 Authorization 头,并且是否以 Bearer 开头
if (StringUtils.isNotBlank(tokenHeader) && tokenHeader.startsWith("Bearer ")) {
return tokenHeader.substring(7); // 去掉 "Bearer " 前缀
}
// 如果没有有效的 Authorization 头,返回 null
return null;
}
// 通用方法:返回 Result 格式数据
private void returnResult(HttpServletResponse response, Result<?> result) throws IOException {
// 设置响应状态码(可选)
response.setStatus(result.getCode());
// 设置响应内容类型
response.setContentType("application/json;charset=UTF-8");
// 将 Result 对象转换为 JSON 并写入响应体
String jsonResult = new ObjectMapper().writeValueAsString(result);
response.getWriter().write(jsonResult);
}
}
LoginService服务实现
import com.cxy.cbms.common.entity.Result;
import com.cxy.cbms.common.utils.RedisService;
import com.cxy.cbms.manage.dto.StaffInfo;
import com.cxy.cbms.manage.dto.StaffLogin;
import com.cxy.cbms.manage.entity.LoginStaff;
import com.cxy.cbms.manage.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.List;
import static com.cxy.cbms.common.utils.TokenUtil.genAccessToken;
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisService redisService;
@Override
public Result login(StaffLogin staffLogin) {
//AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(staffLogin.getUsername(), staffLogin.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证没通过,给出对应的提示
if (authenticate == null || !authenticate.isAuthenticated()) {
return Result.unauthorized("用户名或密码错误",null);
}
//如果认证通过,使用userid生成一个jwt jwt存入StaffInfo返回
LoginStaff loginStaff = (LoginStaff) authenticate.getPrincipal();
//生成jwt
Long id = loginStaff.getStaff().getId();
String jwtToken = genAccessToken(id);
//把完整的员工信息存入redis userid作为key
redisService.setValueWithExpire("Staff:"+id,loginStaff,3600);
//封装返回前端
List<String> authorityList = loginStaff.getPermissions();
StaffInfo staffInfo = loginStaff.getStaff().toStaffInfo(authorityList,jwtToken);
return Result.success("登录成功",staffInfo);
}
}
启用权限配置
在SceurityConfig上添加注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
在Spring Security中,@EnableGlobalMethodSecurity
注解已被弃用。从Spring Security 5.6版本开始,推荐使用 @EnableMethodSecurity
注解来替代它。@EnableMethodSecurity
提供了更现代化的配置方式,并且与Spring Security的其他功能更加兼容。
@EnableMethodSecurity(prePostEnabled = true)
在对应Controller接口前使用@PreAuthorize
配置访问接口需要的权限
在访问接口时,会先读取SecurityContext中存储的权限,有PreAuthorize中对应的权限才会放行接口访问权
@PreAuthorize("hasAuthority('your_authority')")