标签: Parse Platform

分享一些使用Parse Platform开发应用的经验,补充一下官方文档描述不够清晰,或者不适用于国内的内容。parseplatform,baas,backend as a service,后端即服务

  • 在Chrome扩展中使用Parse Platform-邮箱验证码登录

    本来想使用oauth来实现登录,但是国内提供oauth服务要么比较小众,要么居然收费的?传统的用户注册登录使用起来过于繁琐了,很容易把用户挡在最开始的地方,最后决定添加邮箱验证码登录。

    Parse Platform的文档中提到了一个Parse.User.become()方法,但是需要传递一个session token进去,翻了很多遍文档也没发现该如何获取这个session_token,最后在Github仓库的issue中搜到2023年Parse增加了一个loginAs方法,可以通过传递一个userId来将用户登陆进系统,那么通过云函数和Parse.User.become()就可以实现邮箱验证码登录了。

    服务端

    1. 创建发送验证码函数

    const VerifyCode = Parse.Object.extend('VerifyCode')
    
    /**
     * 发送邮件验证码
     */
    Parse.Cloud.define('verifyCode', async (request) => {
        const email = request.params.email
        const code = generateRandomString(6)
    
        // 记录验证码
        const verifyCode = new VerifyCode()
        verifyCode.set('email', email)
        verifyCode.set('code', code)
        verifyCode.set('expiredAt', new Date(Date.now() + 1000 * 60 * 5))
        await verifyCode.save(null, { useMasterKey: true })
    
        // 删除过期的验证码记录
        const query = new Parse.Query(VerifyCode)
        query.lessThan('expiredAt', new Date())
        await Parse.Object.destroyAll(await query.find({ useMasterKey: true }), { useMasterKey: true })
    
        // 发送邮件
        await Parse.Cloud.sendEmail({
            templateName: "verifyCode",
            placeholders: { code },
            recipient: email
        });
    }, {
        fields: {
            email: {
                required: true,
                type: String
            }
        },
        requireUser: false
    })

    2. 创建登录函数

    const VerifyCode = Parse.Object.extend('VerifyCode')
    
    Parse.Cloud.define('login', async (request) => {
        if (!request.params.email) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少email字段');
        }
        if (!request.params.code) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '缺少code字段');
        }
    
        const query = new Parse.Query(Parse.User)
        query.equalTo('email', request.params.email)
        let user = await query.first({ useMasterKey: true })
        if (user === undefined) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '用户不存在');
        }
    
        // 验证
        const codeQuery = new Parse.Query(VerifyCode)
        codeQuery.equalTo('email', request.params.email)
        codeQuery.equalTo('code', request.params.code)
        codeQuery.greaterThan('expiredAt', new Date())
        const code = await codeQuery.first({ useMasterKey: true })
        if (code === undefined) {
            throw new Parse.Error(Parse.Error.OTHER_CAUSE, '验证码已过期');
        }
    
        // 修改邮箱验证字段为验证通过状态
        user.set('emailVerified', true)
        user.save(null, { useMasterKey: true })
    
        // 登录用户
        user = await Parse.User.loginAs(user.id)
        return user.getSessionToken()
    }, {
        fields: {
            email: {
                required: true,
                type: String
            },
            code: {
                required: true,
                type: String
            }
        }
    })

    客户端

    export async function emailLogin(email: string, code: string) {
        const token = await Parse.Cloud.run('login', { email, code })
    
        return await Parse.User.become(token)
    }
  • 在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 文档