(旧)Spring Securit 实现JWT token认证(多平台登录&部分鉴权)

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 {
}
  1. 创建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
    }
相关推荐
廋到被风吹走2 小时前
【Spring】DispatcherServlet解析
java·后端·spring
廋到被风吹走2 小时前
【Spring】PlatformTransactionManager详解
java·spring·wpf
wanghowie2 小时前
01.07 Java基础篇|函数式编程与语言新特性总览
java·开发语言·面试
Cricyta Sevina2 小时前
Java IO 基础理论知识笔记
java·开发语言·笔记
码luffyliu3 小时前
系统优化:从压测到性能飞升
后端·压力测试
小萌新上大分3 小时前
java线程通信 生产者消费者,synchronized,,ReentrantLock,Condition(笔记备份)
java·多线程·lock·java线程间通信的方式·reentrantlock使用·生产者消费者问题java·java多线程与高并发
それども3 小时前
Spring Bean 的name可以相同吗
java·后端·spring
上进小菜猪3 小时前
基于深度学习的农业虫害自动识别系统:YOLOv8 的完整工程
后端
墨雪不会编程3 小时前
C++ string 详解:STL 字符串容器的使用技巧
java·开发语言·c++
Lucky GGBond3 小时前
实践开发:老系统新增字段我是如何用枚举优雅兼容历史数据的
java