博客

  • apache跨域配置

    用Apache比较少,今天正好碰到,记录一下,本质就是修改响应头信息。

    1. 取消注释主配置文件中的下面这行

    # 加载模块
    # LoadModule headers_module modules/mod_headers.so

    2. 修改vhost配置

    <Directory /www/web/yuming.com/public_html/>
        Options FollowSymLinks
        AllowOverride All
        Require all granted
        Header set Access-Control-Allow-Origin *
        # 或者指定具体的主机名
        # Header set Access-Control-Allow-Origin http://example.com
        # Header set Access-Control-Allow-Origin http://localhost:8080
    </Directory>

  • 在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 文档

  • Linux配置Swappiness

    1. 调整交换分区使用配置

    vim /etc/sysctl.conf
    
    vm.swappiness = 40

    2. 让配置生效

    sysctl -p
  • 使用Docker Compose部署NextCloud和WordPress

    一、全局配置

    name: lnmp
    services:
      caddy:
        image: caddy:latest
        volumes:
          - ./www:/var/www/html
          - ./caddy/etc:/etc/caddy
          - ./caddy/data:/data
          - ./caddy/config:/config
        ports:
          - 80:80
          - 443:443/tcp
          - 443:443/udp
        logging:
          driver: "json-file"
          options:
            max-size: "10m"
            max-file: 3
        restart: always
    
      redis:
        image: redis:latest
        volumes:
          - ./redis/config:/etc/redis
          - ./redis/data:/data
        restart: always
        command: /etc/redis/redis.conf
    
      mysql:
        image: mysql:latest
        volumes:
          - ./mysql/config:/etc/mysql
          - ./mysql/data:/var/lib/mysql
          - ./mysql/mysql-files:/var/lib/mysql-files
        cap_add:
          - SYS_NICE
        security_opt:
          - seccomp:unconfined
        environment:
          MYSQL_ROOT_PASSWORD: password
        ports:
          - 3306:3306
        restart: always
    
      php:
        build: ./php
        volumes:
          - ./php/config:/usr/local/etc
          - ./php/logs:/var/log/php
          - ./www:/var/www/html
        depends_on:
          - caddy
          - mysql
          - redis
        cap_add:
          - SYS_PTRACE
        logging:
          driver: "json-file"
          options:
            max-size: "10m"
            max-file: 3
        restart: always
    
      imaginary:
        image: nextcloud/aio-imaginary:latest
        restart: always
        command: -concurrency 2 -enable-url-source
        environment:
          - PORT=9000

    1. PHP

    PHP官方的镜像启用和安装的扩展比较少,直接使用会导致WordPress和Nextcloud的健康检查一堆信息,所以使用Dockerfile来基于官方镜像构建一个专用的镜像,PHP需要的扩展包括:

    • gd(png/jpeg/gif/webp/avif)
    • imagick
    • opcache(考虑性能)
    • apcu(Nextcloud的本地缓存)
    • zip(影响WordPress插件安装)
    • redis(WordPress的对象缓存和Nextcloud的分布式缓存)
    • gmp
    • intl
    FROM php:8.2-fpm-alpine
    RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
        apk add --no-cache --update --virtual .build-deps \
        $PHPIZE_DEPS \
        icu \
        && apk add --no-cache --virtual .php-deps \
        imagemagick-dev \
        imagemagick-svg \
        libzip-dev \
        libpng-dev \
        libavif-dev \
        icu-dev \
        gmp-dev \
        libjpeg-turbo-dev \
        freetype-dev \
        && apk add --no-cache --virtual .tools-deps \
        git \
        openssh-client \
        unzip \
        && apk add --no-cache --virtual .runtime-deps \
        libzip \
        libpng \
        libavif \
        libjpeg-turbo \
        freetype \
        && pecl install https://pecl.php.net/get/imagick-3.7.0.tgz https://pecl.php.net/get/redis-6.0.2.tgz apcu \
        && docker-php-ext-configure intl \
        && docker-php-ext-configure gd \
          --with-freetype=/usr/include/ \
          --with-jpeg=/usr/include/ \
          --with-avif=/usr/include/ \
        && docker-php-ext-install zip gd pdo_mysql pcntl mysqli exif intl gmp bcmath sysvsem && docker-php-ext-enable zip gd pdo_mysql mysqli opcache exif intl gmp bcmath pcntl sysvsem imagick redis apcu

  • APP Store应用上架需要注意的点

    最近公司的应用上架APP Store过程中一直遇到审核问题,记录一下遇到的问题,方便后续其他应用开发避坑。

    1、与安卓通用的审核条件

    2、虚拟支付

    1. 所有的虚拟商品直接使用苹果的内购方式付款,避免后续改造的问题(需要给苹果30%抽成)
    2. 提前考虑宣传物料和价格在不同货币区域的展示,或者避免物料中包含价格
    3. 至少审核阶段,不要在应用界面里放跳转到其他应用的链接,苹果审核会认为这涉及到其他支付方式

    3、强制登录问题

    微信小程序也是一样的要求,所以最好设计阶段就开始考虑这个问题。

    1. 不需要用户信息的数据不设计为强制鉴权
    2. 将引导用户登录放到用户进行必须鉴权的操作时
  • 《老姚的底层逻辑认知课》笔记

    1. 六个经济变化

    未来的六大经济环境变化:

    通缩转为通胀
    利率由负转正
    逆全球化
    基础能源资料暴涨
    生产成本提高
    赚钱效益减弱

    2. 主要股指进出点位评估

    股指参考指数低点高点卖出点位周期
    A股沪深300市盈率中位数16-194030-334年左右
    港股恒生指数市盈率102016-175-6年左右
    美股标普500市盈率12-1427-3027-3010年
  • MySQL配置及优化

    一、配置项及意义

    配置项意义建议
    innodb_buffer_pool_size控制 InnoDB 存储引擎的内存缓存池大小总内存(非专用服务器可以使用希望分配给数据库的内存量)的 50% 到 80%
    innodb_buffer_pool_chunk_size InnoDB 缓冲池大小调整操作的块大小,默认值为 128MBinnodb_buffer_pool_size = innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances
    innodb_buffer_pool_instances决定了 InnoDB 缓冲池(Buffer Pool)的实例数量设置为 CPU 核心数的较小倍数,超过 1GB 时可以适量增加,最高不超过1000
    innodb_buffer_pool_in_core_file从核心文件中排除缓冲池页面,参考15.8.3.7 从核心文件中排除缓冲池页面_MySQL 8.0 参考手册off
    innodb_flush_log_at_trx_commit定义了事务提交时,InnoDB 如何处理未刷入(flush)的重做日志(redo log),默认值为1,设置为0/2,在操作系统崩溃或断电时可能会丢失最后一秒的事务数据对于需要高可靠性和数据完整性的系统设置为 1,对于性能要求更高,且可以接受一定数据丢失风险的系统设置为 0 或 2。
    innodb_change_buffering控制着 InnoDB 执行变更缓冲(Change Buffering)的程度none
    innodb_log_buffer_size设置InnoDB日志缓冲区的大小64m
    innodb_flush_method决定了 InnoDB 如何将数据和日志刷新(flush)到磁盘O_DIRECT
    innodb_purge_threads定义了用于回收不再需要的 UNDO 日志的 Purge 线程的数量cpu > 4 ? 4 : 1
  • Debian配置自动清理Journal日志

    Debian配置自动清理Journal日志

    在Debian系统上(红帽系Linux发行版应该也是一样的),systemd-journald 服务负责管理 journal 日志。这些日志可以占用大量的磁盘空间,特别是当系统持续运行并且产生大量日志条目时。可以配置 systemd-journald 的日志保留策略来自动清理这些日志。

    (更多…)
  • Linux配置SWAP交换分区

    1. 创建交换文件

    dd if=/dev/zero of=/swap bs=1G count=2
    
    # 如果出现Killed,一般是可用内存不足,可以尝试以下
    dd if=/dev/zero of=/swap bs=1M count=2048

    2. 启用SWAP文件

    mkswap /swap
    
    swapon /swap

    3. 配置开机自动挂在

    echo '/swap none swap defualts 0 0' >> /etc/fstab

    4. 配置优先使用内存

    echo 'vm.swappiness=0'>> /etc/sysctl.conf
    
    sysctl -p