JWT令牌验证

发布于:2025-05-18 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、JWT 验证方式详解

JWT(JSON Web Token)的验证核心是确保令牌未被篡改符合业务规则,主要分为以下步骤:

1. 令牌解析与基础校验

收到客户端传递的 JWT 后,首先按 . 分割为三部分:HeaderPayloadSignature

  • Header:解析算法(如 HS256)和令牌类型(固定为 JWT)。
  • Payload:解析标准声明(如 exp 过期时间、iat 签发时间)和自定义声明(如用户 ID)。
2. 签名验证(防篡改)

签名是 JWT 的核心安全屏障。验证逻辑:

  • 使用 Header 中指定的算法(如 HMAC SHA256),用服务端保存的密钥Header.Base64UrlEncode() + "." + Payload.Base64UrlEncode() 重新计算签名。
  • 对比新计算的签名与 JWT 中的 Signature,若不一致则令牌无效(可能被篡改)。
3. 声明校验(业务规则)

验证 Payload 中的声明是否符合业务要求:

  • exp(Expiration Time):令牌过期时间戳,需满足 当前时间 < exp(考虑时钟偏差,如 ±300秒)。
  • iat(Issued At):令牌签发时间,需满足 iat ≤ 当前时间
  • iss(Issuer):令牌签发者,需与服务端配置的签发者(如 https://your-domain.com)一致。
  • aud(Audience):令牌接收方,需与当前服务身份(如 user-service)匹配。
  • 自定义声明:如用户角色(role)、权限(permissions)等,按业务需求验证。
4. 令牌状态校验(可选)

对于需要主动失效的令牌(如用户注销),需维护一个黑名单(如 Redis),验证时检查令牌是否在黑名单中(适用于需要严格控制令牌生命周期的场景)。


二、后端 Java 示例(Spring Boot + JJWT)

以下是基于 Spring Boot 的 JWT 生成与验证接口实现,使用 JJWT 库(Java JWT 工具)。

1. 依赖配置

pom.xml 中添加 JJWT 依赖:

<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>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
2. JWT 工具类(核心逻辑)
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtils {
    // 从配置文件读取密钥和过期时间(示例:密钥为 "your-256-bit-secret",过期时间 30分钟)
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expire}")
    private long expire;

    // 生成 JWT
    public String generateToken(String userId) {
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + expire * 1000); // 转换为毫秒

        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId); // 自定义声明
        claims.put("role", "user");   // 自定义角色

        return Jwts.builder()
                .setClaims(claims)          // 自定义声明
                .setIssuer("your-domain")   // 签发者(iss)
                .setIssuedAt(now)           // 签发时间(iat)
                .setExpiration(expireDate)  // 过期时间(exp)
                .signWith(SignatureAlgorithm.HS256, secret) // 签名算法+密钥
                .compact();
    }

    // 验证 JWT 并解析声明
    public Claims validateToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)    // 使用相同密钥验证
                    .parseClaimsJws(token)    // 解析 JWT
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("令牌已过期");
        } catch (UnsupportedJwtException e) {
            throw new RuntimeException("不支持的令牌类型");
        } catch (MalformedJwtException e) {
            throw new RuntimeException("令牌格式错误");
        } catch (SignatureException e) {
            throw new RuntimeException("签名验证失败(可能被篡改)");
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("令牌为空");
        }
    }
}
3. 接口示例(生成与验证)
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;

@RestController
@RequestMapping("/auth")
public class AuthController {
    @Resource
    private JwtUtils jwtUtils;

    // 模拟登录接口:返回 JWT
    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        // 实际业务中需校验用户名密码(示例直接通过)
        return jwtUtils.generateToken("123"); // 用户 ID 为 123
    }

    // 验证令牌接口(示例)
    @PostMapping("/validate")
    public String validate(@RequestHeader("Authorization") String token) {
        // 提取 Bearer 令牌(去掉 "Bearer " 前缀)
        String actualToken = token.replace("Bearer ", "");
        try {
            Claims claims = jwtUtils.validateToken(actualToken);
            return "验证成功!用户 ID:" + claims.get("userId");
        } catch (Exception e) {
            return "验证失败:" + e.getMessage();
        }
    }
}

三、前端/客户端示例(Uniapp、WPF、Qt)

以下示例演示客户端如何携带 JWT 调用后端验证接口(假设后端地址为 http://localhost:8080)。

1. Uniapp(Vue 跨端框架)
<template>
  <view>
    <button @click="login">登录获取 Token</button>
    <button @click="validateToken">验证 Token</button>
    <text>{{ result }}</text>
  </view>
</template>

<script>
export default {
  data() {
    return {
      token: null,
      result: ""
    };
  },
  methods: {
    // 登录获取 Token
    async login() {
      const res = await uni.request({
        url: "http://localhost:8080/auth/login",
        method: "POST",
        data: { username: "test", password: "123" }
      });
      this.token = res.data;
      this.result = "Token 获取成功:" + this.token;
    },

    // 验证 Token(携带到请求头)
    async validateToken() {
      if (!this.token) {
        this.result = "请先登录获取 Token";
        return;
      }
      const res = await uni.request({
        url: "http://localhost:8080/auth/validate",
        method: "POST",
        header: { Authorization: `Bearer ${this.token}` }
      });
      this.result = res.data;
    }
  }
};
</script>
2. WPF(.NET 桌面应用)
using System;
using System.Net.Http;
using System.Windows;

namespace WpfJwtDemo {
    public partial class MainWindow : Window {
        private string _token;
        private readonly HttpClient _httpClient = new HttpClient();

        public MainWindow() {
            InitializeComponent();
            _httpClient.BaseAddress = new Uri("http://localhost:8080/");
        }

        // 登录获取 Token
        private async void LoginButton_Click(object sender, RoutedEventArgs e) {
            var formData = new FormUrlEncodedContent(new[] {
                new KeyValuePair<string, string>("username", "test"),
                new KeyValuePair<string, string>("password", "123")
            });
            var response = await _httpClient.PostAsync("auth/login", formData);
            _token = await response.Content.ReadAsStringAsync();
            ResultText.Text = "Token获取成功:" + _token;
        }

        // 验证 Token(携带到请求头)
        private async void ValidateButton_Click(object sender, RoutedEventArgs e) {
            if (string.IsNullOrEmpty(_token)) {
                ResultText.Text = "请先登录获取 Token";
                return;
            }
            _httpClient.DefaultRequestHeaders.Authorization = 
                new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token);
            var response = await _httpClient.PostAsync("auth/validate", null);
            var result = await response.Content.ReadAsStringAsync();
            ResultText.Text = result;
        }
    }
}
3. Qt(C++ 跨平台框架)
#include <QApplication>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrlQuery>
#include <QMessageBox>

class JwtDemo : public QObject {
    Q_OBJECT
public:
    JwtDemo(QObject *parent = nullptr) : QObject(parent) {
        manager = new QNetworkAccessManager(this);
        connect(manager, &QNetworkAccessManager::finished, 
                this, &JwtDemo::onRequestFinished);
    }

    // 登录获取 Token
    void login() {
        QUrl url("http://localhost:8080/auth/login");
        QUrlQuery query;
        query.addQueryItem("username", "test");
        query.addQueryItem("password", "123");
        QNetworkRequest request(url);
        request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
        manager->post(request, query.toString(QUrl::FullyEncoded).toUtf8());
    }

    // 验证 Token(携带到请求头)
    void validateToken(const QString &token) {
        QUrl url("http://localhost:8080/auth/validate");
        QNetworkRequest request(url);
        request.setRawHeader("Authorization", "Bearer " + token.toUtf8());
        manager->post(request, ""); // 空 body
    }

private slots:
    void onRequestFinished(QNetworkReply *reply) {
        if (reply->error() == QNetworkReply::NoError) {
            QString result = reply->readAll();
            if (reply->url().path() == "/auth/login") {
                currentToken = result;
                QMessageBox::information(nullptr, "成功", "Token获取成功:" + result);
            } else if (reply->url().path() == "/auth/validate") {
                QMessageBox::information(nullptr, "验证结果", result);
            }
        } else {
            QMessageBox::critical(nullptr, "错误", "请求失败:" + reply->errorString());
        }
        reply->deleteLater();
    }

private:
    QNetworkAccessManager *manager;
    QString currentToken;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    JwtDemo demo;
    // 模拟点击登录(实际需绑定UI事件)
    demo.login();
    // 假设登录后验证(实际需等待登录完成)
    QTimer::singleShot(2000, [&demo]() {
        if (!demo.currentToken.isEmpty()) {
            demo.validateToken(demo.currentToken);
        }
    });
    return app.exec();
}

#include "main.moc" // 需包含 moc 文件(Qt 元对象编译)

四、关键说明

  1. 密钥安全:后端密钥(jwt.secret)需严格保密,避免硬编码在代码中(建议通过配置中心或环境变量获取)。
  2. 过期时间exp 需根据业务场景设置(如用户登录态建议 30分钟~1天,敏感操作建议更短)。
  3. 客户端处理:前端需将 JWT 存储在 localStorage(Web)、SecureStorage(移动端)或 注册表(桌面端)中,避免明文存储。
  4. 跨域问题:若前端与后端不同域,需在后端配置 CORS(跨域资源共享)。

网站公告

今日签到

点亮在社区的每一天
去签到