目录
JWT与守卫实现
学习目标
- 使用用户模块表结构和数据操作
- 实现JWT登录与Token自动刷新
- 实现Auth守卫
流程图
应用编码
预装类库
在开始编码之前请安装以下类库
~ pnpm add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt bcrypt jsonwebtoken
~ pnpm add @types/passport-local @types/passport-jwt @types/jsonwebtoken @types/bcrypt -D
配置和工具函数
新增一个用户模块
// src/modules/user/user.module.ts
@Module({
...
})
export class UserModule {}
添加以下类型
// src/modules/user/types.ts
// 用户配置
export interface UserConfig {
hash?: number;
jwt: JwtConfig;
}
// Jwt配置
export interface JwtConfig {
secret: string;
token_expired: number;
refresh_secret: string;
refresh_token_expired: number;
}
// Jwt签名荷载
export interface JwtPayload {
sub: string;
iat: number;
}
添加用于密码验证的工具函数
// src/modules/core/helpers.ts
// 加密明文密码
export const encrypt = (password: string) => {
return bcrypt.hashSync(password, userConfig().hash);
};
// 验证密码
export const decrypt = (password: string, hashed: string) => {
return bcrypt.compareSync(password, hashed);
};
数据层
模型之间的关系如下
BaseToken
是AccessTokenEntity
与RefreshTokenEntity
两者的父类AccessTokenEntity
与RefreshTokenEntity
一对一关联UserEntity
与AccessTokenEntity
一对多关联UserEntity
与内容模块的PostEntity
一对多关联
BaseToken
类
这是是一个抽象类,为AccessTokenEntity
和RefreshTokenEntity
提供公共字段
value
为accessToken
或refreshToken
的令牌值
// src/modules/user/entities/base.token.ts
export abstract class BaseToken extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ length: 500, comment: '令牌字符串' })
value!: string;
@Column({
comment: '令牌过期时间',
})
expired_at!: Date;
@CreateDateColumn({
comment: '令牌创建时间',
})
createdAt!: Date;
}
AccessTokenEntity
用户认证token模型
这个模型用于存储用户访问的令牌,供JWT策略判断用户请求中的 令牌是否已经失效以及生成新的令牌
- 此模型与用户多对一关联,同时在删用户除时清空他的全部令牌
- 此模型与Token刷新模型一对一关联,同时在删除一个
accessToken
时删除其refreshToken
// src/modules/user/entities/access-token.entity.ts
@Entity('user_access_tokens')
export class AccessTokenEntity extends BaseToken {
@OneToOne(() => RefreshTokenEntity, (refreshToken) => refreshToken.accessToken, {
cascade: true,
})
refreshToken!: RefreshTokenEntity;
@ManyToOne((type) => UserEntity, (user) => user.accessTokens, {
onDelete: 'CASCADE',
})
user!: UserEntity;
}
RefreshTokenEntity
用户刷新token模型
添加此模型的目的在于如果用户的accessToken
过期,那么根据他提供的refreshToken
是否也失效来判定是否能刷新accessToken
// src/modules/user/entities/refresh-token.entity.ts
@Entity('user_refresh_tokens')
export class RefreshTokenEntity extends BaseToken {
@OneToOne(() => AccessTokenEntity, (accessToken) => accessToken.refreshToken, {
onDelete: 'CASCADE',
})
@JoinColumn()
accessToken!: AccessTokenEntity;
}
UserEntity
用于存储用户数据,并与文章进行关联,同时用户数据支持软删除
// src/modules/user/entities/user.entity.ts
@Entity('users')
export class UserEntity extends BaseEntity {
// ...
@OneToMany(() => AccessTokenEntity, (accessToken) => accessToken.user, {
cascade: true,
})
accessTokens!: AccessTokenEntity[];
@OneToMany(() => PostEntity, (post) => post.author, {
cascade: true,
})
posts!: PostEntity[];
@Expose()
@Type(() => Date)
@DeleteDateColumn({
comment: '删除时间',
})
deletedAt!: Date;
@Expose()
trashed!: boolean;
}
UserRepository
构建一个基础的queryBuilder
查询器
// src/modules/user/repositories/user.repository.ts
@CustomRepository(UserEntity)
export class UserRepository extends BaseRepository<UserEntity> {
protected qbName = 'user';
buildBaseQuery() {
return this.createQueryBuilder(this.qbName).orderBy(`${this.qbName}.createdAt`, 'DESC');
}
}
UserSubscriber
这个订阅的作用在本节课程暂时不需要用到, 后面课程会使用短信和邮箱注册用户时将会用到
此订阅者主要实现以下功能
- 在注册用户时没有填写用户名或密码的情况下,自动生成不重复的随机用户名以及随机字符串密码
- 当密码更改时加密密码
// src/modules/user/subscribers/user.subscriber.ts
@EventSubscriber()
export class UserSubscriber extends BaseSubscriber<UserEntity> {
protected entity = UserEntity;
protected setting: SubcriberSetting = {
trash: true,
};
constructor(protected dataSource: DataSource, protected userRepository: UserRepository) {
super(dataSource, userRepository);
}
/**
* 生成不重复的随机用户名
*/
protected async generateUserName(event: InsertEvent<UserEntity>): Promise<string> {
const username = `user_${crypto.randomBytes(4).toString('hex').slice(0, 8)}`;
const user = await event.manager.findOne(UserEntity, {
where: { username },
});
return !user ? username : this.generateUserName(event);
}
/**
* 自动生成唯一用户名和密码
*/
async beforeInsert(event: InsertEvent<UserEntity>) {
// 自动生成唯一用户名
if (!event.entity.username) {
event.entity.username = await this.generateUserName(event);
}
// 自动生成密码
if (!event.entity.password) {
event.entity.password = crypto.randomBytes(11).toString('hex').slice(0, 22);
}
// 自动加密密码
event.entity.password = encrypt(event.entity.password);
}
/**
* 当密码更改时加密密码
*/
async beforeUpdate(event: UpdateEvent<UserEntity>) {
if (this.isUpdated('password', event)) {
event.entity.password = encrypt(event.entity.password);
}
}
protected isUpdated<E>(cloumn: keyof E, event: UpdateEvent<E>): any {
return event.updatedColumns.find((item) => item.propertyName === cloumn);
}
}
请求验证
注意用户登录验证的credential
属性可以是用户名,手机号或邮箱地址
文件位置: src/modules/user/dtos
CredentialDto
: 对用户登录请求进行验证UpdateAccountDto
: 用于对更新当前账户的请求验证QueryUserDto
:对用户列表查询请求进行验证CreateUserDto
:对创建用户请求进行验证UpdateUserDto
: 对更新用户请求进行验证
服务
TokenService
这是操作令牌(AccessToken
与RefreshToken
)的服务
refreshToken
方法: 根据accessToken刷新AccessToken与RefreshTokengenerateAccessToken
方法: 根据荷载签出新的AccessToken并存入数据库,且自动生成新的Refresh也存入数据库generateRefreshToken
方法: 生成新的RefreshToken并存入数据库checkAccessToken
方法: 检查accessToken是否存在removeAccessToken
方法: 移除AccessToken且自动移除关联的RefreshTokenremoveRefreshToken
方 法: 移除RefreshToken且自动移除关联的AccessToken
// src/modules/user/services/token.service.ts
@Injectable()
export class TokenService {
private readonly config: JwtConfig;
constructor(protected readonly jwtService: JwtService) {
this.config = userConfig().jwt;
}
async refreshToken(accessToken: AccessTokenEntity, response: Response)
async generateAccessToken(user: UserEntity, now: dayjs.Dayjs)
async generateRefreshToken(
accessToken: AccessTokenEntity,
now: dayjs.Dayjs,
): Promise<RefreshTokenEntity>
async checkAccessToken(value: string)
async removeAccessToken(value: string)
async removeRefreshToken(value: string)
}
AuthService
用于验证用户及配置JwtModule
等
validateUser
方法: 用户登录验证login
方法: 登录用户,并生成新的accessToken
和refreshToken
logout
方法: 登出用户,并删除这次会话的accessToken
和refreshToken
createToken
方法: 创建accessToken
和refreshToken
// src/modules/user/services/auth.service.ts
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly tokenService: TokenService,
) {}
async validateUser(credential: string, password: string)
async login(user: UserEntity)
async logout(req: Request)
async createToken(id: string)
static jwtModuleFactory()
}
UserService
用户管理服务,此类继承BaseService
,包含基类中的删除恢复和批量操作以及分页等方法
init
方法: 初始化管理员用户create
方法: 新增用户update
方法: 更新用户findOneByCredential
方法: 根据用户名/email等用户凭证查询用户findOneByCondition
方法: 根据条件对象查询用户
// src/modules/user/services/user.service.ts
@Injectable()
export class UserService extends BaseService<UserEntity, UserRepository> {
protected enable_trash = true;
constructor(protected readonly userRepository: UserRepository) {
super(userRepository);
}
async init()
async create(data: CreateUserDto)
async update(data: UpdateUserDto)
async findOneByCredential(credential: string, callback?: QueryHook<UserEntity>)
async findOneByCondition(condition: { [key: string]: any }, callback?: QueryHook<UserEntity>)
}
策略
策略是用于验证用户登录状态的一种方法,本节教程将基于passport
的passport-local
和passport-jwt
策略去分别实现用户登录和通过已登录用户的token解析出用户ID的功能