认证和授权
鉴权
服务器 session 管理
基本原理
- 服务器自身维护一个 session 集合;
- 客户端首次访问应用,服务器生成 session id;
- 服务器通过 cookie 返回客户端 session id;
- 客户端发送请求,cookie 携带 session id;
- 服务器查询 session 集合,从而认证用户;
失效时间
- session 具有失效时间;
- 达到失效时间,服务器销毁 session,并创建新的 session;
- 一般用户在失效时间中访问服务器,服务器延长 session;
缺点
- 会话信息存储在服务器中,浪费服务器存储空间;
- 服务器集群部署时,存在 session 共享问题;
- 服务器集群部署时,存在 cookie 跨域问题;
token
基本原理
- 类似与服务器 session 管理;
- 服务器根据用户信息创建一个登录凭证 token;
- 用户 id;
- 创建时间;
- 过期时间;
- 服务器将登录凭证进行数字签名 (加密),通过 cookie 发送给客户端;
- 客户端发送请求时,通过 cookie 携带 token;
数字签名和加密
- 防止登录凭证信息被篡改;
优缺点
- 缺点;
- cookie 具有大小限制;
- 跨域问题;
- CSRF 攻击;
- 优点;
- 服务器无状态管理;
token 存储方式
- token 也可存储在其他 storage 中;
- 也可以通过 HTTP 首部,url,请求体携带;
- 但由于 token 较短,cookie 是主流方式;
JWT
JWT
- 规范统一的 token 生成标砖;
- JWT 基于用户信息,加密生成 JSON 对象;
- 客户端和服务器通信携带 JSON 对象;
- 服务器通过 JSON 对象判断用户身份;
数据结构
- Header:JWT 元数据,使用 Base64 将 JSON 对象转换为字符串;
- Payload:存放各种数据,使用 Base64 将 JSON 对象转换为字符串;
- Signature:签名,使用 Base64 将 JSON 对象转换为字符串;
存储位置
- cookie;
- localStorage;
使用方式
- 客户端与服务器通信时;
- 通过 cookie,HTTP Header,url,POST 请求体携带;
优点
- 可加密数据;
- 可用于分布式数据库;
- 使用非对称加密;
- 令牌服务器使用私钥加密 token;
- 其余服务器使用公钥解密 token;
refresh token
单 token 的局限性
- token 容易被盗用;
- 如果有效期过短,需要频繁进行登录操作;
双 token
- access token:用于鉴权的 token,有效期短;
- refresh token;
- 用于重新获取 access token 的 token,有效期长;
- 独立服务,验证方式苛刻;
单点登录
单点登录
- 用户登录一次,便可以访问所有具有对应权限的系统;
- 通过共享认证 (token,cookie 等),无论在那个系统中完成登录,便可以访问所有系统;
- 通常使用独立的 SSO 系统记录登录状态;
跨域 SSO 设计
- 用户进入 A 系统,无对应 token,跳转至 SSO;
- 用户登录,记录 SSO 登录凭证,存储至 SSO 域 storage,SSO 向系统 A 返回一个临时 token;
- 系统 A 通过临时 token 验证用户身份,验证成功下发系统 A 的 token;
- 用户记录 token 至系统 A 域 storage,通过对应 token 访问 A 系统;
- 用户进入 B 系统,无对应 token,跳转至 SSO;
- SSO 域 storage 存在 SSO 凭证,凭证经过服务器检验后,直接返回临时 token;
- 系统 B 通过临时 token 验证用户身份,验证成功下发系统 B 的 token;
- 用户记录 token 至系统 B 域 storage,通过对应 token 访问 B 系统;
前端权限管理
基础
权限管理
- 保证用户只能访问具有权限的对应资源;
内容
- 接口权限;
- 路由权限;
- 按钮权限;
- 菜单权限;
接口权限
- 接口权限即登录操作;
- 使用 jwt 实现即可,登录之后保存 token,发送请求时基于路由守卫添加 token;
- 无对应权限自动拒绝;
路由权限
基本流程
- 应用初始化挂载不需要权限的路由 (登录页);
- 进行登录操作后,服务器返回用户权限,以及有权访问的路由;
- 在路由守卫中动态添加 Routes;
局限
- 部分路由为菜单组件,不同用户可以访问不同的菜单项;
- 无法进行动态菜单;
菜单权限
目的
- 将路由与菜单进行解耦;
基本流程
- 前端预先定义好所有的菜单组件;
- 通过后端返回用户可以访问的菜单对应路由和菜单标识符;
- 根据菜单标识符动态创建菜单;
- 在路由守卫中验证该菜单对应路由能否访问;
按钮权限
- 每个按钮定义一个属性表示能被访问的角色;
- 每次渲染组件时根据用户当前角色,决定是否渲染该按钮;
认证
基础
认证流程
- 用户输入用户名和密码完成注册;
- 用户通过用户名和密码登录;
- 服务器验证通过后返回一个 token 存入 storage;
- 用户发送请求发送 token;
- 服务器通过 token 得知用户身份;
数据库设计
user
CREATE TABLE public."user"
(
user_id serial NOT NULL,
login_name text NOT NULL,
password text NOT NULL,
user_name text,
email text,
mobile text,
user_status integer NOT NULL,
login_time bigint,
last_login_time bigint,
login_count bigint,
created_time bigint NOT NULL,
updated_time bigint NOT NULL,
PRIMARY KEY (user_id),
CONSTRAINT login_name UNIQUE (login_name),
CONSTRAINT email UNIQUE (email),
CONSTRAINT mobile UNIQUE (mobile)
);
ALTER TABLE IF EXISTS public."user"
OWNER to postgres;
role
CREATE TABLE public."role"
(
role_id serial NOT NULL,
role_name text NOT NULL,
role_description text NOT NULL,
created_time bigint NOT NULL,
updated_time bigint NOT NULL,
PRIMARY KEY (role_id),
CONSTRAINT role_name UNIQUE (role_name)
);
ALTER TABLE IF EXISTS public."role"
OWNER to postgres;
permission
CREATE TABLE public."permission"
(
permission_id serial NOT NULL,
permission_name text NOT NULL,
permission_description text NOT NULL,
created_time bigint NOT NULL,
updated_time bigint NOT NULL,
PRIMARY KEY (permission_id),
CONSTRAINT permission_name UNIQUE (permission_name)
);
ALTER TABLE IF EXISTS public."permission"
OWNER to postgres;
user_role
CREATE TABLE public."user_role"
(
user_role_id serial NOT NULL,
user_id integer NOT NULL,
role_id integer NOT NULL,
status integer NOT NULL,
created_time bigint NOT NULL,
updated_time bigint NOT NULL,
PRIMARY KEY (user_role_id)
);
ALTER TABLE IF EXISTS public."user_role"
OWNER to postgres;
role_permission
CREATE TABLE public."role_permission"
(
role_permission_id serial NOT NULL,
role_id integer NOT NULL,
permission_id integer NOT NULL,
user_status integer NOT NULL,
created_time bigint NOT NULL,
updated_time bigint NOT NULL,
PRIMARY KEY (role_permission_id)
);
ALTER TABLE IF EXISTS public."role_permission"
OWNER to postgres;
代码实现 (基于 node)
路由
// /user/signup
userRoute.use("/signup", userMiddleware.signUp, userController.signUp);
// /user/signin
userRoute.use("/signin", userMiddleware.signIn, userController.signIn);
中间件
class UserMiddleware {
async signUp(req: Request, res: Response, next: NextFunction) {
try {
const { name, password }: Record<string, string> = req.body;
// Whether name or password is valid
const stringSchema = z.string({
invalid_type_error: StatusCode.NAME_OR_PWD_IS_REQUIRED.message,
});
stringSchema.parse(name);
stringSchema.parse(password);
// Whether name is already existed
const user = await prisma.user.findUnique({
where: {
login_name: name,
},
});
if (user) throw new Error(StatusCode.NAME_ALREADY_EXISTS.message);
next();
} catch (error) {
next(error);
}
}
async signIn(req: Request, res: Response, next: NextFunction) {
try {
const { name, password }: Record<string, string> = req.body;
// Whether name or password is valid
const stringSchema = z.string({
invalid_type_error: StatusCode.NAME_OR_PWD_IS_REQUIRED.message,
});
stringSchema.parse(name);
stringSchema.parse(password);
// Whether name is already existed
const user = await prisma.user.findUnique({
where: {
login_name: name,
},
});
if (!user) throw new Error(StatusCode.USER_IS_NOT_EXISTS.message);
// Whether password is right
if (aesEncrypt(password) !== user.password)
throw new Error(StatusCode.PWD_IS_WRONG.message);
next();
} catch (error) {
next(error);
}
}
async signOut(req: Request, res: Response, next: NextFunction) {
try {
// whether it has a token or not
if (!req.headers.authorization)
throw new Error(StatusCode.TOKEN_IS_EMPTY.message);
// get the token
const token = req.headers.authorization.replace("Bearer ", "");
// whether the token is right
const decoded = (await parseToken(token)) as JwtPayload;
req.body.token = token;
req.body.name = decoded.name;
next();
} catch (error) {
next(error);
}
}
async auth(req: Request, res: Response, next: NextFunction) {
try {
next();
} catch (error) {
next(error);
}
}
}
export const userMiddleware = new UserMiddleware();
控制器
class UserController {
async signUp(req: Request, res: Response, next: NextFunction) {
try {
const { name, password }: Record<string, string> = req.body;
await userService.signUp(name, password);
res.status(200).send(StatusCode.USER_CREATE_SUCCEED);
} catch (error) {
next(error);
}
}
async signIn(req: Request, res: Response, next: NextFunction) {
try {
const { name, expiresMinutes }: { name: string; expiresMinutes: number } =
req.body;
const token = await userService.signIn(name, expiresMinutes);
res.status(200).send({
code: StatusCode.TOKEN_GENERATE_SUCCEED.code,
message: StatusCode.TOKEN_GENERATE_SUCCEED.message,
data: token,
});
} catch (error) {
next(error);
}
}
async signOut(req: Request, res: Response, next: NextFunction) {
try {
const name = req.body.name as string;
await userService.signOut(name);
res.status(200).send({
code: StatusCode.SIGNOUT_SUCCEED.code,
message: StatusCode.SIGNOUT_SUCCEED.message,
});
} catch (error) {
next(error);
}
}
}
export const userController = new UserController();
服务
class UserService {
async signUp(name: string, password: string) {
const encryptPassword = aesEncrypt(password);
const timeStamp = Date.now();
await prisma.user.create({
data: {
login_name: name,
password: encryptPassword,
user_status: 0,
created_time: timeStamp,
updated_time: timeStamp,
},
});
}
async signIn(name: string, expiresMinutes?: number) {
const token = generateToken(name, expiresMinutes);
const timeStamp = Date.now();
const result = await prisma.user.findUnique({
where: {
login_name: name,
},
select: {
login_time: true,
login_count: true,
},
});
await prisma.user.update({
where: {
login_name: name,
},
data: {
user_status: 1,
login_time: timeStamp,
login_count: result?.login_count ? result.login_count + BigInt(1) : 1,
last_login_time: result?.login_time,
updated_time: timeStamp,
},
});
return token;
}
async signOut(name: string) {
const timeStamp = Date.now();
await prisma.user.update({
where: {
login_name: name,
},
data: {
user_status: 0,
updated_time: timeStamp,
},
});
}
}
export const userService = new UserService();
授权
基础
rbac
- 基于角色的权限管理系统;
- 组成:用户 + 角色 + 权限 + 会话;
- 用户:用户扮演多种角色;
- 角色:角色具有多种权限;
- 权限:能够进行的操作;
- 会话:用户 - 角色,角色 - 权限的关系;
数据库设计
- 理想设计中职位,组织和用户组按需选择;
- 用户权限为用户自有权限 + 所在职位,组织和用户组权限之和;