微信扫码授权登录流程:
- 用户在显示二维码的页面用手机扫码授权
 - 页面跳转到指定地址,URL上带有参数code
 - 前端通过code向服务端请求用于权限认证的token
 - 前端后续请求在请求头带上token作为身份标识
 
需要解决的问题
按照上述的流程,前端最简单的做法就是扫完码重新跳转回系统后,从URL获取code进行后续的操作。
然而因为公司的实际情况,整个扫码登录的流程并没有这么简单。
扫码后只能跳转到固定域名
首先,微信扫码后跳转的域名是固定的,但是一般公司的项目在不同的开发(或生产)环境的域名是不一样的,这样就造成用户扫完码以后无法跳转到正确的系统地址。
解决这个问题的思路就是搭建一个中转平台,当扫完码跳转到中转平台后,中转平台在系统对应的开发(或生产)环境地址加上code参数,然后进行跳转,系统在URL里拿到code以后即可进行后续操作。
内嵌二维码
其次,公司领导要求用来登录的二维码需要内嵌到系统自身的登录页中,而不是单独打开个页面显示二维码。
解决这个问题也很简单,我们可以通过iframe将二维码页面内嵌到系统的登录页面,用户扫码后在iframe内部跳转到中转平台,中转平台通过主动推送的方式(比如WebSocket、PostMessage这些,不仅能主动推送而且能跨域主动推送。)向iframe外层的系统推送code,系统接收到code以后进行后续操作。
懒得把中转平台搞成服务
不过问题又来了。主动推送是指服务端能向客户端主动推送信息,而无需等客户端请求以后才能响应信息。在上述的登录流程中,中转平台就相当于服务端,向系统主动推送code。但是我们公司目前并没有部署node项目的经验,而且部署后需要花费精力对中转平台进行维护。
那么还有什么简单的方法可以让中转平台和系统之间进行通信呢?LocalStorage(或者SessionStorage)可以做到。我们可以在中转平台通过LocalStorage在浏览器缓存中存储code,跳转回系统以后,系统从浏览器缓存中获取code。
不过LocalStorage的不足之处在于无法做到跨域通信,不同域之间的缓存是不能共享的。但是好在我们公司的所有项目在某个开发(或生产)环境内都是部署在同一域名下,只是不同的项目拥有不同的路径,这样相同域名下的中转平台和系统是可以共享缓存的。同时,中转平台就可以单纯是一个静态页面,而无需搞成node服务。
完整的登录流程
综上,整个具体的登录流程就是这样:

- 通过iframe将二维码页面内嵌到系统的登录页面中
 - 用户扫码以后,在iframe内跳转到生产环境的中转平台,跳转的地址内携带了当前系统所在开发(或生产)环境的域名参数以及code参数。
 - 生产环境的中转平台从URL中获取域名参数,并携带code参数跳转到对应域名的中转平台。
 - 对应域名的中转平台将code用LocalStorage进行缓存,并跳转到对应域名的系统。
 - 系统从缓存中获取code,并用code向后端请求后续用于权限认证的token。
 
代码实现
系统erp:
const BeforeScan: FC = () => {
  useEffect(() => {
    const nextUrl = `${window.location.protocol}//${window.location.host}/passport`
    const redirect_url = encodeURIComponent(
      `https://amazing.com/passport?redirect=${nextUrl}`,
    )
    // @ts-ignore
    const wwLogin = new WxLogin({
      self_redirect: true,
      id: 'login_container',
      appid: appid,
      scope: 'snsapi_login',
      redirect_uri: redirect_url,
      state: UUID.generate(),
      style: '',
      href: '',
    })
  }, [])
  return <div id='login_container' />
}
const Login: FC = () => {
  const timer = useRef<NodeJS.timer | null>(null)
  
  const getCode = () => {
    const msg = JSON.parse(
      sessionStorage.getItem('passport__wechat_login_code') || '{}',
    )
    return msg?.code
  }
    
  const resetMsg = () => {
    sessionStorage.removeItem('passport__wechat_login_code')
    clearInterval(timerRef.current as NodeJS.Timer)
    timerRef.current = null
  }
  
  const loginLogic = () => {
    const code = getCode()
    if (code) {
      resetMsg()
      getToken(code)
    }
  }
  
  useEffect(() => {
    timerRef.current = setInterval(loginLogic, 1000)
    return () => {
      resetMsg()
    }
  }, [])
  
  return (
  	<div>
      <BeforeScan />
    </div>
  )
}
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 - 30
 - 31
 - 32
 - 33
 - 34
 - 35
 - 36
 - 37
 - 38
 - 39
 - 40
 - 41
 - 42
 - 43
 - 44
 - 45
 - 46
 - 47
 - 48
 - 49
 - 50
 - 51
 - 52
 - 53
 - 54
 - 55
 - 56
 - 57
 - 58
 - 59
 
中转平台passport:
var urlParams = new URLSearchParams(window.location.search)
var code = urlParams.get('code')
var redirect = urlParams.get('redirect')
// 异步调用 window.location.replace,防止一些异步操作未完成
function replaceUrl(url) {
  setTimeout(function () {
    window.location.replace(url)
  }, 500)
}
function handleLogin() {
  var timestamp = (Date.now() / 1000) | 0
  var obj = { code: code, timestamp: timestamp }
  window.sessionStorage.setItem(
    'passport__wechat_login_code',
    JSON.stringify(obj),
  )
  replaceUrl('/erp')
}
// 从 query 中获取 redirect url 并跳转到相应的域名(用于通过 amzaing.com 登录测试环境的 erp)。
// 跳转时保留当前所有的 query,但是要去除 redirect。
function handleRedirect() {
  if (redirect) {
    var redirectUrl = new URL(redirect)
    urlParams.forEach(function (value, key) {
      if (key != 'redirect') {
        redirectUrl.searchParams.set(key, value)
      }
    })
    replaceUrl(redirectUrl.toString())
    return true
  }
  return false
}
function main() {
  var redirected = handleRedirect()
  if (!redirected) {
    handleLogin()
  }
}
main()
- 1
 - 2
 - 3
 - 4
 - 5
 - 6
 - 7
 - 8
 - 9
 - 10
 - 11
 - 12
 - 13
 - 14
 - 15
 - 16
 - 17
 - 18
 - 19
 - 20
 - 21
 - 22
 - 23
 - 24
 - 25
 - 26
 - 27
 - 28
 - 29
 - 30
 - 31
 - 32
 - 33
 - 34
 - 35
 - 36
 - 37
 - 38
 - 39
 - 40
 - 41
 - 42
 - 43
 - 44
 - 45
 
不足之处
上面这种扫码登录方式存在一些不足之处:
- 前端在本地起的环境是无法实现扫码登录的。因为上面这样方式需要系统和中转平台在相同域下,对于本地环境来说某个端口已经被系统占用,那么中转平台就无法使用这个端口了。
 - 需要进行三次页面跳转,登录等待时间过长。
 
所以,扫码登录最好的方式还是需要把中转平台构建成一个真正的系统服务,这样中转平台就可以向系统主动推送数据。而且,把中转平台搭建成系统服务以后,可以把所有系统的登录逻辑都集成到中转平台,由此构建一个统一登录平台,这样其他系统无需再关注登录逻辑。
