1
This commit is contained in:
362
_DEL/wechat-miniprogram/steering/login-auth.md
Normal file
362
_DEL/wechat-miniprogram/steering/login-auth.md
Normal 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
|
||||
Reference in New Issue
Block a user