微信小游戏登录流程中,最关键的环节在于服务器如何验证客户端提交的数据合法性。当客户端通过wx.login获取code并发送给服务器后,服务器会通过微信提供的auth.code2Session接口换取openId和session_key。这个session_key就是后续所有安全校验的基石。
在实际项目中,我遇到过不少开发者直接把session_key返回给客户端的案例,这是非常危险的做法。session_key相当于一把钥匙,如果泄露给客户端,攻击者完全可以伪造任何数据请求。正确的做法应该是服务器将session_key存储在服务端(比如Redis),然后生成一个自定义的token返回给客户端。
验证流程的核心代码可以这样实现:
typescript复制// 服务器端session_key验证示例
import * as crypto from 'crypto';
async function verifySignature(rawData: string, signature: string, sessionKey: string): Promise<boolean> {
const hash = crypto.createHash('sha1');
hash.update(rawData + sessionKey);
const calculatedSignature = hash.digest('hex');
return calculatedSignature === signature;
}
这个简单的函数实现了微信要求的签名验证算法。当客户端调用wx.getUserInfo获取用户信息时,会得到rawData和signature两个关键字段。服务器只需要用存储的session_key重新计算一次签名,与客户端传来的signature比对即可。
一个健壮的校验系统应该包含多个层次的防护。在我的项目实践中,通常会设计三层校验机制:
第一层是基础参数校验,确保所有必填字段都存在且格式正确。这个可以通过TypeScript的接口定义来实现:
typescript复制interface IUserInfoRequest {
rawData: string;
signature: string;
iv?: string;
encryptedData?: string;
timestamp: number;
nonceStr: string;
}
function validateRequestParams(params: any): params is IUserInfoRequest {
return !!params.rawData && !!params.signature;
}
第二层是时效性校验,防止重放攻击。我会要求所有请求必须包含timestamp和nonceStr字段,服务器会检查时间戳是否在合理范围内(比如±5分钟),以及nonce是否已经使用过。
第三层才是前面提到的签名校验。这种分层设计的好处是可以在早期就拦截掉大部分非法请求,减轻服务器压力。
session_key并不是永久有效的,微信官方文档指出它可能会在以下情况失效:
在我的项目中,通常会实现一个session_key的自动刷新机制。具体做法是:
这个流程的TypeScript实现可能长这样:
typescript复制async function checkSession(key: string): Promise<boolean> {
try {
const response = await axios.get(
`https://api.weixin.qq.com/wxa/checksession?access_token=${accessToken}&signature=${signature}&openid=${openid}`
);
return response.data.errcode === 0;
} catch (e) {
return false;
}
}
除了基本的签名验证外,微信还提供了encryptedData和iv字段,可以用来获取更完整的用户信息。这个加密数据使用的是AES-128-CBC算法,session_key作为密钥。
解密过程需要注意几个关键点:
这里分享一个我在项目中使用的解密函数:
typescript复制import * as crypto from 'crypto';
function decryptData(encryptedData: string, iv: string, sessionKey: string): any {
const sessionKeyBuffer = Buffer.from(sessionKey, 'base64');
const encryptedDataBuffer = Buffer.from(encryptedData, 'base64');
const ivBuffer = Buffer.from(iv, 'base64');
const decipher = crypto.createDecipheriv(
'aes-128-cbc',
sessionKeyBuffer,
ivBuffer
);
decipher.setAutoPadding(true);
let decoded = decipher.update(encryptedDataBuffer, 'binary', 'utf8');
decoded += decipher.final('utf8');
return JSON.parse(decoded);
}
这个函数可以解密出包含unionId、手机号等敏感信息的完整用户数据。但要注意,获取这些敏感信息需要用户授权,不能强制获取。
在高并发场景下,频繁的签名校验和加解密操作可能会成为性能瓶颈。经过多次优化,我总结出几个有效的优化方案:
缓存session_key的验证结果:可以设置一个短期缓存(如30秒),避免重复计算相同数据的签名。
预计算常用数据的签名:比如用户基本信息变化不频繁,可以在用户数据变更时预计算签名存入缓存。
使用更快的哈希算法:虽然微信要求使用sha1,但在内部校验环节可以使用更快的算法。
异步校验策略:对于非关键业务,可以采用先放行后校验的异步策略。
下面是一个带缓存的验证函数示例:
typescript复制const signatureCache = new Map<string, boolean>();
async function cachedVerify(
rawData: string,
signature: string,
sessionKey: string
): Promise<boolean> {
const cacheKey = `${rawData}:${sessionKey}`;
if (signatureCache.has(cacheKey)) {
return signatureCache.get(cacheKey)!;
}
const isValid = await verifySignature(rawData, signature, sessionKey);
signatureCache.set(cacheKey, isValid);
// 30秒后自动清除缓存
setTimeout(() => signatureCache.delete(cacheKey), 30000);
return isValid;
}
在实际开发中,经常会遇到各种签名校验失败的情况。根据我的经验,最常见的问题包括:
session_key不匹配:确保服务器存储的是最新的session_key,没有泄露给客户端。
数据编码问题:rawData必须是原始JSON字符串,不能经过任何转义或美化。
时间不同步:确保服务器时间与微信服务器时间同步,差距不能太大。
参数顺序错误:sha1计算时必须是rawData直接拼接session_key,不能调换顺序。
微信接口限制:注意频率限制,单个用户每分钟最多100次请求。
遇到问题时,可以按照这个检查清单逐步排查:
TypeScript的类型系统可以在编译期就发现很多潜在的错误。我们可以定义一套完整的类型来描述整个登录流程:
typescript复制interface ISessionInfo {
openid: string;
session_key: string;
unionid?: string;
}
interface IAuthResponse {
errcode: number;
errmsg: string;
data?: ISessionInfo;
}
interface IUserInfo {
nickName: string;
avatarUrl: string;
// 其他字段...
}
interface IEncryptedData {
openId: string;
nickName: string;
// 其他解密后的字段...
}
enum ErrorCode {
SYSTEM_BUSY = -1,
SUCCESS = 0,
INVALID_CODE = 40029,
// 其他错误码...
}
这些类型定义不仅能让代码更健壮,还能作为文档使用。配合VS Code的智能提示,可以大大减少低级错误的发生概率。