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

EP.02

💻Technology

ColdFusion 学习笔记②

CFM 与 CFC 如何通过 Ajax 通信——从 cfajaxproxy 到 fetch

BY 稻, 草人

在维护 ColdFusion 老项目的时候,经常会看到一种结构:

xxx.cfm
   ↓
JavaScript
   ↓
Ajax
   ↓
xxx.cfc
   ↓
Database

第一次看到的时候,很容易产生一个疑问:

.cfm 和 .cfc 明明都是 ColdFusion 文件,它们为什么还需要 Ajax?

原因在于:

CFM 页面已经返回给浏览器之后,
如果用户不刷新页面,又想向服务器请求新的数据,
就需要 JavaScript 再发送 HTTP 请求。

这个过程就是 Ajax。

这一篇详细整理 ColdFusion 项目里的:

CFM
+
JavaScript
+
CFC
+
Ajax

到底是如何连接起来的。

目次

1. 先理解普通 CFM 请求

例如:

user.cfm?id=100

浏览器:

GET /user.cfm?id=100

ColdFusion:

执行 user.cfm
↓
查询数据库
↓
生成 HTML
↓
返回浏览器

如果用户点击:

<button>
    查看用户
</button>

传统做法可能是:

刷新整个页面

但是 Ajax 可以做到:

不刷新页面
↓
JavaScript 请求服务器
↓
服务器返回 JSON
↓
JavaScript 更新页面

2. Ajax 的基本结构

整个流程:

CFM 页面
   │
   │ JavaScript
   ▼
HTTP Request
   │
   ▼
CFC remote function
   │
   ▼
Database
   │
   ▼
JSON
   │
   ▼
JavaScript
   │
   ▼
更新 HTML

例如:

student.cfm
↓
StudentApi.cfc
↓
getStudent()
↓
JSON

3. 为什么 CFC 函数需要 remote?

例如:

<cffunction
    name="getStudent"
    access="remote"
    returntype="struct"
    returnformat="json"
>

关键就是:

access="remote"

remote 表示:

这个方法允许通过 HTTP 从 ColdFusion 外部调用。

如果只是:

access="public"

那么其他 CFML 可以调用:

<cfset service.getStudent()>

但是 JavaScript 不能直接通过 HTTP 调用。

4. 最基础的 Remote CFC

例如:

<cfcomponent>

    <cffunction
        name="getStudent"
        access="remote"
        returntype="struct"
        returnformat="json"
    >

        <cfargument
            name="studentId"
            type="numeric"
            required="true"
        >

        <cfset var result = {}>

        <cfset result.success = true>
        <cfset result.studentId = arguments.studentId>
        <cfset result.name = "Yamada">

        <cfreturn result>

    </cffunction>

</cfcomponent>

保存为:

StudentApi.cfc

那么理论上可以通过:

StudentApi.cfc?method=getStudent&studentId=100

调用。

ColdFusion 会:

method=getStudent

识别要调用:

getStudent()

然后:

studentId=100

传给:

arguments.studentId

5. CFC 返回 JSON

例如:

<cfset result = {}>

<cfset result.success = true>
<cfset result.message = "OK">

<cfreturn result>

配合:

returnformat="json"

浏览器最终可能得到:

{
    "SUCCESS": true,
    "MESSAGE": "OK"
}

ColdFusion 版本以及序列化设置不同的时候,JSON Key 大小写有可能和预期不同。

所以实际前端处理时需要确认真实响应内容。

6. 用 jQuery Ajax 调用 CFC

老 ColdFusion 项目里非常常见:

$.ajax({
    url: "StudentApi.cfc",
    type: "POST",
    dataType: "json",
    data: {
        method: "getStudent",
        studentId: 100
    },
    success: function(response) {
        console.log(response);
    },
    error: function(xhr) {
        console.error(xhr.responseText);
    }
});

请求实际上类似:

POST /StudentApi.cfc

method=getStudent
studentId=100

ColdFusion 根据:

method

调用:

getStudent()

7. 为什么通常建议 POST?

如果请求只是查询数据:

GET

从 REST 设计角度完全正常。

例如:

GET /StudentApi.cfc?method=getStudent&studentId=100

但是一些老系统出于:

安全策略
WAF
框架设计
参数暴露
既有规范

可能统一要求:

Ajax → POST

例如:

$.ajax({
    type: "POST"
});

这样参数放在 Request Body。

不过要注意:

POST 本身并不会自动让接口变安全。

攻击者仍然可以构造 POST 请求。

真正的安全必须靠:

权限检查
Session
CSRF
参数验证
SQL 参数化
访问控制

8. CFC 中接收参数

例如:

<cffunction
    name="searchStudent"
    access="remote"
    returntype="struct"
    returnformat="json"
>

    <cfargument
        name="keyword"
        type="string"
        required="false"
        default=""
    >

</cffunction>

JavaScript:

data: {
    method: "searchStudent",
    keyword: "yamada"
}

ColdFusion:

arguments.keyword

得到:

yamada

9. Query 参数一定要参数化

不要这样:

<cfquery name="qStudent" datasource="mydb">

    SELECT *
    FROM student_master
    WHERE student_name = '#arguments.keyword#'

</cfquery>

这种写法存在 SQL Injection 风险。

应该:

<cfquery name="qStudent" datasource="mydb">

    SELECT
        student_id,
        student_name
    FROM
        student_master
    WHERE
        student_name =

        <cfqueryparam
            value="#arguments.keyword#"
            cfsqltype="cf_sql_varchar"
        >

</cfquery>

或者 Script 风格:

local.qStudent = queryExecute(
    "
    SELECT
        student_id,
        student_name
    FROM
        student_master
    WHERE
        student_name = :studentName
    ",
    {
        studentName = {
            value = arguments.keyword,
            cfsqltype = "cf_sql_varchar"
        }
    },
    {
        datasource = "mydb"
    }
);

10. 一个完整的搜索 API

例如:

component {

    remote struct function searchStudent(
        string keyword = ""
    )
    returnformat="json" {

        local.result = {
            success = false,
            message = "",
            data = []
        };

        try {

            local.qStudent = queryExecute(
                "
                SELECT
                    student_id,
                    student_name,
                    grade,
                    class_no
                FROM
                    student_master
                WHERE
                    student_name LIKE :keyword
                ORDER BY
                    grade,
                    class_no,
                    student_id
                ",
                {
                    keyword = {
                        value = "%" & arguments.keyword & "%",
                        cfsqltype = "cf_sql_varchar"
                    }
                },
                {
                    datasource = "mydb"
                }
            );

            local.result.success = true;

            for (
                local.row in local.qStudent
            ) {

                arrayAppend(
                    local.result.data,
                    {
                        studentId = local.row.student_id,
                        studentName = local.row.student_name,
                        grade = local.row.grade,
                        classNo = local.row.class_no
                    }
                );

            }

        }
        catch (any e) {

            local.result.message =
                "検索処理でエラーが発生しました。";

        }

        return local.result;

    }

}

11. 前端接收 JSON

例如:

$.ajax({
    url: "StudentApi.cfc",
    type: "POST",
    dataType: "json",
    data: {
        method: "searchStudent",
        keyword: $("#keyword").val()
    },
    success: function(response) {

        if (!response.success) {

            alert(response.message);

            return;
        }

        console.log(response.data);

    }
});

这样就形成:

CFM
↓
JavaScript
↓
POST
↓
CFC
↓
SQL
↓
JSON
↓
JavaScript

12. 使用 Fetch API

现在如果不是受到旧项目限制,我个人会更喜欢原生:

fetch("StudentApi.cfc", {
    method: "POST",
    headers: {
        "Content-Type":
            "application/x-www-form-urlencoded"
    },
    body: new URLSearchParams({
        method: "searchStudent",
        keyword:
            document.querySelector("#keyword").value
    })
})
.then(response => {

    if (!response.ok) {
        throw new Error(
            "HTTP Error: " + response.status
        );
    }

    return response.json();

})
.then(data => {

    console.log(data);

})
.catch(error => {

    console.error(error);

});

这样不依赖 jQuery。

13. application/json 的情况

如果前端发送:

fetch("StudentApi.cfc?method=saveStudent", {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        studentId: 100,
        studentName: "Yamada"
    })
});

这时候数据并不会自动成为:

form.studentId

通常需要读取:

getHttpRequestData()

例如:

local.requestData = getHttpRequestData();

local.body =
    deserializeJSON(
        toString(
            local.requestData.content
        )
    );

之后:

local.body.studentId
local.body.studentName

14. cfajaxproxy 是什么?

老 ColdFusion 项目中经常看到:

<cfajaxproxy
    cfc="components.StudentApi"
    jsclassname="StudentApi"
>

ColdFusion 会自动生成一个 JavaScript Proxy。

之后 JavaScript 可以:

const api = new StudentApi();

api.getStudent(...);

看起来就像直接调用 JavaScript 对象。

实际上背后还是:

JavaScript
↓
HTTP
↓
CFC remote method

并不是 JavaScript 真的直接调用服务器函数。

15. cfajaxproxy 的基本例子

CFM:

<cfajaxproxy
    cfc="components.StudentApi"
    jsclassname="StudentService"
>

CFC:

<cfcomponent>

    <cffunction
        name="getStudent"
        access="remote"
        returntype="struct"
        returnformat="json"
    >

        <cfargument
            name="studentId"
            required="true"
        >

        <cfreturn {
            "studentId" = arguments.studentId,
            "name" = "Yamada"
        }>

    </cffunction>

</cfcomponent>

JavaScript:

const service =
    new StudentService();

service.setCallbackHandler(
    function(result) {

        console.log(result);

    }
);

service.setErrorHandler(
    function(statusCode, statusMsg) {

        console.error(
            statusCode,
            statusMsg
        );

    }
);

service.getStudent(100);

16. cfajaxproxy 和普通 Ajax 的本质区别

cfajaxproxy:

ColdFusion 帮你自动生成 JavaScript Proxy

普通 Ajax:

自己构造 HTTP Request

本质都是:

Browser
↓
HTTP
↓
CFC

17. 为什么 access=”remote” 是安全重点?

假设有:

<cffunction
    name="deleteUser"
    access="remote"
>

很多人会误以为:

页面上没有按钮
=
别人调用不了

实际上并不是。

攻击者完全可以直接构造:

POST /UserApi.cfc

method=deleteUser
userId=100

所以 CFC 内部本身必须检查权限。

例如:

if (
    !structKeyExists(
        session,
        "userId"
    )
) {

    throw(
        type = "Unauthorized",
        message = "Unauthorized"
    );

}

进一步:

if (
    session.role != "ADMIN"
) {

    throw(
        type = "Forbidden",
        message = "Forbidden"
    );

}

18. 不要相信 JavaScript 验证

例如前端:

if (!user.isAdmin) {

    return;

}

这只是 UI 控制。

真正安全检查必须在服务器:

if (session.role != "ADMIN") {

    return {
        success = false,
        message = "権限がありません。"
    };

}

原因很简单:

JavaScript 在用户电脑运行

用户可以:

DevTools 修改
直接调用 API
使用 curl
使用 Postman

19. Ajax 出错时怎么排查?

我现在通常会按照:

① Browser Network
② HTTP Status
③ Request Parameters
④ Response
⑤ CFC method
⑥ Application.cfc
⑦ Server Log

来排查。

20. Network 面板

Chrome / Firefox:

F12
↓
Network

点击 Ajax 请求。

重点看:

Request URL
Request Method
Status Code
Request Payload
Response

例如:

POST
/components/StudentApi.cfc

参数:

method=searchStudent
keyword=yamada

响应:

{
    "success": true
}

21. HTTP 200 不一定代表业务成功

比如:

HTTP 200

但是 JSON:

{
    "success": false,
    "message": "権限がありません"
}

这代表:

HTTP 请求成功
但是业务失败

所以需要区分:

HTTP Level

和:

Business Level

22. HTTP 500

如果:

500 Internal Server Error

通常代表:

CFC
SQL
CFML
Application.cfc

出现服务器端异常。

这时候看:

Response
ColdFusion Log
Exception
Stack Trace

非常重要。

23. 最终结构

我现在理解 ColdFusion Ajax 的核心关系其实很简单:

CFM
=
页面
JavaScript
=
发请求
CFC remote
=
HTTP API
Query
=
访问数据库

最后组合:

Browser
   │
   ▼
CFM
   │
 JavaScript
   │
   ▼
Ajax POST
   │
   ▼
CFC remote
   │
   ▼
DB
   │
   ▼
Struct / Array
   │
   ▼
JSON
   │
   ▼
JavaScript
   │
   ▼
HTML 更新

只要把这张图真正理解,很多 ColdFusion 老项目中的 Ajax 代码就会突然变得容易看懂。