You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Jakarta EE11、Wildfly34的JWT+原生JS Token认证实现问询

原生JS+Jakarta EE 11/WildFly 34/Jetty 12的JWT认证方案

环境背景

服务器端:Java 21 + Jakarta EE 11 + WildFly 34 Preview(后续需适配Jetty ≥12)
客户端:原生HTML/CSS/JavaScript(无第三方框架)


1. 原生JS/HTML中使用JWT的步骤

基础登录表单

先定义原生HTML登录表单:

<form id="login-form">
  <input type="text" name="username" placeholder="用户名" required>
  <input type="password" name="password" placeholder="密码" required>
  <button type="submit">登录</button>
</form>

获取JWT(异步Fetch方式)

通过JS监听表单提交,用Fetch API发送登录请求并接收JWT:

document.getElementById('login-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const formData = new FormData(e.target);
  
  try {
    const response = await fetch('/auth/login', {
      method: 'POST',
      body: formData
    });
    if (!response.ok) throw new Error('登录失败');
    
    // 服务器返回JWT(可根据实际格式调整,比如JSON返回)
    const jwt = await response.text();
    // 存储令牌(见问题5)
    sessionStorage.setItem('authToken', jwt);
    // 跳转至首页或受保护页面
    window.location.href = '/dashboard';
  } catch (err) {
    alert(err.message);
  }
});

携带JWT请求受保护资源

每次请求时在Authorization头中注入JWT:

async function fetchProtectedResource(url) {
  const jwt = sessionStorage.getItem('authToken');
  if (!jwt) {
    window.location.href = '/login';
    return;
  }

  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${jwt}`
    }
  });

  if (response.status === 401) {
    // 令牌过期,清除存储并跳转登录
    sessionStorage.removeItem('authToken');
    window.location.href = '/login';
    return;
  }

  return response.json();
}

前端解析JWT(仅用于展示,不可信任)

若需读取令牌中的非敏感信息(如用户名),可手动解码Base64段(前端解析结果仅作展示,权限判断必须由服务器完成):

function parseJwt(token) {
  const base64Url = token.split('.')[1];
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
  const payload = decodeURIComponent(atob(base64).split('').map(c => 
    '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
  ).join(''));
  return JSON.parse(payload);
}

2. 安全最佳实践

结合你的技术栈和已采用的HTTPS、非对称加密方案,补充以下核心规则:

  • 令牌生命周期管控:设置短过期时间(15-30分钟),搭配刷新令牌机制;刷新令牌必须存储在HttpOnly、SameSite=Strict的Cookie中,避免JS访问。
  • 最小权限原则:JWT仅包含必要信息(如用户ID、角色),绝对禁止存储密码、手机号等敏感数据,即使加密也不建议。
  • 非对称算法选型:签名用RS256/ES256,加密用RSA-OAEP-256/ECDH-ES+A256GCM;私钥仅存于服务器密钥库(WildFly/Jetty配置文件管理,禁止硬编码)。
  • 公钥分发安全:公钥通过HTTPS接口下发,可额外对其签名防止篡改;客户端仅用公钥验证签名/解密令牌,绝不存储私钥。
  • 严格服务器验证:服务器必须校验JWT的签名、过期时间(exp)、受众(aud)、签发者(iss),拒绝任何无效令牌。
  • XSS防护:启用内容安全策略(CSP),对所有用户输入做 sanitize 处理;避免使用localStorage存储令牌(易被XSS窃取)。
  • CSRF防护:若用Cookie存储令牌,需开启SameSite=Strict;若用Bearer头,配置CORS策略仅允许可信域名跨域请求。

3. 两种登录方式的安全性对比

两种方式安全性不等价,核心差异在于密码暴露风险和令牌存储逻辑:

方式1:表单POST + POST/redirect/GET

  • 优势:浏览器原生表单提交,密码不会暴露给JavaScript(除非手动用JS获取表单值),降低XSS窃取密码的风险;重定向机制可避免表单重复提交。
  • 潜在风险:若服务器通过重定向页面渲染JWT(如嵌入HTML的script标签),令牌仍会被JS获取,存在XSS窃取风险;若用URL传递JWT,会暴露在浏览器历史、日志中,风险极高。
  • 优化建议:服务器将JWT存入HttpOnly、SameSite=Strict的Cookie中,而非通过页面渲染或URL传递,此时安全性远优于方式2。

方式2:Fetch异步POST + 返回JWT

  • 优势:无需页面跳转,用户体验更流畅;可直接控制令牌存储逻辑。
  • 潜在风险:JavaScript直接处理密码,若页面存在XSS漏洞,密码会被窃取;令牌存储在JS可访问的位置(如sessionStorage),同样面临XSS窃取风险。
  • 结论:若方式1优化为用HttpOnly Cookie存JWT,安全性更高;若两种方式都将令牌暴露给JS,则安全性接近,但方式1的密码暴露风险更低。

4. 使用Authorization: Bearer 头的合理性

该实现符合标准且合理,但需注意以下细节:

  • 强制开启HTTPS,防止令牌在传输过程中被窃听或篡改。
  • 配置严格的CORS策略,仅允许可信域名发起跨域请求,避免令牌被恶意网站滥用。
  • 处理令牌过期场景:捕获401响应后,清除本地令牌并跳转至登录页,或触发刷新令牌流程。
  • 服务器端配置日志过滤,屏蔽Authorization头内容,防止令牌泄露到日志中。

5. 令牌存储方案可行性分析

单页应用(SPA)

  • 内存存储(全局变量):仅在页面会话期间有效,刷新页面后丢失,用户体验差,但XSS风险相对较低(仅当XSS在令牌有效期内触发时可窃取)。
  • 推荐方案:搭配sessionStorage使用,会话结束自动清除,比localStorage安全。

多页应用

  • sessionStorage:会话级存储,关闭标签页后清除,不会跨标签共享,XSS风险低于localStorage;但页面跳转时需手动传递令牌(或通过Cookie同步)。
  • localStorage:持久化存储,跨标签共享,XSS窃取后可长期滥用,绝对不推荐。
  • Service Worker存储:用Cache API或IndexedDB存储令牌,Service Worker独立于页面线程,XSS需获取Service Worker控制权才能窃取,实现复杂但安全性更高。
  • 最佳实践:优先使用HttpOnly、SameSite=Strict的Cookie存储令牌,完全避免JS访问,从根源降低XSS风险。

Jetty ≥12专属实现方案

Jetty 12支持Jakarta EE 11,可基于Jakarta Security API实现JWT认证:

1. 配置Web安全约束

在web.xml中定义受保护资源和认证规则:

<security-constraint>
  <web-resource-collection>
    <web-resource-name>Protected API</web-resource-name>
    <url-pattern>/api/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>authenticated</role-name>
  </auth-constraint>
</security-constraint>
<login-config>
  <auth-method>BASIC</auth-method> <!-- 后续替换为JWT自定义认证 -->
  <realm-name>JwtRealm</realm-name>
</login-config>

2. 自定义JWT认证机制

实现Jakarta.security.authentication.spi.AuthenticationMechanism,处理Bearer头验证:

@ApplicationScoped
public class JwtAuthMechanism implements AuthenticationMechanism {
  // 注入Jakarta EE 11的JWT API(或使用Nimbus JOSE+JWT库)
  @Inject
  private JsonWebToken jwt;

  @Override
  public AuthenticationStatus validateRequest(HttpServletRequest request, 
                                              HttpServletResponse response, 
                                              HttpMessageContext context) throws AuthenticationException {
    String authHeader = request.getHeader("Authorization");
    if (authHeader != null && authHeader.startsWith("Bearer ")) {
      String token = authHeader.substring(7);
      // 验证令牌签名、过期时间、受众等逻辑
      if (validateTokenSignature(token) && !isTokenExpired(token)) {
        // 设置认证上下文
        context.setCallerPrincipal(new CallerPrincipal(jwt.getSubject()));
        context.getRoles().addAll(jwt.getGroups());
        return AuthenticationStatus.SUCCESS;
      }
    }
    return AuthenticationStatus.SEND_UNAUTHORIZED;
  }

  private boolean validateTokenSignature(String token) {
    // 用服务器公钥验证令牌签名(具体实现参考JWT库文档)
    return true;
  }

  private boolean isTokenExpired(String token) {
    // 检查令牌exp字段是否过期
    return false;
  }
}

3. 配置CORS支持

在jetty.xml中添加CORS过滤器,允许可信前端域名跨域请求:

<Configure id="Server" class="org.eclipse.jetty.server.Server">
  <Call name="addFilter">
    <Arg>org.eclipse.jetty.servlets.CrossOriginFilter</Arg>
    <Arg>/*</Arg>
    <Arg>
      <Call name="init">
        <Arg>
          <New class="java.util.HashMap">
            <Put name="allowedOrigins">https://your-frontend-domain.com</Put>
            <Put name="allowedMethods">GET,POST,PUT,DELETE</Put>
            <Put name="allowedHeaders">Authorization,Content-Type</Put>
            <Put name="allowCredentials">true</Put>
          </New>
        </Arg>
      </Call>
    </Arg>
  </Call>
</Configure>

4. 密钥管理

将私钥存储在Jetty密钥库(jetty.keystore)中,通过配置加载:

<Configure id="Server" class="org.eclipse.jetty.server.Server">
  <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory.Server">
    <Set name="KeyStorePath"><Property name="jetty.base" default="."/>/etc/jetty.keystore</Set>
    <Set name="KeyStorePassword">your-keystore-password</Set>
    <Set name="KeyManagerPassword">your-key-password</Set>
  </New>
</Configure>

内容的提问来源于stack exchange,提问作者gouessej

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 15:57:11