This commit is contained in:
Neo
2026-03-15 10:15:02 +08:00
parent 2dd217522c
commit 72bb11b34f
916 changed files with 65306 additions and 16102803 deletions

View File

@@ -0,0 +1,362 @@
# 登录与鉴权
> 官方文档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<string, string>
}
function request(options: RequestOptions): Promise<any> {
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
<!-- 基础库 2.21.2+ 推荐用法 -->
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">
授权手机号
</button>
```
```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
<!-- 头像选择 -->
<button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image src="{{avatarUrl}}" class="avatar"/>
</button>
<!-- 昵称填写type="nickname" 自动弹出微信昵称) -->
<input type="nickname" placeholder="请输入昵称" bindchange="onNicknameChange"/>
```
```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: 使用头像昵称填写能力(`<button open-type="chooseAvatar">` + `<input type="nickname">`),让用户主动填写。
## 在线查询
- 登录流程https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
- 手机号快速验证https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
- 用户信息https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html
- 授权https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html