전에 한번 정리했던 것을 복습하자면
JWT
JWT 구조
이처럼 Nest.Js를 통해 JWT를 구현해보는 시간을 가졌다.
이것을 다시 한번 복습해보자.
시작하기 전에 앞서 패키지들을 설치해줘야한다.
yarn add @nestjs/jwt passport-jwt
yarn add @nestjs/passport
yarn add --dev @types/passport-jwt
yarn add bcrypt
yarn add --dev @types/bcrypt
설치후 auth 폴더
생성 후 module.ts
& resolver.ts
& service.ts
파일 생성해준다.
기본 뼈대 만들어주기
//auth.module.ts
@Module({
imports: [
],
providers: [
],
})
export class AuthModule {}
//auth.reslover.ts
@Resolver()
export class AuthResolver {
constructor(
private readonly authService: AuthService, //
) {}
@Mutation(() => String)
login(
@Args('email') email: string, //
@Args('password') password: string,
): {}
}
// auth.servcie.ts
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService, //
) {}
login({ email, password }: IAuthServiceLogin): {}
getAccessToken(): {}
}
@Args 인자로 이메일과 비번을 받을 걸 정했으니 그에 따라 login API 작성하기
//auth.reslover.ts
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { AuthService } from './auth.service';
@Resolver()
export class AuthResolver {
constructor(
private readonly authService: AuthService, //의존성주입
) {}
@Mutation(() => String)
async login(
@Args('email') email: string, // @Args()데코레이터 사용하여 데이터 지정
@Args('password') password: string,
): Promise<string> { //accessToken를 받아야하므로 string
// Promise 기다려주는 타입 !
return this.authService.login({ email, password });
}
}
login 하기 위해 검증로직을 짜야하므로 아까 만들었던 service.ts
만들어주기
// auth.service.ts
import { JwtService } from '@nestjs/jwt'; //앞에 설치하였던 Jwt 사용
import * as bcrypt from 'bcrypt'; // 앞에 설치하였던 bcrypt
// 전체 사용을 위한 *
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService, // 의존성 주입 사용
private readonly usersService: UsersService, // 의존성 주입 사용
) {}
async login({ email, password }: IAuthServiceLogin): Promise<string> {
// 1. 이메일이 일치하는 유저를 DB에서 찾기
const user = await this.usersService.findOneByEmail({ email });
// 2. 일치하는 유저가 없으면?! 에러 던지기!!!
if (!user) throw new UnprocessableEntityException('이메일이 없습니다.');
// 3. 일치하는 유저가 있지만, 비밀번호가 틀렸다면?!
// 아래 로직 설명 유저안에있는 비밀번호! 순서틀리지 말기
const isAuth = await bcrypt.compare(password, user.password);
if (!isAuth) throw new UnprocessableEntityException('암호가 틀렸습니다.');
// 4. 일치하는 유저도 있고, 비밀번호도 맞았다면?!
// => accessToken(=JWT)을 만들어서 브라우저에 전달하기
return this.getAccessToken({ user });
}
// JwtSercive 의존성 주입 사용하여 토큰 생성후 4번에 전달
// AccessToken 은 바로 생성되기에 async~awiat 사용 x
getAccessToken({ user }: IAuthServiceGetAccessToken): string {
return this.jwtService.sign(
{ sub: user.id },
{ secret: process.env.JWT_ACCESS_KEY, expiresIn: '1h' },
);
}
}
// auth-service.interface.ts
export interface IAuthServiceLogin {
email: string;
password: string;
}
// users.service.ts
findOneByEmail({ email }: IUsersServiceFindOneByEmail) {
return this.usersRepository.findOne({ where: { email } });
} // 추가
// userDB에 존재하는 email과 login시 작성한 email이 동일한지 확인하는것.
resolver 와 service 합치기
// auth.module.ts
@Module({
imports: [
JwtModule.register({}), // register({}) 사이에는 토큰을 만들때 필요한 설정들을 넣어줄수 있음
TypeOrmModule.forFeature([ // **user table을 조회하기 위해 사용**
User, //
]),
],
providers: [
AuthResolver, //
AuthService,
UsersService,
],
})
export class AuthModule {}
Resource Owner(사용자)
가 인증이 필요한 경우 Client(애플리케이션 서버)
는 발급받은 JWT를
Request Header 에 보내준다.
Backend는 JWT를 받고 Guard를 통해 JWT Strategy를 실행하고,
Secret Key를 통해 JWT를 Decoding 합니다.
JWT를 복호화 한 후에 원하는 API의 Business Logic이 수행된 후, Response 됩니다.
양식은 항상 다음과 같은 형식으로 보내줍니다.
{"Authorization":"Bearer accesstoken정보"}
// Bearer : 토큰을 통해 인증할 때 Bearer 용어를 붙여서 사용하는 약속으로 큰 의미가 없는 문자열
보내주게 되면 브라우저 네트워크에 해당 토큰 정보가 들어옵니다.
Passport
: 인기 있는 node.js 인증 라이브러리로서 자격 증명(JWT, 사용자 이름/암호)을 확인하여 사용자를 인증하고, 인증 상태를 관리하고, 인증된 사용자에 대한 정보를 Route Handler에서 사용할 수 있도록 Request 객체에 첨부해 줍니다.
시작하기 전 설치해야 할 패키지
yarn add @nestjs/jwt passport-jwt
yarn add --dev @types/passport-jwt
yarn add @nestjs/passport
yarn add passport
로그인 한 사람의 정보를 가지고 오는 API를 만들어 준다. 그 전에 만들어둔 fetchUser API는 누구나 사용할 수 있는 API 이기에
로그인을 했든,안했든 누구나 다 사용 할 수있는 API이다.
// users.resolver.ts
@UseGuards(AuthGuard('access'))
@Query(() => String)
fetchLoginUser(): string {
console.log('인가에 성공했습니다.');
return '인가에 성공했습니다.';
}
여기서 로그인을 했는지 안했는지 검증을 하기 위한 방어막을 씌어준다.방어막은 UseGuard()
이다.
이 부분은 GraphQl 구현으로 이따가 설명!
// user.resolver.ts
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import * as bcrypt from 'bcrypt';
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Resolver()
export class UsersResolver {
constructor(
private readonly usersService: UsersService, //
) {}
@UseGuards(AuthGuard('access'))
@Query(() => String)
fetchUser(): string {
console.log('인가에 성공하였습니다')
return '인가에 성공하였습니다.'
}
@Mutation(() => User)
async createUser(
@Args('email') email: string,
@Args('password') password: string,
@Args('name') name: string,
@Args({ name: 'age', type: () => Int }) age: number,
): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 10);
return this.usersService.create({ email, hashedPassword, name, age });
}
}
이제 방어막을 사용할 수 있게 검증 로직을 만들어 준다. 1차 구현 rest-api만을 위한 구현
// jwt-access.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'access'){
constructor(){
//자식이 부모한테 값을 던져주고 싶을 때 super 함수를 쓴다.
//자식 passprt 부모 JwtAccessStrategy
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// bearer을 제외한 문자열 추출
secretOrKey: process.env.JWT_ACCESS_KEY
// 복호화할 키
})
}
validate(payload) { // 인증 성공 시 payload 열람하기 위한 validate
return { //검증 실패시 에러가 반환!
email: payload.email,
id: payload.sub, // payload.sub 는 인증할 때 sub : id 로 담았던 id
};
}
}
jwt-access.strategy.ts
에서 super를 통해 JWT 옵션값들이 PassportStrtegy
로 넘겨져 jwt 토큰 방식으로 검증을 시작 GraphQl에서는 Guards를 사용하기 위해 추가적인 로직이 필요하다.
즉, GraphQL에서는 @UseGuards(AuthGuard('access'))를 사용할 수 없습니다.
GraphQL에 사용 하기 위해선 graphql 에서 필요한 부분을 뽑아서 AuthGuard로 보내주어야 합니다.
// gql-auth.guard.ts
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
// GqlAuthAccessGuard 는 AuthGuard('access')를 상속받음
// GqlAuthAccessGuard 실행 후 AuthGuard 실행 (오버라이딩)
// 오버라이딩: 부모클래스가 실행후 자식클래스 실행 즉,재할당?
export class GqlAuthAccessGuard extends AuthGuard('access') {
// 반드시 getRequest 함수로 해야한다.
getRequest(context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
// rest-api 용으로 들어오는 context 이기에 GraphQL 용으로 다시 만들어줌 (오버라이딩)
return gqlContext.getContext().req;// user.resolver.ts 파일의
// GqlAuthAccessGuard 로 리턴
}
}
AuthGuard('access')
를 GqlAuthAccessGuard
로 상속시켜 주었기에 users.resolver.ts
파일을 수정해주자
// users.resolver.ts
// @UseGuards(AuthGuard('access'))
@UseGuards(GqlAuthAccessGuard) // 수정
@Query(() => String)
fetchLoginUser(): string {
console.log('인가에 성공했습니다.');
return '인가에 성공했습니다.';
}
로그인 하여 accessToken 복사해주자.
Jwt에서 받은 accessToken를 실어서 실행하면 fetchUser API가 성공적으로 실행했다는걸 볼수 있다.
유저정보를 가져오기 위해 resolver 를 수정해준다.
// user.resolver.ts
@UseGuards(GqlAuthAccessGuard)
@Query(() => String)
fetchUser(
@Context() context: IContext, //
//context : 모든 resolver 함수에 전달되며,
// 현재 로그인한 사용자 / DB access 와 같은 중요 정보를 담는다.
//context 는 Request / Response / header / payload 등 에 대한 정보들을 담고 있다.
): string {
// 유저 정보 꺼내오기
console.log('================');
console.log(context.req.user);
console.log('================');
return '인가에 성공하였습니다.';
}
context 타입을 지정해주는 IContext를 만들어주기 위해 새로운 파일을 생성한다.
// context.ts
// context 타입을 지정해주는 IContext를
import { Request, Response } from 'express';
export interface IAuthUser {
user?: { // 검증이 실패할 수도 있어서 값이 필수적이지 않을때는 ?써준다.
email: string;
id: string;
};
}
export interface IContext {
req: Request & IAuthUser; // validate를 통해 보내주는 payload 정보인 로그인한 user 정보까지
res: Response; // 담아준다 : IAuthUser
}
3차 구현 결과
validate에 존재하는 payload값과
fetchUser에 존재하는 유저의 정보도 받아 올수 있다.