完成jwt鉴权

This commit is contained in:
2025-05-07 14:34:49 +08:00
parent d2744689b2
commit 1246613fb1
9 changed files with 139 additions and 17 deletions

View File

@@ -31,6 +31,8 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.15.6", "pg": "^8.15.6",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@@ -44,6 +46,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",

View File

@@ -41,6 +41,12 @@ importers:
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
passport:
specifier: ^0.7.0
version: 0.7.0
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
pg: pg:
specifier: ^8.15.6 specifier: ^8.15.6
version: 8.15.6 version: 8.15.6
@@ -75,6 +81,9 @@ importers:
'@types/node': '@types/node':
specifier: ^20.3.1 specifier: ^20.3.1
version: 20.17.31 version: 20.17.31
'@types/passport-jwt':
specifier: ^4.0.1
version: 4.0.1
'@types/supertest': '@types/supertest':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.3 version: 6.0.3
@@ -693,6 +702,15 @@ packages:
'@types/node@20.17.31': '@types/node@20.17.31':
resolution: {integrity: sha512-quODOCNXQAbNf1Q7V+fI8WyErOCh0D5Yd31vHnKu4GkSztGQ7rlltAaqXhHhLl33tlVyUXs2386MkANSwgDn6A==} resolution: {integrity: sha512-quODOCNXQAbNf1Q7V+fI8WyErOCh0D5Yd31vHnKu4GkSztGQ7rlltAaqXhHhLl33tlVyUXs2386MkANSwgDn6A==}
'@types/passport-jwt@4.0.1':
resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==}
'@types/passport-strategy@0.2.38':
resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==}
'@types/passport@1.0.17':
resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==}
'@types/qs@6.9.18': '@types/qs@6.9.18':
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
@@ -2278,6 +2296,9 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
passport-jwt@4.0.1:
resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
passport-strategy@1.0.0: passport-strategy@1.0.0:
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
@@ -3859,6 +3880,20 @@ snapshots:
dependencies: dependencies:
undici-types: 6.19.8 undici-types: 6.19.8
'@types/passport-jwt@4.0.1':
dependencies:
'@types/jsonwebtoken': 9.0.7
'@types/passport-strategy': 0.2.38
'@types/passport-strategy@0.2.38':
dependencies:
'@types/express': 5.0.1
'@types/passport': 1.0.17
'@types/passport@1.0.17':
dependencies:
'@types/express': 5.0.1
'@types/qs@6.9.18': {} '@types/qs@6.9.18': {}
'@types/range-parser@1.2.7': {} '@types/range-parser@1.2.7': {}
@@ -5750,6 +5785,11 @@ snapshots:
parseurl@1.3.3: {} parseurl@1.3.3: {}
passport-jwt@4.0.1:
dependencies:
jsonwebtoken: 9.0.2
passport-strategy: 1.0.0
passport-strategy@1.0.0: {} passport-strategy@1.0.0: {}
passport@0.7.0: passport@0.7.0:

View File

@@ -11,7 +11,7 @@ import { PassportModule } from '@nestjs/passport';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot(), ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRoot({ TypeOrmModule.forRoot({
type: 'postgres', type: 'postgres',
host: process.env.DATABASE_HOST, host: process.env.DATABASE_HOST,

View File

@@ -1,4 +1,4 @@
import { BadRequestException, Body, Controller, Post } from '@nestjs/common'; import { BadRequestException, Body, Controller, Get, Post, Request } from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';

View File

@@ -1,24 +1,40 @@
import { Module } from '@nestjs/common'; import { forwardRef, Module } from '@nestjs/common';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { UserModule } from 'src/user/user.module'; import { UserModule } from 'src/user/user.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSession } from 'src/user/entities/user-session.entity'; import { UserSession } from 'src/user/entities/user-session.entity';
import { UserSessionService } from 'src/user/services/user-session.service'; import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './strategies/jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({ @Module({
imports: [ imports: [
UserModule, ConfigModule,
forwardRef(() => UserModule),
TypeOrmModule.forFeature([UserSession]), TypeOrmModule.forFeature([UserSession]),
JwtModule.register({ PassportModule.register({ defaultStrategy: 'jwt' }),
secret: process.env.JWT_SECRET || 'tone-page', JwtModule.registerAsync({
signOptions: { imports: [ConfigModule],
expiresIn: process.env.EXPIRES_IN || '1d', inject: [ConfigService],
} useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET', 'tone-page'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1d'),
},
})
}) })
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService, UserSessionService], providers: [
AuthService,
JwtStrategy,
],
exports: [
PassportModule,
JwtStrategy,
AuthService,
]
}) })
export class AuthModule { } export class AuthModule { }

View File

@@ -0,0 +1,33 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { UserSessionService } from "src/user/services/user-session.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private readonly userSessionService: UserSessionService,
private readonly configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET', 'tone-page'),
})
}
async validate(payload: any) {
const { userId, sessionId } = payload ?? {};
const isValidSession = await this.userSessionService.isSessionValid(userId, sessionId);
if (!isValidSession) {
throw new UnauthorizedException('登录凭证已过期,请重新登录');
}
return {
userId,
sessionId,
}
}
}

View File

@@ -18,4 +18,16 @@ export class UserSessionService {
}); });
return await this.userSessionRepository.save(session); return await this.userSessionRepository.save(session);
} }
async isSessionValid(userId: string, sessionId: string): Promise<boolean> {
const session = await this.userSessionRepository.findOne({
where: {
userId,
sessionId,
deletedAt: null,
}
});
return !!session;
}
} }

View File

@@ -1,4 +1,17 @@
import { Controller } from '@nestjs/common'; import { Controller, Get, Injectable, UseGuards } from '@nestjs/common';
import { UserService } from './user.service';
import { AuthGuard } from '@nestjs/passport';
@Controller('user') @Controller('user')
export class UserController {} export class UserController {
constructor(
private readonly userService: UserService
) { }
@UseGuards(AuthGuard('jwt'))
@Get('me')
async getMe() {
return 'ok';
}
}

View File

@@ -1,14 +1,19 @@
import { Module } from '@nestjs/common'; import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity'; import { User } from './entities/user.entity';
import { UserController } from './user.controller'; import { UserController } from './user.controller';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { UserSession } from './entities/user-session.entity'; import { UserSession } from './entities/user-session.entity';
import { AuthModule } from 'src/auth/auth.module';
import { UserSessionService } from './services/user-session.service';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([User, UserSession])], imports: [
TypeOrmModule.forFeature([User, UserSession]),
forwardRef(() => AuthModule),// 解决循环依赖问题
],
controllers: [UserController], controllers: [UserController],
providers: [UserService], providers: [UserService, UserSessionService],
exports: [UserService], exports: [UserService, UserSessionService],
}) })
export class UserModule { } export class UserModule { }