在Chrome扩展中使用Parse Platform-集成Gitee登录

国内提供Oauth认证服务的好少,使用第三方登录降低了认证服务的复杂度,也降低了用户的决策难度,这里用Gitee作为一个示例。应该适用于大多数的Web应用,只有客户端获取authorization_code部分的代码可能不一致。Parse官方文档里的自定义认证写的语焉不详,折腾了好久。

服务端

1. Parse Server配置认证信息

const gitee = require('./auth/gitee');

{
  auth: {
   gitee: {
     module: gitee, // OR object,
     client_id: "", // Gitee第三方应用ID
     client_secret: "", // Gitee第三方应用的密钥
   }
  }
}

2. 实现自定义登录(AuthAdapter)

主要需要实现两个函数:validateAuthData和validateAppId。

const qs = require('querystring')
const Parse = require('parse/node')

async function getAuthData(code, redirect_uri, options) {
    const token = await codeToToken(code, redirect_uri, options)
    const user = await userInfo(token.token_type, token.access_token)

    return {
        id: user.id,
        access_token: token.access_token
    }
}

async function codeToToken(code, redirect_uri, options) {
    const url = `https://gitee.com/oauth/token`
    const query = qs.stringify({
        grant_type: 'authorization_code',
        code,
        redirect_uri,
        client_id: options.client_id,
        client_secret: options.client_secret
    })
    const res = await fetch(`${url}?${query}`, { method: 'POST' })

    return await res.json()
}

async function userInfo(type, token) {
    const url = 'https://gitee.com/api/v5/user'

    const res = await fetch(url, {
        method: 'GET',
        headers: {
            Authorization: `${type} ${token}`
        }
    })

    return await res.json()
}

// Returns a promise that fulfills if this user id is valid.
async function validateAuthData(authData, options) {
    if (!authData.id) {
        throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少id字段');
    }
    if (!authData.access_token) {
        throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少access_token字段');
    }

    const user = await userInfo('bearer', authData.access_token)
    if (user && String(user.id) === String(authData.id)) {
        return
    }

    throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'authData is invalid for this user.');
}

// Returns a promise that fulfills iff this app id is valid.
function validateAppId(appIds, authData, options) {
    console.log('validateAppId:', appIds, authData, options)
    return Promise.resolve();
}

module.exports = {
    validateAuthData,
    validateAppId,
    getAuthData
}

3. 添加一个云函数,用于获取authData

const gitee = require('../auth/gitee')
const config = require('./config')

Parse.Cloud.define("giteeAuthData", async (request) => {
    return await gitee.getAuthData(request.params.code, request.params.redirect_uri, config.auth.gitee)
})

客户端

async function login() {
        const redirectUrl = chrome.identity.getRedirectURL()
        logger.log('redirect_url:', redirectUrl)

        // 获取认证码
        const authUrl = await _oauthCode(redirectUrl)
        if (authUrl === undefined) {
            throw new Error('login failed')
        }
        const url = new URL(authUrl)
        const code = url.searchParams.get('code')
        if (code == null) {
            throw new Error('login failed')
        }
        logger.log('code:', code)

        // 获取认证信息
        const authData = await _oauthData(code, redirectUrl)

        user.value = await Parse.User.logInWith('gitee', {
            authData
        })
}


async function _oauthData(code: string, redirect_uri: string) {
        const authData = await Parse.Cloud.run('giteeAuthData', {
            code,
            redirect_uri
        })
        logger.log('authData:', authData)

        return authData
}

参考

[1] Parse 自定义认证文档地址, Parse Server Guide | Parse (parseplatform.org)

[2] Gitee Oauth文档,Gitee OAuth 文档