四、Spring Security配置与权限管理
4.1 Spring Security概述
Spring Security是一个强大的、可定制的身份验证和访问控制框架。
┌─────────────────────────────────────────────────────────────────┐
│ Spring Security 核心概念 │
├─────────────────────────────────────────────────────────────────┤
│ SecurityContext │ 安全上下文,存储当前用户信息 │
│ Authentication │ 认证信息 │
│ UserDetails │ 用户详情接口 │
│ UserDetailsService │ 用户详情服务(加载用户) │
│ AccessDecisionManager │ 访问决策管理器 │
│ FilterChain │ 过滤器链 │
└─────────────────────────────────────────────────────────────────┘
4.2 Security配置详解
SecurityConfig.java:
package com.hbl.usercenter.config;
import com.hbl.usercenter.security.JwtAuthenticationFilter;
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.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置类
*
* 功能说明:
* 1. 配置认证管理器
* 2. 配置HTTP安全策略(URL权限、Session管理、过滤器等)
* 3. 禁用默认的登录页面(因为使用JWT认证)
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级权限控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 配置认证管理器
*
* @param auth 认证管理器构建器
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置自定义的UserDetailsService
// 设置密码编码器
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
/**
* 配置HTTP安全策略
*
* 安全策略配置详解:
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 1. 禁用CSRF(跨站请求伪造保护)
// 原因:JWT是无状态认证,CSRF Token无法有效验证
// 注意:生产环境需要额外的CSRF防护措施
.csrf().disable()
// 2. 禁用Session管理(使用JWT无状态)
// SessionCreationPolicy.STATELESS表示不创建和使用Session
// 每次请求都需要重新认证
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 3. 配置请求授权规则
.authorizeRequests()
// 公开接口:登录和注册接口
.antMatchers("/api/auth/login", "/api/auth/register").permitAll()
// 公开接口:Swagger文档(API文档)
.antMatchers("/doc.html", "/swagger-ui.html", "/swagger-ui/**",
"/swagger-resources/**", "/webjars/**", "/v2/api-docs").permitAll()
// 公开接口:首页和静态资源
.antMatchers("/", "/*.html", "/*.css", "/*.js", "/favicon.ico").permitAll()
// 其他请求需要认证(必须登录)
.anyRequest().authenticated()
.and()
// 4. 禁用默认的登录页面
// 因为使用JWT API认证,不需要表单登录
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
// 5. 添加JWT认证过滤器
// 在UsernamePasswordAuthenticationFilter之前执行
// 确保请求到达控制器之前已经完成Token认证
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 配置密码编码器
*
* 使用BCrypt加密算法:
* – 自动加盐:每次加密结果都不同
* – 强度可配置:默认为10
* – 不可逆:无法从密文还原密码
*
* 为什么选择BCrypt?
* 1. 自动加盐,防止彩虹表攻击
* 2. 计算成本可调,防止暴力破解
* 3. Spring Security官方推荐
*
* @return 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 强度10表示2^10次迭代
return new BCryptPasswordEncoder(10);
}
/**
* 配置认证管理器Bean
*
* 需要暴露为Bean,因为认证过滤器需要使用
*
* @return 认证管理器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
4.3 BCrypt密码加密详解
为什么密码不能明文存储?
明文密码的危害:
┌─────────────────────────────────────────┐
│ 用户A使用相同密码:123456 │
│ │
│ 网站A泄露:123456 │
│ 网站B泄露:123456 │
│ ↓ │
│ 攻击者用 123456 登录用户A的所有网站 │
└─────────────────────────────────────────┘
密码加密方案对比:
| MD5 | 否 | 极快 | 极差 |
| SHA-256 | 否 | 快 | 差 |
| bcrypt | 自动 | 可配置 | 高 |
| scrypt | 自动 | 可配置 | 高 |
| argon2 | 自动 | 可配置 | 最高 |
BCrypt加密示例:
// 加密密码
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPassword = encoder.encode("123456");
// 输出:$2a$10$Czx0f/DWmI2vJsa6FdMG5.EConk5pblPk6hykk5lGITBWBCSVC82G
// 验证密码
boolean matches = encoder.matches("123456", encodedPassword);
// true
// 相同密码每次加密结果不同(因为加盐)
String encoded1 = encoder.encode("123456");
// $2a$10$KIx6m9X.qL4f8hLZd3Ly3uKj9Qk8vLpT4zK6m8nNs0lJyYQeF3l7q
String encoded2 = encoder.encode("123456");
// $2a$10$AbCdEfGhIjKlMnOpQrUvWxYyZz0tUvWxYz1AaBbCcDdEeFfGgHhI
// 但验证仍然通过
encoder.matches("123456", encoded1); // true
encoder.matches("123456", encoded2); // true
4.4 JWT认证过滤器详解
JwtAuthenticationFilter.java:
package com.hbl.usercenter.security;
import cn.hutool.core.util.StrUtil;
import com.hbl.usercenter.entity.SysUser;
import com.hbl.usercenter.mapper.SysUserMapper;
import com.hbl.usercenter.service.RedisCacheService;
import com.hbl.usercenter.utils.JwtTokenUtil;
import io.jsonwebtoken.Claims;
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.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT认证过滤器
*
* 功能说明:
* 1. 拦截每个HTTP请求
* 2. 从请求Header中提取JWT Token
* 3. 验证Token有效性(包括Redis中的Token验证)
* 4. 加载用户信息并设置到SecurityContext
*
* 单点登录控制(业内通用做法):
* – 用户登录时,Token缓存到Redis,Key为userId
* – 每次请求时,验证Token是否与Redis中的一致
* – 新登录会覆盖旧Token,实现单点登录
*/
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private RedisCacheService redisCacheService;
/**
* 核心过滤方法
*
* 请求处理流程:
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求Header中获取Token
// 格式:Authorization: Bearer eyJhbGci…
String token = jwtTokenUtil.getTokenFromRequest(request);
// 2. 如果Token不为空,验证Token并设置认证信息
if (StrUtil.isNotBlank(token)) {
// 3. 解析Token获取Claims
Claims claims = jwtTokenUtil.parseToken(token);
if (claims != null) {
// 4. 从Claims中获取用户ID和用户名
Long userId = jwtTokenUtil.getUserIdFromClaims(claims);
String username = jwtTokenUtil.getUsernameFromClaims(claims);
// 5. 验证Token是否有效
if (userId != null && username != null && jwtTokenUtil.validateToken(token)) {
// 6. 单点登录控制:验证Token是否与Redis中的一致
// Redis中存储了用户登录时的Token
String cachedToken = redisCacheService.getUserToken(userId);
if (cachedToken == null) {
// Token不在Redis中,可能是已过期或被新登录覆盖
log.warn("用户Token已过期或在其他地方登录,userId: {}", userId);
// 清除SecurityContext
SecurityContextHolder.clearContext();
} else if (!token.equals(cachedToken)) {
// Token与Redis中不一致,说明已在其他地方登录
log.warn("用户Token无效(已在其他地方登录),userId: {}", userId);
// 清除SecurityContext
SecurityContextHolder.clearContext();
} else {
// Token有效,执行认证
// 7. 检查是否已认证(避免重复认证)
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 8. 加载完整的UserDetails
// 会查询数据库获取用户角色和权限
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 9. 创建认证令牌
// 参数:(principal, credentials, authorities)
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 10. 保存到SecurityContext
// SecurityContext是ThreadLocal的,每个线程独立
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("JWT认证成功: {}", username);
}
}
}
}
}
// 11. 继续执行后续过滤器
// 如果未认证,SecurityContext为空
// 后续的控制器可以通过SecurityContextHolder获取当前用户
filterChain.doFilter(request, response);
}
/**
* 判断请求是否需要被过滤
*
* 登录、注册、首页、静态资源等不需要认证
* 其他请求都需要先通过JWT认证
*
* @param request HTTP请求
* @return true-需要过滤 false-跳过
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
// 登录、注册接口不过滤
return !path.startsWith("/api/auth/login") && !path.startsWith("/api/auth/register");
}
}
4.5 用户详情服务实现
UserDetailsServiceImpl.java:
package com.hbl.usercenter.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hbl.usercenter.entity.SysPermission;
import com.hbl.usercenter.entity.SysRole;
import com.hbl.usercenter.entity.SysUser;
import com.hbl.usercenter.mapper.SysPermissionMapper;
import com.hbl.usercenter.mapper.SysUserMapper;
import com.hbl.usercenter.mapper.SysUserRoleMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 用户详情服务实现类
*
* 功能说明:
* 1. 根据用户名加载用户信息
* 2. 加载用户角色
* 3. 加载用户权限
* 4. 构建Spring Security的UserDetails对象
*/
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysUserRoleMapper sysUserRoleMapper;
@Autowired
private SysPermissionMapper sysPermissionMapper;
/**
* 根据用户名加载用户详情
*
* 加载流程:
* 1. 从数据库查询用户信息
* 2. 查询用户角色
* 3. 查询用户权限
* 4. 构建UserDetails对象返回
*
* @param username 用户名
* @return UserDetails对象
* @throws UsernameNotFoundException 用户不存在异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户信息
SysUser sysUser = sysUserMapper.findByUserName(username);
if (sysUser == null) {
log.warn("用户不存在: {}", username);
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 2. 查询用户角色
List<SysRole> roles = sysUserRoleMapper.findRolesByUserId(sysUser.getId());
// 3. 查询用户权限
List<SysPermission> permissions = sysPermissionMapper.findPermissionsByUserId(sysUser.getId());
// 4. 构建权限集合
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
// 添加角色权限(ROLE_前缀)
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.get }
//RoleCode()));
添加操作权限
for (SysPermission permission : permissions) {
if (StrUtil.isNotBlank(permission.getPermissionKey())) {
authorities.add(new SimpleGrantedAuthority(permission.getPermissionKey()));
}
}
// 5. 构建UserDetails对象
// Spring Security的User类提供了默认实现
return new User(
sysUser.getUserName(),
sysUser.getPassword(),
sysUser.getStatus() == 1, // 账户是否启用
true, // 账户是否过期
true, // 凭证是否过期
true, // 账户是否锁定
new ArrayList<>(authorities)
);
}
}
4.6 权限控制方式
方式1:配置文件方式(已使用):
// 在SecurityConfig中配置
.authorizeRequests()
.antMatchers("/api/auth/login", "/api/auth/register").permitAll()
.anyRequest().authenticated()
方式2:注解方式(推荐):
// 在方法上使用注解
@PreAuthorize("hasRole('ADMIN')") // 需要ADMIN角色
@GetMapping("/admin")
public Result<List<User>> admin() {
return Result.success(userService.list());
}
@PreAuthorize("hasPermission('/api/users', 'user:list')") // 需要user:list权限
@GetMapping("/users")
public Result<List<User>> users() {
return Result.success(userService.list());
}
方式3:自定义注解:
// 自定义权限注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value() default "";
}
// 使用
@RequirePermission("user:delete")
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
return Result.success(userService.removeById(id));
}
4.7 权限设计原则
┌─────────────────────────────────────────────────────────────────┐
│ RBAC权限模型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ 用户 │─────▶│ 用户角色 │─────▶│ 角色 │ │
│ └─────────┘ └─────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 角色权限 │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 权限 │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
权限设计规范:



