본문으로 건너뛰기

JWT 인증(JSON Web Token)

토큰 인증의 구성 및 사용.

소개​

JWT(JSON Web Token)는 프런트엔드가 백엔드와 분리된 웹 애플리케이션에서 인증의 보안과 컨텍스트를 보장하기 위한 표준입니다.

웹 애플리케이션의 프런트엔드가 ReactJS, AngularJS, VueJS 등으로 개발되고 백엔드가 애플리케이션의 동적 데이터를 제공하고 받기 위한 REST API를 제공할 때 흔히 사용됩니다.

토큰(JWT)은 사용자를 식별하는 보안 코드입니다.

JWT(JSON Web Token)를 사용하면 백엔드의 REST API가 비공개 서비스에서 인증 컨텍스트를 얻을 수 있어 인증된 사용자가 누구인지 알 수 있습니다. 즉, 토큰으로 어떤 사용자인지 알 수 있으며, 여기서 토큰은 JWT(JSON Web Token)입니다.

jwt 흐름

CORS​

REST API는 웹사이트와 다른 하위 도메인에 있을 수 있으며, 이 경우 서비스에 액세스할 수 있도록 CORS(Cross-Origin Resource Sharing) 구성을 해야 합니다.

따라서 REST API가 프런트엔드와 다른 도메인/하위 도메인에 있는 경우 JWT를 사용하더라도 CORS가 올바르게 구성되어 있어야 합니다.

CORS는 서로 다른 도메인, 하위 도메인 또는 포트에서 리소스를 소비하는 보안 정책입니다. 한 주소가 실제로 다른 주소와 통합될 수 있도록 보장하기 위한 것입니다.

이는 브라우저가 기밀 데이터 탈취 공격을 방지하기 위해 외부 주소의 서비스 또는 기타 유형의 리소스 사용을 차단하기 때문에 발생합니다.

활성화 및 구성​

Netuno 애플리케이션에서 JWT를 활성화하고 구성하려면 사용 중인 환경에 해당하는 애플리케이션 구성 파일을 편집해야 합니다. 예:

  • config/_development.json
  • config/_production.json

Netuno에 구성된 환경에 따라 netuno/config.js 파일에서 config.env 매개변수 설정을 확인하세요.

다음 매개변수를 입력하고 조정하세요:

...
"auth": {
"jwt": {
"enabled": true,
"secret": "비밀키는32자여야합니다!!!",
"expires": {
"access": 1440,
"refresh": 1440
}
}
},
...

secret 매개변수에는 복잡하고 무작위한 문자열을 넣으세요. JWT 암호화 보안을 보장하는 키입니다.

expires 매개변수는 분 단위로 정의됩니다. 예를 들어 60은 1시간, 1440은 하루에 해당합니다.

그룹별 접근 제한​

추가로 groups 매개변수를 전달할 수 있으며, 이는 JWT로 인증할 수 있는 사용자 그룹 코드를 정의합니다. 예:

...
"auth": {
"jwt": {
...
"groups": ["고객", "공급업체"],
...
}
},
...

위 예에서는 다음 그룹에 속한 사용자만 인증할 수 있습니다:

  • 고객, 그룹 코드 고객.
  • 공급업체, 그룹 코드 공급업체

이 그룹에 속한 사용자만 JWT로 인증할 수 있습니다.

액세스 토큰 얻는 방법​

JWT의 _액세스 토큰_을 얻기 위해 Netuno는 인증을 검증하고 성공하면 _액세스 토큰_과 _리프레시 토큰_을 반환하는 _auth 서비스를 제공합니다.

_액세스 토큰_은 다음과 같이 얻습니다:

Fetch - 순수 JavaScript​

브라우저의 JavaScript는 백엔드 REST API 서비스와 통합하기 위한 fetch를 제공합니다.

다음은 JWT 인증과 토큰 갱신을 수행하는 몇 가지 예입니다.

인증​

다음은 fetch로 _액세스 토큰_을 얻는 예입니다:

fetch("http://localhost:9000/services/_auth", {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: payload.username,
password: payload.password,
jwt: true
})
}).then((response) => {
if (response.status != 200) {
if (response.status != 403) {
console.log('예상치 못한 상태로 인증에 실패했습니다:', response.status);
}
alert('로그인이 유효하지 않습니다.');
return null;
}
return response.json();
}).then((res) => {
if (res && res.result === true) {
console.log(`내 액세스 토큰: ${res.access_token}`);
console.log(`내 리프레시 토큰: ${res.refresh_token}`);
console.log(`만료까지: ${res.expires_in}분`);
sessionStorage.setItem('token', JSON.stringify(res));
}
}).catch((error)=> {
console.log(error);
alert('기술적인 문제가 발생했습니다. 나중에 다시 시도하세요.');
});

token은 세션으로 저장되어야 하므로 보안을 위해 localStorage 대신 sessionStorage를 사용하는 것이 좋습니다.

refresh_token으로 만료 시간(expires_in, 분 단위) 전에 새 token을 생성할 수 있습니다.

다른 서비스에서 액세스 토큰 사용​

Netuno 애플리케이션에서 사전 인증이 필요한 맞춤형 서비스를 실행하려면 HTTP 프로토콜 헤더에 _액세스 토큰_을 Authorization 키 값으로 전달해야 합니다. 예:

Authorization: Bearer eyJhbGciOiJIUzU...

다음은 fetch를 사용하여 프런트엔드에서 맞춤형 서비스를 실행하는 예로, Authorization이 headers에 전달됩니다:

const token = JSON.parse(sessionStorage.getItem("token"));

fetch("http://localhost:9000/services/내-서비스", {
method: 'post',
credentials: 'include',
headers: {
'Authorization': `${token.token_type} ${token.access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
내매개변수1: '...',
내매개변수2: '...'
})
}).then((response) => {
if (response.status != 200) {
console.log(`서비스가 상태 ${response.status}로 실패했습니다.`);
if (response.status == 403) {
alert('인증되지 않았습니다.');
} else {
alert('서비스에 문제가 발생했습니다.');
}
return null;
}
return response.json();
}).then((res) => {
if (res) {
console.log(`내 서비스의 응답 데이터:`, res);
alert('서비스가 성공적으로 실행되었습니다.');
}
}).catch((error)=> {
console.log(error);
alert('기술적인 문제가 발생했습니다. 나중에 다시 시도하세요.');
});

HTTP 헤더의 Authorization 매개변수에는 이전 인증 시 sessionStorage에 저장한 JWT 객체에서 얻은 token_type과 access_token이 사용됩니다.

_리프레시 토큰_으로 갱신​

토큰을 갱신하고 인증을 연장하려면 _auth 서비스를 다시 호출해야 합니다. 이때 jwt: true 매개변수 외에도 이전에 성공한 인증에서 받은 값으로 refresh_token 매개변수를 함께 전달해야 합니다.

토큰 갱신은 만료 시간(expires_in, 분 단위) 전에 수행해야 합니다.

그런 다음 인증을 연장하고 토큰을 갱신하려면 다음 예를 참고하세요:

const token = JSON.parse(sessionStorage.getItem("token"));

fetch("http://localhost:9000/services/_auth", {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
refresh_token: token.refresh_token,
jwt: true
})
}).then((response) => {
if (response.status != 200) {
if (response.status != 403) {
console.log('예상치 못한 상태로 토큰 갱신에 실패했습니다:', response.status);
}
alert('유효하지 않은 토큰으로 접근을 갱신할 수 없습니다.');
return null;
}
return response.json();
}).then((res) => {
if (res && res.result === true) {
console.log(`내 새 액세스 토큰: ${res.access_token}`);
console.log(`내 새 리프레시 토큰: ${res.refresh_token}`);
console.log(`만료까지: ${res.expires_in}분`);
sessionStorage.setItem("token", JSON.stringify(res));
}
}).catch((error)=> {
console.log(error);
alert('기술적인 문제가 발생했습니다. 나중에 다시 시도하세요.');
});

새 토큰을 얻으면 이후 서비스 호출에서 이전(구) 토큰 대신 새 토큰을 사용해야 합니다.

이렇게 하면 토큰을 연속적으로 갱신하여 인증을 연장할 수 있으며, 사용자가 계속 인증된 상태로 유지되도록 보장하는 방법입니다.

Auth Client - NPM​

프런트엔드와 쉽게 통합할 수 있도록 다음 NPM 모듈이 제공됩니다:

프런트엔드에서는 Bun으로 설치할 수 있습니다:

  • bun install @netuno/auth-client

PNPM, NPM, YARN 또는 다른 프런트엔드 의존성 관리자를 사용할 수 있습니다.

구성​

이 모듈은 @netuno/service-client에 의존하며, 서비스 주소를 정의할 때 예를 들어 다음과 같이 설정합니다:

import _service from '@netuno/service-client';
...
_service.config({
prefix: 'http://localhost:9000/services/'
});

auth-client은 내부적으로 service-client의 접두사를 사용하여 _auth 서비스 주소를 구성합니다. 이 경우 예를 따르면 다음과 같습니다:

  • http://localhost:9000/services/_auth

따라서 이 주소는 Netuno 애플리케이션의 REST API 서비스 주소 접두사를 사용하여 자동으로 구성됩니다.

auth-client 구성을 사용자 지정하는 방법은 다음과 같습니다:

ES 모듈 버전에서는 config()가 비동기입니다. 구성된 클라이언트를 사용하기 전에 완료될 때까지 기다리세요:

import _auth from '@netuno/auth-client';

await _auth.config({
storage: 'local', // session이 기본값입니다.
onLogin: () => { alert("로그인되었습니다!"); },
onLogout: () => { alert("로그아웃되었습니다!"); }
});

토큰을 localStorage에 저장하려면 구성에서 storage: 'local'을 정의해야 합니다. 기본값은 sessionStorage를 사용하는 storage: 'session'이기 때문입니다.

인증​

_auth를 사용하여 로그인하려면:

import _auth from '@netuno/auth-client';
...
_auth.login({
username: "관리자",
password: "비밀번호",
success: ()=> {
alert("성공.");
},
fail: ()=> {
alert("실패.");
}
});

토큰은 구성에 따라 session(sessionStorage) 또는 local(localStorage)에 자동으로 저장되며, 기본값은 session입니다.

토큰 갱신​

토큰을 갱신하는 예:

import _auth from '@netuno/auth-client';
...
_auth.refreshToken({
success: ()=> {
alert("성공.");
},
fail: ()=> {
alert("실패.");
}
});

토큰은 구성에 따라 session(sessionStorage) 또는 local(localStorage)에 자동으로 저장되며, 기본값은 session입니다.

토큰 제거​

세션을 종료하는 예, 즉 로그아웃하고 토큰을 삭제하는 과정입니다:

import _auth from '@netuno/auth-client';
...
_auth.logout();

토큰은 구성에 따라 sessionStorage 또는 localStorage에서 제거되며, 기본값은 sessionStorage입니다.

백엔드 - REST API 서비스​

통합 시 Authorization 헤더가 유효한 토큰과 함께 전달되면 사용자와 그룹이 누구인지 식별할 수 있습니다.

백엔드에서는 각각 사용자와 그룹을 식별하는 _user 및 _group 리소스를 사용할 수 있습니다.

인증된 사용자의 데이터를 보려면 User 리소스를 사용합니다. 예:

_log.info(
"인증된 사용자:",
_val.map()
.set("id", _user.id())
.set("이름", _user.name())
.set("코드", _user.code())
.set("전체", _user.data())
)

인증된 사용자의 그룹을 보려면 Group 리소스를 사용합니다. 예:

_log.info(
"인증된 그룹:",
_val.map()
.set("id", _group.id())
.set("이름", _group.name())
.set("코드", _group.code())
.set("전체", _group.data())
)

보안​

토큰이 유효하지 않거나 Authorization이 전달되지 않으면 Netuno는 서비스가 실행되지 않도록 보장합니다. 단, 서비스가 공개 액세스로 구성된 경우는 예외이며, 기본적으로 서비스는 비공개입니다.

서비스가 공개인지 비공개인지 관리하려면 애플리케이션의 server/core/_service_config.js에서 구성하세요.

개발 환경에서는 편의상 Netuno가 비공개 서비스를 인증 없이 직접 실행할 수 있도록 허용합니다. 대부분의 맞춤형 서비스는 일반적으로 인증 컨텍스트가 필요하지 않으므로 개발과 테스트가 전반적으로 더 간단해지기 때문입니다.

개발 환경이 아닌 경우, 예를 들어 프로덕션에서는 당연히 인증이 있어야만 서비스를 실행할 수 있습니다.

인증 리소스​

일반적으로 _user와 _group 리소스로 충분하지만, 더 고급 작업을 수행해야 하는 상황이 있으며 이 경우 _auth 리소스를 사용할 수 있습니다.

_auth 리소스는 주요 인증 리소스로, 구성을 관리하고 인증 프로세스와 관련된 모든 작업을 제공합니다.

자세히 알아보려면 Auth 리소스 문서를 참고하세요.

ReAuthKit​

Netuno는 온라인 인증이 포함된 플랫폼을 만들기 위한 기본 프로젝트를 제공하며, 계정 생성, 로그인, 프로필, 아바타, 비밀번호 복구 등 많은 기능이 있습니다.

GitHub에서 확인하세요:

ReAuthKit은 프런트엔드를 React, Ant.Design 및 React Router로 만들고 데이터베이스를 PostgreSQL로 사용하는 Netuno 애플리케이션입니다.

ReAuthKit을 사용하면 인증이 필요한 모든 프로젝트에 필요한 메커니즘을 이미 갖추고 있어 인증이 포함된 새 프로젝트 생성 속도를 크게 높일 수 있습니다.

결론​

간단하게 애플리케이션에서 JWT(JSON Web Token) 인증을 구성하고 활성화할 수 있습니다.

REST API에서 안전한 서비스를 개발하려면 JWT 인증을 사용하세요.

프런트엔드에서 JWT 통합은 fetch를 사용하여 직접 수행할 수 있습니다.

구현을 빠르고 표준화하기 위해 NPM 모듈 @netuno/auth-client가 제공되며, 모든 프런트엔드 기술과 함께 사용할 수 있습니다.

REST API 서비스 실행 시 사용자를 식별해야 할 때마다 JWT 인증을 사용하세요.

백엔드에서는 구성과 인증된 사용자 가져오기가 매우 간단합니다.