feat: 使用中间件 从 jwt 中获取用户信息
This commit is contained in:
parent
5ed17cc04b
commit
02059ee54f
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": []
|
||||
}
|
@ -16,5 +16,9 @@ db:
|
||||
prefix: fennec
|
||||
rabbitmq:
|
||||
uri: 'amqp://fennec:fennec@192.168.31.194:5672'
|
||||
etcd:
|
||||
hosts:
|
||||
- 'http://192.168.31.194:2379'
|
||||
|
||||
workspaces:
|
||||
root: '/Users/ivanli/Projects/fennec/workspaces'
|
12489
package-lock.json
generated
12489
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@golevelup/nestjs-rabbitmq": "^1.16.1",
|
||||
"@nestjs/bull": "^0.3.1",
|
||||
"@nestjs/common": "^7.5.1",
|
||||
"@nestjs/config": "^0.6.2",
|
||||
"@nestjs/core": "^7.5.1",
|
||||
@ -30,19 +29,19 @@
|
||||
"@nestjs/platform-express": "^7.5.1",
|
||||
"@nestjs/typeorm": "^7.1.5",
|
||||
"@types/amqplib": "^0.8.0",
|
||||
"@types/bull": "^3.15.0",
|
||||
"@types/ramda": "^0.27.38",
|
||||
"apollo-server-express": "^2.19.2",
|
||||
"bcrypt": "^5.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"bull": "^3.20.1",
|
||||
"class-transformer": "^0.3.2",
|
||||
"class-validator": "^0.13.1",
|
||||
"debug": "^4.3.1",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-tools": "^7.0.2",
|
||||
"ioredis": "^4.25.0",
|
||||
"jose": "^3.14.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"nestjs-etcd": "^0.2.0",
|
||||
"nestjs-pino": "^1.4.0",
|
||||
"nestjs-redis": "^1.2.8",
|
||||
"observable-to-async-generator": "^1.0.1-rc",
|
||||
@ -96,6 +95,9 @@
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^jose/(.*)$": "<rootDir>/../node_modules/jose/dist/node/cjs/$1"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { CommonsModule } from './commons/commons.module';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
@ -12,13 +13,13 @@ import { PipelineTasksModule } from './pipeline-tasks/pipeline-tasks.module';
|
||||
import configuration from './commons/config/configuration';
|
||||
import { RedisModule } from 'nestjs-redis';
|
||||
import { WebhooksModule } from './webhooks/webhooks.module';
|
||||
import { RawBodyMiddleware } from './commons/middlewares/raw-body.middleware';
|
||||
import { RawBodyMiddleware } from './commons/middleware/raw-body.middleware';
|
||||
import { GiteaWebhooksController } from './webhooks/gitea-webhooks.controller';
|
||||
import { ParseBodyMiddleware } from './commons/middlewares/parse-body.middleware';
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { ParseBodyMiddleware } from './commons/middleware/parse-body.middleware';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
|
||||
import { EtcdModule } from 'nestjs-etcd';
|
||||
import pinoPretty from 'pino-pretty';
|
||||
import { AccountMiddleware } from './commons/middleware/account.middleware';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -66,17 +67,6 @@ import pinoPretty from 'pino-pretty';
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
redis: {
|
||||
host: configService.get<string>('db.redis.host', 'localhost'),
|
||||
port: configService.get<number>('db.redis.port', undefined),
|
||||
password: configService.get<string>('db.redis.password', undefined),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
ProjectsModule,
|
||||
ReposModule,
|
||||
PipelinesModule,
|
||||
@ -91,7 +81,15 @@ import pinoPretty from 'pino-pretty';
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
EtcdModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
hosts: configService.get<string>('db.etcd.hosts', 'localhost:2379'),
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
WebhooksModule,
|
||||
CommonsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, AppResolver],
|
||||
@ -102,6 +100,8 @@ export class AppModule implements NestModule {
|
||||
.apply(RawBodyMiddleware)
|
||||
.forRoutes(GiteaWebhooksController)
|
||||
.apply(ParseBodyMiddleware)
|
||||
.forRoutes('*')
|
||||
.apply(AccountMiddleware)
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PasswordConverter } from './services/password-converter';
|
||||
import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
|
||||
import { JwtService } from './services/jwt.service';
|
||||
|
||||
@Module({
|
||||
providers: [PasswordConverter],
|
||||
exports: [PasswordConverter, RedisMutexModule],
|
||||
providers: [PasswordConverter, JwtService],
|
||||
exports: [PasswordConverter, RedisMutexModule, JwtService],
|
||||
imports: [RedisMutexModule],
|
||||
})
|
||||
export class CommonsModule {}
|
||||
|
8
src/commons/middleware/account.middleware.spec.ts
Normal file
8
src/commons/middleware/account.middleware.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { JwtService } from '../services/jwt.service';
|
||||
import { AccountMiddleware } from './account.middleware';
|
||||
|
||||
describe('AccountMiddleware', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new AccountMiddleware({} as JwtService)).toBeDefined();
|
||||
});
|
||||
});
|
31
src/commons/middleware/account.middleware.ts
Normal file
31
src/commons/middleware/account.middleware.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '../services/jwt.service';
|
||||
|
||||
@Injectable()
|
||||
export class AccountMiddleware implements NestMiddleware {
|
||||
constructor(private readonly jwtService: JwtService) {}
|
||||
async use(req: any, res: any, next: () => void) {
|
||||
const authPayload = req.header('authorization') ?? '';
|
||||
if (!authPayload) {
|
||||
req.user = req.session.user;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const token = authPayload.replace('Bearer ', '');
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('授权凭据不合法!');
|
||||
}
|
||||
try {
|
||||
const { payload } = await this.jwtService.verify(token);
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (err) {
|
||||
throw new UnauthorizedException('登录凭据失效或不合法!');
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
59
src/commons/services/jwt.service.spec.ts
Normal file
59
src/commons/services/jwt.service.spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { generateKeyPair, KeyObject } from 'crypto';
|
||||
import { getClientToken } from 'nestjs-etcd';
|
||||
import { promisify } from 'util';
|
||||
import { JwtService } from './jwt.service';
|
||||
import { SignJWT } from 'jose/jwt/sign';
|
||||
|
||||
describe('JwtService', () => {
|
||||
let service: JwtService;
|
||||
let privateKey: KeyObject;
|
||||
let publicKey: KeyObject;
|
||||
|
||||
beforeAll(async () => {
|
||||
const pair = await promisify(generateKeyPair)('ec', {
|
||||
namedCurve: 'prime256v1',
|
||||
});
|
||||
privateKey = pair.privateKey;
|
||||
publicKey = pair.publicKey;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
JwtService,
|
||||
{
|
||||
provide: getClientToken(),
|
||||
useValue: {
|
||||
get: () => ({
|
||||
buffer: () =>
|
||||
Promise.resolve(
|
||||
publicKey.export({ format: 'pem', type: 'spki' }),
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<JwtService>(JwtService);
|
||||
await service.onModuleInit();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('verify', () => {
|
||||
it('normal', async () => {
|
||||
const token = await new SignJWT({ userId: 'test' })
|
||||
.setProtectedHeader({ alg: 'ES256' })
|
||||
.setIssuedAt()
|
||||
.setIssuer('urn:example:issuer')
|
||||
.setAudience('urn:example:audience')
|
||||
.setExpirationTime('1h')
|
||||
.sign(privateKey);
|
||||
await expect(service.verify(token)).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
23
src/commons/services/jwt.service.ts
Normal file
23
src/commons/services/jwt.service.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { KeyObject, createPublicKey } from 'crypto';
|
||||
import { jwtVerify } from 'jose/jwt/verify';
|
||||
import { Etcd3, InjectClient } from 'nestjs-etcd';
|
||||
|
||||
@Injectable()
|
||||
export class JwtService implements OnModuleInit {
|
||||
publicKey: KeyObject;
|
||||
constructor(@InjectClient() private readonly etcd: Etcd3) {}
|
||||
async onModuleInit() {
|
||||
const buff = await this.etcd
|
||||
.get('commons/auth-jwt-public-key/index')
|
||||
.buffer();
|
||||
this.publicKey = createPublicKey(buff);
|
||||
}
|
||||
|
||||
async verify(token: string) {
|
||||
return await jwtVerify(token, this.publicKey, {
|
||||
algorithms: ['PS256', 'ES256'],
|
||||
});
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"lib": ["ES2021"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
|
Loading…
Reference in New Issue
Block a user