Spring Security 流程中作用的简易文本流程图如下:
+-------------------+ +------------------+ +---------------------------+
| 客户端发起请求 | ---> | Spring Security | ---> | WebSecurityConfigurerAdapter |
| (GET, POST等) | | 过滤器链启动 | | (@Configuration + @Order(100))|
+-------------------+ +------------------+ +---------------------------+
|
v
+----------------------------------+
| configure(HttpSecurity http) |
| - 定义匹配路径 |
| - 设置认证方式(如 formLogin) |
| - 配置忽略/放行规则 |
+----------------------------------+
|
v
+-------------------------+ +---------------------+ +------------------+
| 认证与授权 | <--- | 业务逻辑处理 | <--- | 成功: 继续 |
| (登录验证、权限检查) | | (如Controller方法) | | 失败: 拒绝访问 |
+-------------------------+ +---------------------+ +------------------+
|
v
+-------------------+
| 响应返回给客户端 |
| (200 OK, 403 Forbidden等)|
+-------------------+
整体请求流程
bash
[Client Request]
↓
[MyGlobalFilter] ← 普通 @WebFilter(由 Servlet 容器管理)
↓
[DelegatingFilterProxy] ← Spring 提供的代理 Filter,名字通常为 "springSecurityFilterChain"
↓
┌──────────────────────┐
│ Spring Security │
│ Filter Chain (Bean) │ ← 这是一个由 Spring 管理的 Filter 链(List<Filter>)
│ │
│ - JwtAuthFilter │ ← 你自定义的 OncePerRequestFilter(Spring Bean)
│ - CsrfFilter │
│ - ExceptionTranslationFilter │
│ - FilterSecurityInterceptor │
└──────────────────────┘
↓
[DispatcherServlet] → Controller
↓
[Response 返回,Filter 链逆序执行 doFilter 后半段]
↓
[MyGlobalFilter] 的 doFilter 后半部分(如果有逻辑)
1.创建全局安全配置:启用方法级安全(@PreAuthorize 等)
bash
/**
* 全局安全配置:启用方法级安全(@PreAuthorize 等)。
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
}
- 创建WebSecurityConfigurerAdapter
bash
package com.yumchina.thsg.security.config;
import com.yumchina.thsg.global.HttpResult;
import com.yumchina.thsg.security.filter.AdminAuthFilter;
import com.yumchina.thsg.security.filter.JwtAuthenticationFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
@Configuration
// 启用 @PreAuthorize
@Order(200)
public class AdminSecurityChainConfig extends WebSecurityConfigurerAdapter {
private final AdminAuthFilter adminAuthFilter;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public AdminSecurityChainConfig(AdminAuthFilter adminAuthFilter, JwtAuthenticationFilter jwtAuthenticationFilter) {
this.adminAuthFilter = adminAuthFilter;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**")
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/admin/health").permitAll()
.antMatchers("/admin/jobs/**").permitAll()
.antMatchers("/admin/auth/login").permitAll()
.antMatchers("/admin/auth/logout").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint((req, res, ex) -> write(res, HttpStatus.UNAUTHORIZED))
.accessDeniedHandler((req, res, ex) -> write(res, HttpStatus.FORBIDDEN));
http.addFilterBefore(adminAuthFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
private static void write(HttpServletResponse res, HttpStatus status) throws IOException {
res.setStatus(status.value());
res.setContentType(MediaType.APPLICATION_JSON_VALUE);
HttpResult<Void> body = status == HttpStatus.UNAUTHORIZED ? HttpResult.unauthorized() : HttpResult.forbidden();
res.getWriter().write("{" +
"\"errorcode\":" + body.getErrorcode() + "," +
"\"data\":null," +
"\"msg\":\"" + body.getMsg() + "\"}");
}
}
3.JWT认证过滤器
bash
import com.yumchina.thsg.model.entity.User;
import com.yumchina.thsg.security.AdminIdentityProvider;
import com.yumchina.thsg.security.AdminUserHolder;
import com.yumchina.thsg.security.AdminUserPrincipal;
import com.yumchina.thsg.service.UserBizService;
import com.yumchina.thsg.util.JwtTokenUtil;
import groovy.util.logging.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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;
import java.util.List;
import static com.yumchina.thsg.security.filter.AdminAuthFilter.buildAuthorities;
/**
* JWT认证过滤器
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final AdminIdentityProvider identityProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
// 获取租户信息
String tenantCode = request.getHeader("X-Tenant-Code");
final String path = request.getRequestURI();
// 1. 只处理 /admin/** 路径
if (!path.startsWith("/admin/")) {
chain.doFilter(request, response);
return;
}
// 2. 公开路径直接放行(不认证)
if (path.startsWith("/admin/auth/login") || path.startsWith("/admin/auth/logout") || path.startsWith("/admin/health")) {
chain.doFilter(request, response);
return;
}
// 3. 如果已经认证(例如由 AdminAuthFilter 完成),直接跳过
if (SecurityContextHolder.getContext().getAuthentication() != null) {
chain.doFilter(request, response);
return;
}
String username = null;
String token = null;
// JWT Token的格式为 "Bearer token"
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
token = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(token);
} catch (IllegalArgumentException e) {
logger.error("无法获取JWT Token");
} catch (Exception e) {
logger.error("JWT Token过期或无效");
}
}
if (tenantCode == null) {
logger.error("租户信息为空");
}
try {
// 验证token
if (username != null && tenantCode != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 配置用户信息
AdminUserPrincipal principal = identityProvider.resolveUserFromToken(token, tenantCode);
if (principal != null && jwtTokenUtil.validateToken(token)) {
final List<GrantedAuthority> grantedAuthorities = buildAuthorities(principal);
final UsernamePasswordAuthenticationToken userNamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(principal, null, grantedAuthorities);
userNamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(userNamePasswordAuthenticationToken);
AdminUserHolder.set(principal);
} else {
Object p = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (p instanceof AdminUserPrincipal) {
AdminUserHolder.set((AdminUserPrincipal) p);
}
}
}
chain.doFilter(request, response);
} finally {
AdminUserHolder.clear();
}
}
}
SecurityContextHolder.getContext().getAuthentication() == null 如果有其他过滤器已经认证通过则不进行认证
通过认证的核心方法,这个方法后,就默认已经认证通过了。
✅ 创建一个 authenticated = true 的 Authentication 对象!
bash
final UsernamePasswordAuthenticationToken userNamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(principal, null, grantedAuthorities);
这个构造函数会自动设置 authenticated = true!


创建一个线程局部全量 AdminUserHolder.set(principal); 放置用户信息,用于每个请求使用。
bash
public final class AdminUserHolder {
// Spring Security 默认基于 ThreadLocal,不支持跨线程传递。
private static final TransmittableThreadLocal<AdminUserPrincipal> CONTEXT = new TransmittableThreadLocal<>();
private AdminUserHolder() {}
public static void set(AdminUserPrincipal principal) {
CONTEXT.set(principal);
}
public static AdminUserPrincipal get() {
return CONTEXT.get();
}
public static String getUserId() {
AdminUserPrincipal principal = CONTEXT.get();
return principal != null ? principal.getUserId() : null;
}
public static void clear() {
CONTEXT.remove();
}
}
不过要记得最终删除

4.JWT 工具类
bash
import com.yumchina.thsg.model.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* JWT工具类
*/
@Component
public class JwtTokenUtil {
@Value("${jwt.secret:yumc-netcontrol-secret}")
private String secret;
@Value("${jwt.expiration:7200}")
private Long expiration;
/**
* 从token中获取用户ID
*/
public String getUserIdFromToken(String token) {
return getClaimFromToken(token, claims -> (String) claims.get("userId"));
}
/**
* 从token中获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 从token中获取用户编码
*/
public String getUserCodeFromToken(String token) {
return getClaimFromToken(token, claims -> (String) claims.get("userCode"));
}
/**
* 从token中获取租户编码
*/
public String getTenantCodeFromToken(String token) {
return getClaimFromToken(token, claims -> (String) claims.get("tenantCode"));
}
/**
* 从token中获取过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* 从token中获取声明信息
*/
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 从token中获取所有声明信息
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**
* 检查token是否过期
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 生成token
*/
public String generateToken(User user, String tenantCode) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", String.valueOf(user.getId()));
claims.put("userCode", user.getUserCode());
claims.put("tenantCode", tenantCode);
return doGenerateToken(claims, user.getUserName());
}
/**
* 生成token的具体实现
*/
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expiration * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 验证token
*/
public Boolean validateToken(String token, User user) {
final String userId = getUserIdFromToken(token);
return (userId.equals(String.valueOf(user.getId())) && !isTokenExpired(token));
}
/**
* 验证token是否有效且未过期
* @param token JWT令牌
* @return 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
5.自定义解析请求,返回当前用户主体;解析失败时返回 null。
bash
@Override
public AdminUserPrincipal resolveUserFromToken(String token, String tenantCode) {
final String userCode = jwtTokenUtil.getUserCodeFromToken(token);
// 超管不需要租户,普通用户需要
if ("admin".equals(userCode)) {
return buildAdminUserPrincipal();
}
// 普通用户
String userId = jwtTokenUtil.getUserIdFromToken(token);
String username = jwtTokenUtil.getUsernameFromToken(token);
final UserDetailVO userDetailVO = userMapper.selectUserDetailById(Long.valueOf(userId));
if (userDetailVO == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 数据库配置的超级管理员
if (userDetailVO.getSuperAdmin() == 1) {
return buildAdminUserPrincipal();
} else {
if (tenantCode == null) {
throw new IllegalArgumentException("普通用户必须指定租户");
}
List<UserTenantDetailVO> userTenantDetailList = userBizService.getUserTenantDetailList(userCode, tenantCode);
// 1️⃣ 提取角色编码(roleCode)和角色ID(用于查权限)
Set<String> roleCodes = new LinkedHashSet<>(); // 保持顺序 + 去重
Set<String> roleIds = new HashSet<>();
userTenantDetailList.stream()
.map(UserTenantDetailVO::getRoles)
.flatMap(List::stream)
.forEach(role -> {
roleIds.add(role.getRoleId().toString());
roleCodes.add(role.getRoleCode());
});
// 2️⃣ 批量加载所有角色对应的权限码(避免 N+1)
List<String> permissionCodes = roleIds.stream()
.flatMap(roleId -> {
List<RolePermissionVO> perms = roleBizService.getRolePermissions(Long.valueOf(roleId), tenantCode);
return perms.stream()
.filter(p -> p.getGrantType() == 1) // 只取"允许"
.map(RolePermissionVO::getPermissionCode);
})
.distinct()
.collect(Collectors.toList());
// 3️⃣ 构建 Principal
return AdminUserPrincipal.builder()
.userId(userId)
.username(username)
.userCode(userCode)
.tenantCode(tenantCode)
.roles(new ArrayList<>(roleCodes)) // 👈 显式保存角色编码列表
.permissions(permissionCodes) // 权限(用于 Spring Security)
.build();
}
}
6.用户认证鉴权自定义实体类,类似securtiy 自带的类
bash
/**
* 管理端当前用户主体,放入 SecurityContext 供业务层读取。
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AdminUserPrincipal implements Serializable {
private String userId;
private String userCode;
private String username;
private String tenantCode;
private List<String> roles;
private boolean superAdmin;
private List<String> permissions;
private String sourceIp;
}
可以使用 SpringSecurity 自带的
bash
public interface UserDetails extends Serializable {
// 用户的权限集合(必须非 null)
Collection<? extends GrantedAuthority> getAuthorities();
// 用户密码(通常加密后存储)
String getPassword();
// 用户名(唯一标识,如邮箱、手机号、用户名)
String getUsername();
// 账户是否未过期
boolean isAccountNonExpired();
// 账户是否未锁定
boolean isAccountNonLocked();
// 凭据(密码)是否未过期
boolean isCredentialsNonExpired();
// 账户是否启用
boolean isEnabled();
}
集成好的加载用户信息,看诸位看官选择了
bash
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
return new MyUserDetails(user); // 包装为 UserDetails
}