JWT 알아보기

Bobby·2021년 10월 6일
0

즐거운 개발일지

목록 보기
11/22
post-thumbnail

JWT 알아보기

1. JWT 란?

  • JSON Web Token 의 약자이다. -> JSON 형태로 된 웹 토큰이라는 말!
  • HMAC, RSA, ECDSA 알고리즘을 사용할 수 있다.
    • 이 알고리즘들의 원리는... 어쩌구저쩌구... 이다.. (pass)
    • HMAC 알고리즘은 비밀키(대칭키) 방식 -> 암, 복호화 키가 같다.
    • RSA, ECDSA 알고리즘은 공개키(비대칭키) 방식 -> 암, 복호화 키가 다르다.

2. JWT는 언제 사용할까?

장점

  • 사용자 인증정보가 토큰에 포함되어 있기 때문에 별도의 저장소(세션, DB, ...)가 필요하지 않다.
  • 토큰의 검증 과정을 CPU가 수행하고 프로그램간, 서버간 I/O가 일어나지 않아 빠르고 확장성이 좋다.
  • 다양한 플랫폼에서 사용 및 확장이 쉽다.

단점

  • 토큰에 담긴 사용자 정보가 DB상 변경되면 토큰에 바로 적용 불가, 토큰 재발급 해야한다.
  • 인증이 필요한 모든 요청 헤더에 토큰 값을 전송하므로 요청 데이터 트래픽이 커진다.

3. JWT의 구조

  • Base64Url은 url에서 사용할 수 있도록 +, =, / 을 제외한 url safe 인코딩 방식이다.
  • JWT는 Header, Payload, Signiture 세가지로 구성된다.
  • 현재 사용하고 있는 알고리즘의 종류가 들어있다.
    {
      "alg": "HS256",
      "typ": "JWT"
    }
  • 이 정보를 Base64Url로 인코딩 하면 header 완성!
	eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

PAYLOAD

  • 우리가 사용할 인증 정보들을 담는 곳이다.
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
  • 이 정보를 Base64Url로 인코딩 하면 payload 완성!
	eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
  • 단순하게 Base64Url로 인코딩 했을 뿐 암호화 된 정보가 아니다. 즉, 누구나 디코딩하여 해당 내용을 볼 수 있다. 그러므로 payload에는 중요한 정보는 담으면 안된다!!

SIGNATURE

  • 누구나 다 볼 수 있는데 어떻게 로그인 인증을 할까?
  • HMAC 알고리즘을 사용할 경우 서명은 다음과 같이 생성한다.
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      your-256-bit-secret
    ) 
  • 위에서 생성한 header와 payload, 나만의 secret키를 포함해 HMAC 알고리즘을 사용한다.
  • 이 정보를 Base64Url로 인코딩 하면 signature 완성!
	SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • 누가 header 값과 payload 값을 변경하여 토큰을 서버로 보낸다면? 서버에서는 해당 정보로 다시 서명을 만들어서 검증을 하게 되는데, 값이 변경되었으므로 서명이 달라진다. 따라서 변경된 정보라는 것을 알 수있다.

JSON Web Token

  • 이 세가지 정보를 "." 로 연결하면 JWT 완성!!

4. 토큰 사용하기

  • 간단한 로그인 과정을 살펴보자.
  • 로그인 요청 시 유저정보를 확인하고 토큰을 발급한다.
  • 토큰을 발급받은 이후에는 인증이 필요한 요청의 헤더에 토큰을 담아 요청한다.
  • Authorization: Bearer <token>

5. 예제

프로젝트 생성

  • spring initializer
  • springboot 2.5.5
  • dependency
    • web, lombok

dependency 추가

  • gradle
	implementation 'com.auth0:java-jwt:3.18.2'

Member.java

  • 유저 entity 객체
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    private int id;
    private String username;
    private String password;
}

LoginReq.java

  • 로그인 요청 dto
@Data
public class LoginReq {

    private String username;
    private String password;
}

TestRepository.java

  • 빈으로 등록될 때 유저 하나 추가
@Repository
public class TestRepository {

    private final List<Member> members = new ArrayList<>();
    private static int sequence = 0;

    // 유저 생성
    public TestRepository() {
        members.add(new Member(++sequence, "kim", "1234"));
    }

    // 유저 확인
    public boolean login(LoginReq req) {
         return members.stream()
                .anyMatch(m -> m.getUsername().equals(req.getUsername()) && m.getPassword().equals(req.getPassword()));
    }
}

JwtUtils.java

  • JWT 유틸클래스 생성
  • HS256 알고리즘 사용
  • generateToken()
    • withIssuer : 토큰 발행자(payload에 들어가며 key는 iss)
    • withIssuedAt : 토큰 발행시간(payload에 들어가며 key는 iat)
    • withExpiresAt : 토큰 만료시간(payload에 들어가며 key는 exp)
    • withClaim : 직접 데이터 작성("key", "value" 형태)
public class JwtUtils {

    private JwtUtils() {}

    // 토큰 생성
    public static String generateToken(String username) {
        // HS256 알고리즘
        Algorithm algorithm = Algorithm.HMAC256("secret");
        return JWT.create()
                .withIssuer("k")
                .withIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
                .withExpiresAt(Date.from(LocalDateTime.now().plusDays(1L).atZone(ZoneId.systemDefault()).toInstant()))
                .withClaim("username", username)
                .sign(algorithm);
    }

    // 토큰 검증
    public static boolean verifyToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256("secret");
        try {
            DecodedJWT verify = JWT.require(algorithm)
                    .build().verify(token);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

}

LoginFilter.java

  • 필터에서 토큰 검증을 수행
  • 헤더에 담긴 토큰을 가져와 검증
@WebFilter("/*")
public class LoginFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        // 토큰 검증 제외 요청 URI 리스트
        String[] excludeUri = {
                "/login"
        };
        
        String requestURI = httpServletRequest.getRequestURI();

        if (Arrays.asList(excludeUri).contains(requestURI)) {
            chain.doFilter(httpServletRequest, httpServletResponse);
        } else { 
            try {
                // 헤더에서 Authorization 값을 가져와 검증
                String authorization = httpServletRequest.getHeader("Authorization");
                String token = authorization.replace("Bearer ", "");
                if (JwtUtils.verifyToken(token)) {
                    chain.doFilter(httpServletRequest, httpServletResponse);
                } else {
                    httpServletResponse.getWriter().println("Unauthorized");
                }
            } catch (Exception e) {
                httpServletResponse.getWriter().println("Unauthorized");
            }
        }
    }
}

필터 등록

@ServletComponentScan(basePackages = {"com.example.jwt.filter"})
public class JwtApplication {
	...
}

TestController.java

  • 테스트 컨트롤러
@RestController
@RequiredArgsConstructor
public class TestController {

    private final TestRepository testRepository;

    @PostMapping("/login")
    public ResponseEntity<?> login(LoginReq loginReq) {
        return testRepository.login(loginReq) ?
                ResponseEntity.status(HttpStatus.OK).header("token", JwtUtils.generateToken(loginReq.getUsername())).body("success") :
                ResponseEntity.status(HttpStatus.BAD_REQUEST).body("fail");
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

6. 테스트

로그인

  • 헤더에 토큰 발급

API 요청

  • 인증헤더 없을 때
  • 인증헤더 추가

코드

profile
물흐르듯 개발하다 대박나기

0개의 댓글