JWT 인증(JSON Web Token)
토큰 인증의 구성 및 사용.
소개
JWT(JSON Web Token)는 프런트엔드가 백엔드와 분리된 웹 애플리케이션에서 인증의 보안과 컨텍스트를 보장하기 위한 표준입니다.
웹 애플리케이션의 프런트엔드가 ReactJS, AngularJS, VueJS 등으로 개발되고 백엔드가 애플리케이션의 동적 데이터를 제공하고 받기 위한 REST API를 제공할 때 흔히 사용됩니다.
토큰(JWT)은 사용자를 식별하는 보안 코드입니다.
JWT(JSON Web Token)를 사용하면 백엔드의 REST API가 비공개 서비스에서 인증 컨텍스트를 얻을 수 있어 인증된 사용자가 누구인지 알 수 있습니다. 즉, 토큰으로 어떤 사용자인지 알 수 있으며, 여기서 토큰은 JWT(JSON Web Token)입니다.

CORS
REST API는 웹사이트와 다른 하위 도메인에 있을 수 있으며, 이 경우 서비스에 액세스할 수 있도록 CORS(Cross-Origin Resource Sharing) 구성을 해야 합니다.
따라서 REST API가 프런트엔드와 다른 도메인/하위 도메인에 있는 경우 JWT를 사용하더라도 CORS가 올바르게 구성되어 있어야 합니다.
CORS는 서로 다른 도메인, 하위 도메인 또는 포트에서 리소스를 소비하는 보안 정책입니다. 한 주소가 실제로 다른 주소와 통합될 수 있도록 보장하기 위한 것입니다.
이는 브라우저가 기밀 데이터 탈취 공격을 방지하기 위해 외부 주소의 서비스 또는 기타 유형의 리소스 사용을 차단하기 때문에 발생합니다.
활성화 및 구성
Netuno 애플리케이션에서 JWT를 활성화하고 구성하려면 사용 중인 환경에 해당하는 애플리케이션 구성 파일을 편집해야 합니다. 예:
config/_development.jsonconfig/_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 리소스를 사용합니다. 예:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(
"인증된 사용자:",
_val.map()
.set("id", _user.id())
.set("이름", _user.name())
.set("코드", _user.code())
.set("전체", _user.data())
)
_log.info(
"인증된 사용자:",
_val.map()
.set("id", _user.id())
.set("이름", _user.name())
.set("코드", _user.code())
.set("전체", _user.data())
)
_log.info(
"인증된 사용자:",
_val.map()
.set("id", _user.id())
.set("이름", _user.name())
.set("코드", _user.code())
.set("전체", _user.data())
)
_log.info(
"인증된 사용자:",
_val.map()
.set("id", _user.id())
.set("이름", _user.name())
.set("코드", _user.code())
.set("전체", _user.data())
)
_log.info(
"인증된 사용자:",
_val.map()
.set("id", _user.id())
.set("이름", _user.name())
.set("코드", _user.code())
.set("전체", _user.data())
)
_log.info(
"인증된 사용자:",
_val.map()
.set("id", _user.id())
.set("이름", _user.name())
.set("코드", _user.code())
.set("전체", _user.data())
)
인증된 사용자의 그룹을 보려면 Group 리소스를 사용합니다. 예:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(
"인증된 그룹:",
_val.map()
.set("id", _group.id())
.set("이름", _group.name())
.set("코드", _group.code())
.set("전체", _group.data())
)
_log.info(
"인증된 그룹:",
_val.map()
.set("id", _group.id())
.set("이름", _group.name())
.set("코드", _group.code())
.set("전체", _group.data())
)
_log.info(
"인증된 그룹:",
_val.map()
.set("id", _group.id())
.set("이름", _group.name())
.set("코드", _group.code())
.set("전체", _group.data())
)
_log.info(
"인증된 그룹:",
_val.map()
.set("id", _group.id())
.set("이름", _group.name())
.set("코드", _group.code())
.set("전체", _group.data())
)
_log.info(
"인증된 그룹:",
_val.map()
.set("id", _group.id())
.set("이름", _group.name())
.set("코드", _group.code())
.set("전체", _group.data())
)
_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 인증을 사용하세요.
백엔드에서는 구성과 인증된 사용자 가져오기가 매우 간단합니다.