本指令指导如何实现微信服务号网页授权登录功能,包括后端API开发、前端组件集成和移动端适配。
在用户实体中添加微信相关字段:
// src/server/modules/users/user.entity.ts
@Column({ name: 'wechat_openid', type: 'varchar', length: 255, nullable: true, comment: '微信开放平台openid' })
wechatOpenid!: string | null;
@Column({ name: 'wechat_unionid', type: 'varchar', length: 255, nullable: true, comment: '微信开放平台unionid' })
wechatUnionid!: string | null;
@Column({ name: 'wechat_nickname', type: 'varchar', length: 255, nullable: true, comment: '微信昵称' })
wechatNickname!: string | null;
@Column({ name: 'wechat_avatar', type: 'varchar', length: 500, nullable: true, comment: '微信头像URL' })
wechatAvatar!: string | null;
@Column({ name: 'wechat_sex', type: 'tinyint', nullable: true, comment: '微信性别(1:男,2:女,0:未知)' })
wechatSex!: number | null;
@Column({ name: 'wechat_province', type: 'varchar', length: 100, nullable: true, comment: '微信省份' })
wechatProvince!: string | null;
@Column({ name: 'wechat_city', type: 'varchar', length: 100, nullable: true, comment: '微信城市' })
wechatCity!: string | null;
@Column({ name: 'wechat_country', type: 'varchar', length: 100, nullable: true, comment: '微信国家' })
wechatCountry!: string | null;
在用户Schema中添加微信字段定义:
// src/server/modules/users/user.schema.ts
wechatOpenid: z.string().max(255, '微信openid最多255个字符').nullable().openapi({
example: 'o6_bmjrPTlm6_2sgVt7hMZOPfL2M',
description: '微信开放平台openid'
}),
wechatUnionid: z.string().max(255, '微信unionid最多255个字符').nullable().openapi({
example: 'o6_bmasdasdsad6_2sgVt7hMZOPfL2M',
description: '微信开放平台unionid'
}),
wechatNickname: z.string().max(255, '微信昵称最多255个字符').nullable().openapi({
example: '微信用户',
description: '微信昵称'
}),
wechatAvatar: z.string().max(500, '微信头像URL最多500个字符').nullable().openapi({
example: 'http://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46',
description: '微信头像URL'
}),
wechatSex: z.number().int('微信性别必须是整数').min(0, '微信性别最小值为0').max(2, '微信性别最大值为2').nullable().openapi({
example: 1,
description: '微信性别(1:男,2:女,0:未知)'
}),
wechatProvince: z.string().max(100, '微信省份最多100个字符').nullable().openapi({
example: '广东省',
description: '微信省份'
}),
wechatCity: z.string().max(100, '微信城市最多100个字符').nullable().openapi({
example: '深圳市',
description: '微信城市'
}),
wechatCountry: z.string().max(100, '微信国家最多100个字符').nullable().openapi({
example: '中国',
description: '微信国家'
})
创建微信认证服务:
// src/server/modules/wechat/wechat-auth.service.ts
import axios from 'axios';
import { UserService } from '../users/user.service';
import { AuthService } from '../auth/auth.service';
export class WechatAuthService {
private readonly appId: string = process.env.WECHAT_MP_APP_ID || '';
private readonly appSecret: string = process.env.WECHAT_MP_APP_SECRET || '';
constructor(
private readonly userService: UserService,
private readonly authService: AuthService
) {}
// 获取授权URL
getAuthorizationUrl(redirectUri: string, scope: 'snsapi_base' | 'snsapi_userinfo' = 'snsapi_userinfo'): string {
const state = Math.random().toString(36).substring(2);
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
}
// 通过code获取access_token
async getAccessToken(code: string): Promise<any> {
const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.appId}&secret=${this.appSecret}&code=${code}&grant_type=authorization_code`;
const response = await axios.get(url);
return response.data;
}
// 获取用户信息
async getUserInfo(accessToken: string, openid: string): Promise<any> {
const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openid}&lang=zh_CN`;
const response = await axios.get(url);
return response.data;
}
// 微信登录/注册
async wechatLogin(code: string): Promise<{ token: string; user: any }> {
// 1. 获取access_token
const tokenData = await this.getAccessToken(code);
if (tokenData.errcode) {
throw new Error(`微信认证失败: ${tokenData.errmsg}`);
}
const { access_token, openid, unionid } = tokenData;
// 2. 检查用户是否存在
let user = await this.userService.findByWechatOpenid(openid);
if (!user) {
// 3. 获取用户信息(首次登录)
const userInfo = await this.getUserInfo(access_token, openid);
// 4. 创建新用户
user = await this.userService.createUser({
username: `wx_${openid.substring(0, 8)}`,
password: Math.random().toString(36).substring(2),
wechatOpenid: openid,
wechatUnionid: unionid,
wechatNickname: userInfo.nickname,
wechatAvatar: userInfo.headimgurl,
wechatSex: userInfo.sex,
wechatProvince: userInfo.province,
wechatCity: userInfo.city,
wechatCountry: userInfo.country,
nickname: userInfo.nickname
});
}
// 5. 生成JWT token
const token = this.authService.generateToken(user);
return { token, user };
}
}
创建微信认证API路由:
// src/server/api/auth/wechat/
// wechat-authorize.ts - 微信授权URL获取路由
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from '@hono/zod-openapi';
import { WechatAuthService } from '@/server/modules/wechat/wechat-auth.service';
import { ErrorSchema } from '@/server/utils/errorHandler';
import { AppDataSource } from '@/server/data-source';
import { UserService } from '@/server/modules/users/user.service';
import { AuthService } from '@/server/modules/auth/auth.service';
const AuthorizeSchema = z.object({
redirectUri: z.string().url().openapi({
example: 'https://example.com/auth/callback',
description: '回调地址'
}),
scope: z.enum(['snsapi_base', 'snsapi_userinfo']).default('snsapi_userinfo').openapi({
example: 'snsapi_userinfo',
description: '授权范围'
})
});
const AuthorizeResponseSchema = z.object({
url: z.string().url().openapi({
example: 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxx',
description: '微信授权URL'
})
});
const userService = new UserService(AppDataSource);
const authService = new AuthService(userService);
const wechatAuthService = new WechatAuthService(userService, authService);
const authorizeRoute = createRoute({
method: 'post',
path: '/authorize',
request: {
body: {
content: {
'application/json': {
schema: AuthorizeSchema
}
}
}
},
responses: {
200: {
description: '获取微信授权URL成功',
content: {
'application/json': {
schema: AuthorizeResponseSchema
}
}
},
400: {
description: '请求参数错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono().openapi(authorizeRoute, async (c) => {
try {
const { redirectUri, scope } = await c.req.json();
const url = wechatAuthService.getAuthorizationUrl(redirectUri, scope);
return c.json({ url }, 200);
} catch (error) {
return c.json({
code: 400,
message: error instanceof Error ? error.message : '获取授权URL失败'
}, 400);
}
});
export default app;
// src/server/api/auth/wechat/wechat-login.ts - 微信登录路由
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from '@hono/zod-openapi';
import { WechatAuthService } from '@/server/modules/wechat/wechat-auth.service';
import { ErrorSchema } from '@/server/utils/errorHandler';
import { AppDataSource } from '@/server/data-source';
import { UserService } from '@/server/modules/users/user.service';
import { AuthService } from '@/server/modules/auth/auth.service';
import { TokenResponseSchema } from '../login/password';
import { parseWithAwait } from '@/server/utils/parseWithAwait';
const WechatLoginSchema = z.object({
code: z.string().min(1).openapi({
example: '0816TxlL1Qe3QY0qgDlL1pQxlL16TxlO',
description: '微信授权code'
})
});
const userService = new UserService(AppDataSource);
const authService = new AuthService(userService);
const wechatAuthService = new WechatAuthService(userService, authService);
const wechatLoginRoute = createRoute({
method: 'post',
path: '/login',
request: {
body: {
content: {
'application/json': {
schema: WechatLoginSchema
}
}
}
},
responses: {
200: {
description: '微信登录成功',
content: {
'application/json': {
schema: TokenResponseSchema
}
}
},
400: {
description: '请求参数错误',
content: {
'application/json': {
schema: ErrorSchema
}
}
},
401: {
description: '微信认证失败',
content: {
'application/json': {
schema: ErrorSchema
}
}
}
}
});
const app = new OpenAPIHono().openapi(wechatLoginRoute, async (c) => {
try {
const { code } = await c.req.json();
const result = await wechatAuthService.wechatLogin(code);
// 使用 parseWithAwait 确保返回数据符合Schema定义
const validatedResult = await parseWithAwait(TokenResponseSchema, result);
return c.json(validatedResult, 200);
} catch (error) {
return c.json({
code: 401,
message: error instanceof Error ? error.message : '微信登录失败'
}, 401);
}
});
export default app;
// src/server/api/auth/wechat/index.ts - 路由聚合
import { OpenAPIHono } from '@hono/zod-openapi';
import authorizeRoute from './wechat-authorize';
import loginRoute from './wechat-login';
const app = new OpenAPIHono()
.route('/', authorizeRoute)
.route('/', loginRoute);
export default app;
parseWithAwait 是通用CRUD模块提供的数据验证工具,用于确保返回数据的类型安全,支持异步验证和转换。
所有涉及数据查询和返回的扩展路由都应使用 parseWithAwait 处理响应数据。
import { parseWithAwait } from '@/server/utils/parseWithAwait';
// 验证单个实体
const validatedEntity = await parseWithAwait(YourEntitySchema, entityData);
// 验证实体数组
const validatedEntities = await parseWithAwait(z.array(YourEntitySchema), entitiesData);
// 在微信登录路由中使用 parseWithAwait
const app = new OpenAPIHono().openapi(wechatLoginRoute, async (c) => {
try {
const { code } = await c.req.json();
const result = await wechatAuthService.wechatLogin(code);
// 使用 parseWithAwait 确保返回数据符合Schema定义
const validatedResult = await parseWithAwait(TokenResponseSchema, result);
return c.json(validatedResult, 200);
} catch (error) {
return c.json({
code: 401,
message: error instanceof Error ? error.message : '微信登录失败'
}, 401);
}
});
parseWithAwaitz.array(EntitySchema) 格式验证数组在API入口文件中注册微信路由:
// src/server/api.ts
import wechatRoutes from '@/server/api/auth/wechat/index';
// 注册路由
api.route('/api/v1/auth/wechat', wechatRoutes);
创建React微信登录组件:
// src/client/components/WechatLoginButton.tsx
import React from 'react';
import { Button } from '@/client/components/ui/button';
import { Wechat } from 'lucide-react';
import { authClient } from '@/client/api';
interface WechatLoginButtonProps {
redirectUri: string;
scope?: 'snsapi_base' | 'snsapi_userinfo';
onSuccess?: (data: any) => void;
onError?: (error: Error) => void;
}
const WechatLoginButton: React.FC<WechatLoginButtonProps> = ({
redirectUri,
scope = 'snsapi_userinfo',
onSuccess,
onError,
children = '微信登录'
}) => {
const handleWechatLogin = async () => {
try {
// 使用RPC客户端获取微信授权URL
const response = await authClient.wechat.authorize.$post({
json: { redirectUri, scope }
});
if (response.status !== 200) {
throw new Error('获取微信授权URL失败');
}
const { url } = await response.json();
// 重定向到微信授权页面
window.location.href = url;
} catch (error) {
onError?.(error as Error);
}
};
return (
<Button
variant="default"
onClick={handleWechatLogin}
className="bg-[#07C160] text-white hover:bg-[#06B456] border-none"
>
<Wechat className="w-4 h-4 mr-2" />
{children}
</Button>
);
};
export default WechatLoginButton;
更新移动端认证页面:
// src/client/mobile/pages/AuthPage.tsx - 添加微信登录选项
import WechatLoginButton from '@/client/components/WechatLoginButton';
import { toast } from 'sonner'
// 在表单后添加微信登录按钮
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">或使用以下方式登录</span>
</div>
</div>
<div className="mt-4">
<WechatLoginButton
redirectUri={`${window.location.origin}/mobile/auth/callback`}
onSuccess={(data) => {
localStorage.setItem('token', data.token);
navigate(getReturnUrl(), { replace: true });
}}
onError={(error) => {
toast(error.message);
}}
>
微信一键登录
</WechatLoginButton>
</div>
</div>
在.env文件中添加微信配置:
# 微信服务号配置(公众号)
WECHAT_MP_APP_ID=your_wechat_mp_app_id
WECHAT_MP_APP_SECRET=your_wechat_mp_app_secret
# 微信开放平台配置(可选,用于多端统一)
WECHAT_OPEN_APP_ID=your_wechat_open_app_id
WECHAT_OPEN_APP_SECRET=your_wechat_open_app_secret
redirect_uri参数错误
invalid code
access_token过期
API调用频率限制