Frossky 发布的文章

OpenID Connect (OIDC) 发现:核心概念与实践解析

OpenID Connect (OIDC) 发现是 OIDC 协议的核心特性之一,本质是让客户端(如你的小程序、Web 应用)通过一个统一的“发现端点”,自动获取认证服务器(如微信开放平台、Auth0、Keycloak 等)的所有配置信息,无需手动硬编码,实现客户端与认证服务器的“动态对接”。

对于开发者而言,OIDC 发现的核心价值是简化集成流程、提升兼容性和安全性——尤其在对接微信小程序、企业微信等第三方认证服务时,无需记忆复杂的接口地址、算法类型等细节,通过自动发现即可完成配置,大幅降低集成成本。

一、为什么需要 OIDC 发现?

在 OIDC 出现前,类似的身份认证协议(如 SAML)需要开发者手动配置大量参数:

  • 认证端点(登录地址)、令牌端点(获取 Token 地址)
  • 密钥交换算法(如 RS256、HS256)
  • 公钥(用于验证 Token 签名)
  • 客户端回调地址规则、支持的 Scope(权限范围)等

手动配置存在三大问题:

  1. 易出错:参数多且格式严格,输错一个字符就会导致认证失败;
  2. 难维护:若认证服务器调整配置(如更换端点地址、更新公钥),客户端需同步修改代码,否则会失效;
  3. 兼容性差:不同认证服务器的配置参数格式可能不同,客户端需针对性适配。

OIDC 发现通过“自动拉取配置”解决了这些问题,让客户端与认证服务器的集成更高效、更可靠。

二、OIDC 发现的核心原理

OIDC 发现的核心是 Well-Known 端点(固定地址),流程如下:

1. 固定的发现端点格式

所有遵循 OIDC 协议的认证服务器,都会暴露一个统一格式的“发现端点”:

https://[认证服务器域名]/.well-known/openid-configuration

示例(微信开放平台 OIDC 发现端点):

https://open.weixin.qq.com/.well-known/openid-configuration

(注:部分第三方服务可能未完全遵循标准,需参考其官方文档确认端点地址)

2. 客户端的发现流程

  1. 客户端发起请求:客户端向认证服务器的 Well-Known 端点发送 HTTP GET 请求(无需认证);
  2. 服务器返回配置文档:认证服务器返回一个 JSON 格式的“OIDC 配置文档”,包含所有客户端需要的参数;
  3. 客户端解析并使用:客户端解析 JSON 文档,自动获取认证端点、令牌端点、公钥地址等信息,用于后续的登录、Token 验证等操作。

3. 配置文档的核心字段(JSON 示例)

以下是标准 OIDC 配置文档的关键字段(实际返回字段可能更多,取决于服务器支持的功能):

{
  "issuer": "https://open.weixin.qq.com",  // 认证服务器的唯一标识(Issuer)
  "authorization_endpoint": "https://open.weixin.qq.com/connect/oauth2/authorize",  // 授权端点(跳转登录用)
  "token_endpoint": "https://api.weixin.qq.com/sns/oauth2/access_token",  // 令牌端点(获取 Access Token/ID Token)
  "userinfo_endpoint": "https://api.weixin.qq.com/sns/userinfo",  // 用户信息端点(获取用户昵称、头像等)
  "jwks_uri": "https://open.weixin.qq.com/.well-known/jwks.json",  // 公钥集端点(验证 ID Token 签名用)
  "response_types_supported": ["code", "token"],  // 支持的响应类型(如授权码模式用 "code")
  "subject_types_supported": ["public"],  // 支持的 Subject 类型(用户标识类型)
  "id_token_signing_alg_values_supported": ["RS256"],  // 支持的 ID Token 签名算法
  "scopes_supported": ["openid", "profile", "email"]  // 支持的权限范围(Scope)
}

关键字段说明(开发者必关注):

字段名 作用 开发场景应用
issuer 认证服务器唯一标识 验证 ID Token 中的 iss 字段是否一致,防止 Token 伪造
authorization_endpoint 授权端点 客户端跳转至该地址发起登录(如小程序的“微信授权登录”跳转)
token_endpoint 令牌端点 用授权码(code)兑换 Access Token 和 ID Token
jwks_uri 公钥集端点 拉取认证服务器的公钥,验证 ID Token 的签名是否有效
id_token_signing_alg_values_supported 支持的签名算法 客户端需用对应算法验证 ID Token(如 RS256 非对称加密,安全性更高)
scopes_supported 支持的权限范围 客户端发起授权时,需指定该列表中的 Scope(如 openid 是必选 Scope,用于获取 ID Token)

三、OIDC 发现的实际应用场景(以微信小程序为例)

作为开发者,在小程序中集成微信授权登录(基于 OIDC 协议变种)时,OIDC 发现的应用流程如下:

1. 场景需求

小程序需要通过微信授权登录,获取用户的 openid(用户唯一标识),并验证登录的合法性。

2. 基于 OIDC 发现的集成步骤

  1. 客户端请求发现端点:小程序后端(或前端,建议后端发起以避免跨域)向微信开放平台的 OIDC 发现端点发送 GET 请求;
    GET https://open.weixin.qq.com/.well-known/openid-configuration
  2. 获取配置信息:解析返回的 JSON 文档,提取 authorization_endpoint(授权端点)和 jwks_uri(公钥端点);
  3. 发起授权登录:小程序通过 wx.navigateToMiniProgram 或跳转 H5 页面,拼接授权参数(如 client_id、scope=openid、redirect_uri),跳转至 authorization_endpoint
  4. 兑换 Token:用户授权后,微信回调 redirect_uri 并返回 code,小程序后端用 code 调用 token_endpoint,获取 Access Token 和 ID Token;
  5. 验证 ID Token:小程序后端从 jwks_uri 拉取公钥,验证 ID Token 的签名、issuer、exp(过期时间)等字段,确认登录合法后,提取 openid 作为用户标识。

3. 优势体现

  • 无需手动记忆微信的授权端点、令牌端点地址,即使微信调整接口域名,客户端也能自动适配;
  • 无需手动配置公钥,通过 jwks_uri 自动获取最新公钥,避免公钥过期导致的验证失败;
  • 若后续需要对接其他 OIDC 认证服务(如企业微信、Auth0),只需修改认证服务器域名,无需改动集成逻辑。

四、OIDC 发现的注意事项(开发者避坑)

  1. 端点兼容性:部分第三方服务(如早期的微信开放平台)可能未完全遵循 OIDC 标准,Well-Known 端点可能不存在或字段不完整,需参考官方文档补充配置(如手动指定令牌端点);
  2. HTTPS 强制要求:OIDC 发现端点必须使用 HTTPS 协议,防止配置信息被篡改,客户端需确保请求地址是 HTTPS;
  3. 配置缓存:客户端可缓存获取到的配置文档(建议设置 1 小时缓存),避免频繁请求发现端点,但需定期更新,防止配置变更导致失效;
  4. 公钥验证:ID Token 的签名验证是安全关键,必须通过 jwks_uri 拉取公钥,切勿硬编码公钥(公钥可能会轮换);
  5. Scope 权限:发起授权时,需指定认证服务器支持的 Scope(如 openid 是必选),否则无法获取 ID Token。

五、总结

OIDC 发现的核心是“通过统一的 Well-Known 端点,让客户端自动获取认证服务器的配置信息”,其价值在于:

  • 简化集成:减少手动配置,降低对接第三方认证服务的复杂度;
  • 提升兼容性:客户端无需适配不同认证服务器的配置格式,实现“一次开发,多端兼容”;
  • 增强安全性:自动获取最新公钥和配置,避免因配置过时或错误导致的安全风险。

对于开发者而言,在集成微信小程序、企业微信、Auth0、Keycloak 等身份认证服务时,优先使用 OIDC 发现机制,可大幅提升开发效率和系统可靠性。如果需要具体的代码实现(如 Node.js/Java 后端调用发现端点、验证 ID Token),可以进一步补充需求!

Oauth中用户通过app提供用户名密码向授权服务器认证, 通过后, 授权服务器颁发1小时有效的access token和30天有效的refresh token, 然后app用access token访问资源服务器, access token过期前再去授权服务器请求新的access token和refresh token, 直到refresh token也过期, 需要用户再次输入用户名密码. refresh token可以存储在app中或者app的服务器中(看哪个更安全).
access token一般通过jwt的方式发送给资源服务器做验证. 资源服务器如果相对授权服务器是第三方的, 那么要去授权服务器的JWKS 端点如https://授权服务器域名/.well-known/jwks.json取得公钥, 用来验证jwt, 验证通过后再返回资源.
我猜想Oauth这么设计的原理, 是最不可信的渠道存储和发送最短期的密钥, 按不可信到可信, 依次是:

  • 网络传输, 尽量只传输access token(多数时间使用), 少数时间传输 refresh token(一小时传输一次), 几乎不传输用户名密码(3个月一次)
  • 资源服务器, 只传输access token和验证公钥
  • APP / APP 服务器, 存储access token/ refresh token
  • 用户的大脑/用户的操作系统/授权服务器可以加密存储用户名密码, 授权服务器存储用于验证的公钥私钥

一、OAuth 2.0 核心概念与授权流程

OAuth 2.0 是 授权协议(非认证协议),核心目标是让第三方应用(客户端)在不获取用户账号密码的情况下,安全获取用户在资源服务器上的授权访问权限。

1. 核心角色

  • 资源所有者(User):用户本人(如微信用户),拥有资源的访问权限。
  • 客户端(Client):第三方应用(如小程序、APP),需要访问用户的资源。
  • 授权服务器(Authorization Server):验证用户身份并颁发授权凭证(如 Token)的服务器(如微信开放平台)。
  • 资源服务器(Resource Server):存储用户资源的服务器(如微信用户信息服务器),需验证 Token 合法性后提供资源。

2. 核心授权流程(通用四步)

OAuth 2.0 定义了多种授权模式(授权码模式、密码模式、客户端凭证模式、隐式模式),其中 授权码模式 是最安全、最常用的模式(适用于有后端的应用,如小程序+云开发/自建后端),流程如下:

    客户端->>授权服务器: 1. 请求授权(携带客户端ID、回调地址、权限范围)
    授权服务器->>资源所有者: 2. 询问用户是否授权(如微信登录弹窗)
    资源所有者->>授权服务器: 3. 用户同意授权
    授权服务器->>客户端: 4. 返回授权码(Authorization Code)
    客户端->>授权服务器: 5. 用授权码+客户端密钥,请求访问令牌(Access Token)
    授权服务器->>客户端: 6. 返回访问令牌(+可选刷新令牌 Refresh Token)
    客户端->>资源服务器: 7. 用访问令牌请求资源(如用户信息)
    资源服务器->>客户端: 8. 验证令牌合法,返回资源

3. 关键说明

  • 授权码:短期有效(通常分钟级),仅用于兑换访问令牌,泄露风险低。
  • 访问令牌(Access Token):客户端访问资源的核心凭证,有效期较短(如1小时),避免长期泄露风险。
  • 刷新令牌(Refresh Token):长期有效(如30天),用于访问令牌过期后,无需用户再次授权,直接兑换新的访问令牌(仅授权码模式支持)。
  • 客户端密钥(Client Secret):客户端在授权服务器注册时获取的密钥,需保存在后端(如小程序云函数、自建服务器),禁止暴露在前端(如小程序页面代码)。

二、JWT 核心概念与作用

JWT(JSON Web Token)是 令牌格式标准,用于在各方之间安全传递结构化信息(如用户身份、权限),本质是一种“自包含”的令牌(Token),无需查询数据库即可验证合法性。

1. JWT 结构(三部分用 . 连接)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiSmFja3kiLCJleHAiOjE3MzYxNTM2MDAsImlhdCI6MTczNjE0OTAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • 头部(Header):指定签名算法(如 HS256、RS256)和令牌类型(JWT)。
    { "alg": "HS256", "typ": "JWT" }
  • 载荷(Payload):存储核心信息(如用户ID、权限、过期时间),默认不加密(仅Base64编码),禁止存储敏感信息(如密码)。
    { "userId": 1, "userName": "Jack", "exp": 1736153600, "iat": 1736149000 }
    • 标准字段:exp(过期时间)、iat(签发时间)、sub(主题,如用户ID)等。
    • 自定义字段:如 role(角色)、scope(权限范围)等。
  • 签名(Signature):通过头部指定的算法,用密钥对“头部+载荷”进行签名,用于验证令牌是否被篡改。
    • 对称加密(HS256):用同一个密钥签名和验证(适合内部系统)。
    • 非对称加密(RS256):用私钥签名、公钥验证(适合跨系统,如授权服务器签发、资源服务器验证)。

2. JWT 核心优势

  • 无状态:资源服务器无需存储令牌信息,仅通过签名验证即可确认令牌合法性(依赖 exp 字段控制过期)。
  • 跨平台/跨语言:基于JSON格式,支持所有主流语言(Java、Node.js、Python等)。
  • 自包含:载荷可携带必要信息(如用户ID),减少数据库查询(无需查用户信息)。

三、OAuth 2.0 与 JWT 的关系:互补而非替代

OAuth 2.0 是 授权框架(定义“如何获取授权”的流程),JWT 是 令牌格式(定义“授权凭证”的结构),二者常结合使用,但并非强制绑定。

1. 结合使用的典型场景(最常用)

OAuth 2.0 的授权服务器颁发的 访问令牌(Access Token) 可以是 JWT 格式,流程如下:

  1. 客户端通过 OAuth 2.0 授权码模式,从授权服务器获取 JWT 格式的 Access Token。
  2. 客户端携带该 JWT 令牌请求资源服务器。
  3. 资源服务器收到令牌后,无需调用授权服务器验证(无状态),直接用公钥/密钥验证签名和过期时间。
  4. 验证通过后,从 JWT 载荷中提取用户ID、权限等信息,返回对应资源。

2. 二者的核心区别

维度 OAuth 2.0 JWT
本质 授权协议(流程规范) 令牌格式标准(数据结构)
作用 解决“第三方应用如何安全获取授权” 解决“如何安全传递令牌信息”
是否必须绑定 否(Access Token 可是任意格式) 否(JWT 可独立用于内部系统身份验证)
状态性 可状态(如存储令牌到数据库) 无状态(令牌自包含验证信息)
安全性 依赖流程设计(如授权码模式) 依赖签名算法(如 RS256)和密钥管理

3. 不结合的情况(了解即可)

  • OAuth 2.0 可使用非 JWT 格式的 Access Token(如随机字符串):此时资源服务器需要调用授权服务器的接口验证令牌合法性(有状态,需查询数据库/缓存)。
  • JWT 可独立使用(不依赖 OAuth 2.0):如内部系统的登录认证(用户登录后,服务器签发 JWT 令牌,客户端后续请求携带该令牌)。

四、实际开发中的应用(以小程序为例)

小程序对接第三方登录(如微信登录)时,常结合 OAuth 2.0 和 JWT,流程如下:

  1. OAuth 2.0 授权流程
    • 小程序通过 wx.login() 获取临时登录凭证 code
    • 小程序后端(云函数/自建服务器)将 code、小程序 appidappsecret 发送到微信授权服务器,兑换用户唯一标识 openid(相当于 OAuth 2.0 的 Access Token 流程)。
  2. JWT 签发与使用
    • 后端获取 openid 后,生成 JWT 令牌(载荷包含 openid、过期时间等),用 HS256 算法签名。
    • 后端将 JWT 令牌返回给小程序,小程序存储在 wx.setStorageSync() 中。
    • 后续小程序请求后端接口时,在请求头 Authorization: Bearer <JWT令牌> 中携带令牌。
    • 后端接口收到请求后,验证 JWT 签名和过期时间,提取 openid 识别用户身份,执行对应业务逻辑。

五、关键注意事项

  1. JWT 不加密载荷:载荷仅 Base64 编码,任何人都可解码,禁止存储敏感信息(如密码、手机号)。
  2. 密钥安全
    • 对称加密(HS256)的密钥需严格保密,避免泄露。
    • 推荐使用非对称加密(RS256),私钥仅授权服务器持有(用于签发令牌),公钥可公开给资源服务器(用于验证)。
  3. 令牌过期控制
    • JWT 的 exp 字段必须设置(短期有效,如1小时),避免令牌长期泄露风险。
    • 结合 OAuth 2.0 的 Refresh Token,实现令牌自动刷新(无需用户重新登录)。
  4. 小程序端注意
    • 禁止在前端存储 appsecret、JWT 签名密钥等敏感信息。
    • JWT 令牌建议放在请求头中传递,避免放在 URL 中(易被日志记录)。

总结

  • OAuth 2.0 是“授权的流程规则”,解决第三方应用如何安全获取用户授权的问题。
  • JWT 是“令牌的包装格式”,解决令牌如何安全、无状态传递信息的问题。
  • 实际开发中,二者常结合使用:OAuth 2.0 负责授权流程,JWT 作为 Access Token 的格式,实现无状态、高效的资源访问验证。

OAuth2.1并非全新协议,而是OAuth2.0的安全加固与规范整合版本,核心是剔除不安全流程、强化安全约束,简化开发者的安全配置成本,二者的核心区别集中在授权流程、安全机制、令牌使用等多个关键维度,具体如下:

  1. 授权流程的取舍

    流程类型 OAuth2.0 OAuth2.1
    授权码模式+PKCE PKCE是可选扩展,仅推荐公共客户端(如单页应用)使用,机密客户端(如后端服务器)可省略 强制所有客户端(无论公共还是机密)使用,通过code_verifier和code_challenge参数防止授权码被截获后滥用,成为授权码流程的必备条件
    隐式授权模式 支持该模式,通过URL直接向前端返回访问令牌,曾用于单页应用,但令牌易通过浏览器历史、日志等泄露 彻底废除,改用“授权码模式+PKCE”替代,解决原模式的令牌泄露和无法使用刷新令牌的问题
    资源所有者密码模式 允许该模式,客户端可直接收集用户用户名和密码换取令牌,虽不推荐但未强制禁止 完全移除该模式,因其违背OAuth委托授权的核心理念,不仅扩大密码泄露风险,还无法支持多因素认证等高级安全机制

    除此之外,OAuth2.1仅保留授权码模式(+PKCE)客户端凭证模式两种核心流程,前者适配绝大多数应用场景,后者用于服务端之间的机器通信;而OAuth2.0流程选择更多,开发者需自行判断场景适配性,容易出错。

  2. 安全机制的强化

    • 重定向URI校验:OAuth2.0对重定向URI匹配要求宽松,允许通配符或部分匹配,易被恶意篡改导致授权码泄露;OAuth2.1强制要求精确字符串匹配,彻底杜绝此类开放重定向攻击。
    • 客户端认证:OAuth2.1中所有非公共客户端必须执行严格的客户端认证,如使用Client ID+Secret或证书等方式;而OAuth2.0仅对机密客户端有认证要求,公共客户端无强制约束。
    • 令牌传输安全:OAuth2.0允许在URL查询参数中携带访问令牌,极易泄露;OAuth2.1明确禁止这种用法,要求令牌仅通过HTTP的Authorization请求头传输,降低泄露风险。
  3. 刷新令牌的安全约束

    • OAuth2.0对刷新令牌的约束较弱,部分实现中刷新令牌可被重复使用,且未强制绑定客户端,存在被盗用后持续获取访问令牌的风险。
    • OAuth2.1大幅强化刷新令牌安全性:一是要求刷新令牌与特定客户端实例绑定(发送者约束);二是推行“刷新令牌轮换”,每次使用后旧令牌立即失效,同时返回新的刷新令牌,避免刷新令牌被盗用后的持续风险,且明确刷新令牌仅能用于获取新访问令牌,不可直接访问资源。
  4. 规范整合与兼容性

    • OAuth2.0基于RFC 6749和RFC 6750制定,后续的安全扩展(如PKCE、客户端元数据规范等)未整合进核心协议,开发者需自行集成以提升安全性。
    • OAuth2.1主动整合了多个后续安全相关的RFC规范,比如RFC 7636(PKCE)、RFC 8252(移动和单页应用最佳实践)、RFC 8414(授权服务器元数据),将这些最佳实践固化为核心规范,无需开发者额外集成扩展功能。

综上,OAuth2.1更适合新项目开发,尤其是金融、医疗等对安全性要求高的场景;而OAuth2.0仅适合旧系统维护,且建议逐步迁移至OAuth2.1的安全规范,降低安全漏洞风险。

dotenv是根据十二因素应用方法论创建的, 这是个什么东西呢?

The Twelve-Factor App 方法论:云原生应用的设计准则

The Twelve-Factor App(十二因素应用)是 2011 年由 Heroku 联合创始人 Adam Wiggins 提出的一套云原生应用设计方法论,核心目标是解决应用在开发、部署、扩展、维护全生命周期中的一致性问题,尤其适用于分布式系统、微服务架构和容器化部署场景(如 Docker、K8s)。

它并非强制标准,而是基于无数云原生项目实践总结的“最佳实践集合”,本质是让应用具备 可移植性、可扩展性、可维护性,同时降低团队协作成本(开发环境与生产环境一致、部署流程自动化)。

十二因素的核心原则(附通俗解释与实践建议)

每个因素都针对云原生场景的典型痛点,以下按逻辑分类拆解,结合开发者常用的技术栈(如 Node.js/Java/Python、Docker、Git、云服务)说明实践方式:

一、基础架构:分离与隔离(1-3 因素)

1. 基准代码(Codebase)

  • 核心原则:一份基准代码,多份部署(环境隔离)。
  • 通俗解释:所有环境(开发、测试、生产)共享同一套代码仓库(如 Git),通过分支/标签区分版本(而非多份独立代码)。
  • 痛点解决:避免“开发环境能跑,生产环境报错”(代码不一致导致的兼容问题)。
  • 实践建议
    • 用 Git 管理代码,开发分支(dev)、测试分支(test)、生产分支(main)分离。
    • 禁止在生产环境直接修改代码,所有变更通过“提交-合并-部署”流程。

2. 依赖(Dependencies)

  • 核心原则:显式声明并隔离依赖,禁止依赖系统级库。
  • 通俗解释:应用所需的第三方库(如 npm 包、Maven 依赖)必须通过配置文件(如 package.jsonpom.xml)明确声明,且使用依赖隔离工具(如 npm install --savevirtualenv),避免依赖本地环境的“隐式依赖”。
  • 痛点解决:避免“本地能跑,部署后缺依赖”(环境差异导致的依赖缺失)。
  • 实践建议
    • 前端/Node.js:用 package.json 声明依赖,package-lock.json 锁定版本。
    • Java:用 pom.xml(Maven)或 build.gradle(Gradle)声明依赖,禁止手动添加 JAR 包。
    • 部署时通过 npm cimvn clean package 自动安装依赖,不依赖宿主机器的全局依赖。

3. 配置(Config)

  • 核心原则:配置(环境变量、密钥等)与代码分离,存储在环境中。
  • 通俗解释:数据库地址、API 密钥、环境标识(dev/prod)等可变配置,不能硬编码在代码里,应通过环境变量(如 process.env.DB_URL)或配置服务(如 Nacos、Consul)注入。
  • 痛点解决:避免“切换环境需改代码”(配置与代码耦合导致的部署低效)、“密钥泄露”(硬编码在代码仓库)。
  • 实践建议
    • 开发环境:用 .env 文件(配合 dotenv 库)管理环境变量(不上传 Git)。
    • 生产环境:通过云服务(如阿里云/腾讯云的“环境变量配置”)或容器编排工具(K8s ConfigMap/Secret)注入配置。
    • 敏感信息(如数据库密码)用加密存储(如 K8s Secret、云服务的密钥管理),禁止明文传输。

二、运行时:进程与服务(4-6 因素)

4. 后端服务(Backing Services)

  • 核心原则:将后端服务(数据库、缓存、消息队列等)视为“附加资源”,通过配置与应用解耦。
  • 通俗解释:应用与后端服务(如 MySQL、Redis、RabbitMQ)的连接方式完全通过配置(环境变量)定义,更换后端服务(如本地 MySQL 切换为云数据库)无需修改代码。
  • 痛点解决:避免“绑定特定后端服务”(如硬编码数据库地址,导致迁移困难)。
  • 实践建议
    • 数据库连接:用 process.env.DB_URL 而非硬编码 mysql://localhost:3306/db
    • 本地开发用 Docker 启动后端服务(如 docker run mysql),与生产环境的服务类型一致(避免本地用 SQLite,生产用 MySQL)。

5. 构建、发布、运行(Build, Release, Run)

  • 核心原则:严格区分三个阶段,避免发布后修改代码。
    • 构建(Build):将代码编译为可执行文件(如 JAR 包、前端打包后的静态文件),安装依赖。
    • 发布(Release):将“构建产物 + 配置”打包为不可变的发布版本(如 Docker 镜像,标签为 v1.0.0)。
    • 运行(Run):启动发布版本的进程,不修改发布产物。
  • 痛点解决:避免“发布后手动修改配置/代码”(导致版本混乱,回滚困难)。
  • 实践建议
    • 用 CI/CD 工具(如 Jenkins、GitHub Actions、GitLab CI)自动化构建-发布流程。
    • 构建产物用 Docker 镜像存储(镜像不可修改,修改需重新构建),标签用版本号(如 app:v1.0.0)。
    • 回滚时直接切换镜像标签(如从 v1.0.1 切回 v1.0.0),无需重新构建。

6. 进程(Processes)

  • 核心原则:应用以无状态进程运行,数据存储在后端服务中。
  • 通俗解释:应用进程不存储任何本地状态(如会话数据、临时文件),所有状态通过后端服务(如 Redis 存会话、S3 存文件)共享。进程可随时启停、水平扩展(多实例部署)。
  • 痛点解决:避免“水平扩展时状态不一致”(如会话存在本地,多实例部署后用户登录状态丢失)。
  • 实践建议
    • 会话存储:用 Redis 替代 sessionStorage 或本地 Cookie 存储登录状态。
    • 临时文件:上传至对象存储(如阿里云 OSS、腾讯云 COS),而非本地磁盘。
    • 进程设计为“一次性”:收到停止信号(如 SIGTERM)时,优雅关闭连接(如数据库连接、HTTP 请求),不依赖进程重启后保留状态。

三、扩展性与可观测性(7-12 因素)

7. 端口绑定(Port Binding)

  • 核心原则:应用通过端口绑定暴露服务,自身即是独立服务,无需依赖反向代理的“注入”。
  • 通俗解释:应用启动时监听一个端口(如 3000),通过 http://localhost:3000 提供服务,部署时通过反向代理(如 Nginx、Traefik)转发请求,而非依赖宿主机器的端口映射(如 Tomcat 绑定 8080 需手动配置)。
  • 痛点解决:避免“部署时端口冲突”(应用依赖固定端口,多应用部署时冲突)。
  • 实践建议
    • 应用端口通过环境变量配置(如 process.env.PORT || 3000),不硬编码。
    • Docker 镜像启动时通过 -p 80:3000 映射端口,K8s 用 Service 暴露端口。

8. 并发(Concurrency)

  • 核心原则:通过进程水平扩展实现并发,而非单进程多线程。
  • 通俗解释:应用基于“进程模型”设计,通过增加进程实例(如多容器部署)提升并发能力,而非依赖单进程内的多线程(如 Java 线程池、Node.js 集群模式)。
  • 痛点解决:避免“单进程瓶颈”(单进程多线程受限于 CPU 核心数,扩展能力弱)。
  • 实践建议
    • 用容器编排工具(K8s)或云服务(如 ECS 弹性伸缩)自动扩缩容(根据 CPU/内存使用率增加/减少实例)。
    • 无状态设计是前提(见第 6 因素),确保多实例可共享负载。

9. 易处理(Disposability)

  • 核心原则:进程可快速启动和优雅关闭,支持动态扩缩容和故障转移。
  • 通俗解释:应用启动时间应尽可能短(秒级),关闭时能处理完当前请求(不丢失数据),意外终止(如宕机)时无副作用(因状态存储在后端服务)。
  • 痛点解决:避免“扩缩容慢”(启动时间长导致弹性伸缩失效)、“宕机丢失数据”(进程内存储状态)。
  • 实践建议
    • 优化启动流程:减少初始化操作(如懒加载数据库连接,而非启动时创建所有连接)。
    • 优雅关闭:监听进程停止信号(如 Node.js 的 process.on('SIGTERM', () => { ... })),关闭数据库连接、提交未完成的事务。
    • 避免长时间任务:将耗时操作(如文件上传、数据导出)拆分为异步任务(如用 RabbitMQ 队列),进程仅处理短请求(毫秒/秒级)。

10. 开发环境与生产环境一致(Dev/Prod Parity)

  • 核心原则:缩小开发、测试、生产环境的差异(时间差异、人员差异、工具差异)。
  • 通俗解释:开发环境应尽可能模拟生产环境(如用相同的后端服务版本、相同的部署方式),避免“开发环境能跑,生产环境报错”(如本地用 MySQL 5.7,生产用 MySQL 8.0 导致语法兼容问题)。
  • 痛点解决:减少“环境差异导致的线上 Bug”,降低部署风险。
  • 实践建议
    • 用 Docker Compose 搭建本地开发环境(与生产环境的容器化部署一致)。
    • 后端服务版本统一(如生产用 Redis 6.x,开发环境也用 6.x)。
    • 避免开发环境用“简化工具”(如本地用 SQLite 替代 MySQL,测试用 Mock 替代真实接口),尽量用真实服务。

11. 日志(Logs)

  • 核心原则:应用将日志视为“事件流”,输出到标准输出(stdout),不管理日志文件。
  • 通俗解释:应用不创建、不轮转日志文件(如 app.logapp.2024.log),仅将日志以文本形式输出到控制台(stdout/stderr),由部署环境(如 Docker、K8s、日志收集工具)统一收集、存储、分析。
  • 痛点解决:避免“日志文件分散”(多实例部署时日志存在不同机器,难以排查问题)、“日志轮转导致的性能问题”。
  • 实践建议
    • 前端/Node.js:用 console.log 或日志库(如 winston)输出 JSON 格式日志(便于解析)。
    • 后端(Java):用 Logback/Log4j 输出到 stdout,日志格式包含时间、级别、请求 ID。
    • 日志收集:用 ELK Stack(Elasticsearch+Logstash+Kibana)或云服务日志工具(如阿里云 SLS)收集日志,支持按请求 ID 追踪、全文检索。

12. 管理进程(Admin Processes)

  • 核心原则:管理任务(如数据库迁移、脚本执行)作为一次性进程,与应用主进程使用同一套代码和依赖。
  • 通俗解释:数据库迁移(如 ALTER TABLE)、数据初始化、定时任务等管理操作,应作为独立的一次性进程运行,使用与应用主进程相同的代码仓库和依赖(避免依赖差异导致执行失败)。
  • 痛点解决:避免“管理脚本与应用代码不一致”(如迁移脚本用旧版依赖,导致语法错误)。
  • 实践建议
    • 用命令行工具执行管理任务(如 Node.js 的 npm run migrate、Java 的 java -jar app.jar migrate)。
    • 定时任务(如每日统计)用外部调度工具(如 Crontab、Airflow、K8s CronJob),而非应用内置定时器(如 setInterval)。
    • 管理进程执行时,使用与生产环境相同的配置(环境变量)。

十二因素的适用场景与例外情况

适用场景

  • 云原生应用(部署在 Docker、K8s、云服务器)。
  • 微服务架构(多个小应用协同工作,需统一设计规范)。
  • 团队协作开发(需保证环境一致、部署流程标准化)。
  • 需频繁部署、扩缩容的应用(如互联网产品、API 服务)。

例外情况

  • 桌面应用(依赖本地资源,无需云部署,可不遵循端口绑定、日志输出等原则)。
  • 嵌入式系统(资源受限,难以实现进程隔离、水平扩展)。
  • 遗留系统改造(可逐步适配,无需一次性满足所有因素)。

总结:十二因素的核心价值

十二因素的本质是“标准化”和“解耦”:

  • 标准化:统一开发、部署、维护的流程,降低团队协作成本(新成员快速上手,无需熟悉“特殊配置”)。
  • 解耦:代码与配置、依赖、后端服务解耦,让应用具备“一次构建,到处运行”的可移植性,同时支持无缝扩展和快速迭代。

对于现代开发者(尤其是云原生、微服务方向),十二因素是必须掌握的基础方法论,它不仅能解决实际开发中的诸多痛点,也是设计“健壮、可扩展、易维护”应用的核心指导思想。

官方资料https://www.npmjs.com/package/dotenv
dotenv本质上是一个非常简单的库, 仅仅用来从.env文件中加载环境变量, 其中存储和代码无关的内容或者不能放在代码中的内容 ,尤其是api_key等安全密钥等. .env文件通过.gitignore忽略.
用来替代windows中的set some_var=value和linux中的export some_var=value

运行

很简单的运行方式:

require('dotenv').config()
console.log(process.env) // remove this after you've confirmed it is working

除此之外, 还可以通过命令行运行而不需要加到代码中

 node -r dotenv/config your_script.js

.env文件定义

.env文件的定义方式, 如果值中包括 #, `,\n`转义字符,回车换行等, 需要加双引号, 否则可以使用不加双引号的简写方式.

S3_BUCKET="YOURS3BUCKET"
SECRET_KEY="YOURSECRETKEYGOESHERE"
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...
Kh9NV...
...
-----END RSA PRIVATE KEY-----"
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nKh9NV...\n-----END RSA PRIVATE KEY-----\n"
# This is a comment
SECRET_KEY=YOURSECRETKEYGOESHERE # comment
SECRET_HASH="something-with-a-#-hash"

官方协议文档: https://modelcontextprotocol.io/docs
分为数据层data layer和传输层transport layer

data layer

使用 JSONRPC2.0作为通讯协议, 包括生命周期管理和原语.

生命周期管理

MCP是有状态的协议, 所以需要生命周期管理, 目的是客户端和服务器协商彼此支持的能力(原语).

原语 primitives

包括服务器暴露的原语和客户端暴露的原语, 以及跨领域功能原语.

服务器原语包括:

- 工具 Tools 资源 Resources 提示词 Prompts
控制者 大模型 应用 用户
工具 tools

包括 tools/listtools/call
示例:

{
  name: "searchFlights",
  description: "Search for available flights",
  inputSchema: {
    type: "object",
    properties: {
      origin: { type: "string", description: "Departure city" },
      destination: { type: "string", description: "Arrival city" },
      date: { type: "string", format: "date", description: "Travel date" }
    },
    required: ["origin", "destination", "date"]
  }
}

工具调用在必要时通过User Interaction Model用户交互模型让用户确认.

资源 resources

每个资源都有一个唯一的 URI(例如, file:///path/to/document.md ),并声明其 MIME 类型以进行适当的内容处理。
资源支持两种发现模式:

  • 直接资源Direct Resources- 指向特定数据的固定 URI。示例: calendar://events/2024 - 返回 2024 年的日历可用性
  • 资源模板Resource Templates - 带参数的动态 URI,用于灵活查询。如: travel://activities/{city}/{category} - 按城市和类别返回活动, 或者: travel://activities/barcelona/museums - 返回巴塞罗那的所有博物馆
    操作包括: resources/list, resources/templates/list, resources/read, resources/subscribe
    资源模板示例:
    
    {
    "uriTemplate": "weather://forecast/{city}/{date}",
    "name": "weather-forecast",
    "title": "Weather Forecast",
    "description": "Get weather forecast for any city and date",
    "mimeType": "application/json"
    }

{
"uriTemplate": "travel://flights/{origin}/{destination}",
"name": "flight-search",
"title": "Flight Search",
"description": "Search available flights between cities",
"mimeType": "application/json"
}

支持参数补全和用户交互模型.

Prompts 提示系统

提示提供可重用的模板。它们允许 MCP 服务器作者为特定领域提供参数化提示,或展示如何最佳使用 MCP 服务器。
支持方法: prompts/list, prompts/get
示例:

{
  "name": "plan-vacation",
  "title": "Plan a vacation",
  "description": "Guide through vacation planning process",
  "arguments": [
    { "name": "destination", "type": "string", "required": true },
    { "name": "duration", "type": "number", "description": "days" },
    { "name": "budget", "type": "number", "required": false },
    { "name": "interests", "type": "array", "items": { "type": "string" } }
  ]
}
#### 客户端原语包括: 
##### 采样 `sampling/complete`
采样允许服务器通过客户端请求 LLM 补全而不需要重复内建对LLM的调用,从而实现代理式工作流程。这种方法将客户端置于对用户权限和安全措施的完全控制之下。

##### 提取 `elicitation/requestInput`
提取使服务器能够在交互过程中请求特定信息,为服务器按需收集信息提供了一种结构化方式。
提示是结构化的模板,定义了预期的输入和交互模式。它们由用户控制,需要显式调用而非自动触发。提示可以感知上下文,引用可用的资源和工具以创建全面的流程。类似于资源,提示支持参数完成,以帮助用户发现有效的参数值。
支持方法: `prompts/list` 和 `prompts/get`

##### 根 Roots
根允许客户端指定服务器应关注的目录,通过协调机制传达预期范围。主要用作AI操作文件服务.
结构样式:
```json
{
  "uri": "file:///Users/agent/travel-planning",
  "name": "Travel Planning Workspace"
}
日志 logging

跨领域的功能原语 utility primitives

前有1个

  • Tasks 支持延迟结果获取和状态跟踪

transport layer

支持 stdio和treamable HTTP两种传输机制. 使用 HTTP POST 进行客户端到服务器的消息传递,并可选使用 Server-Sent Events 实现流式功能. 该传输支持远程服务器通信,并支持标准 HTTP 认证方法,包括授权令牌、API 密钥和自定义头信息。MCP 推荐使用 OAuth 获取认证令牌。

MCP协议使用JSONRPC作为底层数据层的传输协议. 这是一个及其简单明了的协议, 官方定义在这儿: https://www.jsonrpc.org/specification

简单说明:

  • JSONRPC是客户端向服务器发起一个请求Request, 服务器返回一个响应Response.
  • 客户端发起的请求有id时, 服务器必须响应并带上id, 如果客户端发起的请求没有id, 则这个请求是一个"通知", 服务器不需要响应.
  • 服务器的响应中必须有id, 如果这个id是null, 则说明客户端给的id有问题
    request格式:
    {
    "jsonrpc": "2.0",  // 必选, 值必须是"2.0"
    "method": "subtract", // 必选, 值为string
    "params": [42, 23],  // 可选, 按具体method的要求
    "id": 1 // 要返回值时必选, 不要返回值是必须省略
    }

    response 成功时格式:

    {
    "jsonrpc": "2.0", // 必选, 值必须是"2.0"
    "result": 19, // 成功时必须有, 失败时必须没有
    "id": 1 // 必须有, 与客户端request中一致
    }

    response 失败时格式

    {
    "jsonrpc": "2.0", 
    "error": { // 失败时必须有, 成功时必须没有
      "code": -32601, // 必选, 为整数. 其中从-32768 到-32000 范围内的错误代码被保留用于预定义错误
      "message": "Method not found" // 必选, 为string, 一句话描述错误.
    }, 
    "id": "1" // 必选, 解析不了client发来的id时候返回null
    }

    更多示例请看官方文档. https://www.jsonrpc.org/specification

一个阳光明媚的清晨,小猫从睡梦中醒了过来,迷迷糊糊的睁开了眼睛。忽然他发现一只蝴蝶停在房间的墙上。蝴蝶长得很漂亮,身上闪着五颜六色的光。
小猫说:“蝴蝶蝴蝶,你真漂亮!你是从哪里来的呀?”
蝴蝶说:“我是从那边的大森林飞过来的。”
小猫说:“我知道,那是古代的魔法大森林!你一定会魔法吧?”
蝴蝶说:“那边有上千年的魔法还有我刚学的新魔法你要不要一起试试呀?”
小猫说:“好呀好呀那我们一起去大森林玩吧!”
蝴蝶说:“那好我带你去。”蝴蝶在空中转了一个八字,身体变大了数倍,让小猫骑在他的背上,一下就飞到了空中。
时间好像倒流了数亿年,他们来到了远古时代的大森林。这里长着各种小猫没有见过的奇花异草。有高耸入云的大树,还有长得像蚂蚁一样大小的小鸟。
蝴蝶看着小鸟,看好像有点害怕。蝴蝶说:“不用害怕这里的日常就是这样的。”
小猫说:“你带我参观一下吧!”
小猫仔细的瞧了瞧森林里的植物和动物,蝴蝶一边飞一边给他介绍这里真的很漂亮又好神奇,很快一天就过去了。
天很快就黑了。“我们回去吧!”说完小猫和蝴蝶就在一处大榕树下告别了。

看了最新一期的罗永浩的十字路口与刘谦的访谈,有感几点:
1、刘谦说努力没那么重要,运气占90%。他说出这个我就相信他说的话都是真的。与我运势学相通。很巧,这和马斯克说的努力10次成功一次的比例概率一模一样。
2、刘谦就此得出第2点结论:享受创造的过程而不是追求成功的结果。因为成功的结果可能性在10%以内,甚至现实往往还要更低得多。并且把这一点放在了孩子的教育上。与此同时他又说了两个例外,一个是不善交际的他做街头魔术必须要与各色人等交谈,另一个是直播带货,这两天对他来说言语方面成长很多。
3、外表和包装真的非常重要,尤其对一个刚刚起步的个体来说。刘谦说他花1万多人民币买套西装,在网站不发达的时候自己做网站还包装出好几个虚拟工作人员来伪装自己的一人公司。而且这个习惯一直持续到现在,他的外在形象包括香水发型衣服全部要自己操办不放心交给别人。这让我想到迈克尔杰克逊、乔布斯都是极其注重外观的。我看不上的那些 html css动画却真的是产品非常重要的一部分。
4、刘谦反复讲了好多次让他的观众感受哇哦一刻,甚至这一刻可以在他的一生中反复跟别人去诉说,变成一个人生的重要经历。然后魔术师为了这一刻苦练多年、准备良久。不能给别人留下这一刻就绝不出手。我想这一刻就是人生的幸福体验。人生留在脑子里值得回味的幸福体验其实很少,无论做任何一个职业,如果能给别人留下一刻的幸福体验并且记住一生那其实是非常了不起、非常有意义的事情。我觉得这可能才是艺术的定义:给别人留下铭刻一生的体验。一个产品如果能做到这一点那他就是个艺术品。

微信小程序云函数超时的问题。微信小程序,云函数有几个超时的问题。其中1个是访问cloud api的时候,这个是可以在云函数端cloud.init的时候设置。但是还有1个调用云函数超时的问题。每次调用函数运行的最长时间不能超过3秒钟。如果超过,则会返回超时。 1般情况下,只有几100毫秒就可以完成云函数的调用,但是如果调用第3方api的时候,就可能因为第3方api处理时间过长导致超时。
让ai给我找了很多办法去延长这个超市时间,但似乎都无法延长。最后只有不使用await调用,直接进行返回,才能避免。

菜鸟的教程不错,很适合做快速入门参考。https://www.runoob.com/r/r-tutorial.html
首先不要把R语言看成正常的通用编程语言,像C/C++/Java/Js那种,而是看做特定用途的语言。虽然不像是HTML/CSS这两种语言形态那么极端。可以把R语言看成是升级版的Excel函数库,当然不是一星半点的升级了。不过,你想象R语言在操作数据表有一些语言层次的支持和天然的优势就对了。
整体语法上和非严格模式的JavaScript比较相似,在此基础上讲最重大的几个区别:

  • 与多数语言不同,.是合法的变量名字符,访问一个对象的子元素要用$
  • 与多数语言不同,安装包install.packages()、修改工作目录getwd() setwd()全在语言内,就好像本身是系统脚本bat或者bash
    与Js相似又有区别的点:
  • =可以用来赋值,但这个是后来才加的,赋值符号的原住民是<-->,是的,还能向右赋值。
  • 语句尾部不用任何符号
  • 打印是 print()
  • 函数形参接受类似python的形参名=形参值的方式

用管理员打开cmd, 运行:

# 输入
net stop MariaDB
# 输出
MariaDB 服务正在停止...
MariaDB 服务已成功停止。

手动运行mysqld服务, 打印输出并跳过权限表的检查

mysqld.exe --console --skip-grant-tables

新开一个cmd,使用mysql无密码登录:

mysql -u root

进入mysql终端, 重置密码:

FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';

切回mysqld的管理员cmd窗口, Ctrl+C结束mysqld运行, 启动mysqld服务:

net start MariaDB

测试新密码登录:

mysql -u root -p

目前来看云函数是不行的,但是函数式云托管可以。sse和websocket都需要运行在云托管或者函数式云托管中。
https://docs.cloudbase.net/cbrf/intro
似乎函数式云托管支持sse也是为了大模型:
https://docs.cloudbase.net/cbrf/example#%E4%BD%BF%E7%94%A8-server-sent-event-%E6%8E%A8%E9%80%81%E6%B6%88%E6%81%AF
官方也给出了与云托管和云函数的对比
https://docs.cloudbase.net/cbrf/vs
但是,函数型云托管的sse和ws实例都是通过context传入的。如const sse = context.sse(), const ws=contex.ws, 对于第三方库里已经封装了sse方法的(如mcp官方库 @modelcontextprotocol/sdk), 似乎就只有去修改官方库源文件才能做到兼容吧? 这样多少有些得不偿失的感觉. 此时,似乎更应该去使用正常的基于容器的云托管.
价格问题. 有两个官方文档 第一个https://cloud.tencent.com/document/product/876/113602 是对云托管的计费, 第二个 https://cloud.tencent.com/document/product/876/120342 是云托管与标准计费项的换算.
那么最重要的一点就是云托管的容器实例是持续运行还是调用以后就下线的. 从 https://docs.cloudbase.net/run/develop/developing-guide 这儿看: 服务必须是无状态服务,不能依赖永久性本地状态。这是为了能够进行水平的自动扩缩容。, 应该是调用后就下线的. 另外在 概述中也提到, 实例数量会根据请求量自动调整,无请求时可缩容到 0,不产生费用。, 这点应该可以大大降低实际费用.

AI 创作中的版权难题
在 AI 的创作中,往往会遇到版权问题。因为 AI 是模拟了一个人物,如果这个人物是一个比较著名的人物,那么他的形象、他的声音都会存在版权难题。这也许就是为什么很少公开的模拟某一个具体的现在存在的人物形象。即便是虚拟的人物,由于这个虚拟人物的归属与某一个特定的公司,那也存在版权的问题。要规避这个问题,可能只能去创作历史人物,这个人物你如果有一些现代的演绎,比如说在电影、电视剧中曾经被人扮演过,那么他就会形成一个认知,这是这个人物的形象,或者他的声音应该是什么样子的。那假如说要规避它的这个已经认知的声音和形象,就需要重新去配音和重新去生成形象。按照现在的 AI 绘画的水平,它的形象的生成很难一次性就生成自己想要的那种样子,就会出现反复多次的修改的情况。会浪费必要多的时间和精力,当然这已经比你请人去从头开始绘画好很多了,声音也是如此。如何才能仿照书不同人的声音,老年人的小孩、女人的声音,正好又让他去符合自己心目中的历史形象,是一个很大的问题。这里也估计有反复尝试,试错的过程,会使一个形象的生成,包括他的头像、声音等等都需要花费比较巨大的时间。这和简单的生成一个人物的背景提示词相比,可能就有比较大的时间成本上的差异。