• 三、Midway 接口安全认证


    阅读本文前,需要提前阅读前置内容:

    一、Midway 增删改查
    二、Midway 增删改查的封装及工具类
    三、Midway 接口安全认证
    四、Midway 集成 Swagger 以及支持JWT bearer
    五、Midway 中环境变量的使用

    样例源码
    DEMO LIVE

    很多时候,后端接口需要登录后才能进行访问,甚至有的接口需要拥有相应的权限才能访问。
    这里实现bearer验证方式(bearerFormat 为 JWT)。

    安装JWT组件

    >npm i @midwayjs/jwt@3 --save
    >npm i @types/jsonwebtoken --save-dev
    
    • 1
    • 2

    安装完后package.json文件中会多出如下配置

    {
      "dependencies": {
        "@midwayjs/jwt": "^3.3.11"
      },
      "devDependencies": {
        "@types/jsonwebtoken": "^8.5.8"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    添加JWT配置

    • 修改src/config/config.default.ts,添加如下内容;
    // src/config/config.default.ts
    jwt: {
      secret: 'setscrew',
      expiresIn: 60 * 60 * 24,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 注册JWT组件;
    // src/configuration.ts
    import * as jwt from '@midwayjs/jwt';
    
    @Configuration({
      imports: [
        jwt,
        //...
      ],
    })
    export class ContainerLifeCycle {
        //...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    关于JWT的详细使用文档,见:http://www.midwayjs.org/docs/extensions/jwt

    安装Redis组件

    >npm i @midwayjs/redis@3 --save
    >npm i @types/ioredis --save-dev
    
    • 1
    • 2

    安装完后package.json文件中会多出如下配置

    {
      "dependencies": {
        "@midwayjs/redis": "^3.0.0"
      },
      "devDependencies": {
        "@types/ioredis": "^4.28.7"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注册Redis组件

    // src/configuration.ts
    import * as redis from '@midwayjs/redis';
    
    @Configuration({
      imports: [
        redis,
        // ...
      ],
    })
    export class ContainerLifeCycle {
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    添加配置

    修改src/config/config.default.ts,添加如下内容:

    添加Redis配置

    // src/config/config.default.ts
    redis: {
      client: {
        host: 127.0.0.1,
        port: 6379,
        db: 0,
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    关于Redis的详细使用文档,见:http://www.midwayjs.org/docs/extensions/redis

    添加安全拦截配置

    // src/config/config.default.ts
    app: {
      security: {
        prefix: '/api',         # 指定已/api开头的接口地址需要拦截
        ignore: ['/api/login'], # 指定该接口地址,不需要拦截
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    添加接口安全拦截中间件

    添加常量定义

    // src/common/Constant.ts
    export class Constant {
      // 登陆验证时,缓存用户登陆状态KEY的前缀
      static TOKEM = 'TOKEN';
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    添加用户访问上下文类

    // src/common/UserContext.ts
    /**
     * 登陆后存储访问上下文的状态数据,同时也会存在redis缓存中
     */
    export class UserContext {
      userId: number;
      username: string;
      phoneNum: string;
      constructor(userId: number, username: string, phoneNum: string) {
        this.userId = userId;
        this.username = username;
        this.phoneNum = phoneNum;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    新增或者编辑src/interface.ts,将UserContext注册到ApplecationContext

    // src/interface.ts
    import '@midwayjs/core';
    import { UserContext } from './common/UserContext';
    
    declare module '@midwayjs/core' {
      interface Context {
        userContext: UserContext;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    新增中间件src/middleware/security.middleware.ts

    // src/middleware/security.middleware.ts
    import { Config, Inject, Middleware } from '@midwayjs/decorator';
    import { Context, NextFunction } from '@midwayjs/koa';
    import { httpError } from '@midwayjs/core';
    import { JwtService } from '@midwayjs/jwt';
    import { UserContext } from '../common/UserContext';
    import { RedisService } from '@midwayjs/redis';
    import { Constant } from '../common/Constant';
    
    /**
     * 安全验证
     */
    @Middleware()
    export class SecurityMiddleware {
    
      @Inject()
      jwtUtil: JwtService;
    
      @Inject()
      cacheUtil: RedisService;
    
      @Config('app.security')
      securityConfig;
    
      resolve() {
        return async (ctx: Context, next: NextFunction) => {
          if (!ctx.headers['authorization']) {
            throw new httpError.UnauthorizedError('缺少凭证');
          }
          const parts = ctx.get('authorization').trim().split(' ');
          if (parts.length !== 2) {
            throw new httpError.UnauthorizedError('无效的凭证');
          }
          const [scheme, token] = parts;
          if (!/^Bearer$/i.test(scheme)) {
            throw new httpError.UnauthorizedError('缺少Bearer');
          }
          // 验证token,过期会抛出异常
          const jwt = await this.jwtUtil.verify(token, { complete: true });
          // jwt中存储的user信息
          const payload = jwt['payload'];
          const key = Constant.TOKEM + ':' + payload.userId + ':' + token;
          const ucStr = await this.cacheUtil.get(key);
          // 服务器端缓存中存储的user信息
          const uc: UserContext = JSON.parse(ucStr);
          if (payload.username !== uc.username) {
            throw new httpError.UnauthorizedError('无效的凭证');
          }
          // 存储到访问上下文中
          ctx.userContext = uc;
          return next();
        };
      }
    
      public match(ctx: Context): boolean {
        const { path } = ctx;
        const { prefix, ignore } = this.securityConfig;
        const exist = ignore.find((item) => {
          return item.match(path);
        });
        return path.indexOf(prefix) === 0 && !exist;
      }
    
      public static getName(): string {
        return 'SECURITY';
      }
    
    }
    
    • 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
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • @Config('app.security')装饰类,指定加载配置文件src/config/config.**.ts中对应的配置信息;
    • 使用JwtService进行JWT编码校验;

    jwt token将用户信息编码在token中,解码后可以获取对应用户数据,通常情况下,不需要存储到redis中;
    但是有个缺点就是,不能人为控制分发出去的token失效。所以,有时人们会使用缓存中的用户信息;
    这里使用了JWT+Redis的方式,是为了演示两种做法;

    注册中间件

    // src/configuration.ts
    this.app.useMiddleware([SecurityMiddleware, FormatMiddleware, ReportMiddleware]);
    
    • 1
    • 2

    添加登陆接口

    • 添加DTO;
    // src/api/dto/CommonDTO.ts
    export class LoginDTO {
      username: string;
      password: string;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 添加VO;
    // src/api/vo/CommonVO.ts
    export class LoginVO {
      accessToken: string;
      expiresIn: number;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 修改src/service/user.service.ts,添加通过用户名查找用户接口;
    import { Provide } from '@midwayjs/decorator';
    import { User } from '../eneity/user';
    import { InjectEntityModel } from '@midwayjs/orm';
    import { Repository } from 'typeorm';
    import { BaseService } from '../common/BaseService';
    
    @Provide()
    export class UserService extends BaseService<User> {
    
      @InjectEntityModel(User)
      model: Repository<User>;
    
      getModel(): Repository<User> {
        return this.model;
      }
    
      async findByUsername(username: string): Promise<User> {
        return this.model.findOne({ where: { username } });
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 添加Controllersrc/controller/common.controller.ts
    // src/controller/common.controller.ts
    import { Body, Config, Controller, Inject, Post } from '@midwayjs/decorator';
    import { Context } from '@midwayjs/koa';
    import { UserService } from '../service/user.service';
    import { RedisService } from '@midwayjs/redis';
    import { LoginDTO } from '../api/dto/CommonDTO';
    import { LoginVO } from '../api/vo/CommonVO';
    import { SnowflakeIdGenerate } from '../utils/Snowflake';
    import { JwtService } from '@midwayjs/jwt';
    import { Assert } from '../common/Assert';
    import { ErrorCode } from '../common/ErrorCode';
    import { UserContext } from '../common/UserContext';
    import { Constant } from '../common/Constant';
    import { ILogger } from '@midwayjs/core';
    import { decrypt } from '../utils/PasswordEncoder';
    import { Validate } from '@midwayjs/validate';
    import { ApiResponse, ApiTags } from '@midwayjs/swagger';
    
    @ApiTags(['common'])
    @Controller('/api')
    export class CommonController {
    
      @Inject()
      logger: ILogger;
    
      @Inject()
      ctx: Context;
    
      @Inject()
      userService: UserService;
    
      @Inject()
      cacheUtil: RedisService;
    
      @Inject()
      jwtUtil: JwtService;
    
      @Inject()
      idGenerate: SnowflakeIdGenerate;
    
      @Config('jwt')
      jwtConfig;
    
      @ApiResponse({ type: LoginVO })
      @Validate()
      @Post('/login', { description: '登陆' })
      async login(@Body() body: LoginDTO): Promise<LoginVO> {
        const user = await this.userService.findByUsername(body.username);
        Assert.notNull(user, ErrorCode.UN_ERROR, '用户名或者密码错误');
        const flag = decrypt(body.password, user.password);
        Assert.isTrue(flag, ErrorCode.UN_ERROR, '用户名或者密码错误');
        const uc: UserContext = new UserContext(user.id, user.username, user.phoneNum);
        const at = await this.jwtUtil.sign({ ...uc });
        const key = Constant.TOKEM + ':' + user.id + ':' + at;
        const expiresIn = this.jwtConfig.expiresIn;
        this.cacheUtil.set(key, JSON.stringify(uc), 'EX', expiresIn);
        const vo = new LoginVO();
        vo.accessToken = at;
        vo.expiresIn = expiresIn;
        return vo;
      }
    
    }
    
    • 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
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    使用Postman验证

    • 调用接口(未设置凭证);
      未设置凭证
    • 使用登陆接口获取token;
      获取凭证
    • 调用接口(使用凭证);
      使用凭证

    版权所有,转载请注明出处 [码道功成]

  • 相关阅读:
    《C++设计模式》
    漏洞复现-log4j
    19 0A-检索服务器支持的所有DTC的状态
    【跟小嘉学习JavaWeb开发】第二章 Java 程序设计概述
    【试题030】C语言之关系表达式例题
    MySQL 外键约束 多表联查 联合查询
    龙蜥开发者说:我眼里的龙蜥社区:一个包容的大家庭 | 第 10 期
    样本对应模型例题
    Matlab 在3D 视觉的应用 01 显示PCD点云
    初识设计模式 - 桥接模式
  • 原文地址:https://blog.csdn.net/bestaone/article/details/125870223