Files
Neo-ZQYY/_DEL/wechat-miniprogram/steering/login-auth.md
2026-03-15 10:15:02 +08:00

363 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 登录与鉴权
> 官方文档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