这一篇实现一个最小但完整的登录流程:用户提交账号和密码,服务器验证凭据,登录成功后建立 Session,受保护页面统一检查登录状态,退出时销毁会话。重点不是把按钮隐藏起来,而是让每一个受保护的服务器入口都执行认证和权限判断。
这一篇会把概念、代码、执行流程和安全边界放在一起说明。
目次
1. 这一篇要解决什么?
最终目标不是只记住一个标签或函数,而是完成下面这条链路:
login.cfm 接收账号与密码
↓
AuthService.cfc 查询用户并验证密码哈希
↓
成功后轮换 Session 标识并保存最少身份信息
↓
onRequestStart() 保护页面
↓
logout.cfm 销毁 Session
2. 先理解核心边界
这个功能至少有三个层次:
页面 / HTTP
↓
业务规则与权限
↓
数据库、文件或外部服务
页面负责收集输入和显示结果,CFC 负责验证与业务处理,底层资源只通过受控代码访问。不要让前端参数直接决定 SQL、文件路径、权限或外部资源名称。
3. 完整流程
- login.cfm 接收账号与密码
- AuthService.cfc 查询用户并验证密码哈希
- 成功后轮换 Session 标识并保存最少身份信息
- onRequestStart() 保护页面
- logout.cfm 销毁 Session
任何一步失败,都应该停止后续处理,返回稳定的错误结构,并在服务器端记录足够排查但不包含秘密的信息。
4. Application.cfc 与登录服务
component {
this.name = "ColdFusionStudyApp";
this.sessionManagement = true;
this.sessionTimeout = createTimeSpan(0, 0, 30, 0);
this.setClientCookies = true;
function onSessionStart() {
session.authenticated = false;
session.user = {};
}
function onRequestStart(required string targetPage) {
local.publicPages = ["/login.cfm", "/AuthApi.cfc"];
local.path = replace(cgi.script_name, "\", "/", "all");
if (!arrayContainsNoCase(local.publicPages, local.path)
&& (!structKeyExists(session, "authenticated") || !session.authenticated)) {
location(url = "/login.cfm", addToken = false);
}
return true;
}
}
示例刻意把参数类型、返回结构和错误边界写清楚。实际项目中,数据源、密钥、目录和第三方客户端应由配置或 Application Scope 提供,不能散落在页面代码中。
5. 登录接口
component output="false" {
remote struct function login(required string account, required string password)
returnformat="json" output="false" {
local.result = {success = false, message = "账号或密码不正确。"};
local.user = queryExecute(
"SELECT user_id, account, display_name, password_hash, role FROM users WHERE account = :account AND enabled = 1",
{account = {value = trim(arguments.account), cfsqltype = "cf_sql_varchar"}},
{datasource = application.datasource, maxrows = 1}
);
if (local.user.recordCount != 1) return local.result;
if (!application.passwordService.verify(arguments.password, local.user.password_hash)) return local.result;
sessionRotate();
session.authenticated = true;
session.user = {
id = local.user.user_id,
name = local.user.display_name,
role = local.user.role
};
local.result.success = true;
local.result.message = "";
return local.result;
}
remote struct function logout() returnformat="json" output="false" {
sessionInvalidate();
return {success = true};
}
}
这部分和前面的服务端实现属于同一条请求链路。排查问题时,不要只看页面结果,还要同时检查浏览器 Network、ColdFusion 日志和下游服务状态。
6. 安全与可靠性重点
- 密码必须通过成熟的 bcrypt、scrypt、Argon2 或 PBKDF2 实现保存,不能明文保存,也不能只做一次 SHA-256。
- 登录成功后轮换 Session 标识,退出时销毁 Session。
- 返回统一的失败消息,避免泄露账号是否存在。
- Cookie 应在 HTTPS 下设置 Secure、HttpOnly 和合适的 SameSite 属性;具体设置方式要按 ColdFusion 版本和部署容器确认。
安全检查必须由服务器执行。JavaScript 验证、隐藏按钮或改变请求方法,只能改善用户体验,不能阻止攻击者自己构造请求。
7. 常见错误
- 只在页面隐藏菜单,没有在 CFC 中检查身份
- 把完整用户 Query 放进 Session
- 登录页面也被 onRequestStart() 重定向,形成 302 循环
- 把密码或 Session ID 写进日志
这些问题往往在开发环境里不明显,但到了并发、异常数据、权限差异或外部服务故障时就会暴露。
8. 建议的排查顺序
① 确认请求 URL、HTTP 方法和 Content-Type
② 确认 Session、身份与资源权限
③ 确认参数经过验证和规范化
④ 确认数据库、文件或外部服务的真实响应
⑤ 确认 HTTP 状态码与 JSON / HTML 响应一致
⑥ 使用请求标识关联浏览器、应用与数据库日志
不要只根据页面上的一句“失败”判断原因。先找到失败发生在哪一层,再决定修复位置。
9. 测试清单
- 正常输入能够完成整个流程
- 缺少必填参数时返回明确的 400 类错误
- 未登录与无权限用户不能执行操作
- 边界长度、空值、特殊字符和重复请求得到预期结果
- 数据库或外部服务失败时不会留下半完成状态
- 日志中没有密码、令牌、密钥或敏感原文
10. 最后的理解
这一篇最重要的不是某一段语法,而是把功能理解成:
输入
↓
认证与授权
↓
验证与业务规则
↓
受控访问资源
↓
稳定响应与可追踪日志
只要这条边界清楚,代码以后无论继续留在 ColdFusion,还是逐步拆到其他服务,都更容易测试、维护和替换。

