Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

前后端配置

Oak 项目里有两类名字很接近、但含义完全不同的配置:

  • src/configuration/access.ts:前端如何访问后端服务;
  • 页面或命名空间里的 route.access:web 页面是否允许当前用户进入。

这两者不要混在一起理解。access.ts 解决的是“请求发到哪里”;route.access 解决的是“这个页面能不能看”。

src/configuration/access.ts

access.ts 是前端连接后端的统一入口。模板里通常长这样:

import accessConfiguration from './access.dev';

export default accessConfiguration;

也就是说,业务代码一般不直接 import access.dev.tsaccess.prod.tsaccess.staging.ts,而是统一从 @project/configuration/access 取当前环境的访问配置。

真正的配置类型是 AccessConfiguration,核心字段是:

import { AccessConfiguration } from '@oak-domain/types/Configuration';

const accessConfiguration: AccessConfiguration = {
    http: {
        hostname: 'localhost',
        port: 3001,
        ssl: false,
        path: 'oak-api',
    },
    timeout: 5000,
    clockDriftDuration: 10000,
};

export default accessConfiguration;

其中:

  • http.hostname 是后端服务域名;
  • http.port 是后端端口,本地开发常用;
  • http.ssl 决定使用 http 还是 https
  • http.path 是反向代理路径,例如 nginx 映射到后端 API 的路径;
  • timeout 是前端请求超时时间;
  • clockDriftDuration 是允许的前后端时钟漂移时间。

AccessConfiguration 还可以配置几个框架路由前缀:

const accessConfiguration: AccessConfiguration = {
    routerPrefixes: {
        aspect: '/aspect',
        endpoint: '/endpoint',
        bridge: '/bridge',
        getSubscribePoint: '/socketPoint',
    },
    socketPath: '/socket',
    http: {
        hostname: 'localhost',
        port: 3001,
    },
};

这些前缀一般不需要改。只有当后端路由或网关规则被项目刻意调整时,才应该同步配置。

它在哪里被使用

access.ts 最直接的消费者是 src/config/connector.ts

import SimpleConnector from '@oak-domain/utils/SimpleConnector';
import accessConfiguration from '@project/configuration/access';
import { makeException } from '@project/types/Exception';
import { EntityDict } from '@project/oak-app-domain';
import FrontendRuntimeContext from '@project/context/FrontendRuntimeContext';

const connector = new SimpleConnector<EntityDict, FrontendRuntimeContext>(
    accessConfiguration,
    makeException
);

export default connector;

SimpleConnector 会根据 accessConfiguration 拼出:

  • aspect 调用地址;
  • endpoint 调用地址;
  • bridge 地址;
  • socket 订阅地址。

随后,生成的 src/initialize.server.ts 会把这个 connector 传给 @oak-frontend-base/initialize,web、小程序、native 的平台入口再统一 import @project/initialize。因此,access.ts 配错时,现象通常不是某个页面单独失败,而是前端所有 aspect、endpoint、订阅或缓存同步请求都可能连不上后端。

加密连接 EncConnector

如果项目需要在 Oak connector 层对请求和响应做加密,可以用 oak-internal-sdk 提供的 EncConnector 替换默认的 SimpleConnector

EncConnector 的入口通常这样引入:

import { EncConnector } from '@oak-internal-sdk/utils/EncConnector';

这个入口会按编译平台选择实现:web、小程序、native 使用前端实现;server 使用后端实现。因此同一个 import 在前后端可以写一样,但构造参数不能完全一样。

前端侧的 src/config/connector/index.web.ts 可以这样写:

import accessConfiguration from '@project/configuration/access';
import { makeException } from '@project/types/Exception';
import { EntityDict } from '@project/oak-app-domain';
import FrontendRuntimeContext from '@project/context/FrontendRuntimeContext';
import { EncConnector } from '@oak-internal-sdk/utils/EncConnector';

const connector = new EncConnector<EntityDict, FrontendRuntimeContext>({
    configuration: accessConfiguration,
    makeException,
    allowUnsafe: process.env.NODE_ENV !== 'production',
});

export default connector;

前端不需要 sessionStore。如果 allowUnsafefalse,前端会先通过内置 aspect oak:connector:key-exchange 和后端做密钥交换,然后用会话密钥加密后续请求中的 context 和 data。开发环境可以临时把 allowUnsafe 设为 true,让前端跳过密钥交换、继续发送未加密请求;生产环境一般应关闭。

后端侧也必须使用 EncConnector,否则后端无法解析前端发来的加密请求。后端的 src/config/connector/index.backend.ts 通常需要提供会话存储:

import accessConfiguration from '@project/configuration/access';
import { makeException } from '@project/types/Exception';
import { EntityDict } from '@project/oak-app-domain';
import FrontendRuntimeContext from '@project/context/FrontendRuntimeContext';
import { EncConnector } from '@oak-internal-sdk/utils/EncConnector';
import { createRedisSessionStore } from '@oak-internal-sdk/adaptor/redisAdaptor';
import redisConfig from '../../../configuration/redis.json';

const connector = new EncConnector<EntityDict, FrontendRuntimeContext>({
    configuration: accessConfiguration,
    makeException,
    allowUnsafe: (options) => {
        if (process.env.NODE_ENV !== 'production') {
            return true;
        }
        const { headers } = options;
        const oakVersion = headers['oak-version'];
        const oakPlatform = headers['oak-platform'];
        return !oakVersion && oakPlatform === 'mp';
    },
    sessionStore: createRedisSessionStore(redisConfig, {
        keyPrefix: 'my_app:session:',
        ttl: 24 * 60 * 60,
    }),
    cleanupInterval: 60 * 1000,
    sessionMaxAge: 24 * 60 * 60 * 1000,
    nonceMaxAge: 60 * 60 * 1000,
});

export default connector;

服务端 EncConnector 必须提供 sessionStoreoak-internal-sdk 提供了两种常用适配器:

  • createRedisSessionStore(...):适合生产部署,多个服务实例可以共享会话;
  • createMemorySessionStore(...):适合单进程、本地测试或临时验证。

这两个创建函数只会在 process.env.OAK_PLATFORM === 'server' 时返回实例。自定义后端启动脚本如果没有先设置 OAK_PLATFORM=serversessionStore 会是 undefined,服务端构造 EncConnector 时会抛出 error::backend.sessionStoreRequired

一个项目如果要按平台拆 connector,可以采用类似结构:

src/config/connector/
    index.ts          // 默认导出后端实现,供 server 编译使用
    index.backend.ts  // EncConnector + sessionStore
    index.web.ts      // EncConnector,不传 sessionStore
    index.mp.ts       // 视项目情况使用 EncConnector 或 SimpleConnector
    index.native.ts   // 视项目情况使用 EncConnector 或 SimpleConnector

EncConnector 会额外使用这些 Oak 请求/响应头:

  • oak-session-id
  • oak-encrypted
  • oak-timestamp
  • oak-nonce
  • oak-sequence
  • oak-version
  • oak-platform

Oak CLI 的 server 中间件会把 connector 的 getCorsHeader() 合并进允许请求头;开发和预发环境还会设置 connector 的响应头暴露。生产环境如果启用了自定义 CORS、nginx、网关或 CDN,需要额外确认这些头没有被拦截,并且响应头里的 oak-encryptedoak-nonce 等能被浏览器读取。否则前端可能无法解密响应,或者 SSE 加密流无法正确解析。

EncConnector 也支持 endpoint 和 SSE endpoint。后端 endpoint 如果配置了 useConnector,Oak CLI 会先调用 connector.parseRequest(...) 解密请求;SSE endpoint 还会通过 serializeSSEEndpointResult(...) 加密 data: 数据包。加密 SSE 要求前后端 connector 同步升级,不能只升级其中一端。

迁移时最容易出错的地方有三个:

  1. 只把前端改成 EncConnector,后端仍然是 SimpleConnector
  2. 后端没有提供 sessionStore,或者启动时没有设置 OAK_PLATFORM=server
  3. 生产代理没有放行或暴露 Oak 加密相关 header。

多环境写法

模板通常会放三份访问配置:

  • src/configuration/access.dev.ts
  • src/configuration/access.staging.ts
  • src/configuration/access.prod.ts

本地开发常见写法是:

import { AccessConfiguration } from '@oak-domain/types/Configuration';
import { port } from './common';

export const hostname = 'localhost';

const accessConfiguration: AccessConfiguration = {
    http: {
        hostname,
        port,
    },
};

export default accessConfiguration;

生产环境如果通过 nginx 代理,常见写法是:

import { AccessConfiguration } from '@oak-domain/types/Configuration';
import { nginxServerProxyPath } from './common';

export const hostname = 'www.your-site.com';
export const ssl = true;

const accessConfiguration: AccessConfiguration = {
    http: {
        hostname,
        ssl,
        path: nginxServerProxyPath,
    },
};

export default accessConfiguration;

access.ts 是最终统一出口。当前模板不会因为文件名存在就自动替你选择 access.prod.ts;如果项目有预发、生产构建,需要确保 access.ts 或项目自己的构建流程导出正确的环境配置。

src/configuration/server.ts

server.ts 是后端服务自己的运行配置,类型是 ServerConfiguration。它和 access.ts 经常引用同一批常量,但职责不同:

  • access.ts 给前端 connector 用,描述“前端访问后端的地址”;
  • server.ts 给后端启动、代理和部署用,描述“后端自己怎么监听、如何暴露给 nginx”。

模板里的 server.ts 会从 access.dev.tsaccess.staging.tsaccess.prod.ts 读取域名和 ssl 配置,再根据 process.env.NODE_ENV 选择:

const serverConfiguration: ServerConfiguration = {
    workDir: {
        path: join(__dirname, '..', '..'),
    },
    port,
    hostname: HostnameDict[process.env.NODE_ENV as string],
    nginx: NginxConfDict[process.env.NODE_ENV as string],
};

所以部署时要同时检查两边:

  • 前端构建产物里 access.ts 指向的后端地址是否正确;
  • 后端启动时 NODE_ENVserver.ts、nginx 路径和实际监听端口是否一致。

src/configuration/index.ts

src/configuration/index.ts 是另一类配置入口,它导出的是 CommonConfiguration,会参与 Oak 前端运行时和内置 checker 初始化。模板中通常包含:

import attrUpdateMatrix from './attrUpdateMatrix';
import { CommonConfiguration } from '@oak-domain/types/Configuration';
import { actionDefDict } from '@project/oak-app-domain/ActionDefDict';
import { selectFreeEntities, authDeduceRelationMap, updateFreeDict } from './relation';
import cacheSavedEntities from './cache';
import { EntityDict } from '@project/oak-app-domain';

export default {
    attrUpdateMatrix,
    actionDefDict,
    authDeduceRelationMap,
    selectFreeEntities,
    updateFreeDict,
    cacheSavedEntities,
} as CommonConfiguration<EntityDict>;

这一组配置不是网络地址,而是 Oak 运行时规则,例如:

  • attrUpdateMatrix:属性更新矩阵,生成内置 checker;
  • actionDefDict:实体 action / 状态矩阵定义;
  • authDeduceRelationMap:权限关系推导;
  • selectFreeEntities:允许自由查询的对象;
  • updateFreeDict:允许自由更新的对象和动作;
  • cacheSavedEntities:前端缓存持久化对象。

如果你是在改“前端访问哪个后端”,看 access.ts;如果你是在改“哪些对象能查、哪些属性能改、哪些 action 合法”,看 configuration/index.ts 以及它引用的 relation.tsattrUpdateMatrix.ts 等文件。

页面访问控制 route.access

页面访问控制写在页面或命名空间的 index.config.ts,和 src/configuration/access.ts 没有直接关系。例如:

import { CreatePageConfig } from '@oak-frontend-base/config';

export default CreatePageConfig({
    route: {
        access: { type: 'login' },
    },
});

常见取值包括:

  • public:直接允许;
  • login:需要登录;
  • root:需要 root;
  • deny:始终拒绝;
  • relation:按 Oak 关系权限判断;
  • operation:用前端 checker 判断某个 Oak operation 是否允许;
  • anyOf / allOf:组合多条规则。

组合规则使用 items

export default CreatePageConfig({
    route: {
        access: {
            type: 'allOf',
            items: [
                { type: 'login' },
                { type: 'ref', refs: ['../system/detail'] },
            ],
        },
    },
});

也可以直接写规则数组,数组按 anyOf 语义处理。relation 规则使用 anyOfoperation 规则使用 target,不要把 anyOf / allOf 写成没有 items 的裸对象。

命名空间还可以通过 access.unconfiguredAccess 声明没有单独配置访问规则的页面默认如何处理:

export default CreateNamespaceConfig({
    access: {
        unconfiguredAccess: 'deny',
    },
});

因此,排查访问问题时可以按这个顺序看:

  1. 前端请求是否打到了正确后端:看 src/configuration/access.tssrc/config/connector.ts
  2. 后端是否按正确端口和代理路径启动:看 src/configuration/server.ts
  3. 页面是否被路由权限拦住:看页面 index.config.tsroute.access 和命名空间 access.unconfiguredAccess
  4. operation 类访问控制是否被 checker 拦住:继续看 src/checkersattrUpdateMatrix.ts 和权限配置。