配套后端:
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}。两条流程并存。
① 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 并按参数分流。
App 端要能被 com.twanmsdyh.app://... 拉起,需在 manifest.json 注册该 URL Scheme(HBuilderX:App 模块配置 → App 常用其它设置 / 各平台):
{
"app-plus": {
"distribute": {
"android": { "schemes": "com.twanmsdyh.app" },
"ios": { "urltypes": "com.twanmsdyh.app" }
}
}
}
iOS 的
urltypes填的是 scheme 名(不含://)。注册后系统遇到com.twanmsdyh.app://...就会拉起你的 App 并把完整 URL 传进来。
前端用系统/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,但建议传,后续会加)。uniapp 在 App.vue 里读取拉起 App 的 URL:
// 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)看真实值再解析。
后端 302 回的 URL 形如 com.twanmsdyh.app://oauthLogin?<参数>,参数只有下列三种组合之一:
| 情况 | 参数 | 含义 | 前端动作 |
|---|---|---|---|
| ✅ 已绑定、登录成功 | ?token=<JWT> |
该 LINE 已绑过手机号账号,已签发 token | 存 token → 跳首页 |
| 🟡 未绑定、需绑手机 | ?needPhone=1&tempKey=<32位> |
首次用该 LINE 登录,要绑手机号 | 跳「绑手机」页 → 收手机号+短信码 → 调 /oauthBindPhone |
| ❌ 失败 | ?error=<原因> |
换 token 失败 / 账号停用 / 缺 code 等 | 提示并回登录页 |
token —— 登录成功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 同款,直接复用现有请求拦截器。needPhone + tempKey —— 需要绑手机if (params.needPhone === '1' && params.tempKey) {
// tempKey 5 分钟有效,跳到绑手机页把它带上
uni.navigateTo({ url: '/pages/login/bindPhone?tempKey=' + params.tempKey })
return
}
绑手机页:输入手机号 → 调 GET /infouser/user/getcode?phone=<手机号> 发短信 → 填验证码 → 调下面接口完成绑定+登录:
error —— 失败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 解开)。POST /infouser/user/oauthBindPhoneneedPhone 分支收集完手机号 + 短信码后调用。
请求体(JSON):
{
"tempKey": "5.2 拿到的 tempKey",
"phone": "+8869xxxxxxxx",
"code": "短信验证码(万能码 8888 可用,仅测试)",
"cid": "推送 cid(个推/uni 推送)",
"cidType": "推送类型,与现有登录一致(本项目一般传 \"\")",
"deviceToken": "APNs deviceToken(iOS)",
"voIPToken": "VoIP push token(iOS)"
}
响应:
成功(已注册关联 / 未注册新建 + 写绑定):
{
"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分支,不再要绑手机。
redirect_uri、后端 application.yml 的 oauth.line.redirect-uri、LINE Console 回调白名单,三者必须完全相同(当前 = https://api.awayqtw.com/auth/line/callback),否则 LINE 回 400 redirect_uri_mismatch。/oauthBindPhone 回 tempkey.expired,需重新点 LINE 登录拿新的。com.twanmsdyh.app://... 拉起 App。若联调发现 LINE 内置浏览器不跟随 302(App 没被拉起),后端可改为返回 HTML(meta refresh + 手动「打开 App」链接)兜底 —— 这是待联调确认项,目前是 302。cid / deviceToken / voIPToken 在回调时更新不到;绑手机走 /oauthBindPhone 时会带上,已绑定的老用户建议 5.1 拿到 token 后补报一次(或复用既有设备上报逻辑)。state,前端仍建议生成并传,后续后端会加 CSRF 校验。/oauthLogin 并存:如果某端(如 H5)前端能自己拿到 code,直接 POST /infouser/user/oauthLogin {provider:"line", credential:code, ...},不必走本回调;返回值结构与本回调的 token / needPhone+tempKey 对应一致。