访问控制

在服务端我们要知道当前接待的用户是谁,决定是否要给这个用户提供服务的时候还需要检查一下用户的权限,这个就是权限控制,也叫访问控制。有很多方法可以实现,比如基于角色的访问控制,就是你可以在应用里面添加一些角色,然后把这些角色分配给用户,不同的用户可以拥有一些权限,一个用户如果属于一个角色,就拥有这个角色里所有的权限。下面会记录一个检查资源拥有权的访问控制,用户在访问一个资源的时候去检查用户是否拥有这个资源,就是检查被访问的内容资源是不是这个用户发布的,如果是,就允许访问,不是的话就不允许访问,比如在更新内容和删除内容的接口上面就可以去检查用户对内容资源的拥有权。限制用户只能修改或删除自己发布的内容资源。

定义检查用户是否拥有指定资源的功能

用户发布的资源就可以认为用户拥有这个资源,下面可以设计一个检查用户对某一种资源拥有权的功能,要做这种拥有权的检查需要准备用户的 id、资源的类型、资源的 id。
auth.service.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import jwt from "jsonwebtoken";
import { PRIVATE_KEY } from "../app/app.config";
/**
* 检查用户是否拥有指定资源
*/
interface PossessOptions {
resourceId:number;
resourceType:string;
userId:number;
}
export const possess = async (options:PossessOptions)=>{
//准备选项
const {resourceId,resourceType,userId} = options;

//准备查询,查询最终结果里会包含一个count字段,值应该是1或者0,如果是1就表示用户拥有这个内容资源,如果是0就没拥有。
const statement = `
SELECT COUNT(${resourceType}.id) as count
FROM ${resourceType}
WHERE ${resourceType}.id = ? AND userId = ?
`;

//检查拥有权
const [data] = await connection
.promise()
.query(statement, [resourceId, userId]);

//提供检查结果
return data[0].count ? true : false;
}

定义访问控制中间件

auto.middleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { Request, Response, NextFunction, request } from "express";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import * as userService from "../user/user.service";
import { PUBLIC_KEY } from "../app/app.config";
import { TokenPayload } from "./auto.interface";
import { possess } from "./auth.service";

/**
* 访问控制
*/
interface AccessControlOptions {
possession?: boolean;
}
export const accessControl = (options: AccessControlOptions) => {
return async (request: Request, response: Response, next: NextFunction) => {
console.log("访问控制");

//解构选项
const { possession } = options;

//当前用户 ID
const { id: userId } = request.user;

// 管理员不做权限控制检查
if (userId == 1) return next();

// 准备资源
const resourceIdParam = Object.keys(request.params)[0];
const resourceType = resourceIdParam.replace("Id", "");
const resourceId = parseInt(request.params[resourceIdParam], 10);

// 检查资源拥有权
if (possession) {
try {
const ownResource = await possess({ resourceId, resourceType, userId });

if (!ownResource) {
return next(new Error("USER_DOES_NOT_OWN_RESOURCE"));
}
} catch (error) {
return next(error);
}
}

//下一步
next();
};
};

app.middleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 默认异常处理器
*/
export const defaultErrorHandler = (
error: any,
request: Request,
response: Response,
next: NextFunction
) => {
if (error.message) {
console.log(error.message);
}
let statusCode: number, message: string;

/**
* 处理异常
*/
switch (error.message) {
case "NAME_IS_REQUIRED":
statusCode = 400;
message = "请提供用户名";
break;
case "PASSWORD_IS_REQUIRED":
statusCode = 400;
message = "请提供用户密码";
break;
case "USER_ALREADY_EXIST":
statusCode = 409;
message = "用户名已被占用";
break;
case "USER_DOES_NOT_EXIST":
statusCode = 400;
message = "用户不存在";
break;
case "PASSWORD_DOES_NOT_MATCH":
statusCode = 400;
message = "密码不对";
break;
case "UNAUTHORIZED":
statusCode = 401;
message = "请先登录";
break;
case "USER_DOES_NOT_OWN_RESOURCE":
statusCode = 403;
message = "您不能处理这个内容";
break;
default:
statusCode = 500;
message = "服务暂时出了点问题~";
break;
}
response.status(statusCode).send({ message });
};

配置接口的访问控制

post.router.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import express from "express";
import * as postController from "./post.controller";
import { requestUrl } from "../app/app.middleware";
import { authGuard, accessControl } from "../auth/auth.middleware";

const router = express.Router();

/**
* 内容列表
*/
router.get("/posts", requestUrl, postController.index);

/**
* 创建内容
*/
router.post("/posts", authGuard, postController.store);

/**
* 更新内容
*/
router.patch(
"/posts/:postId",
authGuard,
accessControl({ possession: true }),
postController.update
);

/**
* 删除内容
*/
router.delete(
"/posts/:postId",
authGuard,
accessControl({ possession: true }),
postController.destroy
);

/**
* 导出路由
*/
export default router;