rbac基于用户(user)、角色(role)、权限(permission)的权限控制,用nestjs实现一个小demo。
github:链接
用户登录成功后返回token,token中携带当前用户的角色信息和权限信息
/**
* 用户登录
* @param loginDto
*/
async login(loginDto: LoginDto) {
const { email, password, code, uuid } = loginDto;
const isUUID = await this.redisService.get(`captcha_${uuid}`);
if (!isUUID) {
throw new BussException("验证码错误");
}
if (isUUID.toString().toLocaleLowerCase() !== code.toString().toLocaleLowerCase()) {
throw new BussException("验证码错误");
}
// 当前用户
const currentUser = await this.userRepository.findOne({
where: {
email
},
relations: ["roles", "roles.permissions"]
});
if (!currentUser) {
throw new BussException("当前用户未注册");
}
const isPass = await verify(currentUser.password, password);
if (!isPass) {
throw new BussException("密码错误");
}
if (!currentUser.status) {
throw new BussException("该账户已被冻结,请联系管理员");
}
// 获取当前用户的角色和权限
const roles = currentUser.roles.map(v => v.name);
const permissions = currentUser.roles.flatMap(role => role.permissions);
// 生成本次验证token
const token = this.jwtService.sign(
{
userId: currentUser.id,
email: currentUser.email,
username: currentUser.username,
roles,
permissions
},
{
expiresIn: this.configService.get("jwt_expiresIn") || "30m"
});
// 重置token
const refreshToken = this.jwtService.sign(
{
userId: currentUser.id
},
{
expiresIn: "7d"
}
);
// 剔除一些不需要返回的状态
const { password: discardPassword, status: discardStatus, ...userInfo } = currentUser;
return {
token,
refreshToken,
userInfo
};
}通过nestjs guard判断鉴权
login guard
@Injectable()
export class LoginGuard implements CanActivate {
@Inject()
private readonly reflector: Reflector;
@Inject(JwtService)
private readonly jwtService: JwtService;
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
// 获取是否需要鉴权
const requireLogin = this.reflector.getAllAndOverride("require-login", [
context.getClass(),
context.getHandler()
]);
const authorization = request.headers.authorization;
// 没有则放行,公共接口(不需要登录)
if (!requireLogin) {
return true;
}
// 需要鉴权,切没有token
if (!authorization) {
throw new UnauthorizedException("用户未登录");
}
try {
const token = authorization.split(" ")[1];
const data = this.jwtService.verify(token);
// userInfo中包含登录成功的用户信息
let { iat, exp, ...userInfo } = data;
request.userInfo = {
...userInfo,
};
return true;
} catch (error) {
throw new UnauthorizedException("用户未登录");
}
}
}
permission guard
@Injectable()
export class PermissionGuard implements CanActivate {
@Inject()
private readonly reflector: Reflector;
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
if (!request.userInfo) {
return true;
}
// 拿到用户的权限信息
const permissions = request.userInfo.permissions;
// 是否需要鉴权
const requirePermission = this.reflector.getAllAndOverride("require-permission", [
context.getClass(),
context.getHandler()
]);
if (!requirePermission) {
return true;
}
for (let i = 0; i < requirePermission.length; i++) {
const curPermission = requirePermission[i];
const found = permissions.find((item: any) => item.identifying === curPermission);
if (!found) {
throw new BussException("您没有访问该接口的权限");
}
}
return true;
}
}通过decorator设置装饰器
import { createParamDecorator, ExecutionContext, SetMetadata } from '@nestjs/common';
// 登录鉴权
export const requireLogin = () => SetMetadata('require-login', true)
// 用户权限鉴权
export const requirePermission = (...permissions: string[]) => SetMetadata('require-permission', permissions)
// 获取用户信息
export const UserInfo = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request: any = ctx.switchToHttp().getRequest<Request>();
if (!request.userInfo) {
return null;
}
return data ? request.userInfo[data] : request.userInfo;
},
)在需要用到鉴权的controller设置requireLogin和require-permission
require-permission中传入当前接口需要的key值,也就是权限字段的值。