authority.mdc 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. ---
  2. description: 权限管理(Authority)
  3. globs:
  4. ---
  5. # 权限管理(Authority)
  6. cool-admin 采用是是一种无状态的权限校验方式。[jwt](mdc:https:/jwt.io/introduction), 通俗地讲他就是把用户的一些信息经过处理生成一段加密的字符串,后端解密到信息进行校验。而且这个信息是带有时效的。
  7. cool-admin 默认约定每个模块下的 `controller/admin`为后台编写接口,`controller/app`编写对外如 app、小程序的接口。
  8. - 框架会对路由前缀 `/admin/**` 开头的接口进行权限校验,校验逻辑写在`base`模块下的`middleware/authority.ts`中间件
  9. - 框架会对路由前缀 `/app/**` 开头的接口进行权限校验,校验逻辑写在`user`模块下的`middleware/app.ts`中间件
  10. ::: tip
  11. 也就是说模块`controller/admin`与`controller/app`是需要进行 token 校验的,如果你不想 token 校验有两种方式:
  12. - 使用路由标签的形式,忽略 token 校验,详细查看[路由标签](mdc:src/guide/core/controller.html#路由标签);
  13. - 新建其他的文件夹比如:`controller/open`;
  14. 这样就不会提示登录失效~
  15. :::
  16. ## 登录
  17. 查询校验用户信息,然后将用户信息用 jwt 的方式加密保存返回给客户端。
  18. `src/app/modules/base/service/sys/login.ts`
  19. ```ts
  20. /**
  21. * 登录
  22. * @param login
  23. */
  24. async login(login: LoginDTO) {
  25. const { username, captchaId, verifyCode, password } = login;
  26. // 校验验证码
  27. const checkV = await this.captchaCheck(captchaId, verifyCode);
  28. if (checkV) {
  29. const user = await this.baseSysUserEntity.findOne({ username });
  30. // 校验用户
  31. if (user) {
  32. // 校验用户状态及密码
  33. if (user.status === 0 || user.password !== md5(password)) {
  34. throw new CoolCommException('账户或密码不正确~');
  35. }
  36. } else {
  37. throw new CoolCommException('账户或密码不正确~');
  38. }
  39. // 校验角色
  40. const roleIds = await this.baseSysRoleService.getByUser(user.id);
  41. if (_.isEmpty(roleIds)) {
  42. throw new CoolCommException('该用户未设置任何角色,无法登录~');
  43. }
  44. // 生成token
  45. const { expire, refreshExpire } = this.coolConfig.jwt.token;
  46. const result = {
  47. expire,
  48. token: await this.generateToken(user, roleIds, expire),
  49. refreshExpire,
  50. refreshToken: await this.generateToken(
  51. user,
  52. roleIds,
  53. refreshExpire,
  54. true
  55. ),
  56. };
  57. // 将用户相关信息保存到缓存
  58. const perms = await this.baseSysMenuService.getPerms(roleIds);
  59. const departments = await this.baseSysDepartmentService.getByRoleIds(
  60. roleIds,
  61. user.username === 'admin'
  62. );
  63. await this.coolCache.set(
  64. `admin:department:${user.id}`,
  65. JSON.stringify(departments)
  66. );
  67. await this.coolCache.set(`admin:perms:${user.id}`, JSON.stringify(perms));
  68. await this.coolCache.set(`admin:token:${user.id}`, result.token);
  69. await this.coolCache.set(`admin:token:refresh:${user.id}`, result.token);
  70. return result;
  71. } else {
  72. throw new CoolCommException('验证码不正确');
  73. }
  74. }
  75. ```
  76. ## 权限配置
  77. admin 用户拥有所有的权限,无需配置,但是对于其他只拥有部分权限的用户,我们得选择他们的权限,在这之前我们得先录入我们的系统有哪些权限是可以配置的
  78. 可以登录后台管理系统,`系统管理/权限管理/菜单列表`
  79. ![authority](mdc:admin/node/authority.png)
  80. ## 选择权限
  81. 新建一个角色,就可以为这个角色配置对应的权限,用户管理可以选择对应的角色,那么该用户就有对应的权限,一个用户可以选择多个角色
  82. ![authority](mdc:admin/node/authority-role.png)
  83. ## 全局校验
  84. 通过一个全局的中间件,我们在全局统一处理,这样就无需在每个 controller 处理,显得有点多余。
  85. `src/app/modules/base/middleware/authority.ts`
  86. ```ts
  87. import { App, Config, Middleware } from "@midwayjs/core";
  88. import * as _ from "lodash";
  89. import { RESCODE } from "@cool-midway/core";
  90. import * as jwt from "jsonwebtoken";
  91. import { NextFunction, Context } from "@midwayjs/koa";
  92. import { IMiddleware, IMidwayApplication } from "@midwayjs/core";
  93. /**
  94. * 权限校验
  95. */
  96. @Middleware()
  97. export class BaseAuthorityMiddleware
  98. implements IMiddleware<Context, NextFunction>
  99. {
  100. @Config("koa.globalPrefix")
  101. prefix;
  102. @Config("module.base")
  103. jwtConfig;
  104. coolCache;
  105. @App()
  106. app: IMidwayApplication;
  107. resolve() {
  108. return async (ctx: Context, next: NextFunction) => {
  109. let statusCode = 200;
  110. let { url } = ctx;
  111. url = url.replace(this.prefix, "");
  112. const token = ctx.get("Authorization");
  113. const adminUrl = "/admin/";
  114. // 路由地址为 admin前缀的 需要权限校验
  115. if (_.startsWith(url, adminUrl)) {
  116. try {
  117. ctx.admin = jwt.verify(token, this.jwtConfig.jwt.secret);
  118. } catch (err) {}
  119. // 不需要登录 无需权限校验
  120. if (new RegExp(`^${adminUrl}?.*/open/`).test(url)) {
  121. await next();
  122. return;
  123. }
  124. if (ctx.admin) {
  125. // 超管拥有所有权限
  126. if (ctx.admin.username == "admin" && !ctx.admin.isRefresh) {
  127. await next();
  128. return;
  129. }
  130. // 要登录每个人都有权限的接口
  131. if (new RegExp(`^${adminUrl}?.*/comm/`).test(url)) {
  132. await next();
  133. return;
  134. }
  135. // 如果传的token是refreshToken则校验失败
  136. if (ctx.admin.isRefresh) {
  137. ctx.status = 401;
  138. ctx.body = {
  139. code: RESCODE.COMMFAIL,
  140. message: "登录失效~",
  141. };
  142. return;
  143. }
  144. // 需要动态获得缓存
  145. this.coolCache = await ctx.requestContext.getAsync("cool:cache");
  146. // 判断密码版本是否正确
  147. const passwordV = await this.coolCache.get(
  148. `admin:passwordVersion:${ctx.admin.userId}`
  149. );
  150. if (passwordV != ctx.admin.passwordVersion) {
  151. ctx.status = 401;
  152. ctx.body = {
  153. code: RESCODE.COMMFAIL,
  154. message: "登录失效~",
  155. };
  156. return;
  157. }
  158. const rToken = await this.coolCache.get(
  159. `admin:token:${ctx.admin.userId}`
  160. );
  161. if (!rToken) {
  162. ctx.status = 401;
  163. ctx.body = {
  164. code: RESCODE.COMMFAIL,
  165. message: "登录失效或无权限访问~",
  166. };
  167. return;
  168. }
  169. if (rToken !== token && this.jwtConfig.sso) {
  170. statusCode = 401;
  171. } else {
  172. let perms = await this.coolCache.get(
  173. `admin:perms:${ctx.admin.userId}`
  174. );
  175. if (!_.isEmpty(perms)) {
  176. perms = JSON.parse(perms).map((e) => {
  177. return e.replace(/:/g, "/");
  178. });
  179. if (!perms.includes(url.split("?")[0].replace("/admin/", ""))) {
  180. statusCode = 403;
  181. }
  182. } else {
  183. statusCode = 403;
  184. }
  185. }
  186. } else {
  187. statusCode = 401;
  188. }
  189. if (statusCode > 200) {
  190. ctx.status = statusCode;
  191. ctx.body = {
  192. code: RESCODE.COMMFAIL,
  193. message: "登录失效或无权限访问~",
  194. };
  195. return;
  196. }
  197. }
  198. await next();
  199. };
  200. }
  201. }
  202. ```
  203. ## 令牌续期
  204. jwt 加密完的字符串是有时效的,系统默认时效时间为 2 个小时。这期间就需要续期令牌才可以继续操作。
  205. 框架登录设置了一个 refreshToken,默认过期时间为 30 天。可以使用这个去换取新的 token,这时候又可以延长 2 个小时。
  206. ## 其他权限
  207. 你可以单独编写一个中间间来控制其他权限,如 app、小程序及其他对外接口,但是可以参考后台管理系统权限过滤、token 生成校验的实现方式