line-callback-frontend.md 8.5 KB

LINE 登录回调 — 前端(uniapp)接入说明

配套后端:LineCallbackControllerGET /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 常用其它设置 / 各平台):

{
  "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:

// 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 —— 登录成功

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 —— 需要绑手机

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 —— 失败

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):

{
  "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 分支,不再要绑手机。


7. 注意事项

  1. redirect_uri 三方一致铁律:前端 authorize 里的 redirect_uri、后端 application.ymloauth.line.redirect-uri、LINE Console 回调白名单,三者必须完全相同(当前 = https://api.awayqtw.com/auth/line/callback),否则 LINE 回 400 redirect_uri_mismatch
  2. tempKey 有效期 5 分钟:超时后 /oauthBindPhonetempkey.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 对应一致。