add login
This commit is contained in:
157
functions/api/login.ts
Normal file
157
functions/api/login.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
interface Env {
|
||||
bgkv: KVNamespace;
|
||||
JWT_SECRET: string;
|
||||
}
|
||||
|
||||
export const onRequestPost: PagesFunction<Env> = async (context) => {
|
||||
try {
|
||||
const { request, env } = context;
|
||||
|
||||
// 获取请求体
|
||||
const body = await request.json();
|
||||
const { phone, code } = body;
|
||||
|
||||
// 验证手机号格式
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: '无效的手机号码'
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 验证码格式检查
|
||||
if (!code || !/^\d{6}$/.test(code)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: '验证码格式错误'
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 从 KV 中获取存储的验证码
|
||||
const storedCode = await env.bgkv.get(`sms:${phone}`);
|
||||
|
||||
if (!storedCode || storedCode !== code) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: '验证码错误或已过期'
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 验证成功,生成 JWT token,传入 env
|
||||
const token = await generateToken(phone, env);
|
||||
|
||||
// 删除验证码
|
||||
await env.bgkv.delete(`sms:${phone}`);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
token,
|
||||
phone
|
||||
}
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: '服务器错误'
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改为 async 函数
|
||||
async function generateToken(phone: string, env: Env): Promise<string> {
|
||||
const header = {
|
||||
alg: 'HS256',
|
||||
typ: 'JWT'
|
||||
};
|
||||
|
||||
const payload = {
|
||||
phone,
|
||||
exp: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60), // 7天过期
|
||||
iat: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
// Base64Url 编码
|
||||
const encodeBase64Url = (data: object) => {
|
||||
return btoa(JSON.stringify(data))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
};
|
||||
|
||||
// 生成签名
|
||||
const generateSignature = async (input: string, secret: string) => {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
'HMAC',
|
||||
key,
|
||||
encoder.encode(input)
|
||||
);
|
||||
|
||||
return btoa(String.fromCharCode(...new Uint8Array(signature)))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
};
|
||||
|
||||
const headerEncoded = encodeBase64Url(header);
|
||||
const payloadEncoded = encodeBase64Url(payload);
|
||||
|
||||
const signature = await generateSignature(
|
||||
`${headerEncoded}.${payloadEncoded}`,
|
||||
env.JWT_SECRET
|
||||
);
|
||||
|
||||
return `${headerEncoded}.${payloadEncoded}.${signature}`;
|
||||
}
|
||||
72
functions/api/sendCode.ts
Normal file
72
functions/api/sendCode.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
interface Env {
|
||||
bgkv: KVNamespace;
|
||||
}
|
||||
|
||||
export const onRequestPost: PagesFunction<Env> = async (context) => {
|
||||
try {
|
||||
const { request, env } = context;
|
||||
|
||||
// 获取请求体
|
||||
const body = await request.json();
|
||||
const { phone } = body;
|
||||
|
||||
// 验证手机号格式
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: '无效的手机号码'
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 生成6位随机验证码
|
||||
let verificationCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
// 使用 CF_PAGES_ENVIRONMENT 判断环境
|
||||
// 值为 'production' 或 'preview'
|
||||
if (env.CF_PAGES_ENVIRONMENT !== 'production') {
|
||||
verificationCode = '123456';
|
||||
}
|
||||
// 将验证码存储到 KV 中,设置5分钟过期
|
||||
await env.bgkv.put(`sms:${phone}`, verificationCode, {
|
||||
expirationTtl: 300 // 5分钟过期
|
||||
});
|
||||
console.log(env.CF_PAGES_ENVIRONMENT, await env.bgkv.get(`sms:${phone}`));
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: '验证码发送成功',
|
||||
// 注意:实际生产环境不应该返回验证码
|
||||
code: verificationCode // 仅用于测试
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: '服务器错误'
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
60
functions/middleware/auth.ts
Normal file
60
functions/middleware/auth.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
interface Env {
|
||||
JWT_SECRET: string;
|
||||
}
|
||||
|
||||
export async function verifyToken(token: string, env: Env) {
|
||||
try {
|
||||
const [headerB64, payloadB64, signature] = token.split('.');
|
||||
|
||||
// 验证签名
|
||||
const input = `${headerB64}.${payloadB64}`;
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(env.JWT_SECRET),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
|
||||
const signatureBytes = Uint8Array.from(atob(
|
||||
signature.replace(/-/g, '+').replace(/_/g, '/')
|
||||
), c => c.charCodeAt(0));
|
||||
|
||||
const isValid = await crypto.subtle.verify(
|
||||
'HMAC',
|
||||
key,
|
||||
signatureBytes,
|
||||
encoder.encode(input)
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid signature');
|
||||
}
|
||||
|
||||
// 解码 payload
|
||||
const payload = JSON.parse(atob(
|
||||
payloadB64.replace(/-/g, '+').replace(/_/g, '/')
|
||||
));
|
||||
|
||||
// 检查过期时间
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
// 中间件函数
|
||||
export const authMiddleware = async (request: Request, env: Env) => {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('No token provided');
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
return await verifyToken(token, env);
|
||||
};
|
||||
Reference in New Issue
Block a user