1. 設定認證
# Cài packages
npm install @nestjs/passport passport passport-local passport-jwt
npm install @nestjs/jwt bcrypt
npm install -D @types/passport-local @types/passport-jwt @types/bcrypt
# Generate auth module
nest g module auth
nest g service auth
nest g controller auth
2. 帶密碼的使用者實體
// users/entities/user.entity.ts
import * as bcrypt from 'bcrypt';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column({ select: false }) // Không trả về mặc định
password: string;
@Column()
name: string;
@Column({ type: 'enum', enum: ['admin', 'user'], default: 'user' })
role: string;
@Column({ nullable: true })
refreshToken: string;
// Hash password trước khi save
@BeforeInsert()
@BeforeUpdate()
async hashPassword() {
if (this.password) {
this.password = await bcrypt.hash(this.password, 12);
}
}
async validatePassword(plainPassword: string): Promise<boolean> {
return bcrypt.compare(plainPassword, this.password);
}
}
3. 認證服務
// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
interface JwtPayload {
sub: string; // user id
email: string;
role: string;
}
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
// Validate user cho Local Strategy
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email, true); // include password
if (!user) throw new UnauthorizedException('Email hoặc mật khẩu sai');
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) throw new UnauthorizedException('Email hoặc mật khẩu sai');
const { password: _, ...result } = user;
return result;
}
// Login — trả về tokens
async login(user: any) {
const payload: JwtPayload = {
sub: user.id,
email: user.email,
role: user.role,
};
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: process.env.JWT_ACCESS_SECRET,
expiresIn: '15m',
}),
this.jwtService.signAsync(payload, {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
}),
]);
// Lưu refresh token (hashed)
await this.usersService.updateRefreshToken(
user.id,
await bcrypt.hash(refreshToken, 10),
);
return {
accessToken,
refreshToken,
user: { id: user.id, email: user.email, name: user.name, role: user.role },
};
}
// Refresh token
async refreshTokens(userId: string, refreshToken: string) {
const user = await this.usersService.findOne(userId);
if (!user || !user.refreshToken) {
throw new UnauthorizedException('Access denied');
}
const isMatch = await bcrypt.compare(refreshToken, user.refreshToken);
if (!isMatch) throw new UnauthorizedException('Access denied');
return this.login(user);
}
// Logout — xóa refresh token
async logout(userId: string) {
await this.usersService.updateRefreshToken(userId, null);
}
// Register
async register(dto: RegisterDto) {
const existing = await this.usersService.findByEmail(dto.email);
if (existing) throw new ConflictException('Email đã tồn tại');
const user = await this.usersService.create(dto);
return this.login(user);
}
}
4. 護照策略
// auth/strategies/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' }); // Dùng email thay vì username
}
async validate(email: string, password: string) {
return this.authService.validateUser(email, password);
// Return value sẽ gắn vào request.user
}
}
// auth/strategies/jwt.strategy.ts
import { Strategy, ExtractJwt } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_ACCESS_SECRET,
});
}
async validate(payload: JwtPayload) {
// Return value gắn vào request.user
return { id: payload.sub, email: payload.email, role: payload.role };
}
}
// auth/strategies/jwt-refresh.strategy.ts
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_REFRESH_SECRET,
passReqToCallback: true,
});
}
validate(req: Request, payload: JwtPayload) {
const refreshToken = req.get('Authorization').replace('Bearer ', '').trim();
return { ...payload, refreshToken };
}
}
5. 認證模組
// auth/auth.module.ts
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({}), // Config động trong service
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy, JwtRefreshStrategy],
exports: [AuthService],
})
export class AuthModule {}
6. 認證控制器
// auth/auth.controller.ts
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(AuthGuard('local'))
@Post('login')
@HttpCode(200)
async login(@Req() req) {
return this.authService.login(req.user);
}
@Post('register')
async register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@UseGuards(AuthGuard('jwt-refresh'))
@Post('refresh')
@HttpCode(200)
async refreshTokens(@Req() req) {
const { sub, refreshToken } = req.user;
return this.authService.refreshTokens(sub, refreshToken);
}
@UseGuards(AuthGuard('jwt'))
@Post('logout')
@HttpCode(200)
async logout(@Req() req) {
await this.authService.logout(req.user.id);
return { message: 'Logged out' };
}
@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile(@Req() req) {
return req.user;
}
}
7. 保護路線
// Cách 1: UseGuards trực tiếp
@UseGuards(AuthGuard('jwt'))
@Get('protected')
protectedRoute() { return 'Only authenticated users'; }
// Cách 2: Custom JwtAuthGuard (khuyến nghị)
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException('Token không hợp lệ');
}
return user;
}
}
// Cách 3: Global Guard + @Public() decorator
// auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// Global guard kiểm tra mọi route
@Injectable()
export class GlobalJwtGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) { super(); }
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
}
// Sử dụng
@Public()
@Get('health')
health() { return 'OK'; } // Không cần auth
八、總結
- 護照:本地原則(郵箱/密碼)+JWT策略(token)
- 訪問令牌:短期(15m),在授權標頭中發送
- 刷新令牌:長期(7天),將雜湊保存在資料庫中
- 代幣輪換:每次刷新都會建立一對新的令牌
- 全域守衛 + @Public():模式保護所有預設路由
下一篇文章將探討 警衛與授權——RBAC。