跳到主要内容
稻草人
プロフィール

EP.02

💻Technology

ColdFusion 学习笔记④

登录认证与 Session

BY 稻, 草人

这一篇实现一个最小但完整的登录流程:用户提交账号和密码,服务器验证凭据,登录成功后建立 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,还是逐步拆到其他服务,都更容易测试、维护和替换。