# 登录与鉴权 > 官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html ## 登录流程概览 ``` ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 小程序 │ │ 开发者服务器 │ │ 微信服务器 │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ 1. wx.login() │ │ │ ──────────────>│ │ │ 返回 code │ │ │ │ │ │ 2. wx.request │ │ │ 发送 code │ │ │ ──────────────>│ │ │ │ 3. code2Session│ │ │ ──────────────>│ │ │ 返回 openid + │ │ │ session_key │ │ │ <──────────────│ │ │ │ │ │ 4. 生成自定义 │ │ │ 登录态 token │ │ │ │ │ 5. 返回 token │ │ │ <──────────────│ │ │ │ │ │ 6. 后续请求 │ │ │ 携带 token │ │ │ ──────────────>│ │ │ │ 7. 校验 token │ │ │ 查 openid │ ``` ## 前端登录实现 ### 基本登录 ```javascript // app.js 或 utils/auth.js async function login() { // 1. 调用 wx.login 获取 code const { code } = await wx.login() // 2. 发送 code 到后端换取 token const res = await wx.request({ url: 'https://your-server.com/api/auth/login', method: 'POST', data: { code } }) if (res.data.token) { // 3. 存储 token wx.setStorageSync('token', res.data.token) return res.data } else { throw new Error('登录失败') } } ``` ### 检查登录态 ```javascript // 检查 session_key 是否过期 function checkSession() { return new Promise((resolve, reject) => { wx.checkSession({ success: () => resolve(true), // session_key 未过期 fail: () => resolve(false) // session_key 已过期,需重新 login }) }) } // 启动时检查 async function checkAndLogin() { const token = wx.getStorageSync('token') if (!token) { return await login() } const isSessionValid = await checkSession() if (!isSessionValid) { // session_key 过期,重新登录 return await login() } return { token } } ``` ### 封装请求(自动携带 token) ```typescript // utils/request.ts interface RequestOptions { url: string method?: 'GET' | 'POST' | 'PUT' | 'DELETE' data?: any header?: Record } function request(options: RequestOptions): Promise { const token = wx.getStorageSync('token') return new Promise((resolve, reject) => { wx.request({ ...options, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '', ...options.header }, success(res) { if (res.statusCode === 401) { // token 过期,重新登录 wx.removeStorageSync('token') login().then(() => { // 重试原请求 request(options).then(resolve).catch(reject) }) return } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data) } else { reject(res) } }, fail: reject }) }) } ``` ## 后端登录实现(Python / FastAPI 示例) ```python import httpx from fastapi import APIRouter, HTTPException, Depends from datetime import datetime, timedelta import jwt router = APIRouter() APPID = "your_appid" SECRET = "your_secret" JWT_SECRET = "your_jwt_secret" @router.post("/auth/login") async def login(code: str): # 1. 用 code 换取 openid + session_key url = "https://api.weixin.qq.com/sns/jscode2session" params = { "appid": APPID, "secret": SECRET, "js_code": code, "grant_type": "authorization_code" } async with httpx.AsyncClient() as client: resp = await client.get(url, params=params) data = resp.json() if "errcode" in data and data["errcode"] != 0: raise HTTPException(400, f"微信登录失败: {data.get('errmsg')}") openid = data["openid"] session_key = data["session_key"] unionid = data.get("unionid") # 2. 查找或创建用户 user = await find_or_create_user(openid, unionid) # 3. 生成 JWT token token = jwt.encode({ "sub": str(user.id), "openid": openid, "exp": datetime.utcnow() + timedelta(days=7) }, JWT_SECRET, algorithm="HS256") # 4. 缓存 session_key(用于后续解密) await cache_session_key(openid, session_key) return {"token": token, "user": user.to_dict()} ``` ## 获取手机号 ### 前端 ```xml ``` ```javascript Page({ getPhoneNumber(e) { if (e.detail.errMsg === 'getPhoneNumber:ok') { const code = e.detail.code // 动态令牌(新版) // 发送 code 到后端 wx.request({ url: 'https://your-server.com/api/auth/phone', method: 'POST', data: { code }, success(res) { console.log('手机号:', res.data.phoneNumber) } }) } else { console.log('用户拒绝授权') } } }) ``` ### 后端 ```python @router.post("/auth/phone") async def get_phone(code: str, token: str = Depends(get_current_token)): access_token = await get_access_token() url = f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}" async with httpx.AsyncClient() as client: resp = await client.post(url, json={"code": code}) data = resp.json() if data.get("errcode") != 0: raise HTTPException(400, f"获取手机号失败: {data.get('errmsg')}") phone_info = data["phone_info"] phone_number = phone_info["phoneNumber"] # 更新用户手机号 await update_user_phone(token.openid, phone_number) return {"phoneNumber": phone_number} ``` ## 用户信息获取(当前方案) `wx.getUserProfile` 已于基础库 2.27.1 废弃。当前获取用户头像和昵称的方式: ### 头像昵称填写能力 ```xml ``` ```javascript Page({ data: { avatarUrl: '/images/default-avatar.png', nickname: '' }, onChooseAvatar(e) { const { avatarUrl } = e.detail // avatarUrl 是临时路径,需上传到自己服务器 this.setData({ avatarUrl }) this.uploadAvatar(avatarUrl) }, onNicknameChange(e) { this.setData({ nickname: e.detail.value }) }, async uploadAvatar(tempPath) { wx.uploadFile({ url: 'https://your-server.com/api/upload/avatar', filePath: tempPath, name: 'file', success(res) { const data = JSON.parse(res.data) // 保存头像 URL } }) } }) ``` ## 授权管理 ```javascript // 查看当前授权状态 wx.getSetting({ success(res) { res.authSetting['scope.userLocation'] // true/false/undefined res.authSetting['scope.writePhotosAlbum'] // undefined = 未请求过,true = 已授权,false = 已拒绝 } }) // 提前请求授权 wx.authorize({ scope: 'scope.userLocation', success() { /* 授权成功 */ }, fail() { /* 用户拒绝 */ } }) // 打开设置页(用户之前拒绝后,引导重新授权) wx.openSetting({ success(res) { res.authSetting // 最新的授权状态 } }) ``` ### 常用 scope | scope | 说明 | 对应 API | |-------|------|----------| | scope.userLocation | 精确地理位置 | wx.getLocation | | scope.userFuzzyLocation | 模糊地理位置 | wx.getFuzzyLocation | | scope.record | 麦克风 | wx.startRecord | | scope.camera | 摄像头 | camera 组件 | | scope.writePhotosAlbum | 保存到相册 | wx.saveImageToPhotosAlbum | | scope.bluetooth | 蓝牙 | wx.openBluetoothAdapter | | scope.addPhoneContact | 添加到联系人 | wx.addPhoneContact | | scope.addPhoneCalendar | 添加到日历 | wx.addPhoneCalendar | | scope.werun | 微信运动步数 | wx.getWeRunData | | scope.userInfo | 已废弃 | - | ## 安全注意事项 1. **session_key 绝不能下发到前端** 2. **code 只能使用一次**,且有效期很短(约 5 分钟) 3. **不要在前端存储 openid**,通过 token 在后端关联 4. **JWT token 设置合理过期时间**(建议 7 天,配合 refresh token) 5. **HTTPS 是强制要求**,所有网络请求必须 HTTPS 6. **敏感数据解密**要在服务端进行,不要在前端解密 7. **access_token 要在服务端缓存**,不要每次请求都重新获取 8. **unionid 需要绑定开放平台**才能获取,用于跨应用用户关联 ## 常见问题 ### Q: wx.login 的 code 可以多次使用吗? A: 不可以,每个 code 只能使用一次,且有效期约 5 分钟。 ### Q: session_key 什么时候会过期? A: 微信不会通知过期时间,需要通过 `wx.checkSession` 检查。用户越频繁使用小程序,session_key 有效期越长。 ### Q: 如何获取 unionid? A: 需要在微信开放平台绑定小程序。绑定后,code2Session 会返回 unionid。 ### Q: getUserProfile 废弃后怎么获取用户信息? A: 使用头像昵称填写能力(`