CSRF토큰과 액세스 토큰 모두 사용자의 신원이나 요청의 정당성을 확인하는 데 사용된다. 하지만 이 둘의 목적과 작동 방식은 다르다.
요청의 정당성 확인
: CSRF 토큰은 특정 요청이 사용자가 의도한 것인지 확인하기 위해 사용된다. 즉, 요청이 사용자의 브라우저에서 실제로 발생했는지 확인하는 것이다.사용자가 폼을 로드할 때 서버가 CSRF 토큰을 생성하여 폼에 포함시킨다.
사용자가 폼을 제출하면 CSRF 토큰도 함께 제출된다.
서버는 제출된 CSRF 토큰을 검증하여 요청이 신뢰할 수 있는 출처에서 온 것인지 확인한다.
이를 통해 CSRF 공격을 방지한다. 이는 악의적인 사이트가 사용자를 속여서 요청을 보내는 것을 방지한다.
사용자의 신원 및 권한 확인
: 액세스 토큰은 사용자가 인증된 사용자임을 확인하고, 그 사용자가 특정 리소스나 서비스에 접근할 수 있는 권한이 있는지 확인하는 데 사용된다.사용자가 로그인하면 서버가 사용자의 자격 증명을 확인하고 액세스 토큰을 발급한다.
클라이언트는 이후의 API 요청에 이 액세스 토큰을 포함하여 서버에 보낸다.
서버는 액세스 토큰을 검증하여 요청이 인증된 사용자로부터 온 것인지, 그리고 해당 사용자가 요청한 리소스에 접근할 권한이 있는지를 확인한다.
액세스 토큰은 특정 기간 동안 유요하며, 만료되면 갱신해야 한다.
NestJS 서버는 csurf
미들웨어를 사용하여 CSRF 보호를 설정한다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as csurf from 'csurf';
import * as cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(cookieParser());
app.use(csurf({ cookie: true }));
await app.listen(3000);
}
bootstrap();
CSRF 토큰을 제공하는 엔드포인트를 설정한다ㅏ.
import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller()
export class AppController {
@Get('csrf-token')
getCsrfToken(@Req() req: Request, @Res() res: Response) {
res.cookie('XSRF-TOKEN', req.csrfToken());
res.send({ csrfToken: req.csrfToken() });
}
}
클라이언트 측에서 CSRF 토큰을 받아서 API 호출 시 사용한다.
async function getCsrfToken() {
const response = await fetch('/csrf-token');
const data = await response.json();
return data.csrfToken;
}
async function updateProfile() {
const csrfToken = await getCsrfToken();
const response = await fetch('/profile/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken,
},
body: JSON.stringify({
username: 'new_username',
}),
});
const result = await response.text();
console.log(result);
}
updateProfile();
서브 측에서는 csurf
미들웨어가 CSRF 토큰을 자동으로 검증한다.
import { Controller, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('profile')
export class ProfileController {
@Post('update')
updateProfile(@Req() req: Request, @Res() res: Response) {
// CSRF 토큰 검증은 자동으로 처리된다.
// 프로필 업데이트 로직
res.send('Profile updated successfully');
}
}
NestJS 서버는 JWT(JSON Web Token)를 사용하여 액세스 토큰을 발급하고 검증한다.
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth/auth.service';
import { AuthController } from './auth/auth.controller';
import { JwtStrategy } from './auth/jwt.strategy';
import { PassportModule } from '@nestjs/passport';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'your_jwt_secret_key',
signOptions: { expiresIn: '60m' },
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
})
export class AppModule {}
사용자가 로그인하면 서버는 액세스 토큰을 발급한다.
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
async validateUser(username: string, pass: string): Promise<any> {
// 사용자 검증 로직
return { userId: 1, username: 'testuser' }; // 예시 사용자
}
}
import { Controller, Post, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Req() req) {
const user = await this.authService.validateUser(req.body.username, req.body.password);
return this.authService.login(user);
}
}
클라이언트는 API 호출 시 액세스 토큰을 HTTP 헤더에 포함하여 서버에 요청한다.
클라이언트 측 API 호출 예시 (JavaScript)
const accessToken = 'abcdef1234567890';
fetch('https://api.example.com/user-data', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
서버는 요청에 포함된 액세스 토큰을 검증하여 사용자의 신원과 권한을 확인한다.
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'your_jwt_secret_key',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
보호된 API 엔드포인트를 작성한다.
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
@Controller('user')
export class UserController {
@Get('data')
@UseGuards(JwtAuthGuard)
getUserData() {
return { username: 'testuser', email: 'testuser@example.com' };
}
}