// 에드센스

또또 너무 오랜만에 글을 쓴다. 퇴근하면 왤케 피곤한걸까.

기존에 사용하던 stoplight에서 swagger로 전환하는 작업을 했다.

왜? 코드 안에서 바로 수정할 수 있기에 더 간편해서.

근데 한땀한땀 핸드메이드로 스웨거 주석을 작성하다가 스웨거 자동생성 라이브러리를 발견했고 그것을 사용해 보았다.


1. swagger란?

  • Open Api Specification(OAS)를 위한 프레임워크이다.
  • API들이 가지고 있는 스펙(spec)을 명세, 관리할 수 있는 프로젝트/문서
  • API 사용 방법을 사용자에게 알려주는 문서
  • express에서는 주석형태의 yaml형식으로 swagger 문서를 정의할 수 있다.
  • URL에 /api-docs으로(내가 지정한) 접근하면 swagger가 만들어주는 페이지에 접근할 수 있다.

 

 

 


2. swagger 자동 생성 라이브러리 swagger-autogen

https://github.com/davibaltar/swagger-autogen

 

GitHub - davibaltar/swagger-autogen: This module performs the automatic construction of the Swagger documentation. The module ca

This module performs the automatic construction of the Swagger documentation. The module can identify the endpoints and automatically capture methods such as to get, post, put, and so on. The modul...

github.com

 

일반적인 express swagger 적용은 이미 다른 블로그에도 친절하게 잘 적혀있다.

하지만 api가 하나 추가될때마다 한땀 한땀 주석을 작성(혹은 복붙)하기엔 너무 귀찮을 것 같았다.

 

그래서 찾아본 결과 router가 위치한 파일의 경로를 알려주면 자동으로 해당 router를 인식하고,

그 밑에 딸린 router들도 인식하여 api 문서를 위한 json파일을 자동 생성해주는 라이브러리를 찾았다.

 

 

npm start를 한 후의 동작을 보자면

  1. package.json의 prestart 스크립트를 통해서 swagger.js를 실행한다. 이 파일은 지정된 경로에 존재하는 파일들에서 라우터와 swagger 주석들을 읽는다.
  2. swagger.js의 실행 결과로 swagger-output.json 파일이 생성된다. 이 파일은 우리가 생성할 swagger 문서의 구조를 나타낸다.
  3. 생성된 json을 바탕으로 swagger 문서가 생성된다.

 

주의사항.

swagger-output.json은 prestart 스크립트에 의해서 생성되기에 nodemon처럼 저장후 자동으로 서버가 재실행 되는 경우 prestart 스크립트가 실행 안될 수 있다.

즉, 서버를 완전히 종료하고 다시 npm start를 해줘야 prestart가 실행되며 swagger-output.json 파일이 생성(갱신)된다.

 


3. 적용하기

0.  npm install

npm i swagger-ui-express swagger-autogen

 

 

1.  프로젝트 구조

├ src

└─ swagger

      └─ swagger.js

      └─ swagger-output.json

└─ loader

      └─ express.ts

 

swagger.js을 prestart 스크립트로 실행하여 swagger-output.json을 얻어내고,

express.ts에서 swagger-output.json을 참조하여 swagger 문서를 생성하는 함수를 호출한다.

 

 

2.  코드

swagger.js

const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });

const options = {
  info: {
    title: 'This is my API Document',
    description: '이렇게 스웨거 자동생성이 됩니다.',
  },
  servers: [
    {
      url: 'http://localhost:3000',
    },
  ],
  schemes: ['http'],
  securityDefinitions: {
    bearerAuth: {
      type: 'http',
      scheme: 'bearer',
      in: 'header',
      bearerFormat: 'JWT',
    },
  },
};
const outputFile = './src/swagger/swagger-output.json';
const endpointsFiles = ['./src/loaders/express.ts'];
swaggerAutogen(outputFile, endpointsFiles, options);

securityDefinitions 부분은 JWT를 위한 설정이다.

저 속성을 지정하면 authorize라는 버튼이 생기며 JWT Access Token을 header에 등록할 수 있다.

여러 api들을 요청하고 응답을 확인하기위해 일일이 Access Token을 넣어줄 필요가 없다.

 

보다시피 outputFile의 경로를 지정해 주었고 swagger middleware를 설정할 때 이곳을 참조하면 된다.

endpointsFiles는 router가 위치한 파일이다. 나의 경우 이곳에 최상단 router가 있기에 이곳을 지정했다.

 

├ /api

└─ /user

      └─ /

      └─ /marketing

      └─ /phone

└─ /board

      └─ /

      └─ /admin

└─ /ping

 

이와 같은 router구조를 가진다고 했을 때, /api가 위치한 곳을 가리켜야 swagger가 만들어졌을때 

/api/user/phone

이렇게 전체 경로가 표시된다.

 

 

아무튼 이렇게 생성된 json파일을 가지고 swagger를 생성해준다.

import swaggerFile from '../swagger/swagger-output.json';
import swaggerUi from 'swagger-ui-express';

//Swagger
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile, { explorer: true }));

 

 

이제 /api-docs로 접속하면 다음과 같은 화면을 볼 수 있다.

 

 

근데 지금은 모든 api들이 무질서하게 나열돼 있어서 보기 안좋다.

tag를 걸어보자.

tag를 걸면서 request, response 양식도 정해주자.

 

 

다음과 같은 주석을 각 api 함수 내부에 넣어주자. 이게 좀 가독성을 해칠 수 있겠지만 그나마 가장 간단하고 편리한 방법이라고 생각한다...

다음 api는 user의 phone을 수정하는 put api이다.

  /*
   #swagger.tags = ['Users']
   #swagger.summary = '유저 phone 수정'
   #swagger.description = '유저 phone을 업데이트한다.'
   #swagger.security = [{
       "bearerAuth": []
   }]
   #swagger.requestBody = {
        required: true,
        content: {
            "application/json": {
                schema: {
                  "type": "object",
                    "properties": {
                      "phone": {
                        "type": "string",
                        "required": false
                      }
                    }
                },
                example: {
                    "phone": "010-1111-2222",
                },
            },
        }
    }
    #swagger.responses[200] = {
        description: "유저 phone 수정 완료",
        content: {
            "application/json": {
                schema: {
                    "data": {
                        "phone": "010-0000-0000",
                    }
                },
                example: {
                    "data": {
                        "phone": "010-1111-2222",
                    }
                },
            }
        }
    }
*/

 

 

태그를 Users로 지정한 것들은 다음과 같이 이쁘게 그룹화된다.

 

 

또 요청/응답 형식의 예시를 지정해 둘 수 있다.

이렇게 하면 다른 개발자들과 협업하기 편리할 것이다.

나의 경우는 api 함수 내부 가독성을 최대한 살리기위해 200 응답만 예시를 정해두었고 나머지는 생략했다.

 

추가적인 속성들은 위에 첨부한 swagger-autogen 공식 깃허브 문서를 참고하면 쉽게 따라할 수 있을 것이다.

 

 

 

 


4. 마치며

나는 응애개발자라 아직 주석이 가득한 코드가 익숙하지 않다.

자바의 경우 어노테이션으로 깔끔 세련되게 됐던거같은데 노드는 좀 애매한 것 같다..

처음부터 swagger 자동 생성을 찾아볼걸, 괜히 한땀 한땀 주석만 쓰고있느라 시간낭비 한 것 같다. 큿소!

tsc --init

을 통해 생성하는 tsconfig.json파일.

타입스크립트를 시작한지 얼마되지 않았기에 대충 설정파일이구나 하고 넘겼다. 

여기 있는 설정들을 살펴보며 타입스크립트의 전체적인 그림을 살펴보자.


1. 멋모르고 사용해 온 설정

처음 init을 통해 파일을 생성하면 뭔가 엄청 많다.

이거처럼

더보기
{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Enable incremental compilation */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es5",                                     /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
    // "reactNamespace": "",                             /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like `./node_modules/@types`. */
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "resolveJsonModule": true,                        /* Enable importing .json files */
    // "noResolve": true,                                /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
    "outDir": "dist",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like `__extends` in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing `const enum` declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied `any` type.. */
    // "strictNullChecks": true,                         /* When type checking, take into account `null` and `undefined`. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when `this` is given the type `any`. */
    // "useUnknownInCatchVariables": true,               /* Type catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when a local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Include 'undefined' in index signature results */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  },
  "include": [
    "src/**/*"
  ],
  "exclude":[
    "node_modules"
  ]
}

 

이 많은 옵션들 중 내가 사용하고 있는 옵션들부터 살펴보겠다.

 

 

 

compilerOptions과 include/exclude

tsconfig.json 파일을 살펴보면 최상위 경로는 compilerOptions, include, exclude 프로퍼티로 구성돼있다.

 

  • compilerOptions은 말 그대로 어떤 컴파일 설정을 사용할지에 대한 속성이다.
  • include는 프로그램에 포함하고 싶은 파일들의 목록을 지정한다. 보통
{
    "include": ["src/**/*", "tests/**/*"]
}

이와 같은 형태로 표기하여 한번에 지정한다.

  • exclude는 include에 속한 파일들 중에서 제외시킬 파일들을 지정한다. 이때 프로그램에서 포함되지 않도록 제외시키는 메커니즘이 아니라 단순이 include 프로퍼티에서만 제외시킨다는 것에 주의하자.

 

 

"target"

여기서부터는 compilerOptions 프로퍼티 내부의 옵션들이다.

target은 어떤 버전의 자바스크립트로 컴파일할지 지정한다.

ts파일들을 tsc로 컴파일하여 js로 만드는 과정이 필요한 타입스크립트이기에 실제 런타임은 대부분 js다. 이 js의 버전을 지정한다.

 

 

"module"

프로그램에서 사용할 모듈 시스템을 결정한다. 즉, 모듈 내보내기/불러오기 코드가 어떠한 방식의 코드로 컴파일 될지를 결정한다.

 

 

"lib"

타입스크립트 파일을 자바스크립트로 컴파일 할 때 포함될 라이브러리의 목록이다. 이렇게만 하면 와닿지 않는데, async코드를 컴파일 할 때 Promise객체가 필요하므로 "es2017"과 같은 항목을 넣어준다고 한다.

 

 

"baseUrl"

비상대적 import의 모듈 해석시 기준이 되는 경로를 지정한다. 예시를 들자면, 프로젝트의 루트 디렉토리에 존재하는 src 디렉토리를 기준으로 각종 모듈들을 불러오고 싶다면 이 프로퍼티를 './src'로 지정한다.

 

 

"typeRoots'

기본적으로 @types 패키지들은 컴파일 목록에 포함된다. 하지만 만약 typeRoots 프로퍼티가 특정 경로들로 지정돼 있다면 오직 그 경로에 존재하는 패키지들만 컴파일 목록에 포함된다. 

 

 

"paths"

baseUrl 기준으로 상대 위치로 가져오기를 다시 매핑하는 항목 설정

 

 

"emitDecoratorMetadata"

"experimentalDecorators"

데코레이터 설정이다. 데코레이터란, 자바의 어노테이션과 비슷한 느낌의 기능으로, 데코레이터가 붙은 클래스, 메소드 등에 데코레이터에서 지정한 기능이 동작하도록 하는 기능이다.

 

 

"allowSyntheticDefaultImports"

불러오려는 모듈에 default export가 없어도 import * as XXX가 아닌 import XXX로 사용할 수 있게 해주는 설정이다.

 

 

"forceConsistentCasingInFileNames"

사용할 파일의 이름을 대소문자까지 정확하게 작성하도록 강제하는 설정이다.

 

 

"moduleResolution"

모듈 해석 전략을 결정한다. Nodejs 방식대로 모듈 해석을 하려면 "Node"를, 1.6버전 이전의 타입스크립트에서 사용하던 방식대로 모듈을 해석하려면 "Classic"을 입력한다.

 

 

"pretty"

에러와 메시지를 색과 컨텍스트를 사용해서 스타일을 지정하는 옵션

 

 

"sourceMap"

빌드시 map파일을 생성할지 설정한다. 생성된 소스맵 파일은 크롬 개발자 도구로 디버깅에 사용된다.

 

 

"allowJs"

js파일을 허용하는 옵션이다.타입스크립트는 .js확장자를 허용하지 않는다. 이에대한 예외를 허락하는 옵션.

 

 

"esModuleInterop"

"모든 가져오기에 대한 네임스페이스 객체 생성을 통해 CommonJS와 ES 모듈 간의 상호 운용성을 제공"이라고 돼있는데 이게 무슨소리인가?

피부에 와닿진 않지만 Commonjs방식으로 내보낸 모듈을 es모듈 방식의 import로 가져올 수 있게 해주는 기능 정도로 일단 이해하자.

 

 

"outDir"

컴파일 후 생성되는 js파일이 생성될 폴더를 지정한다

 

 

 

 

 

참고:

더보기

 

거의 한달만에 글을 쓴다. 그간 참 많은 일이 있었다. 

취업을 했고 킥보드를 타다가 응급실도 갔다. 너무 정신없는 시간이었다고 변명을 해본다ㅠ

요즘은 TypeScript+Express를 통한 개발을 하고있는데 Express 사용시 헤더의 설정을 통하여 웹 취약점으로부터 서버를 보호해주는 보안 모듈인 Helmet이라는 것을 알게 됐고 간단하게 정리해보겠다.

(헤더에 씌운다고해서 이름이 헬멧인 것이 너무 귀엽다. 내 머리에 헬멧을 썻어야했는데 익스프레스에만 씌우고있다 아ㅋㅋ)

 


1. 사용법

그냥 한 줄만 추가해 주면 된다.

npm install helmet

헬멧을 설치하고,

 

 

const helmet = require('helmet');
const express = require('express');
const app = express();

app.use(helmet());

이렇게 붙혀주기만 하면 된다.

 

 

 

 


2. 무엇을 보호해주는가?

세부적인 미들웨어 함수들을 포함하고있는 헬멧은 다음과 같은 기능이 있다.

 

1. csp

csp는 Content-Security-Policy이다. 브라우저에서 사용하는 컨텐츠 기반의 보안 정책으로 XSS나 Data Injection, Click Jacking 등 웹 페이지에 악성 스크립트를 삽입하는 공격기법들을 막기 위해 사용된다.

 

2. hidePoweredBy

헤더에서 X-Powered-By를 제거한다. 이는 서버에 대한 정보를 제공해주는 역할로 나 같은 경우는 이 영역에 Express라고 표기됨을 확인할 수 있었다. 이 정보는 악의적으로 활용될 가능성이 높기에 헬멧을 통해서 제거해 주는 것이 좋다.

 

3. HSTS

HTTP Strict Transport Security의 약자로 웹 사이트에 접속할 때 강제적으로 HTTPS로 접속하게 강제하는 기능이다. 

사용자가 특정 사이트에 접속할 때 해당 사이트가 HTTPS를 지원하는지, 하지 않는지를 미리 모르는 경우가 대부분이다. 그렇기에 브라우저는 디폴트로 HTTP로 먼저 접속을 시도한다. 이때 HTTPS로 지원되는 사이트였다면 301Redirect나 302 Redirect를 응답하여 HTTPS로 다시 접속하도록 한다.

 

하지만 이때 해커가 중간자 공격을 하여, 중간에 프록시 서버를 두고

[나] <-> [해커] 사이에서는 HTTP 통신을 하고 [해커] <-> [웹사이트] 사이에선 HTTPS 통신을 한다면,

우리의 개인정보가 HTTP 프로토콜을 통해 해커에게로 전해지는 참사가 일어난다.

이러한 공격을 SSL Stripping이라고 하며 이런 공격을 방지하기 위해 HSTS를 설정한다.

 

4. IeNoOpen

IE8 이상에 대해 X-Download-Options를 설정한다. 이 옵션은 8 버전 이상의 인터넷 익스플로러에서 다운로드된 것들을 바로 여는 것 대신 저장부터 하는 옵션이다. 사용자는 저장부터 하고 다른 응용프로그램에서 열어야 한다.

 

5. noCache

클라이언트 측에서 캐싱을 사용하지 않도록 하는 설정이다. 

 

6. noSniff

X-Content-Type-Options 를 설정하여 선언된 콘텐츠 유형으로부터 벗어난 응답에 대한 브라우저의 MIME 스니핑을 방지한다. MIME이란 Multipurpose Internet Mail Extensions의 약자로 클라이언트에게 전송된 문서의 다양성을 알려주기 위한 포맷이다. 브라우저는 리소스를 내려받을 때 MIME 타입을 보고 동작하기에 정확한 설정이 중요하다.

 

MIME 스니핑이란 브라우저가 특정 파일을 읽을 때 파일의 실제 내용과 Content-Type에 설정된 내용이 다르면 파일로 부터 형식을 추측하여 실행하는 것인데, 편리함을 위한 기능이지만 공격자에게 악용 될 가능성이 있다.

 

7. frameguard

 X-Frame-Options 헤더를 설정하여 클릭재킹에 대한 보호를 제공한다.

클릭재킹이란 사용자가 자신이 클릭하고 있다고 인지하는 것과 다른 것을 클릭하도록 하여 속이는 해킹 기법이다. 속이기 위해 보이지 않는 레이어에 보이지 않는 버튼을 만드는 방법이 있다.

 

8. xssFilter

xss필터는 xss필터.

 

 

 

 

 

 

 

 

 

 

 

참고:

더보기


1. JWT란?

Json Web Token의 줄임말이다. 

두 개체에서 JSON객체를 사용하여 가볍고 자가수용적인 방식으로 정보를 안정성 있게 전달해 주는 인증 방식이다.

 

 


2. 세션과의 차이점

세션

보통 로그인을 구현할때 세션로그인을 많이 사용했다. 세션로그인이란, 

이런 흐름을 가지고 진행된다.

  1. 클라이언트에서 서버로 로그인 요청을 보낸다.
  2. 서버는 로그인 정보 확인 후 세션아이디를 응답한다. 이 세션아이디는 서버에서도 가지고있는다.
  3. 이후 클라이언트의 요청에는 2번에서 응답받은 세션아이디를 쿠키에 담아서 함께 요청한다.
  4. 서버는 함께 요청된 쿠키 속의 세션아이디를 확인하여 로그인된 사용자인지 확인한다.

 

이러한 세션 로그인 방식은 단점이 있다.

 

단점.

  1. 만약 여러대의 서버가 운영된다면, 서버측에서 저장하고있는 모든 세션을 공유해야한다.
  2. 사용자가 많아질수록 서버측에서 모든 사용자의 세션들을 저장하기 부담스럽다.

 

 

그러면 JWT는 이 문제를 어떻게 해결할까?

JWT

JWT방식은 다음과 같은 흐름을 가진다.

  1. 클라이언트에서 서버로 로그인 요청을 보낸다.
  2. 서버는 로그인 정보 확인 후 토큰을 응답한다. 이 토큰은 서버에서 보관하지 않는다.
  3. 이후 클라이언트의 2번에서 응답받은 토큰을 요청에 함께 보낸다.
  4. 서버는 토큰이 유효한지 검증하고 유효하면 로그인된 사용자라고 생각한다.

 

세션방식과 다른 가장 큰 포인트는, 로그인 후 무언가를 응답으로 보내주긴하는데 JWT는 서버측에서 그걸 기억하지 않는다는 것이다. 

세션방식의 문제점 중 하나가 기억해야할 세션 아이디가 너무 많다는 것이었는데 이 문제점을 해결하는 부분이다.

서버는 자신이 발행한 토큰을 기억하지 않고, 나중에 토큰을 받으면 그 토큰이 유효한지 검증만 하면 되는 것이다.

또한 여러 디바이스나 도메인에서도 토큰에 대한 인증만 하면 되니 여러 서버가 운영될 때에도 문제 없다.

 

 

JWT가 마냥 좋은것만은 아니다.

세션은 시간에 따라 바뀌는 값을 갖는 stateful한 값이므로 어떠한 장점이 있느냐, 세션 값을 가지고있는 대상들을 제어할 수 있다. 예를 들어서 한 기기에서만 로그인이 가능하도록 구현하려 한다고 가정해보자.

1번 기기에 로그인이 돼있는데 2번 기기에서 로그인을 하면, 1번 기기의 세션을 종료하면 된다.

하지만 JWT는 사용자의 상태를 모르기 때문에 (stateless) 이것이 불가능하다. 이미 줘버린 토큰을 다시 회수할 수도 없고, 그 토큰의 발급 내용이나 정보를 서버가 추적하고 있지도 않기 때문이다. 반면 세션은 서버측의 세션저장소에 있기에 가능하다.

 

 

여담으로 지금 말한 단일 기기 로그인을 JWT로 해결하기 위한 기법도 존재한다. 

  • 최초 토큰 발행시 refresh 토큰과 access 토큰, 총 2개의 토큰을 발행한다.
  • refresh토큰은 만료기한(수명)이 꽤 길고 access토큰은 매우 짧다. 
  • refresh토큰의 상응값을 데이터베이스에도 저장한다.
  • 사용자가 요청할때 access토큰을 사용하는데 access토큰의 수명이 끝나면 refresh토큰을 사용해서 요청을 보낸다.
  • 서버는 서버측의 refresh 토큰 상응값과 비교해보고 맞다면 새로운 access토큰을 발행해 준다.
  • 즉, refresh토큰만 안전하게 관리된다면 중간에 access토큰이 탈취당해도 어자피 수명이 짧기 때문에 보안상 위협을 줄일 수 있고, 로그인 유지도 가능하다. 

 

 


3. JWT의 구성

JWT토큰은 3개의 부분으로 구성된다.

실제로는

 

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

 

이렇게 생겼다. 각 부분을 디코딩해보면 JSON형태로 나온다.

 

 

헤더 (header)

헤더는 두가지 정보를 갖는다.

typ: 토큰의 타입을 지정. 여기는 JWT가 고정으로 들어간다. 여기가 JWT여야지만 JWT기 때문에

alg: 어떤 해싱 알고리즘을 사용할지 지정한다. 해싱 알고리즘으로는 보통 HMAC SHA256 혹은 RSA 가 사용되며, 이 알고리즘은, 토큰을 검증 할 때 사용되는 signature 부분에서 사용된다.

 

{ 
  "typ": "JWT", 
  "alg": "HS256" 
}

 

 

내용 (payload)

이 부분에는 토큰에 담을 정보가 들어있다. 이 정보의 한 조각을 "클레임(Claim)"이라고 부르고 key : value의 한 쌍으로 이루어져 있다. 클레임의 종류는 3가지로 분류된다.

 

등록된(registered) 클레임

등록된 클레임은 서비스에 필요한 정보가 아니라 토큰에 대한 정보를 담기위해 이름이 이미 정해진 클레임들이다. 등록된 클레임은 Optional하다.

  • iss : 토큰 발급자 (issuer)
  • sub : 토큰 제목 (subject)
  • aud : 토큰 대상자 (audience)
  • exp : 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 언제나 현재 시간보다 이후로 설정돼야한다.
  • nbf : Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.
  • iat : 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있다.
  • jti : JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용된다. 일회용 토큰에 사용하면 유용.

 

공개(public) 클레임

공개 클레임들은 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 한다. 충돌을 방지하기 위해서는, 클레임 이름을 URI 형식으로 짓는다.

 

{
    "https://velopert.com/jwt_claims/is_admin": true
}

 

비공개(private) 클레임

등록된 클레임도아니고, 공개된 클레임들도 아니다. 양 측간에 (보통 클라이언트 <->서버) 협의하에 사용되는 클레임 이름들이다. 공개 클레임과는 달리 이름이 중복되어 충돌이 될 수 있으니 사용할때에 유의.

 

{
    "username": "velopert"
}

 

Payload의 예시

{
    "iss": "llshl.com",
    "exp": "1485270000000",
    "https://llshl.com/jwt_claims/is_admin": true,
    "userId": "11028373727102",
    "username": "llshl"
}

 

 

서명 (signature)

서명은 헤더의 인코딩값과, 정보의 인코딩값을 합친후 주어진 비밀키로 해쉬를 하여 생성한다.

서버에서 요청에서 토큰을 받으면 헤더와 페이로드의 값을 서버의 비밀키와 함께 돌려서 계산된 결과값이 서명값과 일치하는지 확인한다.

 

 


한 줄 요약

"세션은 서버에서 세션아이디 보관,

JWT는 보관 없이 인증만"

 

 

 

 

참고:

'Web' 카테고리의 다른 글

[Web] REST API  (0) 2021.07.03
[Web] 세션과 쿠키  (0) 2021.07.02

+ Recent posts