天天看點

nodejs企業級開發架構nest學習總結 - 4.NestJS入門Authentication認證,TypeORM資料庫連接配接

NestJS入門Authentication認證,TypeORM資料庫連接配接

官網API點選這裡跳轉

1.Authentication依賴包安裝(passport-jwt)

yarn add @nestjs/passport passport passport-local @types/passport-local @nestjs/jwt passport-jwt @types/passport-jwt --save-dev
           

2.書寫加密類,繼承内置類、重寫validate方法

// jwt.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
/**
 * * 我們遵循了之前描述的所有Passport政策。在我們使用passport-local的用例中,沒有配置選項,是以我們的構造函數隻是調用super(),沒有選項對象。
 */
/**
 * 把使用者資訊存儲到user中的類,繼承内置類PassportStrategy、重寫validate方法
 */
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'encry') { // 繼承PassportStrategy方法抛出的類,傳遞一個Strategy ,第二個參數是自定義的加密的字元串
    constructor(private readonly authService: AuthService) { // 依賴注入服務
        super(); // 并且調用父類的構造函數
    }
    public async validate(username: string, password: string): Promise<any> {
        const user = await this.authService.validateUser(username, password); // (模拟)去資料庫 驗證是否成功
        if (!user) {
            throw new UnauthorizedException(); // 抛出未授權異常
        }
        return user; // validate()方法傳回的值自動建立一個對象,并将其配置設定給Request對象:擷取例如:req.user
    }
}
           

validate方法的兩個參數,當使用守衛裝飾路由後自動在body中注入成形參,然後提供給validate方法内部使用,當驗證沒有發生錯誤的情況下,資料在傳回的時候被自動注入到request請求對象中,增加了一個user屬性,裡面包含了傳回的資料,類型不限

validate由于繼承了内置的類,是以隻需要書寫守衛裝飾器,該方法會被系統自動調用

3.書寫認證服務,模拟實作賬号密碼驗證,登入成功生成token

// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; // 引入jwt
/**
 * 驗證登入賬号密碼正确性,并産生token
 * @function validateUser 驗證登入賬号密碼正确性
 * @function login 産生token
 */
@Injectable()
export class AuthService {
    constructor(
        // private readonly usersService: UsersService,
        private readonly jwtService: JwtService,
    ) { }
    /**
     * 模拟資料庫比對賬号密碼
     * @param username 使用者名
     * @param pass 密碼
     */
    public async validateUser(username: string, pass: string): Promise<any> {
		// 模拟資料
        const user = {
            id: '12fDAA267CCFa9932c',
            username: 'liu',
            password: '1111',
        };
        // 模拟查詢資料庫,判斷密碼什麼的是否正确
        if (user && user.password === pass) {
            const { password, ...result } = user;
            return result; // 然後傳回除了密碼的資料,給local.strategy.ts那邊使用
        }
        return null;
    }

    public async login(user: any) { // 登入,建立一個登入的token并傳回
        const { username, id } = user;
        const payload = { username, id };
        return {
           // 調用内置的nestjs的jwt生成token的對象方法
            token: 'Bearer ' + this.jwtService.sign(payload),
        };
    }
}
           

JwtService這個jwt内置服務,會提供生成token的對象方法,當然還需要在子產品中配置一些對應的才能生效

4.建立一個路由控制器類,用于使用jwt安全認證,登入成功後生成token

// auth.controller.ts
import { Controller, Request, Post, UseGuards, Get } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
/**
 * @UseGuards(AuthGuard('local'))我們使用的AuthGuard是@nestjs/passport自動設定
 */
@Controller('/auth')
export class AuthController {
	// 前面也提到過的,注入服務,提供給整個路由控制器類使用
    public constructor(private readonly authService: AuthService) { }
    /**
     * 使用jwt安全認證,登入成功後生成token
     */
    @UseGuards(AuthGuard('encry')) // encry 自定義的,預設是local,在local.strategy.ts檔案中修改
    @Post('/login')
    public async login(@Request() req): Promise<{ access_token: string }> { // 登入驗證在authService裡面的validateUser進行
        return this.authService.login(req.user); // 發送登入請求,擷取token
    }
}
           

AuthGuard守衛,内置安全認證的守衛,通過傳遞的字元串來辨識是驗證資料還是token解析

1.預設情況下local字元串是對資料進行一系列操作,執行local.strategy.ts方法的validate

2.而後面使用的jwt字元串就是解析token的操作,會在Authentication字段中提取出token并解析出之前生成token放入的資料

3.預設情況下的這字元串可以修改,例如local.strategy.ts的PassportStrategy方法的第二個參數,就是自定義的字元串,然後使用也就用相應的字元串

這時候通路http://localhost:3000/auth/login是沒有用的,因為我們還沒配置對應的子產品

5.配置子產品,用于注冊控制器,服務,内置jwt加解密子產品

// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { LocalStrategy } from './local.strategy';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
@Module({
    imports: [
        JwtModule.register({
            secret: jwtConstants.secret, // 設定secret
            signOptions: { expiresIn: '36000s' }, // 設定token的屬性,時間為3600*10就是十小時,其餘配置可以看jwt的一些相關配置
        }),
    ],
    controllers: [AuthController], // 注冊控制器
    providers: [AuthService, LocalStrategy], // 把AuthService,LocalStrategy注冊成提供者
    exports: [AuthService], // 把這個服務抛出,給其他子產品使用
})
export class AuthModule { }
           

constants.ts裡面是一個常量對象,建立即可,密鑰可以自定義

/**
 * jwt的密匙
 */
export const jwtConstants = {
    secret: 'secretKey',
};
           
這樣就注冊了一個登入時候生成token的配置,也有設定了密鑰和有效期

6.解析token,建立一個解析jwt的類

// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
/**
 * 解析token的類
 * * 命名政策,實作政策時,可以通過向PassportStrategy函數傳遞第二個參數來為其提供名稱。如果你不這樣做,每個政策都有一個預設名稱(例如,jwt-strategy的'jwt'
 */
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy/*, 'myjwt'*/) {// 自定義成myjwt後,解析也要使用myjwt
    constructor() {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: jwtConstants.secret,
        });
    }

    async validate({id, username}: any) {
        return { id , username };
    }
}
           

差別不大,就是調用父類的時候,傳遞了三個參數

  • jwtFromRequest:接收一個passport-jwt對象的ExtractJwt的fromAuthHeaderAsBearerToken方法執行的結果(JwtFromRequestFunction類型)
  • ignoreExpiration 接收一個布爾類型,是否忽略過期時間,正常是false,不忽略過期
  • secretOrKey 就是加密的時候的密鑰,需要一緻才能解密

    validate,然後重寫父類的這個方法會接收到一個token解析後的payload資料,和生成token所用的payload一緻

7.解析token的服務配置到子產品中

// auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
@Module({
    imports: [
        // PassportModule,使用@nestjs/passport裡面的PassportModule子產品
        // 使用該register()方法可以以相同的方式傳遞任何标準的Passport自定義選項。可用選項取決于正在實施的政策。
        PassportModule.register({ defaultStrategy: 'jwt'/*, session: true */ }), // 預設政策,之後在裝飾器中使用就不需要傳遞
        JwtModule.register({
            secret: jwtConstants.secret, // 設定secret
            signOptions: { expiresIn: '36000s' }, // 設定token的屬性,時間為3600*10就是十小時,其餘配置可以看jwt的一些相關
        }),
    ],
    controllers: [AuthController], // 注冊控制器
    providers: [AuthService, LocalStrategy, JwtStrategy], // 把AuthService,LocalStrategy,JwtStrategy注冊成提供者
    exports: [AuthService], // 把這個服務抛出,給其他子產品使用
})
export class AuthModule { }
/**
 * 在第一種情況下(使用者未登入),我們需要執行兩個不同的功能:
 * * 限制未經身份驗證的使用者可以通路的路由(即拒絕通路受限制的路由)。我們将以熟悉的方式使用Guards來處理此功能,方法是在受保護的路由上放置一個Guard。
 * * 正如您所預料的那樣,我們将在此Guard中檢查是否存在有效的JWT,是以我們将在稍後成功釋出JWT後繼續使用此Guard。
 */
           

增加了PassportModule.register子產品和JwtStrategy這個自定義繼承實作的服務

使用該register()方法可以以相同的方式傳遞任何标準的Passport自定義選項。可用選項取決于正在實施的政策。

PassportModule.register({ defaultStrategy: ‘jwt’ }), // 預設政策,之後在裝飾器中使用就不需要傳遞,相當于提取出來AuthGuard()傳遞的字元串’jwt’,預設就是jwt,也可以在jwt.strategy.ts裡面設定PassportStrategy的第二個參數

8.在控制器路由中使用,加上以下路由方法

/**
     * 解析token并傳回資料
     */
    // @UseGuards(AuthGuard('jwt'))
    @UseGuards(AuthGuard()) // 設定了預設政策之後
    @Get('me')
    public getProfile(@Request() req): any {
        return req.user;
    }
           

@UseGuards(AuthGuard('jwt'))方式是在注冊的時候沒有配置 defaultStrategy: 'jwt'的情況下使用的,配置了就不需要傳遞'jwt'字元串

這樣通路http://localhost:3000/auth/me 并在請求頭設定Authentication是登入生成的token即可

TypeORM連接配接使用Mysql資料庫

8.TypeORM結合Nest的依賴庫安裝

yarn add @nestjs/typeorm typeorm mysql --save
           

9.建立資料庫連接配接,單個資料庫連接配接

// app.module.ts
import { Module } from '@nestjs/common';
// 使用typeorm
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
@Module({
  imports: [TypeOrmModule.forRoot(
    { // 注意:使用ts或者es6以上的方式,不支援ormconfig.json的方式
      type: 'mysql', // 類型
      host: 'localhost', // 本地host
      port: 3306, // 3306 mysql 端口号
      username: 'root', // mysql資料庫賬号
      password: 'password', // 資料庫密碼
      database: 'test', // 對應的資料庫
      entities: [
        __dirname + '/**/entity/*.entity{.ts,.js}',
      ],
      synchronize: true,
    }),
  ],
})
export class AppModule {
  // 完成此操作後,TypeORM Connection和EntityManager對象将可用于**在整個項目中進行注入**(無需導入任何子產品),
  public constructor(private readonly connection: Connection) { }
}
           

引入,配置資料庫連接配接,entities是typeORM掃描的實體路徑,當資料庫中不存在該表/集合會建立

10.抽離出配置到ormconfig.json檔案中,放置在和package.json檔案同等目錄下

(記得不是src,是dist運作自動打包的項目目錄)

{
    "type": "mysql",
    "host": "localhost",
    "port": 3306,
    "username": "root",
    "password": "test",
    "database": "test",
    "entities": [
       "dist/**/entity/*.entity{.ts,.js}"
    ],
    "synchronize": true
}
           

11.建立執行個體類,與資料庫字段一緻

// entity/users.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Users {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ length: 100, update: false })
    username: string;

    @Column({ length: 100 })
    password: string;

    @Column({
        length: 10,
        default: 'admin',
        update: false,
     })
    type: string;

    @Column({ length: 255 })
    header: string;

    @Column({ length: 100 })
    post: string;

    @Column({ length: 100 })
    info: string;

    @Column({ length: 100 })
    company: string;

    @Column({ length: 100 })
    salary: string;
}
           

typeorm的裝飾器的一些使用,可以觀看官方文檔或者,本人寫的惡補typeorm

Nest惡補Typeorm - Typeorm快速入門學習(結合typescript)

TypeOrm官方API文檔,跳轉

12.建立服務,用于注入查找資料

// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Users } from './entity/users.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(Users) // 注入實體,傳遞實體類參數
    private readonly usersRepository: Repository<Users>, // 類型為Repository的泛型類
  ) { }

  public findAll(): Promise<Users[]> {
    return this.usersRepository.find();
  }
}
           

13.建立路由控制器

// users.controller.ts
import { Controller, Get, Post, Param } from '@nestjs/common';
import { Users } from './entity/users.entity';
import { UsersService } from './users.service';
@Controller('/user')
export class UsersController {
    public constructor(private readonly usersService: UsersService) { }
    @Get()
    public findAll(): Promise<Users[]> {
        return this.usersService.findAll(); // get請求查詢全部user資料
    }
}
           

14.建立子產品,然後imports注入到app子產品中

// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { Users } from './entity/users.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Users/*, 'name' */])], // 加入功能子產品,為一個數組的實體類 , 第二個參數為一個字元串,表示你在連接配接資料庫設定的name,也是辨別多個資料庫連接配接的name
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule { }
           

TypeOrmModule.forFeature(參數1,參數2) 加入功能子產品,為一個數組的實體類 , 第二個參數為一個字元串,表示你在連接配接資料庫設定的name,也是辨別多個資料庫連接配接的

引入子產品到app.module.ts中
// app.module.ts
import { Module } from '@nestjs/common';
// 使用typeorm
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { UsersModule } from './users.module'
@Module({
  imports: [TypeOrmModule.forRoot(
    {
      type: 'mysql', // 類型
      host: 'localhost', // 本地host
      port: 3306, // 3306 mysql 端口号
      username: 'root', // mysql資料庫賬号
      password: 'password', // 資料庫密碼
      database: 'test', // 對應的資料庫
      entities: [
        __dirname + '/**/entity/*.entity{.ts,.js}',
      ],
      synchronize: true,
    }),
    UsersModule
  ],
})
export class AppModule {
  // 完成此操作後,TypeORM Connection和EntityManager對象将可用于**在整個項目中進行注入**(無需導入任何子產品),
  public constructor(private readonly connection: Connection) { }
}
           
這樣就可以使用typeOrm進行對資料庫的操作了

public constructor(private readonly connection: Connection) {}

這個構造函數的目的是可以讓Connection和EntityManager對象将可用于**在整個項目中進行注入**(無需導入任何子產品)

15.使用Connection和EntityManager對象

// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository, InjectConnection, InjectEntityManager } from '@nestjs/typeorm';
import { Repository, EntityManager, Connection } from 'typeorm';
import { Users } from './entity/users.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(Users) // 注入實體,傳遞實體類參數
    private readonly usersRepository: Repository<Users>, // 類型為Repository的泛型類,生成兩個參數,manager和metadata
    // 或者把連接配接資料庫設定的name傳遞連接配接注入Connection或EntityManager
    @InjectConnection(/*'personsConnection'*/) // 擷取對應的連接配接
    private readonly connection: Connection,
    @InjectEntityManager(/*'personsConnection'*/) // 擷取注入的實體管理
    private readonly entityManager: EntityManager,
  ) { }

  public findAll(): Promise<Users[]> {
    return this.entityManager.find(Users);
    // or
    // return this.connection.manager.find(Users);
    // return this.usersRepository.find();
  }
}
           
上面就使用了Connection對象和EntityManager對象,替換了前面的Repository泛型類的操作

16.自定義存儲庫,定制findById查詢單個資料操作等

// custom.repository.ts
import { EntityRepository, Repository } from 'typeorm';
import { Users } from './entity/users.entity';

/**
 * 定制存儲庫
 * * TypeORM提供了一個稱為自定義存儲庫的功能。自定義存儲庫允許您擴充基本存儲庫類,并使用幾種特殊方法對其進行豐富
 * * 下一步是将執行個體化責任委派給Nest。為此,我們必須将AuthorRepository類傳遞給TypeOrm.forFeature()方法。
 * * 之後在imports裡面使用的TypeOrmModule.forFeature([Users] 替換成TypeOrmModule.forFeature([UsersRepository]
 */
@EntityRepository(Users) // 擴充了基本的存儲庫操作等
export class UsersRepository extends Repository<Users> {
    constructor( // 使用Object.assign對this上面添加連接配接成功後的操作對象
        connection: Connection,
    ) {
        super();
        Object.assign(this, connection.getRepository(Users));
    }
  // 擴充基本庫,增加以下方法,通過id查找單條資料
  public findById(id: number): Promise<Users> {
    return this.findOne({ id });
  }
  // 擴充基本庫,增加add這個增加資料的操作
  public add(users: Users): Promise<Users> {
    return this.save(users);
  }
}
           

在存儲庫擴充的情況下,繼承Repository<Users> 這個泛型類,可以使用先前預設的find,findOne....等等等增删改查的方法

替換掉全部使用Users實體的地方(users.module.ts和users.service.ts)
users.module.ts
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { UsersRepository } from './custom.repository';

@Module({
  // imports: [TypeOrmModule.forFeature([Users/*, 'name' */])], // 加入功能子產品,為一個數組的實體類 , 第二個參數為一個字元串,表示你在連接配接資料庫設定的name,也是辨別多個資料庫連接配接的name
  imports: [TypeOrmModule.forFeature([UsersRepository])], // 注入自定義擴充存儲庫後的實體
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule { }
           
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectConnection, InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager, Connection } from 'typeorm';
import { Users } from './entity/users.entity';
import { UsersRepository } from './custom.repository';

@Injectable()
export class UsersService {
  constructor(
    private readonly usersRepository: UsersRepository, // 類型為Repository的泛型類,生成兩個參數,manager和metadata
    // 或者把連接配接資料庫設定的name傳遞連接配接注入Connection或EntityManager
    @InjectConnection(/*'personsConnection'*/) // 擷取對應的連接配接
    private readonly connection: Connection,
    @InjectEntityManager(/*'personsConnection'*/) // 擷取注入的實體管理
    private readonly entityManager: EntityManager,
  ) { }

  public findAll(): Promise<Users[]> {
    // return this.entityManager.find(Users);
    // or
    // return this.connection.manager.find(Users);
    return this.usersRepository.find();
  }
  public getFindOne(id: number): Promise<Users> {
    return this.usersRepository.findById(id); // 使用自定義的存儲庫操作
  }
  public addUser(user: Users): Promise<Users> { // 添加一條資料的方法
    return this.userRepository.add(user);
  }
}
           

使用了擴充的UsersRepository存儲庫後,會增加兩個自定義的方法,findById和add

17.增加路由控制器一些新操作嘗試擴充的方法

// users.controller.ts
import { Controller, Get, Post, Param, Body } from '@nestjs/common';
import { Users } from './entity/users.entity';
import { UsersService } from './users.service';
@Controller('/user')
export class UsersController {
    public constructor(private readonly usersService: UsersService) { }
    @Get()
    public findAll(): Promise<Users[]> {
        return this.usersService.findAll();
    }
    @Post('/select/:id') // 通過id查詢單個使用者資訊
    public findOne(@Param('id') id: number): Promise<Users> {
        return this.usersService.getFindOne(id);
    }
    @Post('/add') // 通過id查詢單個使用者資訊
    public addUser(@Body() user: Users): Promise<Users> {
        return this.usersService.addUser(user);
    }
}
           

這樣擴充了存儲庫,也可以進行一些複雜的對資料庫的操作封裝,本人隻是簡單使用

18.異步配置連接配接資料庫子產品

// async.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './typeOrmConfig.service';

/**
 * 異步配置存儲庫
 * * 異步傳遞存儲庫子產品選項而不是靜态傳遞。在這種情況下,請使用該forRootAsync()方法,該方法提供了幾種處理異步配置的方法。
 * * 1.工廠功能
 */
@Module({
    imports: [TypeOrmModule.forRootAsync({
    	// 方式1,直接使用useFactory方法配置,傳回一個連接配接對象
        useFactory: () => ({
            type: 'mysql',
            host: 'localhost',
            port: 3306,
            username: 'root',
            password: 'root',
            database: 'test',
            entities: [__dirname + '/**/*.entity{.ts,.js}'],
            synchronize: true,
        }),
        // 方式2 使用以下useClass文法,配置一個建立連接配接的服務,
        // useClass: TypeOrmConfigService,
    })],
})
export class AsyncModule { }
           
// typeOrmConfig.service.ts
import { Injectable } from '@nestjs/common';
import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory { // 實作選項factory工廠
    createTypeOrmOptions(): TypeOrmModuleOptions { // 傳回一個子產品選項
        return {
            type: 'mysql',
            host: 'localhost',
            port: 3306,
            username: 'root',
            password: 'root',
            database: 'test',
            entities: [__dirname + '/**/*.entity{.ts,.js}'],
            synchronize: true,
        };
    }
}
           

暫時了解異步配置即可,也可以在正常項目中使用

其它的部分請前往首頁檢視,或者直接官方檢視

本部落客的首頁位址(點選跳轉)