分类: 服务端

  • 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

  • 使用Caddy反向代理WordPress

    具体配置

    xxx.xxxx.xxx {
        tls youremail@yourmailserver
    
        root * /path/to/wordpress
    
        # GZIP和FPM配置
        encode gzip
        file_server
        php_fastcgi php:9000
    
        # 静态文件配置
        @static_files {
            path_regexp \.(?:css|js|woff2?|svg|gif|map|png|jpg|webp|gif|jpeg|mp4|mp3|wav|mov|heic)
        }
        header @static_files {
            Cache-Control "public, max-age=15778463"
            X-Robots-Tag "none"
            X-Permitted-Cross-Domain-Policies "none"
            X-Frame-Options "SAMEORIGIN"
            X-Download-Options "noopen"
            X-Content-Type-Options "nosniff"
            # Referrer-Policy "no-referrer"
        }
    
        # 禁止访问的目录/文件
        @disallowed {
            #path /wp-cron.php
            #path /xmlrpc.php
            path *.sql
            path /wp-content/uploads/*.php
            path /wp-content/uploads/*.html
            path /wp-content/debug.log
        }
        rewrite @disallowed =404
    }
  • 使用WordPress作为小程序后端——APPID有效性前置检查

    上一篇实现了一个简单的前置检查,这一篇我们来聊一聊如何实现APPID的有效性检查。上一篇中,我们只是简单的将APPID获取到并传递到了请求处理函数中,这一篇,我们来实现一个APPID有效性的前置检查,或者叫中间件。

    APPID的检查相对比较简单,我们可以透过一种比较Wordpress的方式来实现:

    add_filter('wechat_mp_permission_callback', function ($permission, WP_REST_Request $request) {
        if ($permission === false) {
            return false;
        }
    
        $wechat_mp_apps = apply_filters( 'wechat_mp_apps', [] );
        $attrs = $request->get_attributes();
        return array_key_exists($attrs['app_id'], $wechat_mp_apps);
    }, 10, 2);

  • 使用WordPress作为小程序后端——小程序请求前置检查

    小程序默认提供了一个固定格式的referer格式,具体可以参考官方文档:网络请求。如下:

    https://servicewechat.com/{appid}/{version}/page-frame.html

    由于这个referer是不可以手动设置的,因此透过这个referer,我们可以对请求进行一个简单的前置检查,过滤一些简单的脚本。同时,我们也能通过这个前置检查来了解请求的身份,即小程序APPID,当需要同时支持多个小程序时,这一点还是很有意义的。

    一个简单实现

    /**
     * 小程序请求通用前置检查
     */
    function precheck($referer) {
        $result = preg_match("/^https:\/\/servicewechat\.com\/(.*?)\/(.*?)\/page-frame\.html$/", $referer, $matches);
        if (!$result || empty($matches) || !isset($matches[1]) || !isset($matches[2])) {
            return false;
        }
    
        return true;
    }

    在Wordpress中的使用

    通过这个简单的函数,我们可以对小程序发起的请求进行一个简单的前置检查。那么如何应用到Wordpress中呢?

    通过阅读Wordpress的文档,可以了解注册rest路由的函数是register_rest_route,具体参考文档:register_rest_route。注册路由可以配置三个回调函数,分别是permission_callback,validate_callback和callback。我觉得这个检查更适合用在permission_callback,即当无法通过检查的时候,我们实际上可以认为这是一个非法请求,我们对之前的实现进行一些修改:

    /**
     * 小程序请求通用前置检查
     */
    function precheck(WP_REST_Request $request) {
        $referer = $request->get_header('referer');
        $result = preg_match("/^https:\/\/servicewechat\.com\/(.*?)\/(.*?)\/page-frame\.html$/", $referer, $matches);
        if (!$result || empty($matches) || !isset($matches[1]) || !isset($matches[2])) {
            return false;
        }
    
        return true;
    }

    暂存获取到的APPID和VERSION,方便之后使用

    在前面的代码里,我们通过一个简单的正则,对referer进行了一个简单的检查,但是匹配的一些结果我们没有暂存下来,为了方便之后获取APPID和VERSION信息,我们再次扩展一下:

    /**
     * 小程序请求通用前置检查
     */
    function precheck(WP_REST_Request $request) {
        $referer = $request->get_header('referer');
        $result = preg_match("/^https:\/\/servicewechat\.com\/(.*?)\/(.*?)\/page-frame\.html$/", $referer, $matches);
        if (!$result || empty($matches) || !isset($matches[1]) || !isset($matches[2])) {
            return false;
        }
    
        $request->set_attributes([
            'app_id' => $matches[1],
            'version' => $matches[2]
        ]);
    
        return true;
    }

    封装

    为了更方便使用,我们再次进行一点简单的封装。

    /**
     * 给所有的路由添加默认的前置检查钩子
     */
    function register_wechat_mp_rest_route($route_namespace, $route, $args = [], $override = false) {
        add_filter( 'wechat_mp_permission_callback', precheck, 10, 2 );
    
        if (isset($args['permission_callback'])) {
            if (!is_array($args['permission_callback'])) {
                $args['permission_callback'] = [ $args['permission_callback'] ];
            }
    
            foreach ($args['permission_callback'] as $callback) {
                add_filter( 'wechat_mp_permission_callback', $callback, 10 , 2 );
            } 
        }
        $args['permission_callback'] = function (WP_REST_Request $request) {
            return apply_filters( 'wechat_mp_permission_callback', true, $request );
        };
        return register_rest_route($route_namespace, $route, $args, $override);
    }
    
    /**
     * 小程序请求通用前置检查
     */
    function precheck($permission, WP_REST_Request $request) {
        if ($permission === false) {
            return false;
        }
    
        $referer = $request->get_header('referer');
        $result = preg_match("/^https:\/\/servicewechat\.com\/(.*?)\/(.*?)\/page-frame\.html$/", $referer, $matches);
        if (!$result || empty($matches) || !isset($matches[1]) || !isset($matches[2])) {
            return false;
        }
    
        $request->set_attributes([
            'app_id' => $matches[1],
            'version' => $matches[2]
        ]);
    
        return true;
    }

    好,到这里,我们的前置检查相关的代码就完成了。下一篇的时候我们来实现一下用户认证。

  • Windows rclone挂载sftp

    手动挂载命令:

    rclone mount sftp:/mnt o: --cache-dir %SystemRoot%\TEMP --allow-other --vfs-cache-mode writes --vfs-cache-max-age 60s  --allow-non-empty

    创建服务:

    // 安装nssm
    winget install nssm
    
    // 创建服务
    nssm install rclone

    配置信息(具体路径需要根据实际情况来确定):

    Path: C:\Users\Administrator\Documents\bin\rclone.exe
    Arguments: mount config:/root A: --volname sftp --config C:\Users\Administrator\AppData\Roaming\rclone\rclone.conf

    遇到的问题

    1. 无法修改文件

    是因为nssm默认使用LocalSystem用户。

    任务管理器-服务-右键rclone选择打开服务->找到rclone右键选择属性->登录->选择此账户->点击右侧浏览->弹出框中选择高级->立即查找->选择一个本地账户比如Adminstrator->一路确定。

  • php: /usr/local/lib/libcurl.so.4: no version information available (required by php)

    出现原因是安装了多个版本的库,ubuntu的一些库似乎增加了一些附加补丁,导出的符号与ubuntu有关联的版本信息,但在标准的库中是没有的。

    (更多…)