身份驗證是大多數(shù)應(yīng)用程序的重要組成部分嵌言。有許多不同的方法和策略來處理身份驗證。任何項目所采用的方法都取決于其特定的應(yīng)用需求。本章介紹了幾種可以適應(yīng)各種不同要求的身份驗證方法积仗。
Passport是最流行的 node.js 身份驗證庫品姓,在社區(qū)中廣為人知寝并,并成功用于許多生產(chǎn)應(yīng)用程序。使用該模塊將此庫與Nest應(yīng)用程序集成起來非常簡單腹备。@nestjs/passport
在高層次上衬潦,Passport 執(zhí)行一系列步驟來:
- 通過驗證用戶的“憑據(jù)”(例如用戶名/密碼、JSON Web 令牌 ( JWT ) 或來自身份提供者的身份令牌)對用戶進(jìn)行身份驗證
- 管理經(jīng)過身份驗證的狀態(tài)(通過發(fā)布便攜式令牌植酥,例如 JWT镀岛,或創(chuàng)建Express 會話)
- 將有關(guān)經(jīng)過身份驗證的用戶的信息附加到
Request
對象,以便在路由處理程序中進(jìn)一步使用
Passport 具有豐富的策略生態(tài)系統(tǒng)友驮,可實現(xiàn)各種身份驗證機(jī)制漂羊。雖然概念簡單,但您可以選擇的 Passport 策略集非常豐富且種類繁多卸留。Passport 將這些不同的步驟抽象為一個標(biāo)準(zhǔn)模式走越,該@nestjs/passport
模塊將此模式包裝并標(biāo)準(zhǔn)化為熟悉的 Nest 結(jié)構(gòu)。
在本章中耻瑟,我們將使用這些強(qiáng)大而靈活的模塊為 RESTful API 服務(wù)器實現(xiàn)一個完整的端到端身份驗證解決方案旨指。您可以使用此處描述的概念來實施任何 Passport 策略來自定義您的身份驗證方案。您可以按照本章中的步驟來構(gòu)建這個完整的示例喳整。您可以在此處找到包含完整示例應(yīng)用程序的存儲庫。
使用
http-headers
{
"Authorization": "Bearer xxxxxx"
}
目標(biāo)
- webapi登錄接口獲取jwt
- 請求驗證
- 獲取當(dāng)前登錄對象
安裝依賴
$ yarn add @nestjs/passport passport passport-local passport-jwt @nestjs/jwt crypto-js
$ yarn add @types/passport-local -D
$ yarn add @types/crypto-js -D
$ yarn add @types/passport-jwt -D
基礎(chǔ)輔助類
- /src/utils/aes-secret.ts
import CryptoJS from 'crypto-js';
const key = CryptoJS.enc.Utf8.parse('i8761286317826ABCDEF'); //十六位十六進(jìn)制數(shù)作為密鑰
const iv = CryptoJS.enc.Utf8.parse('fasdo978ouiojiocsdj'); //十六位十六進(jìn)制數(shù)作為密鑰偏移量
/**
* 解密
* @param word
* @returns
*/
export const secretDecrypt = (word: string) => {
const encryptedHexStr = CryptoJS.enc.Hex.parse(word);
const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
const decrypt = CryptoJS.AES.decrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
};
/**
* 加密
* @param word
* @returns
*/
export const secretEncrypt = (word: string) => {
const srcs = CryptoJS.enc.Utf8.parse(word);
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.ciphertext.toString().toUpperCase();
};
創(chuàng)建auth module
$ nest g module auth
$ nest g service auth
$ nest g controller auth
- /src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { SequelizeModule } from '@nestjs/sequelize';
import { UserModel } from 'src/model/customer/user.model';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { ConfigurationType } from 'config/configuration';
@Module({
imports: [
SequelizeModule.forFeature([UserModel]),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService<ConfigurationType>) => {
const setting = {
secret: configService.get<string>('jwtsecret'),
signOptions: { expiresIn: '7d' },
};
return setting;
},
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [
AuthService,
JwtService,
LocalStrategy,
JwtService,
JwtStrategy,
ConfigService,
],
})
export class AuthModule {}
- /src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/sequelize';
import { USER, UserModel } from 'src/model/customer/user.model';
import { User } from 'src/user/entities/user.entity';
import { secretEncrypt } from 'src/utils/aes-secret';
import { CONST_CONFIG } from 'src/utils/const-config';
@Injectable()
export class AuthService {
constructor(
@InjectModel(UserModel)
private userModel: typeof UserModel,
private jwtService: JwtService,
private configService: ConfigService,
) {}
/**
* 用戶名密碼校驗
* @param username
* @param password
* @returns
*/
async validateUser(username: string, password: string): Promise<User> {
const secretPwd = secretEncrypt(password);
const user = await this.userModel.findOne({
where: {
[USER.USERNAME]: username.trim(),
[USER.PASSWORD]: secretPwd,
},
});
if (!user) {
return user;
}
return null;
}
/**
* 登錄
* @param user
* @returns
*/
async login(user: User) {
const payload = {
username: user.username,
userId: user.id,
};
return {
accessToken: this.jwtService.sign(payload, {
secret: this.configService.get<string>(CONST_CONFIG.JWTSECRET),
expiresIn: '7d',
}),
user: {
id: user.id,
phoneNumber: user.phoneNumber,
userName: user.username,
},
};
}
}
本地策略
策略使用方法 @UseGuards(LocalGuard) 根據(jù)接口場景采用不同策略
本地策略指本地登錄策略(用戶用戶名密碼請求認(rèn)證,返回jwt 后續(xù)認(rèn)證走jwt策略)
- /src/auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { AuthService } from './auth.service';
import { User } from 'src/user/entities/user.entity';
/**
* 本地登錄策略
*/
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<User> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new HttpException('用戶名或者密碼錯誤蜂筹!', HttpStatus.UNAUTHORIZED);
}
return user;
}
}
我們可以在調(diào)用中傳遞一個選項對象來自super()
定義護(hù)照策略的行為不翩。在此示例中津坑,默認(rèn)情況下眉反,護(hù)照本地策略需要在請求正文中調(diào)用username
和的屬性耿币。password
傳遞一個選項對象來指定不同的屬性名稱,例如:super({ usernameField: 'email' })
. 有關(guān)詳細(xì)信息雇初,請參閱Passport 文檔支示。
- /src/auth/jwt.strategy.ts
jwt策略是指請求的校驗方式采用jwt校驗(登錄后校驗)
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ConfigurationType } from 'config/configuration';
/**
* jwt 校驗策略
*/
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService<ConfigurationType>) {
const secret = configService.get<string>('jwtsecret');
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: secret,
});
}
async validate(payload: any) {
console.log(payload);
return payload;
}
}
async nacos secretKey 方案
Configure Strategy
The JWT authentication strategy is constructed as follows:
new JwtStrategy(options, verify)
options
is an object literal containing options to control how the token is extracted from the request or verified.
-
secretOrKey
is a string or buffer containing the secret (symmetric) or PEM-encoded public key (asymmetric) for verifying the token's signature. REQUIRED unlesssecretOrKeyProvider
is provided. -
secretOrKeyProvider
is a callback in the formatfunction secretOrKeyProvider(request, rawJwtToken, done)
, which should calldone
with a secret or PEM-encoded public key (asymmetric) for the given key and request combination.done
accepts arguments in the formatfunction done(err, secret)
. Note it is up to the implementer to decode rawJwtToken. REQUIRED unlesssecretOrKey
is provided. -
jwtFromRequest
(REQUIRED) Function that accepts a request as the only parameter and returns either the JWT as a string or null. See Extracting the JWT from the request for more details. -
issuer
: If defined the token issuer (iss) will be verified against this value. -
audience
: If defined, the token audience (aud) will be verified against this value. -
algorithms
: List of strings with the names of the allowed algorithms. For instance, ["HS256", "HS384"]. -
ignoreExpiration
: if true do not validate the expiration of the token. -
passReqToCallback
: If true the request will be passed to the verify callback. i.e. verify(request, jwt_payload, done_callback). -
jsonWebTokenOptions
: passport-jwt is verifying the token using jsonwebtoken. Pass here an options object for any other option you can pass the jsonwebtoken verifier. (i.e maxAge)
verify
is a function with the parameters verify(jwt_payload, done)
-
jwt_payload
is an object literal containing the decoded JWT payload. -
done
is a passport error first callback accepting arguments done(error, user, info)
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigurationType } from '../config/configuration';
import { NacosService } from '../config/nacos.service';
/**
* jwt 校驗策略
*/
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService<ConfigurationType>,
private readonly nacosService: NacosService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// secretOrKey: secret,
secretOrKeyProvider: async (request, rawJwtToken, done) => {
const dataId = this.configService.get<string>('nacos.databaseConfigId');
const group = this.configService.get<string>('nacos.databaseGroup');
const initialConfig = await this.nacosService.getConfig(dataId, group);
done(undefined, initialConfig.jwtSecret);
},
});
}
async validate(payload: any) {
console.log(payload);
return payload;
}
}
api
post -> /auth/login ->加載本地策略(local-auth.guard.ts)-> 牌照校驗(local.strategy.ts)validate -> auth.controller.login(req 被牌照校驗后返回值替換)-> return result
- /src/user/auth.controller.ts
import {
Controller,
HttpException,
HttpStatus,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { User } from 'src/user/entities/user.entity';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('/login')
async login(@Req() req: { user: User }) {
const result = this.authService.login(req.user);
if (result) {
return result;
}
throw new HttpException('用戶名或者密碼錯誤墅冷!', HttpStatus.FORBIDDEN);
}
}
- /src/auth/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
- /src/auth/jwt-auth-entity.ts
/**
* CurrentUser
*/
export class JwtAuthEntity {
username: string;
/**
* user.id
*/
userId: string;
iat: number;
exp: number;
}
graphql 使用
- /src/auth/current-user.ts
graphql resolver 請求參數(shù)獲取
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
/**
* 自定義參數(shù)裝飾器
*/
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
- /src/auth/gql-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
- /src/user/user.resolver.ts
import {
Resolver,
Query,
Mutation,
Args,
Info,
Parent,
ResolveField,
} from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { FindAllInput } from 'src/utils/common.input';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from 'src/auth/gql-auth.guard';
import { CurrentUser } from 'src/auth/current-user';
import { JwtAuthEntity } from 'src/auth/jwt-auth-entity';
import { OrgroleUser } from 'src/orgrole-user/entities/orgrole-user.entity';
import { OrgroleUserService } from 'src/orgrole-user/orgrole-user.service';
@Resolver(() => User)
export class UserResolver {
constructor(
private readonly userService: UserService,
private readonly orgroleUserService: OrgroleUserService,
) {}
@UseGuards(GqlAuthGuard)
@Mutation(() => User)
createUser(
@Args('createUserInput') createUserInput: CreateUserInput,
@CurrentUser() user: JwtAuthEntity,
) {
return this.userService.create(createUserInput, user);
}
@UseGuards(GqlAuthGuard)
@Query(() => [User], { name: 'UserAll' })
findAll(
@Args('param') param: FindAllInput,
@CurrentUser() user: JwtAuthEntity,
) {
return this.userService.findAll(param, user);
}
@UseGuards(GqlAuthGuard)
@Query(() => User, { name: 'User' })
findOne(
@Args('id', { type: () => String }) id: string,
@CurrentUser() user: JwtAuthEntity,
) {
return this.userService.findByPk(id, user);
}
@UseGuards(GqlAuthGuard)
@Mutation(() => User)
updateUser(
@Args('updateUserInput') updateUserInput: UpdateUserInput,
@CurrentUser() user: JwtAuthEntity,
) {
return this.userService.update(updateUserInput.id, updateUserInput, user);
}
@UseGuards(GqlAuthGuard)
@Mutation(() => User)
removeUser(
@Args('id', { type: () => String }) id: string,
@CurrentUser() user: JwtAuthEntity,
) {
return this.userService.remove(id, user);
}
@ResolveField(() => [OrgroleUser], { nullable: true })
async orgroleUserUserId(
@Parent() parent: User, // Resolved object that implements Character
@Info() { info }, // Type of the object that implements Character
@Args('param', { type: () => FindAllInput, nullable: true })
param: FindAllInput,
) {
if (parent.id) {
return undefined;
}
// Get character's friends
return this.orgroleUserService.findAll({
...param,
where: {
userId: parent.id,
...param?.where,
},
});
}
}