JWT 생성

이호영·2023년 9월 4일
0

JWT

목록 보기
3/5

들어가기 전에...

이전 포스트에서 JWT에 대한 기본적인 개념들과 저장 위치별 장단점을 확인 하였습니다.
이번 포스트 부터는 JWT를 생성, 검증, 폐기를 알아보겠습니다.
Language: java
FrameWork: spring 2.7.6
Build: Gradle
DataBase: MySQL, Redis

의존성 추가

    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

jwt를 사용하기 위해 위 3개의 의존성을 추가해주세요.

Login

	@GetMapping("/main/login")
    public String login(@AuthenticationPrincipal UserDetails userDetails, HttpServletResponse response) {
        if (userDetails != null) {
            response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
            response.setHeader("Location", "/main/index");
            return null;
        }
        return "/main/login";
    }
    
    @ResponseBody
    @PostMapping("/main/login")
    public ResponseEntity<?> login(@RequestBody Login login, HttpServletResponse response) throws Exception {

        User authenticatedUser = userService.loadUserByLoginId(login.getLoginId()).orElseThrow(
                () -> new RuntimeException("해당 유저를 찾을 수 없습니다.")
        );

        final String JwtToken = jwtTokenUtil.generateToken(authenticatedUser);

        Cookie jwtCookie = tokenService.createJwtCookie(authenticatedUser, JwtToken);

        response.addCookie(jwtCookie);

        return ResponseEntity.ok(jwtCookie);
    }

위 코드를 보면 사용자가 로그인 페이지(/main/login)에 접근 요청(Get)을 보냈을 경우
사용자가 이미 로그인한 사용자는 main/index로 튕겨나갑니다.
로그인을 하지 않은 사용자라면 loginId와 loginPw를 입력하면 loadUserByLoginId 해당 유저를 검색 하고 해당하는 유저가 없다면 exception을 발생 시킵니다.
우리가 첫번째로 볼 메소드는 generateToken 입니다.

토큰 생성(generateToken)

@Component
public class JwtTokenUtil {

    public String generateToken(User user) {
        return JWT.create()
                .withSubject(user.getEmail())
                .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
                .withClaim("id", user.getId())
                .withClaim("nickname", user.getName())
                .sign(Algorithm.HMAC512(JwtProperties.SECRET));
    }
}

위 코드에서는 User 객체로부터 이메일, ID, 닉네임 등의 정보를 가져와서 토큰에 포함시킵니다.

  • .withSubject(user.getEmail()): 이메일을 Subject Claim으로 설정합니다.
  • .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME)): 만료 시간을 현재 시간으로부터 일정 시간 후로 설정합니다.
  • .withClaim("id", user.getId()), .withClaim("nickname", user.getName()): Custom Claim으로 ID와 닉네임을 추가합니다.
  • .sign(Algorithm.HMAC512(JwtProperties.SECRET)): Secret Key와 함께 HMAC512 알고리즘으로 서명하여 마지막으로 토큰을 완성시킵니다.
    해더는 JWT.create() 메서드를 호출시 자동으로 완성됩니다.
    여기서 JwtProperties.EXPIRATION_TIME과 JwtProperties.SECRET은 각각 만료 시간과 Secret Key를 의미하는 상수입니다. 이 값들은 프로젝트에 따라 적절하게 설정해야 합니다.

JWTProperties

public interface JwtProperties {

String SECRET = "Secret Key";

int EXPIRATION_TIME = 864000000; //60000 1분 //864000000 10일

String TOKEN_PREFIX = "Bearer ";

String HEADER_STRING = "Authorization";

}
JWT의 특성을 정의한 인터페이스 입니다.

  • SECRET: 토큰에 사용할 secret Key입니다.
    예를 들어, HS512 알고리즘을 사용하면 512bit 이상의 문자열 이여야 합니다.
    알파멧 하나에 8bit 입니다.
  • EXPIRATION_TIME: 유효시간을 정의합니다. ms단위(1s = 1000ms) 입니다.
  • TOKEN_PREFIX: 토큰의 접두사 입니다.

generateToken으로 jwt토큰이 생성 되었습니다.

생성된 토큰

  • BearereyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjNjU2MjFAbmF2ZXIuY29tIiwibmlja25hbWUiOiLsnbTtmLjsmIEiLCJpZCI6MSwiZXhwIjoxNjk0Njg0MjUzfQ.9SdxQMb6BX7WXJy6KXwlPP3iiPP81k8RWpatvzaHAX_k0ySSPm-hgT3JkR47K6mL1HLp8csMVNTIbeF75ufTvA

위 토큰을 jwt 링크에 들어가서 디코딩 하면 디코딩이 안됩니다.
왜? Bearer은 우리가 임의로 붙인 접두어 입니다. 제거 후 디코딩 하면 아래와 같이 표시됩니다.

HEADER
{
  "typ": "JWT",
  "alg": "HS512"
}
PAYLOAD

{
  "sub": "test01@naver.com",
  "nickname": "test01",
  "id": 1,
  "exp": 1694684253 //토큰 유효 시간 마우스를 가져다 대면 시간형식으로 출력
}

SIGNATURE

HMACSHA512(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-512-bit-secret
)

우리가 생성한 토큰입니다. 이 토큰을 Cookie에 저장해 봅시다.

토큰을 쿠키에 저장

코드

public Cookie createJwtCookie(User authenticatedUser, String jwtToken) {
        UserDetails userDetails = this.userService.loadUserByUsername(authenticatedUser.getLoginId());
        Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAu```
코드를 입력하세요
```thentication(auth);
        
        jwtToken = JwtProperties.TOKEN_PREFIX.trim() + jwtToken;
        
        Cookie jwtCookie = new Cookie("Authorization", jwtToken);
        jwtCookie.setHttpOnly(true);
        jwtCookie.setMaxAge(JwtProperties.EXPIRATION_TIME);
        jwtCookie.setPath("/");

        return jwtCookie;
    }

작동 순서

  1. loadUserByUsername: 사용자의 로그인 ID를 기반으로 사용자 상세 정보(UserDetails)를 가져옵니다.
  2. UsernamePasswordAuthenticationToken: 인증 객체(Authentication)를 생성합니다.
  3. SecurityContextHolder.getContext().setAuthentication(auth): 현재 보안 컨텍스트에 인증 객체를 설정하여, 이후 처리에서 해당 사용자가 인증된 것으로 간주되도록 합니다.
  4. 토큰 앞에 "Bearer " 문자열(TOKEN_PREFIX)을 붙여서 완전한 형태의 Authorization 헤더 값을 만듭니다.
  5. 새 Cookie 객체를 생성하고 이름을 "Authorization"으로 설정한 후 JWT 토큰 값을 넣습니다.
  6. .setHttpOnly(true) 메서드 호출로 JavaScript 등 클라이언트 사이드 스크립트에서 이 쿠키에 접근할 수 없도록 합니다. 이는 XSS 공격을 방어하는 데 도움이 됩니다.
  7. .setMaxAge(JwtProperties.EXPIRATION_TIME) 메서드 호출로 쿠키의 만료 시간을 설정합니다.
  8. .setPath("/") 메서드 호출로 쿠키가 전체 사이트에서 접근 가능하도록 설정합니다.

이렇게 생성된 Cookie객체를 응답으로 클라이언트에서 반환합니다.

response.addCookie(jwtCookie);
return ResponseEntity.ok(jwtCookie);

로그인 끝

위에서 토큰을 생성하고 토큰을 Cookie객체에 담아 클라이언트에 보냈습니다.
우리는 이 토큰을 어디서 확인 할 수 있을까요???
저는 로그인 후 index로 이동하니 index기준으로 설명 드립니다.
크롬 또는 다른 웹 브라우저에서 개발자 도구를 들어갑니다.
Network탭에서 index를 선택 하고 Headers 탭 -> RequestHeader에 보면 아래와 같이 저장되었습니다.(Cookie 탭에 들어가서 확인하여도 됩니다.)
Cookie:

  • Idea-93839e88=
    fd531c74-5344-4f72-a445-04275e982485;
  • Idea-93839e89=
    fdd4cd64-2261-458e-bd0a-6fd928efe80e; XSRF-TOKEN=fd06d259-be2f-44ba-8ae4-b57b973a59c8;
  • Authorization=
    BearereyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjNjU2MjFAbmF2ZXIuY29tIiwibmlja25hbWUiOiLsnbTtmLjsmIEiLCJpZCI6MSwiZXhwIjoxNjk0Njg0MjUzfQ.9SdxQMb6BX7WXJy6KXwlPP3iiPP81k8RWpatvzaHAX_k0ySSPm-hgT3JkR47K6mL1HLp8csMVNTIbeF75ufTvA;
  • JSESSIONID=
    950B6729900E557C1440960AB5B6A2AE
    우리가 생성한 토큰은 Authorization의 name을 가지고 BearereyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjNjU2MjFAbmF2ZXIuY29tIiwibmlja25hbWUiOiLsnbTtmLjsmIEiLCJpZCI6MSwiZXhwIjoxNjk0Njg0MjUzfQ.9SdxQMb6BX7WXJy6KXwlPP3iiPP81k8RWpatvzaHAX_k0ySSPm-hgT3JkR47K6mL1HLp8csMVNTIbeF75ufTvA; 의 value를 가지고 있습니다.

0개의 댓글