news 2026/10/10 12:29:33

基于JJWT的JWT登录认证改造实战:从Session到无状态Token

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于JJWT的JWT登录认证改造实战:从Session到无状态Token

这阵子帮一个团队改造老项目的登录模块,他们把用户状态全部放在服务端Session里,一到线上多实例部署就出问题,登录状态动不动就掉。后来我们用JJWT 0.11.5重写了认证这条链路,从依赖配置到工具类封装再到登录接口改动,整套流程走下来,坑也踩了不少。这篇就把完整过程整理出来,包括密钥怎么配、Token怎么生成和解析、登录接口怎么接、以及线上排查的几个典型问题,希望给正要接JWT的后端同学一份能直接上手的参考。

1. 为什么从Session切到JWT,以及什么场景下别硬上

1.1 Session认证的痛点:集群同步、移动端适配、跨域

先说说我们当时遇到的问题。原来那套系统用的是传统的Session认证方式:用户登录成功后,服务端把用户信息存在Session里,同时给客户端返回一个JSESSIONID,客户端后续请求带上这个ID,服务端再去Session里查对应状态。

单机部署的时候这套逻辑没毛病,但业务一旦起来,服务就需要横向扩容,一扩就出事了。请求被负载均衡分发到不同实例,A实例上创建的Session,B实例上查不到,用户就莫名其妙被登出。常见的解决方案是引入Session共享,把Session扔到Redis里,或者用Sticky Session让同一用户的请求固定打到同一台机器上。这两种方案都能解决问题,但都引入了额外的运维成本和复杂度。

另一个痛点来自移动端和多端场景。现在的应用基本都同时有Web端、App端、小程序端,Session天然依赖Cookie,而App端对Cookie的处理远不如浏览器顺手。再加上前后端分离之后,前端可能部署在不同域名下,跨域请求要带Cookie就得额外配置Access-Control-Allow-Credentials,各种跨域限制会让联调变得很烦。

我们那会儿还被另一个问题卡着:部分接口要开放给第三方系统调用,这些调用方根本不是浏览器,也没有Cookie的概念,怎么维持认证状态就成了一个绕不开的问题。这才动了换认证方案的心思。

1.2 JWT的核心结构:三段式与单纯数据签名

JWT全称是JSON Web Token,本质上是一段自包含的令牌。说它"自包含",是因为用户的身份信息和过期时间等元数据直接被编码在Token里,服务端拿到Token后不需要查数据库、不需要查缓存,解出来就能确认身份。

一个JWT由三部分组成,用点号分隔:

  • Header:声明令牌类型和签名算法,通常是{"alg":"HS256","typ":"JWT"}。
  • Payload:存放实际数据,比如用户ID、用户名、角色、过期时间等。
  • Signature:对Header和Payload进行签名的结果,用于防止数据被篡改。

这三部分各自经过Base64Url编码后拼在一起,就形成了一个完整的Token。注意这里用的是Base64Url而不是标准Base64,区别在于字符集里+和/会被替换成-和_,并且省略掉末尾的=,这样Token才能安全地放在URL里而不产生歧义。

签名过程我们当时折腾了一下才彻底搞明白。拿HS256算法来说,签名值是这样算出来的:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)

也就是把前两段编码后的字符串用点号连接,再拿密钥做一次HMAC-SHA256计算。服务端解析Token时,会用同样的密钥重新计算签名,拿算出来的结果和Token里携带的签名做比对,不一致就说明Token在传输过程中被改过了。

这段逻辑里有个关键点值得多说一句:Payload里的信息只是Base64Url编码,不是加密。也就是说,任何人拿到Token,都可以直接解码看到里面的数据。所以敏感信息比如密码、身份证号这些,绝对不能放进Payload里。JWT的核心安全边界是"防篡改",不是"防窥探",这个认知从一开始就要建立起来。

1.3 什么时候不要用JWT

这一点我必须放在前面说,因为很多人一听说JWT能解决Session共享问题,就把所有项目都往JWT上迁,结果踩出一堆新的坑。

JWT特别适合的场景是:无状态接口认证、移动端App登录态、前后端完全分离的项目、以及需要开放API给第三方调用的平台。在这些场景里,Token本身携带身份信息,服务端不用维护会话状态,天然适合分布式部署。

但如果你的项目是服务端渲染的传统Web应用,页面都在服务端生成,用户量也不大,Session方案其实更合适。理由很实在:Session可以随时在服务端主动失效,而JWT在签发之后、到期之前,服务端是没有办法主动让它失效的。你没法把已经发出去的Token"叫回来",只能等它自然过期。如果一定要实现"踢人"或"强制下线"功能,就得维护一个黑名单或者主动刷新机制,这些补偿逻辑写下来,代码量和复杂度并不比管理Session低。

另一个不适合的场景是频繁变更权限的权限系统。Session里存角色信息,权限变了随时改;JWT里存了角色,权限一变你就得让用户重新登录,或者在网关层做额外的角色比对,绕一圈又回到了"有状态"的老路上。

我们当时之所以坚持切JWT,是因为系统确实是前后端分离加多端共用,而且接口要给第三方开放,Session方案在这些场景下已经力不从心。如果你不属于这类情况,建议还是再权衡一下,别为了用JWT而用JWT。

2. JJWT依赖配置:版本选型和最容易踩的三个坑

2.1 为什么选JJWT,而不是java-jwt

Java生态里做JWT的库不少,主流的有两个:一个是本文主角JJWT,另一个是auth0的java-jwt。还有个别项目在用Nimbus JOSE + JWA,不过那个偏底层,日常业务开发里用得不多。

我们最终选JJWT,看中的是这几点:

  • API设计直观,Jwts.builder()和Jwts.parserBuilder()这样的链式调用,读代码的时候几乎不需要注释就能看懂在做什么。
  • 文档和维护质量相对稳定,不会有"升级一个小版本就换一套用法"的背刺感。
  • 支持多种签名算法,从HMAC到RSA都能配,给我们后面做多环境密钥隔离留了余地。

java-jwt其实也不错,API风格同样清爽,但我们在对比时注意到一个实际细节:JJWT在密钥长度校验上做得很明确,密钥太短会直接抛异常告诉我们,而java-jwt在某些版本里对弱密钥的提示不够直观,容易被忽略。对团队里新手友好程度,JJWT更胜一筹。

2.2 Maven依赖坐标与版本差异

如果是Maven项目,JJWT的依赖拆成了三个骨架模块,分别是api、impl、jackson。我们在项目里实际引入的是jjwt-api和jjwt-impl,另外还加了一个jjwt-jackson用来做JSON序列化。依赖坐标长这样:

<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </dependency>

这里有两个版本雷区,提前说一下,能帮你省不少排查时间。

第一个雷区是版本配套。JJWT的api和impl必须使用完全相同的版本,否则运行时大概率会出现NoClassDefFoundError或者奇怪的ClassCastException。我们有一次升级api到0.12.3却忘了同步impl,结果一调用Jwts.parserBuilder()就炸,报错信息指向的类一看就是从impl里加载的,核对依赖树才发现版本不一致。

第二个雷区是0.12.x和0.11.x的API不兼容。0.12.0之后签名相关的构造方式发生了较大调整,很多网上的老教程代码在新版本下直接编译不过。我们项目里还在用0.11.5,不是因为它最新,而是项目里其他组件对它的兼容性验证过,没必要为了追新而承担回归风险。如果你是新项目,直接上0.12.x就可以,但要注意你是跟着新版本的写法来写,别把老代码原样拷过来。

2.3 配置文件里的密钥长度陷阱

接下来是这次改造中印象最深的一个坑:密钥长度。

我们用HS256算法,它要求密钥至少256位,也就是32字节。在application.yml里如果配了一个很短的字符串,比如my-secret-key,JJWT在生成Token的时候可能不报错,但一旦运行在生产环境或者换了更严格的环境,就可能抛出WeakKeyException,提示密钥强度不足。

这个异常的根因是JJWT 0.10.0以后默认开启了安全增强校验,对HS系算法的密钥长度有强制要求。如果用的是HS256,密钥必须不少于256位;用HS384则要求384位以上。

我们最终的密钥配置方式是生成一个固定长度的随机字符串,然后Base64编码之后放到配置文件里,代码里解码后再传给JJWT。这样做的好处是密钥可以包含任意字符,不会因为配置文件对特殊字符的转义而踩坑。

jwt: secret: bXktc2VjcmV0LWtleS1nZW5lcmF0ZWQtYnktcmFuZG9tLXN0cmluZy0xMjM0NTY3ODkw expire-hours: 24

这里再强调一次:密钥必须保存在服务端,绝不能出现在前端代码里,也不能通过接口暴露出去。密钥一旦泄露,别人就能用同样的密钥伪造任意身份的Token,整个认证体系形同虚设。

3. JWT工具类封装:密钥管理、Token生成与解析

3.1 工具类的整体设计思路

依赖配好之后,下一步就是写工具类。我们当时没有把JWT相关代码散落在各个Service里,而是集中封装在一个工具类中,所有涉及Token生成和解析的操作都走这个入口。好处很明显:密钥管理、过期时间、异常处理这些关注点全部收敛在一处,后续要改算法或者换密钥,只需要改一个文件。

工具类的成员变量直接用@Value注入配置项:

@Component public class JwtUtils { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire-hours}") private Long expireHours; }

3.2 生成Token的核心逻辑

生成Token的方法我们最终写成这样:

public String generateToken(Long userId, String username, String role) { SecretKey key = getSigningKey(); Date now = new Date(); Date expiry = new Date(now.getTime() + TimeUnit.HOURS.toMillis(expireHours)); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", role) .setIssuedAt(now) .setExpiration(expiry) .signWith(key, SignatureAlgorithm.HS256) .compact(); }

这段代码里有几个设计上的细节可以说说。

Subject字段通常用于存放用户的唯一标识,我们放的是用户ID转成的字符串。自定义信息通过claim方法放进去,这里放了username和role。注意claim里的值最好是基本类型、字符串或者能被正常序列化的对象,别塞一个带循环引用的复杂对象进去,否则序列化阶段就会出问题。

setIssuedAt和setExpiration分别用来设置签发时间和过期时间。过期时间我们允许通过配置调整,默认24小时,方便不同环境用不同的时长。

signWith(key, SignatureAlgorithm.HS256)这行是签名的关键。我们用的SecretKey是通过Keys.hmacShaKeyFor从密钥字符串解析出来的。这里有个重要细节:不要直接拿字符串去调signWith,有时会走偏门签名逻辑,规范的做法是先构建SecretKey对象再传入。

private SecretKey getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(keyBytes); }

3.3 解析Token与异常处理

解析Token比生成Token更需要细心,因为你要面对的是各种各样非法的、伪造的、过期的Token。我们的解析方法长这样:

public Claims parseToken(String token) { SecretKey key = getSigningKey(); return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); }

这里返回的是Claims对象,它继承了Map<String, Object>,你可以直接通过claims.get("username")取出之前放进去的自定义数据,也可以调用getSubject()、getExpiration()这些专用方法。

这个方法虽然短,但异常处理才是重点。运行时可能抛出的异常主要有这么几类:

  • ExpiredJwtException:Token已过期,通常意味着用户需要重新登录。
  • UnsupportedJwtException:Token格式不对,或者算法与预期不符。
  • MalformedJwtException:Token被篡改过,三段式结构不完整或编码有问题。
  • SignatureException:签名验证失败,说明Token可能在传输途中被改过。
  • IllegalArgumentException:传入的Token是空字符串或null。

我们在调用层做了统一捕获,返回到前端时给出不同提示。这样做的目的是避免把底层异常信息直接抛给前端,既泄露了实现细节,用户体验也差。

3.4 统一放行与拦截的配合

工具类只是基础,真正让JWT生效的是拦截器。我们在项目中写了一个JwtInterceptor,实现HandlerInterceptor接口,在preHandle里完成Token的提取和校验。

整体流程是这样的:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (request.getMethod().equals("OPTIONS")) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); } try { Claims claims = jwtUtils.parseToken(token); request.setAttribute("userId", claims.getSubject()); request.setAttribute("username", claims.get("username")); return true; } catch (ExpiredJwtException e) { // 返回401和明确提示 } catch (JwtException e) { // 返回401和明确提示 } return false; }

这里有个很容易被忽略的点:预检请求OPTIONS。前后端分离部署在不同域名下时,浏览器在发起真正的POST请求前会先发一个OPTIONS预检请求,这个请求是不带Authorization头的,如果拦截器直接拦截,跨域请求就全部失败。所以必须先对OPTIONS放行。

4. 登录流程落地:从用户名密码校验到Token下发

4.1 登录接口的流程设计

工具类封装好之后,就要把它嵌进登录流程了。我们最终的登陆接口流程分四步:

  1. 接收前端传来的用户名和密码。
  2. 校验验证码或者其它前置条件。
  3. 查询用户表,比对密码哈希值。
  4. 通过校验后调用JwtUtils生成Token返回给前端。

密码比对这里多说两句。用户表里存的绝对不能是明文密码,必须是加盐后的哈希值。我们用的工具是BCrypt,BCryptPasswordEncoder.matches(rawPassword, encodedPassword)来判断密码是否正确。很多项目在改造JWT时顺手把密码存储逻辑也重构一遍,这个习惯是好的,但要注意操作顺序,先确认密码比对逻辑没问题,再去动Token相关代码,否则出了错都分不清是哪一块的问题。

登录接口的伪代码大致是这样的:

public LoginResponse login(LoginRequest request) { User user = userMapper.findByUsername(request.getUsername()); if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BusinessException("用户名或密码错误"); } String token = jwtUtils.generateToken(user.getId(), user.getUsername(), user.getRole()); return new LoginResponse(token, user.getId(), user.getUsername(), user.getRole()); }

返回的响应我们把Token和相关用户信息一起给前端。之所以把用户基本信息也返回,是为了让前端在本地保存用户状态,避免每次请求都要额外调一个接口去拿用户信息。实际项目中确实有团队这么做,但不是必须,看前端的需求而定。

4.2 Token下发后的前端存储与Header传递

Token生成之后,前端怎么存、怎么带,这个问题看起来简单,实际坑也不少。

市面上最常见的两种方案是localStorage和HttpOnly Cookie。localStorage的实现简单,前端从接口拿到Token后直接存进去,后续请求手动拼到Authorization头里。但localStorage有一个众所周知的隐患:任何在页面里执行的XSS脚本都能读取它,Token一旦被脚本偷走,攻击者就拿到了用户的完整身份。

HttpOnly Cookie这个方案不推荐。因为这个方案能防XSS,但HttpOnly Cookie不能在JavaScript中被读取,前后端分离时需要通过后端Set-Cookie来设置。这个方式要求前端进行CSRF防护。我们当时的项目前端是单页应用,接口调用由前端代码发起,没有用到后端模板渲染,所以我们决定用localStorage + Authorization头的方案,同时配合前端框架做基本的XSS过滤,目前看下来是够用的。

实际请求长这样:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.xxxxx

注意Bearer这个前缀,它是一种约定的鉴权Scheme,后端读取时要能正确处理带前缀和不带前缀两种情形。

4.3 刷新Token与退出登录的处理要点

Token过期之后怎么办?这是JWT落地时绕不开的问题。简单粗暴的做法是:过期就要求用户重新登录。但是一个24小时过期的Token,用户可能早上登录后一直用到第二天,频繁要求重新登录显然影响体验。更合理的方案是引入refresh_token双Token机制:access_token短期有效(比如2小时),refresh_token长期有效(比如7天)。access_token过期后,前端用refresh_token去换一个新的access_token,整个过程对用户无感。

双Token机制从实现上看并不算复杂,生成时额外签发一个refresh_token,新增一个刷新接口处理兑换逻辑。但副作用是服务端状态管理的复杂度又回来了:refresh_token通常需要存库或者存在Redis里,因为你得能主动作废它。这一层绕回来之后,其实和Session方案的部分痛点又重叠了。

我们当时的取舍是:先不上双Token,把access_token的过期时间设为一个合理的业务周期,线上跑了两个多月,用户反馈可以接受。如果之后有更长的会话保持需求,再考虑引入刷新机制也不迟。

退出登录这里同样需要想清楚。很多人以为后端调用一个logout接口把Token标记为无效就完事了,但JWT本身是无状态的,服务端没有会话记录可删。在纯JWT方案里,退出登录最常见的手段是前端直接丢弃本地Token,后端不做任何处理。如果业务上要求Token在过期前作废,就得上Redis黑名单:把退出登录的Token存进一个短时效的黑名单里,拦截器每次比对一下。这个方案我们暂时没用,只是整理需求时聊过,因为当前业务对"强制下线"的要求不高。

5. 实际项目中踩过的坑:从时区问题到密钥轮换

5.1 服务器时区不一致导致的过期时间偏差

第一次联调的时候就出现了一个诡异的现象:明明Token设置的过期时间是24小时,但客户端提交Token过来,偶尔会提示已经过期。排查了很久,发现是服务器之间的时区问题。

当时我们有开发机、测试机和正式机的几套环境,有的机器系统时区设置成了UTC,有的设置成了Asia/Shanghai。JWT的setIssuedAt和setExpiration用的都是时间戳,本身不携带时区信息,但一旦服务器在生成和解析之间跨了时区,时间计算上就可能出现偏差。具体来说,生成端在UTC时区下计算过期时间,解析端在Asia/Shanghai时区下读取,相差8小时,如果Token的过期时间设得比较短,偏差就很容易暴露。

解决方式分两步:

  • 统一所有服务器的基础时区,在启动脚本里加上-Duser.timezone=Asia/Shanghai参数,或者在应用配置里设置spring.jackson.time-zone。
  • 生成Token时不要依赖本地时间的获取方式,统一用System.currentTimeMillis()或者是注入的时钟源。

当时我们图省事在代码里直接new Date(),这实际上就是取当前系统时间。多个服务器时间不同步的时候,同样的代码在不同机器上跑出来的行为就不一样,联调起来特别折磨人。

5.2 解析异常的完整排查链路

有一次线上反馈部分用户登录失效频繁,我们拉日志一看,报的是SignatureException。当时的第一反应是密钥被改了,但排查了很久发现并没有人动过配置。后来仔细对了日志和环境,才发现是某台服务器的配置文件没有同步更新,用的还是上一轮的旧密钥。TTL缓存加上服务一直没重启,旧进程持有的还是旧密钥,新旧密钥不一致导致签名验证失败。

那次排查把完整链路重新走了一遍,整理出来这样的排查顺序,分享给你参考:

  1. 先确定报错类型。SignatureException看密钥和算法,ExpiredJwtException看时间,MalformedJwtException看Token内容本身。
  2. 核对密钥。把所有环境的配置拉出来比对,确认加解密用的密钥完全一致。
  3. 核对Token来源。用在线解析工具解码Token的Header部分,确认签名算法和你预期的一致。如果Token里alg字段是none,说明压根没签名,这个Token是伪造的。
  4. 核对代码版本。确认线上运行的Jar包里的工具类是最新版本,别出现灰度发布时新旧逻辑并存的情况。

这个顺序看起来简单,但真到告警满天飞的时候,人容易慌,跳着排查反而更浪费时间。

5.3 密钥管理与轮换策略

JWT密钥属于核心机密,它的管理级别至少应该和其他数据库密码相同。我们在项目里做了几件事,算是把密钥管理的底线兜住了。

第一,不同环境使用不同的密钥。开发环境、测试环境、生产环境的密钥独立配置,互不通用。这样即使开发环境密钥泄露,也不会波及生产环境。

第二,密钥定期轮换。理论上密钥应该有生命周期,但我们并没有做得很复杂,目前的做法是在大版本发版时顺便轮换一次,同时保证新旧密钥有一段重叠窗口,让已签发的Token能平滑过渡。要是做更严格的轮换,就得维护一个密钥版本列表,解析Token时用对应版本的密钥去验签,这个复杂度更高,我们暂时没上。

第三,密钥不入代码仓库。application.yml里的密钥引用的其实是环境变量,仓库里只保留一个占位符。这样可以避免开发者的本地配置被不小心推到远端仓库里。

jwt: secret: ${JWT_SECRET}

这几条看着基础,但在实际项目中能坚持做到的并不多。安全上的事,往往是那些看起来最笨、最不高级的措施,最终救了命。

最后说点实际的

整个改造从动手到稳定运行,前后差不多花了一周。真正写代码的时间其实不长,大量时间都耗在依赖选型、密钥设计和各种边角问题上。做完之后最大的感受是:JWT本身不复杂,复杂的是把它放进一个真实的生产环境里,让它和各种已有的机制共存。如果你也在做类似的改造,建议先把密钥和过期时间这两个基础项理清楚,再去碰那些花哨的刷新机制和黑名单逻辑。基础不牢的话,上层设计再精巧,线上出问题的时候照样抓瞎。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 12:29:26

Python爬虫实战:豆瓣Top250数据分析与可视化全流程

简介&#xff1a;基于Python爬取豆瓣电影Top250并完成数据分析与可视化的完整项目&#xff0c;面向计算机相关专业正在做课程大作业的学生&#xff0c;以及需要实战练习的Python学习者。资源共2000个文件&#xff0c;以1830个Python源码文件为主&#xff0c;涵盖爬虫抓取、数据…

作者头像 李华
网站建设 2026/10/10 12:28:32

MATLAB粒子群算法求解多微网优化模型实战指南

多微网优化这两年特别热&#xff0c;电网侧在做区域协同调度&#xff0c;园区侧也在搞多个微电网之间的功率互济。但真上手做优化的人都知道&#xff0c;多微网模型比单微网复杂不少——变量多、约束多、目标之间还互相牵制&#xff0c;用传统数学规划工具碰非线性、非凸问题时…

作者头像 李华
网站建设 2026/10/10 12:27:50

编程小白入门指南:从零基础到实战项目避坑路线图

“编程小白的梦”这个标题&#xff0c;一看就带着一股既憧憬又忐忑的劲儿。这些年我在社区里见过太多人立下“学会编程”的flag&#xff0c;有的确实转了行、做出了自己的小工具&#xff0c;但更多人卡在环境安装&#xff0c;或者学着学着就迷失了方向。我最初接触编程时也完全…

作者头像 李华
网站建设 2026/10/10 12:27:25

Unet+Resnet多尺度训练:宫颈细胞核语义分割实战指南

简介&#xff1a;面向深度学习者与医学图像处理研究者的 UnetResnet 多尺度分割项目&#xff0c;以子宫颈细胞核分割任务为例&#xff0c;完成二分类语义分割全流程。已内置数据集、训练与预测脚本、训练好的权重&#xff0c;仅训练50轮即达全局像素准确率0.89、mIoU 0.72&…

作者头像 李华
网站建设 2026/10/10 12:27:12

网络货运系统搭建全解析:核心模块、技术架构与合规落地

做网络货运系统这事&#xff0c;我从零搭过完整的一套&#xff0c;也帮几个物流公司做过改造升级。圈子里聊起来&#xff0c;很多人第一反应是"这不就是做个APP让司机接单嘛"&#xff0c;真踩进去才发现完全不是那么回事。网络货运系统说白了&#xff0c;是把传统物流…

作者头像 李华
网站建设 2026/10/10 12:27:08

CurveMoE:基于模式连通的多范数对抗防御方案深度解读

分享一套 Robust CurveMoE 的深度解读与防御方案思路&#xff0c;覆盖多范数对抗、模式连通与混合专家模型落地最近在复盘混合专家模型&#xff08;Mixture-of-Experts, MoE&#xff09;的安全性时&#xff0c;发现一个很有意思的攻防方向&#xff1a;传统对抗训练大多在单范数…

作者头像 李华