前端安全架构:自动鉴权与全局Token注入的深度实践

前端自动鉴权与全局 Token 注入的深度实践

“安全不是终点,而是每一次请求的起点。”

在现代 Web 应用中,接口安全和统一鉴权已成为前端架构设计的核心环节。尤其在微服务、API 网关盛行的今天,如何让前端自动、无感地完成鉴权,保障每一次数据交互的安全与合规,是每个前端工程师都绕不开的话题。

本文以实际项目中的 index.js 为例,结合关键代码,深入剖析如何在前端实现自动鉴权、全局 token 注入,并对方案的优缺点进行评价。


背景与目标

  • 目标:所有前端 AJAX 请求都需带上动态获取的 access_token,且无需业务方手动处理。
  • 难点:token 获取流程复杂,需多步加密、签名、指纹识别,且要保证首批请求不丢失。

“让安全成为默认,而不是负担。”


核心流程与关键实现

1. 获取 URL 参数

用于动态获取鉴权所需的 ckey 等参数。

function getUrlParams(variable) {
    // ...existing code...
}

2. 鉴权流程封装为类

VerificationProcess 封装了整个鉴权流程,便于复用和扩展。

2.1 构造与配置


class VerificationProcess {
    constructor(ckey) {
        this.config = {
            encryptKey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCt8e352hOSzQ+PEWC9PmulRfRdCJgsXMNFlgivAOCi/+49+GZakeYE1sfS0XhtNOlnUna0g220Y9WyQyHsotgGK12pexjLSVl2gvcWAbVIT6CDDUGMs6AFDxYdg6lgkdUup+sizgE4wIDAQAB",
            globalKey: ["\x34", "\x43", "\x38", "\x39", "\x37", "\x38", "\x33","\x35", "\x44", "\x35"],
            evenKey: ["2", "9", "6", "B", "F", "6","9", "1", "B", "A"],
            oddKey: ["A", "5", "F", "6", "5", "6", "4", "3", "D", "A"]
        };
        this.fingerPrint = "";
        this.requestQueue = [];
        this.ckey = ckey || "";
        this.verificationRes = { verificationCode: "", date: "" };
        this.userInfo = "";
        this.signNature = "";
        this.access_token = "";
    }
    // ...其余方法见下文...
}

2.2 获取浏览器指纹

利用 fingerprintjs2 生成唯一指纹,作为鉴权因子。


async getFingerPrint() {
    var fp = window.Fingerprint2;
    return new Promise(function (resolve, reject) {
        fp.get(function (components) {
            var values = components.map(function (component) {
                return component.value;
            });
            var murmur = fp.x64hash128(values.join(""), 31);
            if (components) {
                resolve(murmur);
            } else {
                reject("");
            }
        });
    });
}

2.3 获取验证码与加密信息

  • 先用指纹请求验证码
  • 再用公钥加密、异或加密、Base64 编码等多重加密生成 userInfo

async getVerificationAndCode() {
    if (!this.fingerPrint) {
        console.error("VerificationProcess未获取到fingerprint");
        return;
    }
    const res = await axios.get(
        "https://b2b-api.10jqka.com.cn/gateway/service-mana/encryption/sendVerificationCode",
        { params: { fingerPrint: this.fingerPrint } }
    );
    Object.assign(this.verificationRes, {
        verificationCode: res.data.data.verificationCode || "",
        date: res.data.data.date || "",
    });
}

getUserInfo() {
    if (!this.ckey || !this.fingerPrint || !this.verificationRes.verificationCode) {
        console.error("VerificationProcess未获取到ckey或fingerprint或verificationCode");
        return;
    }
    var data =
        "verificationCode=" + this.verificationRes.verificationCode +
        ";ckey=" + this.ckey +
        ";fingerPrint=" + this.fingerPrint;
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey(this.config.encryptKey);
    var encryptCode = encrypt.encrypt(data);
    var xorEncryptCode = this.xorEncode(
        encryptCode,
        this.config.globalKey,
        this.config.evenKey,
        this.config.oddKey
    );
    this.userInfo = xorEncryptCode;
}

2.4 生成签名


getSignNature(randomNum) {
    var data =
        "verificationCode=" + this.verificationRes.verificationCode +
        ";date=" + this.verificationRes.date +
        ";randomCode=" + randomNum;
    this.signNature = CryptoJS.SHA1(data).toString();
}

2.5 获取 access_token


async getToken() {
    if (!this.fingerPrint) {
        this.fingerPrint = await this.getFingerPrint();
    }
    await this.getVerificationAndCode();
    this.setCKey();
    this.getUserInfo();
    var randomNum = this.random16();
    this.getSignNature(randomNum);
    var requestData = new URLSearchParams();
    requestData.append("userInfo", this.userInfo);
    requestData.append("signature", this.signNature);
    requestData.append("randomCode", randomNum);
    requestData.append("date", this.verificationRes.date);
    var res = await axios({
        method: "post",
        url: "https://b2b-api.10jqka.com.cn/gateway/service-mana/encryption/requestToken",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
        },
        data: requestData,
    });
    var access_token = res.data.data.access_token;
    this.access_token = access_token;
    // ...后续全局注入见下文...
}

3. 全局劫持 XMLHttpRequest,自动注入 token

3.1 劫持 send 方法,队列化所有请求


var origSend = xhrProto.send;
xhrProto.send = function () {
    // 除了鉴权相关接口,其他请求全部暂存
    if (
        !this.requestURL ||
        (this.requestURL &&
            !this.requestURL.includes("/service-mana/encryption/sendVerificationCode") &&
            !this.requestURL.includes("/service-mana/encryption/requestToken"))
    ) {
        verificationProcess.addRequestQueue({
            _this: this,
            params: arguments,
        });
    } else {
        return origSend.apply(this, arguments);
    }
};

3.2 token 获取后,自动补发所有请求


xhrProto.send = function () {
    this.setRequestHeader("Open-Authorization", "Bearer " + access_token);
    return origSend.apply(this, arguments);
};
// 补发队列
this.requestQueue.forEach(function (item) {
    item._this.setRequestHeader("Open-Authorization", "Bearer " + access_token);
    origSend.apply(item._this, item.params);
});
this.requestQueue = [];

本方案通过前端自动鉴权与全局 token 注入,极大提升了接口安全性和开发效率。它让安全成为前端的“默认能力”,而不是额外负担。对于需要统一鉴权、token 动态获取的前端项目,这一模式值得深入学习和推广。

posted @ 2025-06-12 18:02  Justus-  阅读(79)  评论(0)    收藏  举报