
먼저 jwt폴더 안에 jwt.middleware.ts파일을 만듭니다.
middleware를 class로 정의하거나 함수로도 정의할 수 있습니다.
nestjs의 미들웨어는 express와 같기때문에 next()를 해주어야 합니다.
import { NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
//implements는 해당 클래스가 interface로 행동하도록 한다.
// export class JwtMiddleware implements NestMiddleware {
// use(req: Request, res: Response, next: NextFunction) {
// console.log(req.headers);
// next();
// }
// }
export function jwtMiddleware(req: Request, res: Response, next: NextFunction) {
console.log(req.headers);
next();
}
Applying middleware
@Module() 데코레이터에는 미들웨어를 위한 위치가 없습니다. 대신 모듈 클래스의 configure() 메소드를 사용하여 설정합니다. 미들웨어를 포함하는 모듈은 NestModule 인터페이스를 구현해야 합니다. AppModule 레벨에서 jwtMiddleware를 설정해 보겠습니다.
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { jwtMiddleware } from './jwt/jwt.middleware';
@Module({
...
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(jwtMiddleware)
.forRoutes({ path: '/graphql', method: RequestMethod.ALL });
}
}
path로 지정된 url로 유저가 요청을 보낼때 미들웨어가 작동합니다.
Global middleware
미들웨어를 등록된 모든 경로에 한번에 바인딩하려면 INestApplication 인스턴스에서 제공하는 use() 메서드를 사용할 수 있습니다.
app.use에서는 argument로 들어간 값이 function일때만 가능합니다.
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { jwtMiddleware } from './jwt/jwt.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.use(jwtMiddleware);
await app.listen(3000);
}
bootstrap();
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateAccountInput } from './dtos/create-account.dto';
import { LoginInput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { ConfigService } from '@nestjs/config';
import { JwtService } from 'src/jwt/jwt.service';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly users: Repository<User>,
private readonly config: ConfigService,
private readonly jwtService: JwtService,
) {}
//create User
...
//Login User
...
async findById(id: number): Promise<User> {
return this.users.findOne({ id });
}
}
먼저 id를 통해 user를 찾는 메서드를 만듭니다.
users.module.ts
UsersService를 다른곳에서 의존성 주입을 하기위해서 exports해줍니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsesrResolver } from './users.resolver';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsesrResolver, UsersService],
exports: [UsersService],
})
export class UsersModule {}
jwt/jwt.service.ts
사용자는 서버에게서 토큰을 받은 후, 서버에게 요청을 보낼 때, request.Header에 토큰을 포함하여 요청을 보냅니다.
그러면 서버는 사용자에게서 받은 토큰이 유효한 것인지 확인해야합니다.
jwt.verify()함수를 이용하여 토큰 유효성을 확인할 수 있습니다.
jwt.verify() 함수에 들어가는 매개변수는 다음과 같습니다.
첫번째로 token : client에게서 받은 token
두번째로 secretkey : token 생성 시 사용했던 secretkey
import { Inject, Injectable } from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import { JwtModuleOptions } from './jwt.interfaces';
import { CONFIG_OPTIONS } from './jwt.constants';
@Injectable()
export class JwtService {
constructor(
@Inject(CONFIG_OPTIONS)
private readonly options: JwtModuleOptions,
) {
console.log(this.options);
}
sign(userId: number): string {
return jwt.sign({ id: userId }, this.options.privateKey);
}
verify(token: string) {
return jwt.verify(token, this.options.privateKey);
}
}
jwt/jwt.middleware.ts
if('x-jwt' in req.headers) 를 통해 header에 클라이언트에서 준 x-jwt이 있는지 확인후 있다면
jwtService에있는 verify메서드를 통해 jwt.sign으로 넘겨주었던 token값을 얻을 수 있습니다.
decoded를 콘솔에 찍어보면 { id: 3, iat: 1639162850 } 을 얻을 수 있습니다.
decoded에 id라는 프로퍼티를 가지고 있다면 userService의 findById메서드를 통해 해당 유저를 찾습니다.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { UsersService } from 'src/users/users.service';
import { JwtService } from './jwt.service';
// implements는 해당 클래스가 interface로 행동하도록 한다.
@Injectable()
export class JwtMiddleware implements NestMiddleware {
constructor(
private readonly jwtService: JwtService,
private readonly userService: UsersService,
) {}
async use(req: Request, res: Response, next: NextFunction) {
if ('x-jwt' in req.headers) {
const token = req.headers['x-jwt'];
try {
const decoded = this.jwtService.verify(token.toString());
if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {
const user = await this.userService.findById(decoded['id']);
// 이 request는 HTTP request같은건데 이걸 graphql resolver에 전달해 줘야한다.
req['user'] = user;
}
} catch (e) {}
}
next();
}
}
req['user'] = user 이 request는 HTTP request같은건데 이걸 graphql resolver에 전달해 줘야합니다.
req.user = user이 아닌 req['user'] = user로 사용하는 이유 : 동적으로 object key를 활용할때는 obj[key]로 쓴다고 합니다. 또한 req에는 원래 user이라는 프로퍼티가 없기 때문에 타입스크립트에서 오류를 표시합니다.
GraphQLModule.forRoot에는 context를 사용할 수 있습니다
context가 힘수로 정의되면 매 request마다 호출됩니다.
context는 req property를 포함한 object를 express로부터 받습니다.
즉 우리가 미들웨어로 넘겨준 req['user'] = user의 값을 context.user의 값으로 넘겨줍니다.
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { jwtMiddleware } from './jwt/jwt.middleware';
@Module({
...
GraphQLModule.forRoot({
autoSchemaFile: true, //메모리에 저장
//request context는 각 request에서 사용이 가능하다
//context기 힘수로 정의되면 매 request마다 호출된다.
//이것은 req property를 포함한 object를 express로부터 받는다.
context: ({ req }) => ({ user: req['user'] }),
}),
...
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(jwtMiddleware)
.forRoutes({ path: '/graphql', method: RequestMethod.ALL });
}
}
nest g mo auth를 통해 auth모듈을 만듭니다.
auth/auth.guard.ts 파일 만들기
canActivate는 함수인데 true를 리턴하면 request를 진행시키고 false면 request를 멈추게합니다.
canActivate(context: ExecutionContext)의 context를 통해 request의 context에 접근할 수 있습니다.
하지만 문제는 context가 http로 되어 있다는 겁니다. 그래서 이것을 graphql로 바꿔줘야합니다.
GqlExecutionContext.create(context).getContext()을 통해 graphql context값을 가져올 수 있습니다.
user기 있을시 return true로 해줌으로써 request를 진행시키고 user가 없을시 리턴값을 false로 해줌으로써 request를 멈추게합니다.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
// guard는 함수인데 request를 다음 단계로 진행할지 말지 결정한다.
// CanActivate는 함수인데 true를 리턴하면 request를 진핼시키고 false면 request를 멈추게한다
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context).getContext();
const user = gqlContext['user'];
if (!user) {
return false;
}
return true;
}
}
Guard사용
@UseGuards의 argument로 우리가 미들웨어로 만든 AuthGuard를 넣어줌으로 써 사용할 수 있습니다.
이제 Query로 me를 요청할때 토큰값이 있으면 request를 진행시키고 없으면 request를 멈추게합니다.
import { UseGuards } from '@nestjs/common';
import { Resolver, Query, Mutation, Args, Context } from '@nestjs/graphql';
import { AuthGuard } from 'src/auth/auth.guard';
import {
CreateAccountInput,
CreateAccountOutput,
} from './dtos/create-account.dto';
import { LoginInput, LoginOutput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsesrResolver {
constructor(private readonly usesrService: UsersService) {}
...
@Query((returns) => User)
@UseGuards(AuthGuard)
me() {}
}
AuthUser Decorator 만들기
auth/auth-user.decorator.ts 파일을 만듭니다.
createParamDecorator은 factory function이 필요합니다.
factory function에는 항상 unknown value인 data와 context가 있습니다
이전에 Guard만들 때 사용한 context를 이용해 user을 불러오고 리턴해줍니다.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const AuthUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const gqlContext = GqlExecutionContext.create(context).getContext();
const user = gqlContext['user'];
return user;
},
);
AuthUser Decorator 사용
우리가 만든 AuthUser을 함수의 인자에 넣어 사용할 수 있습니다.
AuthUser에서 return한 값이 authUser에 들어갑니다.
import { UseGuards } from '@nestjs/common';
import { Resolver, Query, Mutation, Args, Context } from '@nestjs/graphql';
import { AuthGuard } from 'src/auth/auth.guard';
import {
CreateAccountInput,
CreateAccountOutput,
} from './dtos/create-account.dto';
import { LoginInput, LoginOutput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Resolver((of) => User)
export class UsesrResolver {
constructor(private readonly usesrService: UsersService) {}
...
@Query((returns) => User)
@UseGuards(AuthGuard)
me(@AuthUser() authUser: User) {
//AuthUser에서 return한 값이 authUser에 들어간다.
console.log(authUser);
return authUser;
}
}