[배치 만들어보기 샘플]

  • 스프링 배치 아키텍처
    • SpringBoot Batch를 사용할 때 설정해야하는 부분과 코드작성해야 하는 부분을 구별하기

  • 배치 Job 개요 및 시나리오
    • 일일 배치로 실행
    • 공통된 포맷의 CSV 파일로 도서관 관련 데이터가 유입
      (공공데이터포털 - 전국도서관표준데이터
    • 해당 데이터를 시군구, 읍면동으로 정규화하여 도서관 정보를 저장
      (추가적으로 필요하다고 생각하는 의미있는 데이터 구분 기준이 있으면 추가 가능)
    • 정규화 및 분석된 도서관 데이터를 레포트화하여 확인

  • ERD 설계
    1. CSV 파일의 데이터를 가공되지 않은 채로 DB에 저장하여 1차적으로는 컬럼의 기준을 정함
    2. 가공되지 않은 데이터를 시군구, 읍면동으로 분류화 작업
    3. 시군구, 읍면동을 키 값으로 갖는 도서관 데이터를 저장
    4. 도서관 데이터의 기본 값과 상세값을 구분하여 데이터 저장

  • 최종 목표 배치 Job 실행 프로세스 설계
    • 하나의 Job으로 설계
    • 해당 Job에는 3단계의 Step 으로 구분하여 실행 (임의)
    • 데이터를 읽고 쓰는 클래스는 개발환경에 따라 유연하게 적용한다.

  • 개선
    • 정규화 작업에 대한 Step을 추가
    • 각 작업에 대한 로그를 확인하여 배치 작업으로 인하여 수행된 수치를 상세하게 파악 가능
    • Report 작업 수행 시 Excel File로 생성 추가

  • 개선해야하는 사항
    • JpaItemWriter를 사용하는 것처럼 JpaReader를 생성하고 있지 않다는 점
    • 복잡한 쿼리를 사용하는 경우에 대해서 대비되어 있지 않다는 점
      • 현재 배치의 속도 개선을 위한 선택지가 없음 > 학습이 더 필요
  • Git

[SpringBatch] 예제 구현하면서 공부하기

스프링부트 기반 인증 서버 API 만들기

구현 스펙

  • SpringBoot
    • Interceptor
  • Json Web Token(JWT)
  • Server
    • Redis
  • DB
    • MySQL

코드정리

 

SpringFramework-Sample/springboot_redis

SpringBoot & Redis. Contribute to SpringFramework-Sample/springboot_redis development by creating an account on GitHub.

github.com

  • 추후
    • SpringSecurity
      • Authentication
      • Authorization
    • Role 추가

[Redis]

  • 얼마나 많은 요청을 한꺼번에 처리할 수 있는지
  • 요청 하나를 처리하는 데 걸리는 시간(Latency) 등을 측정

Redis 성능 확인하기

  • 명령어를 사용하기 위한 경로
root@97bd8fbf6f8d:/usr# 
  • 명령어 결과
root@97bd8fbf6f8d:/usr# redis-benchmark
====== PING_INLINE ======
  100000 requests completed in 2.79 seconds        # 만 개의 명령을 처리하는 데 걸린 시간 2.79초
  50 parallel clients                            # 50개의 클라이언트 동시 연결
  3 bytes payload                                # 저장 데이터의 크기 3바이트
  keep alive: 1                                    # 클라이언트 연결 유지 상태 정보
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

0.00% <= 0.4 milliseconds
0.08% <= 0.5 milliseconds
2.13% <= 0.6 milliseconds
10.04% <= 0.7 milliseconds
25.00% <= 0.8 milliseconds
43.93% <= 0.9 milliseconds
63.18% <= 1.0 milliseconds
78.15% <= 1.1 milliseconds
87.81% <= 1.2 milliseconds
93.52% <= 1.3 milliseconds
96.47% <= 1.4 milliseconds
98.01% <= 1.5 milliseconds
98.73% <= 1.6 milliseconds
99.11% <= 1.7 milliseconds
99.33% <= 1.8 milliseconds
99.45% <= 1.9 milliseconds
99.54% <= 2 milliseconds
99.92% <= 3 milliseconds
99.98% <= 4 milliseconds
100.00% <= 4 milliseconds
35842.29 requests per second                    # 초당 처리된 명령 수

====== PING_BULK ======
  100000 requests completed in 2.71 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

54.86% <= 1 milliseconds
99.78% <= 2 milliseconds
99.98% <= 3 milliseconds
100.00% <= 3 milliseconds
36954.91 requests per second

====== SET ======
  100000 requests completed in 2.87 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

65.98% <= 1 milliseconds
99.96% <= 2 milliseconds
100.00% <= 2 milliseconds
34867.50 requests per second

====== GET ======
  100000 requests completed in 2.83 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

66.07% <= 1 milliseconds
99.98% <= 2 milliseconds
100.00% <= 2 milliseconds
35360.68 requests per second

====== INCR ======
  100000 requests completed in 2.82 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

66.94% <= 1 milliseconds
99.99% <= 2 milliseconds
100.00% <= 2 milliseconds
35486.16 requests per second

====== LPUSH ======
  100000 requests completed in 2.87 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

64.74% <= 1 milliseconds
99.95% <= 2 milliseconds
99.99% <= 3 milliseconds
100.00% <= 3 milliseconds
34806.82 requests per second

====== RPUSH ======
  100000 requests completed in 2.87 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

64.67% <= 1 milliseconds
99.87% <= 2 milliseconds
99.99% <= 3 milliseconds
100.00% <= 3 milliseconds
34867.50 requests per second

====== LPOP ======
  100000 requests completed in 2.89 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

65.48% <= 1 milliseconds
99.92% <= 2 milliseconds
100.00% <= 3 milliseconds
100.00% <= 3 milliseconds
34602.07 requests per second

====== RPOP ======
  100000 requests completed in 2.91 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

64.82% <= 1 milliseconds
99.96% <= 2 milliseconds
100.00% <= 2 milliseconds
34399.72 requests per second

====== SADD ======
  100000 requests completed in 2.94 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

0.00% <= -33 milliseconds
0.03% <= -32 milliseconds
0.04% <= 0 milliseconds
58.66% <= 1 milliseconds
99.55% <= 2 milliseconds
99.98% <= 3 milliseconds
100.00% <= 3 milliseconds
34013.61 requests per second

====== HSET ======
  100000 requests completed in 2.96 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

61.09% <= 1 milliseconds
99.89% <= 2 milliseconds
100.00% <= 2 milliseconds
33726.81 requests per second

====== SPOP ======
  100000 requests completed in 2.74 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

64.93% <= 1 milliseconds
99.94% <= 2 milliseconds
99.99% <= 3 milliseconds
100.00% <= 3 milliseconds
36496.35 requests per second

====== LPUSH (needed to benchmark LRANGE) ======
  100000 requests completed in 2.88 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

63.70% <= 1 milliseconds
99.95% <= 2 milliseconds
100.00% <= 2 milliseconds
34698.12 requests per second

====== LRANGE_100 (first 100 elements) ======
  100000 requests completed in 3.45 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

47.14% <= 1 milliseconds
99.90% <= 2 milliseconds
100.00% <= 2 milliseconds
28993.91 requests per second

====== LRANGE_300 (first 300 elements) ======
  100000 requests completed in 5.92 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

0.01% <= 1 milliseconds
97.25% <= 2 milliseconds
99.96% <= 3 milliseconds
100.00% <= 3 milliseconds
16889.04 requests per second

====== LRANGE_500 (first 450 elements) ======
  100000 requests completed in 7.05 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

0.00% <= 1 milliseconds
84.77% <= 2 milliseconds
99.68% <= 3 milliseconds
99.96% <= 4 milliseconds
100.00% <= 4 milliseconds
14176.35 requests per second

====== LRANGE_600 (first 600 elements) ======
  100000 requests completed in 8.47 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

0.00% <= -32 milliseconds
0.00% <= -31 milliseconds
0.02% <= -30 milliseconds
0.03% <= 0 milliseconds
0.04% <= 1 milliseconds
20.78% <= 2 milliseconds
97.89% <= 3 milliseconds
99.84% <= 4 milliseconds
99.94% <= 5 milliseconds
99.99% <= 6 milliseconds
100.00% <= 6 milliseconds
11800.80 requests per second

====== MSET (10 keys) ======
  100000 requests completed in 3.03 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

49.40% <= 1 milliseconds
99.90% <= 2 milliseconds
100.00% <= 2 milliseconds
32959.79 requests per second

성능 측정 세부 설정

  • 50 바이트와 1,024바이트의 데이터를 문자열, 리스트, set 데이터형으로 저장하고 읽어내는 데 걸리는 시간 측정 결과를 진행률을 제외하고 출력
root@97bd8fbf6f8d:/usr# redis-benchmark -d 50 -q -t get,set,lpush,lpop,sadd,spop
SET: 34566.20 requests per second
GET: 35511.36 requests per second
LPUSH: 34698.12 requests per second
LPOP: 34518.46 requests per second
SADD: 33978.93 requests per second
SPOP: 34770.52 requests per second

root@97bd8fbf6f8d:/usr# redis-benchmark -d 1024 -q -t get,set,lpush,lpop,sadd,spop
SET: 34129.69 requests per second
GET: 34734.29 requests per second
LPUSH: 33715.44 requests per second
LPOP: 33222.59 requests per second
SADD: 34806.82 requests per second
SPOP: 34566.20 requests per second
  • 데이터의 크기가 20배 정도 늘었으나 읽기와 쓰기 횟수는 10% 정도 차이 밖에 나지 않는다.
  • 백만 개의 키를 문자열 데이터로 추가하는 테스트 명령
root@97bd8fbf6f8d:/usr# redis-benchmark -t set -n 1000000 -r 1000000
====== SET ======
  1000000 requests completed in 29.68 seconds        # 백만 개의 데이터를 입력하는 데 약 29초 소요
  50 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

0.00% <= -32.8 milliseconds
0.00% <= -32.7 milliseconds
0.00% <= -32.6 milliseconds
0.00% <= -32.5 milliseconds
0.00% <= -32.4 milliseconds
0.00% <= -32.3 milliseconds
0.00% <= -32.2 milliseconds
0.00% <= -32.0 milliseconds
0.00% <= 0.4 milliseconds
0.01% <= 0.5 milliseconds
0.52% <= 0.6 milliseconds
4.87% <= 0.7 milliseconds
18.67% <= 0.8 milliseconds
39.65% <= 0.9 milliseconds
60.82% <= 1.0 milliseconds
77.10% <= 1.1 milliseconds
87.37% <= 1.2 milliseconds
93.07% <= 1.3 milliseconds
96.13% <= 1.4 milliseconds
97.75% <= 1.5 milliseconds
98.59% <= 1.6 milliseconds
99.03% <= 1.7 milliseconds
99.28% <= 1.8 milliseconds
99.43% <= 1.9 milliseconds
99.51% <= 2 milliseconds
99.87% <= 3 milliseconds
99.99% <= 4 milliseconds
100.00% <= 5 milliseconds
100.00% <= 5 milliseconds
33697.26 requests per second                    # 초당 3만 3천개 이상의 데이터를 저장

 

- 레디스 성능 측정 도구 옵션

옵션 설명 기본값
-h <hostname> 테스트를 수행하기 위해 접속할 레디스 서버의 호스트명 127.0.0.1
-p <port> 테스트를 수행하기 위해 접속할 레디스 서버의 포트 6379
-s <socket> 테스트를 수행하기 위해 접속할 레디스 서버의 유닉스 서버 소켓 -
-c <clients> 테스트를 위한 가상 클라이언트의 동시 접속 수 50
-n <requests> 각 명령의 테스트 횟수 10000
-d <size> 테스트에 사용할 데이터 크기 3*
-k <boolean> 테스트를 위한 가상 클라이언트의 접속 유지 여부 1: 접속유지,
0: 접속 유지하지 않음
-r <keyspacelen> 테스트에 사용할 랜덤 키의 범위 0
-P <numreq> 파이프라인 명령을 사용한 테스트와 파이프라인당 요청할 명령의 개수 0: 파이프라인 미사용
-q 테스트 진행 상황을 출력하지 않고 결과만 출력하기 -
--csv 테스트 결과를 csv 포맷으로 출력하기 -
-l 브레이크(ctrl + c)를 걸기 전까지 계속 수행 -
-t <tests> 쉼표로 구분된 테스트 명령의 목록 -
-l 명령 전송 없는 연결 생성 후 브레이크 입력(ctrl + c) 때까지 대기 -

 

JPA 영속성 컨텍스트의 특징

  • SpringBoot를 하면서 JPA의 개념적으로 모르고 사용했었기에 정리

영속성 컨텍스트와 식별자 값

  • 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분한다.
  • 영속 상태는 식별자 값이 반드시 있어야 한다.
  • 식별자 값이 없으면 예외가 발생한다.

영속성 컨텍스트와 데이터베이스 저장

  • 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영(동기화)한다.(플러시: Flush)

영속성 컨텍스트가 엔티티를 관리할 때 장점

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

엔티티 조회

엔티티 등록

엔티티 수정

엔티티 삭제

'Basic > JPA' 카테고리의 다른 글

[Annotation] @MappedSuperclass  (0) 2020.06.22
[JPA] JPA 연관  (0) 2020.06.16
[JPA] 살펴보기  (0) 2020.06.16

SpringBoot 공부 중에 @Builder 패턴에 대한 이해가 필요하여 @Builder 어노테이션설정으로 인하여 어떻게 동작 하는지 알아보기 위하여 정리

1. Java Builder Pattern

@Builder

public class Response {

    private Long id;
    private String name;

    public Response(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public static ResponseBuilder builder() {
        return new ResponseBuilder();
    }

    public static class ResponseBuilder {
        private Long id;
        private String name;

        ResponseBuilder() {}

        public ResponseBuilder id(Long id) {
            this.id = id;
            return this;
        }

        public ResponseBuilder name(String name) {
            this.name = name;
            return this;
        }

        public Response build() {
            return new Response(id, name);
        }

        @Override
        public String toString() {
            return "ResponseBuilder{" + "id=" + id + ", name='" + name + '\'' +  '}';
        }
    }
}

2. @Builder

  • 빌더패턴을 이용했을 때 가장 큰 장점은 멤버변수의 optional과 required를 컴파일하는 시점에서 체크할 수 있다는 점
  • 특정 멤버변수에 접근하기 위한 메서드가 정해져 있기 때문에 실수로 다른 멤버변수에 다른 값을 집어넣는 실수를 막을 수 있다.
@Builder
public class Response {

    private Long id;
    private String name;

    public Response(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

3. Builder를 활용하기

import static java.lang.Thread.sleep;

public class ThreadBuilderTest {

    public static void main(String[] args) {

        ThreadOuter th = new ThreadOuter();
        th.start();
        ThreadOuter01 th01 = new ThreadOuter01();
        th01.start();

        for(int i = 11 ; i < 20 ; i++) {
            Response res = Response.builder()
                    .id(Long.valueOf(i))
                    .name("thread" + i)
                    .build();
            System.out.println(res.toString() + " :: " + Thread.currentThread().getName());
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class ThreadOuter extends Thread {

    @Override
    public void run() {
        for(int i = 0 ; i < 10 ; i++) {
            Response res = Response.builder()
                    .id((long) i)
                    .name("thread01" + i)
                    .build();
            System.out.println(res.toString() + " :: " + getName());
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class ThreadOuter01 extends Thread {

    @Override
    public void run() {
        for(int i = 21 ; i < 30 ; i++) {
            Response res = Response.builder()
                    .id((long) i)
                    .name("thread02" + i)
                    .build();
            System.out.println(res.toString() + " :: " + getName());
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

'Basic > DesignPattern' 카테고리의 다른 글

[Pattern] DTO, VO, Entity  (0) 2020.06.02

SpringSecurity Architecture

  • 스프링 보안 인증 프로세스에 관련된 클래스 및 필터 목록 을 시연하기 위한 다이어그램

1. HttpRequest 수신

  • 스프링 시큐리티는 일련의 필터를 갖고 있다.
  • 따라서 요청이 오면 인증과 인가를 위한 필터 체인을 거치게 된다.
  • 사용자 인증 요청이 있는 경우, 인증 메커니즘/모델에 기반한 관련 인증 필터를 찾을 때까지 필터 체인을 거치게 된다.

HTTP 기본 인증 요청은 BasicAuthenticationFilter에 도달할 때까지 필터체인을 거친다.

ex)

  • HTTP 다이제스트 인증 요청DigestAuthenticationFilter에 도달할 때까지 필터 체인을 통과한다.
  • 로그인 양식 제출 요청(로그인 양식 인증 요청)이 UsernamePasswordAuthenticationFilter에 도달할 때까지 필터체인을 거친다.
  • X509 인증 요청이 X509AuthenticationFilter에 도달할 때까지 필터 체인을 통과한다.

2. 사용자 자격증명 기반 AuthenticationToken 생성

  • 관련 AuthenticationFilter에서 인증 요청을 받으면 수신된 요청에서 사용자 이름과 암호를 추출한다. (인증 메커니즘의 대부분은 사용자 이름과 암호를 요구한다.)
  • 그 후 추출된 사용자 자격 증명 기반으로 인증 객체를 생성한다.
  • 추출된 자격증명이 사용자 이름과 암호인 경우 UsernamePasswordAuthenticationToken은 사용자 이름과 비밀번호를 추출/발견하여 생성된다.

3. AuthenticationManager에게 위임하기 위한 AuthenticationToken생성

  • UsernamePasswordAuthenticationToken객체를 생성한 후에 AuthenticationManagerauthenticate 메서드를 호출하기 위해 사용된다.
  • AuthenticationManager는 단지 인터페이스이며 실제 구현체는 ProviderManager이다.
  • ProviderManager는 사용자 요청을 인증하는데 사용하기 위해 AuthenticationProvider를 설정하기 위한 목록을 갖고 있다.
  • ProviderManager는 제공된 각 AuthenticationProvider를 검토하고 전달된 Authentication 객체를 기반으로 사용자 인증을 시도한다. (UsernamePasswordAuthenticationToken)

4. AuthenticationProvider 목록으로 인증 시도

  • AuthenticationProvider는 제공된 인증 객체로 사용자 인증을 시도한다.
  • 프레임워크와 함께 제공되는 기존 Authentication Provider 중 일부
    1. CasAuthenticationProvider
    2. JaasAuthenticationProvider
    3. DaoAuthenticationProvider
    4. OpenIDAuthenticationProvider
    5. RememberMeAuthenticationProvider
    6. LdapAuthenticationProvider

5. UserDetailsService가 필요한가 ?

  • 일부 AuthenticationProvider는 사용자 이름 기반으로 사용자 세부 정보를 검색하기 위하여 UserDetailsService를 사용할 수 있다. (DaoAuthenticationProvider)

6 ~ 7. UserDetails와 User

  • UserDetailsService는 username 기반으로 UserDetail (실제 구현은 User)를 검색한다.

8. Authentication 과 AuthenticationException

  • 사용자가 인증에 성공하면 완전히 채워진 Authentication 객체가 반환된다.
  • 그렇지 않은 경우 AuthenticationException가 발생한다.
  • AuthenticationProvider 인터페이스에 따르면, 성공적으로 인증하는 경우 AuthenticationProvider는 완전히 채워진 인증 객체를 정확히 반환하거나, 예외 발생 시 AuthenticationProvider 예외를 던진다.
  • Fully populated Authentication Object란
    • authenticated - true
    • grant authorities list
    • user credentials (username only)
  • AuthenticationException가 예외 발생하는 경우, 인증 메커니즘을 지원하는 AuthenticationEntryPoint에 의해 처리된다.

9. Authentication 완료

  • AuthenticationManager는 획득한 Fully populated Authentication객체를 관련 AuthenticationFilter로 반환한다.

10. SecurityContext에서 Authentication 객체를 설정

  • 관련된 AuthenticationFilter가 획득한 Authentication 객체를 향후 필터 사용을 위하여 SecurityContext에 저장한다. (Authorization Filters를 위해 사용)
SecurityContextHolder.getContext().setAuthentication(authentication);

1. JWT(JSON Web Token) 이란?

  • JSON Web Token의 약자로 전자서명 된 URL-safe (URL로 이용할 수 있는 문자로만 구성된)의 JSON
  • 전자서명(Signature)은 JSON의 변조를 체크할 수 있게 되어 있다.
  • 속성 정보(Claims)를 JSON 데이터 구조로 표현한 토큰으로 RFC 7519 표준
  • 서버와 클라이언트 간 정보를 주고 받을 때 HttpRequest header에 JSON 토큰을 넣은 후 서버는 별도의 인증 과정없이 헤더에 포함되어 있는 JWT 정보를 통해 인증한다.
    이때 사용되는 JSON 데이터는 URL-safe 하도록 URL에 포함할 수 있는 문자만으로 만든다.
  • HMAC 알고리즘을 사용하여 비밀키 또는 RSA를 이용한 Public Key / Private Key 쌍으로 서명할 수 있다.
Base64 인코딩의 경우 "+", "/", "="이 포함되지만 JWT는 URI에서 파라미터로 사용할 수 있도록 URL-Safe 한 Base64url 인코딩을 사용한다.

2. JWT 구조

  • Claims를 userId, username으로 하고, JWA 알고리즘 SHA-256으로 암호화
jwt:
  secret: 12345678901234567890123456789000
public String createToken(Long userId, String name) {
        return Jwts.builder()
                .setHeaderParam("typ", Header.JWT_TYPE)
                .signWith(key, SignatureAlgorithm.HS256)
                .claim("userId", userId)
                .claim("userName", name)
                .setExpiration(new Date(System.currentTimeMillis() + 864000000))
                .compact();
}
  • 위 작업을 통해 생성된 토큰 구성
// Header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
// Payload
eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoic2VvayIsImV4cCI6MTU5MzUyMTU0MX0.
// signature
AWsWjhGSeAIr9d0LGdDuFbGK57iK_mdsEnxt983h_1o

[Header]

  • token의 type과 JWT를 digitally sign할 때 사용한 algorithm을 정의
  1. typ
  • 토큰의 타입을 지정 ("JWT")
  1. alg
  • 해싱 알고리즘을 지정한다.
  • 해싱 알고리즘으로는 보통 HMAC SHA256 혹은 RSA가 사용되며, 이 알고리즘은 토큰을 검증할 때 사용되는 signature 부분에서 사용된다.
{
    "typ" : "JWT",
    "alg" : "HS256"
}
  • 위 내용을 Base64url 인코딩 한 결과
// Header
eyJhbGciOiJIUzI1NiJ9.

[Payload]

  • JWT에 담아서 전달할 data를 정의

  • 정보의 한 "조각"을 Claim이라 부르고, name / value의 한 쌍으로 이루어져있다.

  • Claim의 종류

    • Registered Claim

      • 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기 위하여 이름이 이미 정해진 Claim

      • 모두 선택적(optional) 이다.

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

      • 충돌이 방지된(collision-registant)이름을 가지고 있어야 한다.
      • 충돌을 방지하기 위하여 Claim 이름을 URI 형식으로 짓는다.
    • Private Claim

      • 서버 클라이언트 양측 간 협의하에 사용되는 Claim이름
      • Public Claim과는 달리 이름이 중복될 수 있으니 사용시 유의
public String createToken(Long userId, String name) {
        return Jwts.builder()
                .setHeaderParam("typ", Header.JWT_TYPE)
                .signWith(key, SignatureAlgorithm.HS256)
                .claim("userId", userId)
                .claim("userName", name)
                .setExpiration(new Date(System.currentTimeMillis() + 864000000))
                .compact();
}
  • 위 내용을 암호화한 Payload
// Payload
eyJ1c2VySWQiOjEwMDQsInVzZXJOYW1lIjoiSm9obiJ9.

[Signature]

  • JSON Web Token의 마지막 부분으로 HeaderPayload를 base64로 encode인코딩한 값을 합친 후 JWT secret key값을 encrypt한 값으로 Hash를 하여 생성
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  my-secret-key

) secret base64 encoded
  • 위 내용으로 인코딩한 결과 값
// signature
0nwaeM3fpDPvRGc64pyIp-JYNnuigCN9t_5ApVhPClQ

JWT Token 예시

  • 생성된 토큰은 HTTP 통신을 할 때 Authorization이라는 key의 value로 사용된다.
  • 일반적으로 value에 Beare이 앞에 붙여진다.
{
    "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoic2VvayIsImV4cCI6MTU5MzUyMTU0MX0.AWsWjhGSeAIr9d0LGdDuFbGK57iK_mdsEnxt983h_1o"
}

JWT Decoding

3. JWT를 사용하는 상황

  • 회원 인증
    1. 사용자가 로그인을 하면, 서버는 사용자의 정보를 기반으로한 토큰을 발급
    2. 그 후, 사용자가 서버에 요청을 할 때마다 JWT를 포함하여 전달
    3. 서버는 클라이언트에서 요청을 받을 때 마다, 해당 토큰이 유효하고 인증되었는지 검증을 하고, 사용자가 요청한 작업에 권한이 있는지 확인하여 작업을 처리
    4. 서버에서는 사용자에 대한 세션을 유지할 필요가 없다.
      즉, 사용자가 로그인되어 있는지 여부를 신경 쓸 필요가 없고, 사용자가 요청을 했을 때 토큰만 확인하면 되므로 세션 관리가 필요 없어서 서버 자원과 비용을 절감할 수 있다.
  • 정보 교류
    • JWT는 두 개체 사이에서 안정정있게 정보를 교환하기에 좋은 방법
    • 정보가 서명이 되어있기 때문에 정보를 보낸이가 바뀌진 않았는지, 또 정보가 도중에 조작되지 않았는지 검증을 할 수 있다.
* UI Layer, REST API 서버를 따로 두는 경우 JWT를 사용
* 하나의 End Point가 아닌 Mobile / Web 등의 multiple EndPoint 환경이라면 통합적인 인증 / 인가 환경을 제공하기 위해 JWT를 사용
* Third Party에게 public하게 open 한 REST EndPoing가 존재하는 경우 해당 Third Party의 인증 인가를 관리하기 위해 JWT 사용

+ Recent posts