로그인, 인증 기능 구축 프로젝트[미니 프로젝트 1] - 01. jwt를 이용한 인증 앱 생성하기 : 02. 간단한 인증 시스템 구현
2. 간단한 인증 시스템 구현
이제 전에 배웠던 JWT의 인증 과정을 실제로 소스코드로 구현해보자.
2-1. 기본 세팅
먼저 express 앱을 생성해주자.
//package.json 생성
npm init -y
그리고 추가적으로 설치할 모듈은 dotenv, jsonwebtoken, nodemon, express이다.
각각의 모듈을 설명해보자면
2-1-1. dotenv 모듈
dotenv 모듈은 환경 변수(environment variables)를 .env 파일에서 불러와 애플리케이션에서 사용할 수 있도록 해주는 Node.js 라이브러리이다.
환경 변수는 주로 애플리케이션의 설정이나 비밀 키와 같은 민감한 정보를 코드 외부에서 관리할 때 사용된다.
그리고 우리 프로젝트에서도 이런 노출되지 않기 위한 설정 혹은 키에 대해서 코드를 노출시키지 않기 위해서 사용될 모듈이다.
보안성: 비밀번호, API 키, 데이터베이스 연결 문자열과 같은 민감한 정보를 코드에 하드코딩하는 것은 보안상 위험하기에 .env 파일에 환경 변수를 저장하면, 이 파일을 Git과 같은 버전 관리 시스템에서 제외하여 보안을 강화할 수 있다.
환경 설정 관리: 개발, 테스트, 운영 등 각 환경마다 다른 설정을 사용할 가능성이 있기에 .env 파일을 통해 환경별로 다른 변수 값을 설정하고, 코드에서 이를 불러와 사용하도록 만들 수 있다.
간편한 설정: 애플리케이션의 설정을 별도로 코드에 작성하지 않고, .env 파일에 한 곳에서 관리할 수 있어 프로젝트 관리가 용이하다.
2-1-2. jsonwebtoken 모듈
jsonwebtoken 모듈은 JWT (JSON Web Token)를 생성하고 검증하는 데 사용되는 Node.js 라이브러리이다
이 모듈을 사용하면 토큰 기반 인증을 구현할 수 있으며 주로 사용자 인증, 권한 관리, 세션 관리 등에서 사용된다.
2-1-3. Nodemon 모듈
nodemon은 알다시피 서버를 종료하지 않아도 서버의 변경점이 실행중에도 반영이 되도록 해주는 모듈이다
모듈 설치하는 방법은 아래와 같다.
npm install dotenv jsonwebtoken nodemon express
2-2. entry file인 index.js 생성
이제 index.js파일을 생성해주고 그안에 express를 사용해서 기본적으로 세팅을 해주자.
먼저 express 모듈을 불러오고
express를 통해 app 객체를 생성해주자.
그리고 express는 기본적으로 본문 body에 들어올 값을 json으로 파싱하지 않고 문자열로만 받기 때문에 직접 파싱을 해주는 express.json()을 use메서드를 사용해서 미들웨어로 모든요청에 걸리도록 생성해주자(저 내부에 next()가 존재하기에 그냥 사용해도 됨).
그리고 포트를 설정해주고 server listen을 통해서 서버를 켜주는 코드를 설정해주자.
2-3. login API 생성
/login으로 요청이 들어오면 받아줄 post 메서드를 하나 생성해주고
요청에서 body를 통해서 받아올 userId를 변수로 넣어주며 변수를 생성해주자.
이제 토큰을 만들어야하는데 그 전에 JWT를 어떻게 사용 하는 지를 먼저 알아보기 위해서 jwt 사이트로 들어가보면
https://www.npmjs.com/package/jsonwebtoken
jsonwebtoken
JSON Web Token implementation (symmetric and asymmetric). Latest version: 9.0.2, last published: a year ago. Start using jsonwebtoken in your project by running `npm i jsonwebtoken`. There are 32625 other projects in the npm registry using jsonwebtoken.
www.npmjs.com
기본적으로 어떻게 토큰을 생성해야하는지 나와있다.
가장 먼저 모듈을 불러온 후에 sign() 메서드를 사용해서 토큰을 생성해서 사용하면 된다.
- jwt.sign(payload, secret, [options, callback])
1. payload (필수)
JWT에 내용(데이터)이 담긴 객체 또는 문자열로 이 데이터는 서명되지 않은 상태에서 Base64URL 인코딩하여 JWT 본문에 포함된다.
var payload = { userId: 123, role: 'admin' };
var token = jwt.sign(payload, 'shhhhh');
보통은 JWT의 payload 부분에 userId와 role에 대한 정보를 포함시킨다.
Base64Url값은 암호화 방식이 아니기에 누구든 인코딩할 수 있고 누구든 디코딩할 수 있기에 민감한정보(비밀번호, 신용카드 번호 등)는 저장하지 않아야한다.
2. secretOrPrivateKey (필수)
JWT 서명(Signature)을 생성할 때 사용되는 키로 HMAC 알고리즘 (HS256, HS384, HS512)을 사용할 경우 비밀 키 (Secret Key)를 사용하고
var token = jwt.sign(payload, 'mySecretKey');
=> 여기서 "mySecretKey" 가 비밀키가 됨
RSA, ECDSA 알고리즘 (RS256, ES256 등)를 사용할 경우 비공개 키(Private Key)를 사용한다.
var fs = require('fs');
var privateKey = fs.readFileSync('private.key');
var token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
=> 여기서 private.key 파일이 비공개 키로 사용됨
HMAC 방식에서는 동일한 키를 사용하여 서명과 검증을 수행해야하고 RSA/ECDSA 방식에서는 비공개 키로 서명하고, 공개 키로 검증한다.
3. options (선택, 추가 설정)
sign() 메서드의 옵션을 설정하는 객체로 토큰 만료 시간, 알고리즘, 발급자 정보 등의 추가설정을 하는 값이다
옵션명 | 설명 | 예제 |
algorithm | 사용할 해싱 알고리즘 (기본값: HS256) | { algorithm: 'RS256' } |
expiresIn | 토큰 만료 시간 설정 (s, m, h, d 지원) | { expiresIn: '1h' } (1시간) |
notBefore | 토큰 활성화 시간 (s, m, h, d 지원) | { notBefore: '10s' } (10초 후부터 유효) |
audience | 토큰의 대상 (aud 클레임) | { audience: 'https://example.com' } |
issuer | 토큰 발급자 (iss 클레임) | { issuer: 'MyCompany' } |
subject | 토큰의 주체 (sub 클레임) | { subject: 'user123' } |
예를 들어 1시간 후에 만료되는 JWT 토큰을 생성한다면
var token = jwt.sign({ userId: 123 }, 'shhhhh', { expiresIn: '1h' });
와 같이 코드를 구성하면 된다.
추가로 특정 발급자와 대상을 설정한 JWT 토큰을 생성한다면
var token = jwt.sign({ userId: 123 }, 'shhhhh', {
expiresIn: '1h',
issuer: 'MyCompany',
audience: 'https://example.com'
});
와 같이 구성하면 발급자(issuer)와 대상(audience)가 포함된 토큰이 생성된다.
4. callback (선택, 비동기 방식)
sign()은 기본적으로 동기(Synchronous) 함수이나 콜백 함수를 제공하면 비동기(Asynchronous) 방식으로 동작할 수 있다.
jwt.sign({ userId: 123 }, 'shhhhh', { expiresIn: '1h' }, function(err, token) {
if (err) {
console.error("토큰 생성 오류:", err);
} else {
console.log("생성된 토큰:", token);
}
});
이렇게 비동기 방식으로 JWT를 생성하면 에러 핸들링이 가능해진다.
jwt.sign() 실행 과정
jwt.sign()이 실행되면 다음과 같은 과정으로 JWT가 생성된다.
먼저 Header(헤더)를 생성한다.
아래는 기본적으로 설정되는 헤더 값의 예시이다.
{
"alg": "HS256",
"typ": "JWT"
}
보통 옵션에 추가되는 algorithm에 의해서 해당 값이 결정되고 추가적으로 옵션에
const token = jwt.sign(
{ userId: 123 },
'mySecretKey',
{
header: { kid: 'myKey123' } // 헤더 추가 정보 설정
}
);
이런 식으로 헤더를 추가할 수 있는데 헤더를 추가하면
{
"alg": "RS256",
"typ": "JWT",
"kid": "myKeyId"
}
이렇게 헤더에 값이 추가된다.
이렇게 생성된 헤더를 Basic64URL으로 인코딩한다.
( ex. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9)
그리고 Payload (페이로드)를 생성해준다.
페이로드에는 첫번쨰 전달인자인 페이로드 값에 옵션값이 추가되어 생성된다
var token = jwt.sign(
{ userId: 123 },
'shhhhh',
{
expiresIn: '1h',
issuer: 'MyCompany',
audience: 'https://example.com'
}
);
이렇게 옵션값이 추가되어 있다면 페이로드는 아래와 같이 생성된다.
{
"userId": 123,
"iss": "MyCompany",
"aud": "https://example.com",
"exp": 1707723600 // UNIX 타임스탬프 (1시간 후)
}
이걸 또 Basic64URL으로 인코딩하여 출력하고
( ex. eyJ1c2VySWQiOjEyMywiaXNzIjoiTXlDb21wYW55IiwiYXVkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImV4cCI6MTcwNzcyMzYwMH0)
마지막으로 signature를 생성 (HMAC-SHA256 사용)한다
signature는 아래와 같이 생성한다.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
여기서 secret은 sign함수의 두번째 인자로 전달된 값에 해당한다.
그리고 이렇게 생성한 코드를 헤더.페이로드.시그니쳐 순으로 .을 경계로 해서 붙여준다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiaXNzIjoiTXlDb21wYW55IiwiYXVkIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImV4cCI6MTcwNzcyMzYwMH0
.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
이렇게 생성된 값이 JWT의 토큰이다.
이제 VSCode로 돌아가서
생성해둔 userId를 Payload에 넣을 형태로 구성해주고 이걸 변수에 담아주자.
그리고 이제 jwt를 이용해서 토큰을 생성해보자.
jwt모듈을 사용해서 sign함수를 상용하고
이 안에 페이로드를 넣어주고
두번째 인자로 비밀키로 "secret"이라는 문자열을 넣어주자.
이렇게 생성해주면 token이라는 변수에 토큰 값이 저장되게 된다.
그리고 이렇게 생성된 값을 일단 그대로 사용자에게 응답으로 보내줘보도록 하자.
Postman을 통해서 body에 userId를 넣고
요청을 보내보면 아래와 같이 생성된 토큰이 전달된다.
3. 서버에 토큰으로 인증받기
이번엔 이렇게 생성된 토큰을 통해서 인증된 사람만 서버에서 데이터를 가져갈 수 있는 코드를 한번 생성해보자.
보통은
이렇게만 있어도 데이터를 정상적으로 받을 수 있을텐데
토큰을 사용해서 인증을 할 수 있도록 설정해두면 아무 사용자나 맘대로 접근할 수 없도록 만들 수 있다.
이때는 JWT의 verify()메서드를 사용해서 인증을 확인할 수 있고 이를 미들웨어로 생성해두고 요청에 걸어두면 해당 요청이들어 올때는 JWT 토큰을 확인해서 인증되어야만 값을 내보낼 수 있도록 만들 수 있다.
이는 다음에 확인해보자