Skip to content

Commit

Permalink
Merge pull request #19 from heesung6701/feature/seminar5
Browse files Browse the repository at this point in the history
Feature/seminar5
  • Loading branch information
heesung6701 authored Nov 14, 2019
2 parents 3b3b641 + 0dbde49 commit b732d1f
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 2 deletions.
121 changes: 120 additions & 1 deletion seminar5/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,123 @@ multer를 사용하는 것과 동일하게 사용할 수 있다.

> 또한 size와 mimetype등을 이용해서 의도하지 않는 파일 업로드에 제한을 주는 코드를 구현할 수 있다.
# jwt
# jwt

## 세션 기반 인증 (참고용)

### 세션의 과정
1. 클라이언트가 로그인
2. 성공하면 서버가 유저 세션을 만들고 메모리나 데이터베이스에 저장한다.
3. 서버가 클라이언트에게 세션 ID를 보낸다.
4. 클라이언트의 브라우저에 세션의 ID만 쿠키에 저장하게 한다.

세션 데이터가 서버의 메모리에 저장되므로, 확장 시 모든 서버가 접근할 수 있도록 별도의 중앙 세션 관리 시스템이 필요하다.

### 단점

- 중앙 세션 관리 시스템이 없으면, 시스템 확장에 어려움이 생긴다.
- 중앙 세션 관리 시스템이 장애가 일어나면, 시스템 전체가 문제가 생긴다.
- 만약 메모리에 세션 정보가 들어있다면, 메모리가 많이 사용될 수 있다.
- 규모 확장이 필요없는 소규모 프로그램 작성에서는 세션 기반 인증 방식을 사용해도 상관 없을 것이다.

[출처](https://yonghyunlee.gitlab.io/node/jwt/)

## 토큰 기반 인증

토큰 기반 인증 방식에는 아래와 같은 특징이 있습니다.

### 1. stateless 서버

stateless 서버의 가장 큰 특징은 상태를 유지하지 않는 것입니다. <br/>
즉 서버는 클라이언트에서 **들어오는 요청**만으로 작업을 처리합니다. <br/>
이런 서버구조를 갖게 된다면 서버와 클라이언트의 **연관관계가 낮아**지기 때문에 **확장성**에 유리해집니다.

> ### Stateful 서버
>
> 반면 Stateful 서버는 클라이언트에게서 요청을 받을 때 마다, 클라이언트의 상태를 계속해서 유지하고, 이 정보를 서비스 제공에 이용하는 서버를 의미합니다.
### 2. 보안성

토큰을 사용하면 쿠키를 전달하지 않아도 되기 때문에 쿠키로 인해 발생하는 취약점이 사라지게 됩니다.

### 3. 플랫폼간 권한 공유

토큰을 사용한다면 다른 서비스에서도 권한을 공유 할 수 있습니다.
예를 들어서 소셜 로그인을 하고 토큰을 발급 받은 이후에 다른 플랫폼에서 토큰을 이용할 수 있습니다.

### 4. 모바일 어플리케이션에 적합

세션 기반의 인증을 하게 된다면 쿠키 매니저를 따로 관리해야 하지만 토큰을 이용한다면
헤더에 토큰 값을 넣어 주기만 하면 됩니다.

## JWT 란

JWT란 JSON Web Token의 약자로 [클레임 토큰 기반 인증 방식](https://elfinlas.github.io/2018/08/12/whatisjwt-01/#%EC%9D%BC%EB%B0%98-%ED%86%A0%ED%81%B0-%EA%B8%B0%EB%B0%98%EC%9D%98-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%ED%81%B4%EB%A0%88%EC%9E%84-Claim-%ED%86%A0%ED%81%B0-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D) 입니다.
클라이언트의 세션 상태를 저장하는 것이 아니라 필요한 정보를 토큰 body에 저장해서 클라이언트가 가지고 이를 증명서 처럼 사용한다.



### JWT의 구성

크게 3가지로 나눠진다.
```
XXX.YYY.ZZZ
```
- Header(XXX)
JWT 토큰의 유형이나 용된 해시 알고리즘의 정보가 들어간다.
- Payload(YYY)
클라이언트에 대한 정보가 담겨져있다.
또한 여기에는 iss,sub,aud,exp,nbf,iat,jti 와 같은 기본 정보가 들어간다.
- Signature(ZZZ)
header에서 지정한 알고리즘과 secret key로 Header와 Payload를 담는다.

> ### Payload는 공개 데이터
> JWT에 정보는 누구나 https://jwt.io/ 페이지에 접속해서 정보를 확인할 수 있습니다.<br/>
> 따라서 비밀번호와 같은 보안이 필요한 정보는 payload에 저장하면 안됩니다.

> ### JWT의 Secret Key
> JWT에서는 정보는 공개가 되어있지만 해시 값을 통해서 정보가 유효한지 확인을 하게 됩니다. 따라서 시크릿 키가 유출이 된다면 JWT에서 보안상에 큰 위협이 됩니다.
[출처](https://yonghyunlee.gitlab.io/node/jwt/)

## JWT 인증방식

> 로그인(토큰 발행)
1. 클라이언트가 유저에 대한 정보(ex. ID, Password)에 대한 정보를 서버에게 보낸다.
2. 서버는 DB를 이용해서 정보의 유효성을 확인한다.
3. User 정보 중 일부를 JWT body에 넣고 토큰을 발행이후 클라이언트에게 응답한다.
> 토큰 검증
1. HTTP header에 토큰값을 넣어서 보낸다.
2. 서버는 토큰값을 받아서 JWT 정보와 서버가 가지고 있는 secret key를 이용해서 서명을 만든다. 이때 JWT의 서명과 일치하다면 유효하고 일치하지 않는다면 유효하지 않는 요청으로 판단한다.

## JWT 수명

JWT 인증 방식에서는 이미 발급된 토큰에 대한 제어가 불가능 합니다. 따라서 토큰의 유효기간을 지정을 해줘야 합니다.
서버에서는 이 유효기간을 확인해서 유효한 토큰인지 확인 할 수 있습니다.
또한 유효기간이 지나면 새로운 토큰을 발급 하도록 만들 수 있습니다.

이때 유효기간이 짧을 수록 보안은 높아지게 되지만 클라이언트는 매번 토큰을 갱신해야 합니다.

## Refresh Token

일반 JWT방식에서 수명으로 발생하는 문제를 보완하기 위한 방법이 Refresh Token을 이용하는 것입니다.

토큰을 발행할때 수명이 짧은 Token와 수명이 긴 RefreshToken을 발급합니다.
일반 적인 인증은 Token을 이용해서 진행하며 만약 Token이 만료되는 경우에만 Refresh Token을 이용해서 토큰을 재발급 요청을 합니다.

### 시나리오

> 로그인(토큰 발행)
1. 클라이언트가 유저에 대한 정보(ex. ID, Password)에 대한 정보를 서버에게 보낸다.
2. 서버는 DB를 이용해서 정보의 유효성을 확인한다.
3. 서버에서 Token과 RefreshToken를 발행하고 클라이언트에 응답한다.

> 토큰 재발급
1. HTTP header에 토큰값을 넣어서 보낸다.
2. 서버에서 유효기간이 지난 토큰에 대한 메세지를 보낸다.
3. Token Refresh Api에 Refresh Token를 보낸다.
4. 서버에서 Refresh 토큰을 확인한 이후에 새로운 Token을 응답한다.
5. 클라이언트는 서버로 부터 재발급 받은 Token을 사용해서 통신한다.

[자세히보기](https://yonghyunlee.gitlab.io/node/jwt/)
4 changes: 4 additions & 0 deletions seminar5/practice5/config_sample/secretKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

module.exports = {
secretOrPrivateKey: "jwtSecretKey!"
}
85 changes: 85 additions & 0 deletions seminar5/practice5/module/jwt-ext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const jwt = require('jsonwebtoken');

const {
secretOrPrivateKey
} = require('../config/secretKey');
const resMessage = require('./util/responseMessage');
const statusCode = require('./util/statusCode');

const options = {
algorithm: "HS256",
expiresIn: "1m",
issuer: "with-sopt"
};

const refreshOptions = {
algorithm: "HS256",
expiresIn: "2h",
issuer: "with-sopt"
};

module.exports = {
publish: (payload) => {
const token = jwt.sign(payload, secretOrPrivateKey, options);
const refreshToken = jwt.sign({
refreshToken: payload
}, secretOrPrivateKey, refreshOptions);
return {
token,
refreshToken
};
},
create: (payload) => {
return jwt.sign(payload, secretOrPrivateKey, options);
},
verify: (token) => {
try {
const data = jwt.verify(token, secretOrPrivateKey);
return {
isError: false,
data
};
} catch (err) {
if (err.message === 'jwt expired') {
console.log('expired token');
return {
isError: true,
data: {
code: statusCode.UNAUTHORIZED,
json: resMessage.EXPIRED_TOKEN
}
};
}
if (err.message === 'invalid token') {
console.log('invalid token');
return {
isError: true,
data: {
code: statusCode.UNAUTHORIZED,
json: resMessage.INVALID_TOKEN
}
};
}
console.log(err);
return {
isError: true,
data: err
};
}
},
reissue: (payload, refreshToken) => {
const result = jwt.verify(refreshToken);
if(result.isError){
return result;
}
if(result.data.userIdx != payload.userIdx) {
return {
isError: true,
data: {
code: statusCode.UNAUTHORIZED,
json: resMessage.INVALID_TOKEN
}
};
}
}
};
53 changes: 53 additions & 0 deletions seminar5/practice5/module/jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const randToken = require('rand-token');
const jwt = require('jsonwebtoken');
const {secretOrPrivateKey} = require('../config/secretKey');
const options = {
algorithm: "HS256",
expiresIn: "1h",
issuer: "genie"
};

module.exports = {
sign: (user) => {
const payload = {
idx: user.idx,
grade: user.grade,
name: user.name
};
//발급받은 refreshToken은 반드시 디비에 저장해야 한다.
const result = {
token: jwt.sign(payload, secretOrPrivateKey, options),
refreshToken: randToken.uid(256)
};
//refreshToken을 만들 때에도 다른 키를 쓰는게 좋다.
//대부분 2주로 만든다.

return result;
},
verify: (token) => {
let decoded;
try {
decoded = jwt.verify(token, secretOrPrivateKey);
} catch (err) {
if (err.message === 'jwt expired') {
console.log('expired token');
return -3;
} else if (err.message === 'invalid token') {
console.log('invalid token');
return -2;
} else {
console.log("invalid token");
return -2;
}
}
return decoded;
},
refresh: (user) => {
const payload = {
idx: user.idx,
grade: user.grade,
name: user.name
};
return jwt.sign(payload, secretOrPrivateKey, options);
}
};
34 changes: 34 additions & 0 deletions seminar5/practice5/module/util/authUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const jwt = require('../jwt-ext');

const resMessage = require('./responseMessage');
const statusCode = require('./statusCode');
const util = require('./utils');

const authUtil = {
LoggedIn: async(req, res, next) => {
var token = req.headers.token;

if (!token) {
return res.status(statusCode.BAD_REQUEST).json(util.successFalse(resMessage.EMPTY_TOKEN));
}
const result = jwt.verify(token);

if(result.isError){
const {code, json} = result.data;
if(code && json) {
return res.status(code).send(util.successFalse(json));
}
const err = result.data;
return res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.successFalse(err.message));
}

const {userIdx} = result.data;
if (!userIdx){
return res.status(statusCode.UNAUTHORIZED).send(util.successFalse(resMessage.INVALID_TOKEN));
}
req.decoded = userIdx;
next();
},
};

module.exports = authUtil;
13 changes: 13 additions & 0 deletions seminar5/practice5/module/util/responseMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
NULL_VALUE: "필요한 값이 없습니다",

INVALID_TOKEN: "잘못된 형식의 토큰입니다.",
EMPTY_TOKEN: "토큰값이 존재하지 않습니다.",
EXPIRED_TOKEN: "만료된 토큰입니다.",
EMPTY_REFRESH_TOKEN: "재발급 토큰이 존재하지 않습니다.",
CREATE_TOKEN: "토큰 발급 완료.",
REFRESH_TOKEN: "토큰 재발급 완료.",

NO_SELECT_AUTHORITY: "조회 권한 없음.",
USER_SELECTED: "회원 조회 성공."
};
12 changes: 12 additions & 0 deletions seminar5/practice5/module/util/statusCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
OK: 200,
CREATED: 201,
NO_CONTENT: 204,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
DB_ERROR: 600,
};
17 changes: 17 additions & 0 deletions seminar5/practice5/module/util/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const util = {
successTrue: (message, data) => {
return {
success: true,
message: message,
data: data
}
},
successFalse: (message) => {
return {
success: false,
message: message
}
}
};

module.exports = util;
4 changes: 3 additions & 1 deletion seminar5/practice5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
"multer": "^1.4.2",
"multer-s3": "^2.9.0"
"multer-s3": "^2.9.0",
"rand-token": "^0.4.0"
}
}
2 changes: 2 additions & 0 deletions seminar5/practice5/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ var express = require('express');
var router = express.Router();

router.use('/multerTest', require('./multerTest'));
router.use('/jwtTest', require('./jwtTest'));
router.use('/jwtExtTest', require('./jwtExtTest'));

module.exports = router;
Loading

0 comments on commit b732d1f

Please sign in to comment.