diff --git a/tone-page-server/package.json b/tone-page-server/package.json index 6ef34d7..bb8e001 100644 --- a/tone-page-server/package.json +++ b/tone-page-server/package.json @@ -31,6 +31,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "jsonwebtoken": "^9.0.2", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "pg": "^8.15.6", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -44,6 +46,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", diff --git a/tone-page-server/pnpm-lock.yaml b/tone-page-server/pnpm-lock.yaml index 5ffbe5c..11fe871 100644 --- a/tone-page-server/pnpm-lock.yaml +++ b/tone-page-server/pnpm-lock.yaml @@ -41,6 +41,12 @@ importers: jsonwebtoken: specifier: ^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: specifier: ^8.15.6 version: 8.15.6 @@ -75,6 +81,9 @@ importers: '@types/node': specifier: ^20.3.1 version: 20.17.31 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 '@types/supertest': specifier: ^6.0.0 version: 6.0.3 @@ -693,6 +702,15 @@ packages: '@types/node@20.17.31': 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': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} @@ -2278,6 +2296,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + passport-strategy@1.0.0: resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} engines: {node: '>= 0.4.0'} @@ -3859,6 +3880,20 @@ snapshots: dependencies: 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/range-parser@1.2.7': {} @@ -5750,6 +5785,11 @@ snapshots: 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@0.7.0: diff --git a/tone-page-server/src/app.module.ts b/tone-page-server/src/app.module.ts index 9c71e46..16c8577 100644 --- a/tone-page-server/src/app.module.ts +++ b/tone-page-server/src/app.module.ts @@ -11,7 +11,7 @@ import { PassportModule } from '@nestjs/passport'; @Module({ imports: [ - ConfigModule.forRoot(), + ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DATABASE_HOST, diff --git a/tone-page-server/src/auth/auth.controller.ts b/tone-page-server/src/auth/auth.controller.ts index 3857856..c56d86c 100644 --- a/tone-page-server/src/auth/auth.controller.ts +++ b/tone-page-server/src/auth/auth.controller.ts @@ -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 { AuthService } from './auth.service'; diff --git a/tone-page-server/src/auth/auth.module.ts b/tone-page-server/src/auth/auth.module.ts index 8453737..5a37392 100644 --- a/tone-page-server/src/auth/auth.module.ts +++ b/tone-page-server/src/auth/auth.module.ts @@ -1,24 +1,40 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { UserModule } from 'src/user/user.module'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; 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({ imports: [ - UserModule, + ConfigModule, + forwardRef(() => UserModule), TypeOrmModule.forFeature([UserSession]), - JwtModule.register({ - secret: process.env.JWT_SECRET || 'tone-page', - signOptions: { - expiresIn: process.env.EXPIRES_IN || '1d', - } + PassportModule.register({ defaultStrategy: 'jwt' }), + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET', 'tone-page'), + signOptions: { + expiresIn: configService.get('JWT_EXPIRES_IN', '1d'), + }, + }) }) ], controllers: [AuthController], - providers: [AuthService, UserSessionService], + providers: [ + AuthService, + JwtStrategy, + ], + exports: [ + PassportModule, + JwtStrategy, + AuthService, + ] }) export class AuthModule { } diff --git a/tone-page-server/src/auth/strategies/jwt.strategy.ts b/tone-page-server/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..b121219 --- /dev/null +++ b/tone-page-server/src/auth/strategies/jwt.strategy.ts @@ -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('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, + } + } +} \ No newline at end of file diff --git a/tone-page-server/src/user/services/user-session.service.ts b/tone-page-server/src/user/services/user-session.service.ts index dc52a42..24b33a5 100644 --- a/tone-page-server/src/user/services/user-session.service.ts +++ b/tone-page-server/src/user/services/user-session.service.ts @@ -18,4 +18,16 @@ export class UserSessionService { }); return await this.userSessionRepository.save(session); } + + async isSessionValid(userId: string, sessionId: string): Promise { + const session = await this.userSessionRepository.findOne({ + where: { + userId, + sessionId, + deletedAt: null, + } + }); + + return !!session; + } } \ No newline at end of file diff --git a/tone-page-server/src/user/user.controller.ts b/tone-page-server/src/user/user.controller.ts index ad8c2a6..57b5824 100644 --- a/tone-page-server/src/user/user.controller.ts +++ b/tone-page-server/src/user/user.controller.ts @@ -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') -export class UserController {} +export class UserController { + + constructor( + private readonly userService: UserService + ) { } + + @UseGuards(AuthGuard('jwt')) + @Get('me') + async getMe() { + return 'ok'; + } +} diff --git a/tone-page-server/src/user/user.module.ts b/tone-page-server/src/user/user.module.ts index 6d0a82a..00f7280 100644 --- a/tone-page-server/src/user/user.module.ts +++ b/tone-page-server/src/user/user.module.ts @@ -1,14 +1,19 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { UserController } from './user.controller'; import { UserService } from './user.service'; import { UserSession } from './entities/user-session.entity'; +import { AuthModule } from 'src/auth/auth.module'; +import { UserSessionService } from './services/user-session.service'; @Module({ - imports: [TypeOrmModule.forFeature([User, UserSession])], + imports: [ + TypeOrmModule.forFeature([User, UserSession]), + forwardRef(() => AuthModule),// 解决循环依赖问题 + ], controllers: [UserController], - providers: [UserService], - exports: [UserService], + providers: [UserService, UserSessionService], + exports: [UserService, UserSessionService], }) export class UserModule { }