身份验证是大多数应用程序的重要组成部分。有许多不同的方法和策略来处理身份验证。任何项目所采用的方法都取决于其特定的应用需求。本章介绍了几种可以适应各种不同要求的身份验证方法。
Passport是最流行的 node.js 身份验证库,在社区中广为人知,并成功用于许多生产应用程序。使用该模块将此库与Nest应用程序集成起来非常简单。@nestjs/passport
在高层次上,Passport 执行一系列步骤来:
- 通过验证用户的“凭据”(例如用户名/密码、JSON Web 令牌 ( JWT ) 或来自身份提供者的身份令牌)对用户进行身份验证
- 管理经过身份验证的状态(通过发布便携式令牌,例如 JWT,或创建Express 会话)
- 将有关经过身份验证的用户的信息附加到
Request
对象,以便在路由处理程序中进一步使用
Passport 具有丰富的策略生态系统,可实现各种身份验证机制。虽然概念简单,但您可以选择的 Passport 策略集非常丰富且种类繁多。Passport 将这些不同的步骤抽象为一个标准模式,该@nestjs/passport
模块将此模式包装并标准化为熟悉的 Nest 结构。
在本章中,我们将使用这些强大而灵活的模块为 RESTful API 服务器实现一个完整的端到端身份验证解决方案。您可以使用此处描述的概念来实施任何 Passport 策略来自定义您的身份验证方案。您可以按照本章中的步骤来构建这个完整的示例。您可以在此处找到包含完整示例应用程序的存储库。
使用
http-headers
{
"Authorization": "Bearer xxxxxx"
}
目标
- webapi登录接口获取jwt
- 请求验证
- 获取当前登录对象
安装依赖
$ 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
基础辅助类
- /src/utils/aes-secret.ts
import CryptoJS from 'crypto-js';
const key = CryptoJS.enc.Utf8.parse('i8761286317826ABCDEF'); //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse('fasdo978ouiojiocsdj'); //十六位十六进制数作为密钥偏移量
/**
* 解密
* @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();
};
创建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) 根据接口场景采用不同策略
本地策略指本地登录策略(用户用户名密码请求认证,返回jwt 后续认证走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;
}
}
我们可以在调用中传递一个选项对象来自super()
定义护照策略的行为。在此示例中,默认情况下,护照本地策略需要在请求正文中调用username
和的属性。password
传递一个选项对象来指定不同的属性名称,例如:super({ usernameField: 'email' })
. 有关详细信息,请参阅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 请求参数获取
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
/**
* 自定义参数装饰器
*/
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,
},
});
}
}