|
|
@@ -0,0 +1,206 @@
|
|
|
+# LINE 登录回调 — 前端(uniapp)接入说明
|
|
|
+
|
|
|
+> 配套后端:`LineCallbackController` → `GET /auth/line/callback`(2026-08-05 增量,017-oauth-login FR-009)。
|
|
|
+> 适用场景:**「唤起 LINE App / 系统浏览器」** 授权 —— 这种方式前端拿不到 code,由后端接住 LINE 的重定向、完成登录后,再 302 跳回 App。
|
|
|
+>
|
|
|
+> 若你的场景是「H5 / 自家 webview / LINE SDK」(前端能自己拿到 code),**不走本回调**,直接调 `POST /infouser/user/oauthLogin {provider:"line", credential:code}`。两条流程并存。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 整体流程
|
|
|
+
|
|
|
+```
|
|
|
+① uniapp 打开 LINE 授权页(用 redirect_uri = https://api.awayqtw.com/auth/line/callback)
|
|
|
+② 用户在 LINE App 里一键同意
|
|
|
+③ LINE 跳转:GET https://api.awayqtw.com/auth/line/callback?code=xxx&state=xxx
|
|
|
+④ 后端:code → 换 token → 取 userId → 查绑定 → 签 JWT 或 生成 tempKey
|
|
|
+⑤ 后端 302 跳回 App scheme:
|
|
|
+ com.twanmsdyh.app://oauthLogin?<参数>
|
|
|
+⑥ uniapp 被 scheme 拉起,读参数,按下面三种情况处理
|
|
|
+```
|
|
|
+
|
|
|
+**关键:** 前端不碰 code,只接收后端 302 回来的 scheme URL 并按参数分流。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. scheme 注册(manifest.json)
|
|
|
+
|
|
|
+App 端要能被 `com.twanmsdyh.app://...` 拉起,需在 `manifest.json` 注册该 URL Scheme(HBuilderX:App 模块配置 → App 常用其它设置 / 各平台):
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "app-plus": {
|
|
|
+ "distribute": {
|
|
|
+ "android": { "schemes": "com.twanmsdyh.app" },
|
|
|
+ "ios": { "urltypes": "com.twanmsdyh.app" }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> iOS 的 `urltypes` 填的是 scheme 名(不含 `://`)。注册后系统遇到 `com.twanmsdyh.app://...` 就会拉起你的 App 并把完整 URL 传进来。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 打开 LINE 授权页
|
|
|
+
|
|
|
+前端用系统/LINE 打开如下 URL(`redirect_uri` **必须**与 LINE Console 白名单、后端 `application.yml` 三方一致):
|
|
|
+
|
|
|
+```
|
|
|
+https://access.line.me/oauth2/v2.1/authorize
|
|
|
+ ?response_type=code
|
|
|
+ &client_id=2010911071
|
|
|
+ &redirect_uri=https%3A%2F%2Fapi.awayqtw.com%2Fauth%2Fline%2Fcallback
|
|
|
+ &scope=profile%20openid
|
|
|
+ &state=<前端生成的随机串>
|
|
|
+```
|
|
|
+
|
|
|
+打开方式(uniapp App 端):`plus.runtime.openURL(authorizeUrl)` 或 `plus.share.launchLaunch` / 系统 webview,交由 LINE App / 系统浏览器接管。
|
|
|
+
|
|
|
+- `scope=profile openid`:后端 `v2/profile` 取 userId 要 `profile` 权限,**不能少**。
|
|
|
+- `state`:前端随机生成,用于防 CSRF(后端当前**未强校验** state,但建议传,后续会加)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 接收 302 回跳的 scheme URL
|
|
|
+
|
|
|
+uniapp 在 `App.vue` 里读取拉起 App 的 URL:
|
|
|
+
|
|
|
+```js
|
|
|
+// App.vue
|
|
|
+export default {
|
|
|
+ onLaunch() {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ // 冷启动:URL 落在 plus.runtime.arguments
|
|
|
+ this.handleLineCallback(plus.runtime.arguments)
|
|
|
+ // 热启动(App 已开,LINE 回跳再拉起):监听 newargs
|
|
|
+ plus.runtime.addEventListener('newargs', (e) => {
|
|
|
+ this.handleLineCallback(e.args)
|
|
|
+ })
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handleLineCallback(url) {
|
|
|
+ if (!url || !url.startsWith('com.twanmsdyh.app://oauthLogin')) return
|
|
|
+ const queryStr = url.split('?')[1] || ''
|
|
|
+ const params = this.parseQuery(queryStr)
|
|
|
+ routeByLineParams(params)
|
|
|
+ },
|
|
|
+ parseQuery(q) {
|
|
|
+ const o = {}
|
|
|
+ q.split('&').forEach(kv => {
|
|
|
+ const [k, v] = kv.split('=')
|
|
|
+ if (k) o[k] = decodeURIComponent(v || '')
|
|
|
+ })
|
|
|
+ return o
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> `plus.runtime.arguments` 的具体形态随 uniapp 版本略有差异(可能是完整 URL,也可能只是 query 段),联调时打印一次确认即可。建议先 `console.log('LINE cb url=', url)` 看真实值再解析。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 三种返回值处理(核心)
|
|
|
+
|
|
|
+后端 302 回的 URL 形如 `com.twanmsdyh.app://oauthLogin?<参数>`,参数只有下列三种组合之一:
|
|
|
+
|
|
|
+| 情况 | 参数 | 含义 | 前端动作 |
|
|
|
+|------|------|------|----------|
|
|
|
+| ✅ 已绑定、登录成功 | `?token=<JWT>` | 该 LINE 已绑过手机号账号,已签发 token | 存 token → 跳首页 |
|
|
|
+| 🟡 未绑定、需绑手机 | `?needPhone=1&tempKey=<32位>` | 首次用该 LINE 登录,要绑手机号 | 跳「绑手机」页 → 收手机号+短信码 → 调 `/oauthBindPhone` |
|
|
|
+| ❌ 失败 | `?error=<原因>` | 换 token 失败 / 账号停用 / 缺 code 等 | 提示并回登录页 |
|
|
|
+
|
|
|
+### 5.1 `token` —— 登录成功
|
|
|
+
|
|
|
+```js
|
|
|
+function routeByLineParams(params) {
|
|
|
+ if (params.token) {
|
|
|
+ // 与现有手机号登录一致:token 存本地
|
|
|
+ uni.setStorageSync('token', params.token)
|
|
|
+ // 也可顺手把 cid/deviceToken 上报一次(后端回调时没有设备信息)
|
|
|
+ reportDeviceInfo()
|
|
|
+ uni.reLaunch({ url: '/pages/index/index' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- `token` 是后端签的 JWT(`Authorization` / `token` header 带它即可),与 `/lodeing` 返回的 token 同款,**直接复用现有请求拦截器**。
|
|
|
+- 后端回调发生在服务端,**拿不到 cid/deviceToken**,故设备信息要么这里补报、要么等首次业务请求时按原流程更新。
|
|
|
+
|
|
|
+### 5.2 `needPhone` + `tempKey` —— 需要绑手机
|
|
|
+
|
|
|
+```js
|
|
|
+if (params.needPhone === '1' && params.tempKey) {
|
|
|
+ // tempKey 5 分钟有效,跳到绑手机页把它带上
|
|
|
+ uni.navigateTo({ url: '/pages/login/bindPhone?tempKey=' + params.tempKey })
|
|
|
+ return
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+绑手机页:输入手机号 → 调 `GET /infouser/user/getcode?phone=<手机号>` 发短信 → 填验证码 → 调下面接口完成绑定+登录:
|
|
|
+
|
|
|
+### 5.3 `error` —— 失败
|
|
|
+
|
|
|
+```js
|
|
|
+if (params.error) {
|
|
|
+ const map = {
|
|
|
+ no_code: '回调缺少授权码,请重试',
|
|
|
+ user_stopped: '该账号已被停用',
|
|
|
+ fail: 'LINE 登录失败,请重试'
|
|
|
+ }
|
|
|
+ uni.showModal({ title: '登录失败', content: map[params.error] || decodeURIComponent(params.error), showCancel: false })
|
|
|
+ uni.reLaunch({ url: '/pages/login/login' })
|
|
|
+ return
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- `error` 可能是固定码(`no_code` / `user_stopped`),也可能是后端异常文案(已 URL 编码,`decodeURIComponent` 解开)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 绑定接口契约:`POST /infouser/user/oauthBindPhone`
|
|
|
+
|
|
|
+`needPhone` 分支收集完手机号 + 短信码后调用。
|
|
|
+
|
|
|
+**请求体(JSON):**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "tempKey": "5.2 拿到的 tempKey",
|
|
|
+ "phone": "+8869xxxxxxxx",
|
|
|
+ "code": "短信验证码(万能码 8888 可用,仅测试)",
|
|
|
+ "cid": "推送 cid(个推/uni 推送)",
|
|
|
+ "cidType": "推送类型,与现有登录一致(本项目一般传 \"\")",
|
|
|
+ "deviceToken": "APNs deviceToken(iOS)",
|
|
|
+ "voIPToken": "VoIP push token(iOS)"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**响应:**
|
|
|
+- 成功(已注册关联 / 未注册新建 + 写绑定):
|
|
|
+ ```json
|
|
|
+ {
|
|
|
+ "code": 200,
|
|
|
+ "msg": "登录成功",
|
|
|
+ "data": { ...用户信息... },
|
|
|
+ "token": "<JWT>"
|
|
|
+ }
|
|
|
+ ```
|
|
|
+ → 存 `token` → 跳首页(同 5.1)。
|
|
|
+- 失败:`code != 200`,常见 `msg`:短信码错误(`no.user.jcaptcha.error`)、账号停用(`no.user.stop`)、tempKey 过期(`no.oauth.tempkey.expired`,需重新走一遍 LINE 登录拿新 tempKey)。
|
|
|
+
|
|
|
+> 绑定成功后 `info_user_oauth(user_id, provider="line", provider_uid=<LINE userId>)` 已写入;**下次同一 LINE 登录就走 5.1 的 `token` 分支**,不再要绑手机。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 注意事项
|
|
|
+
|
|
|
+1. **redirect_uri 三方一致铁律**:前端 authorize 里的 `redirect_uri`、后端 `application.yml` 的 `oauth.line.redirect-uri`、LINE Console 回调白名单,三者必须**完全相同**(当前 = `https://api.awayqtw.com/auth/line/callback`),否则 LINE 回 `400 redirect_uri_mismatch`。
|
|
|
+2. **tempKey 有效期 5 分钟**:超时后 `/oauthBindPhone` 回 `tempkey.expired`,需重新点 LINE 登录拿新的。
|
|
|
+3. **302 到自定义 scheme 的兼容性**:标准系统浏览器会跟随 302 到 `com.twanmsdyh.app://...` 拉起 App。若联调发现 **LINE 内置浏览器**不跟随 302(App 没被拉起),后端可改为返回 HTML(`meta refresh` + 手动「打开 App」链接)兜底 —— 这是待联调确认项,目前是 302。
|
|
|
+4. **设备信息**:后端回调链路没有 App 上下文,`cid` / `deviceToken` / `voIPToken` 在回调时更新不到;绑手机走 `/oauthBindPhone` 时会带上,已绑定的老用户建议 5.1 拿到 token 后补报一次(或复用既有设备上报逻辑)。
|
|
|
+5. **state 暂未校验**:后端目前不验证 `state`,前端仍建议生成并传,后续后端会加 CSRF 校验。
|
|
|
+6. **与 `/oauthLogin` 并存**:如果某端(如 H5)前端能自己拿到 code,直接 `POST /infouser/user/oauthLogin {provider:"line", credential:code, ...}`,不必走本回调;返回值结构与本回调的 `token` / `needPhone+tempKey` 对应一致。
|