[TIL] Cookie & Session & JWT 씹어 먹기

정석·2024년 8월 20일

TIL

목록 보기
22/40
post-thumbnail

제목에 대한 내용들을 처음 코드로 구현한다.
따라서 느끼는 점들과 새롭게 알게된 내용들을 따끈따끈하게 기록하려 한다.

우선, AuthController 를 통해 Cookie, Session, JWT 를 생성 및 조회 할 수 있도록 관리하고

JwtUtil 에서 JWT 에 관한 기능 수행을 담당하도록 구현한 코드로 연습한다.

각 기능별로 어떤 궁금증이 있었고, 어떻게 결론을 내렸는지 사고의 흐름대로 작성할 예정이다.


🍪 Cookie 생성과 조회

@RestController
@RequestMapping("/api")
public class AuthController {

    public static final String AUTHORIZATION_HEADER = "Authorization";

    private final JwtUtil jwtUtil;

    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @GetMapping("/create-cookie")
    public String createCookie(HttpServletResponse res) {
        addCookie("Moon Auth", res);

        return "createCookie";
    }

자 일단 HttpServletResponse 부터 헷갈렸는데, 상식상으로는 뭔가를 생성하려면 내가 서버에게 요청을 줘야하지 않나? 란 생각에 그럼 Response 가 아니라 Request 아님 ? 이런 생각이 들었다.

  • 결론
    Cookie 는 생성이 되고 서버가 아닌 클라이언트가 저장하게 된다. 따라서 Cookie 가 생성이 되고 클라이언트가 받아야 하기에 Response 를 통해 해당 Cookie 를 저장해야한다. 그래서 Response 가 쓰인다.

그럼 createCookie 가 실행되고 addCookie 는 어떻게 구현 됐는지 보자.

2. addCookie

public static void addCookie(String cookieValue, HttpServletResponse res) {
        try {
            cookieValue = URLEncoder.encode(cookieValue, "UTF-8").replaceAll("\\+", "%20");

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue);  // Name : Value
            cookie.setPath("/");
            cookie.setMaxAge(30 * 60);

            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
  • Cookie 에 넣을 데이터를 인코딩하는 과정을 거쳐야 한다.
  • Cookie 객체를 만들기 위해선 Name : Value 형태로 값이 들어가야 생성된다.
  • setPath 를 지정해 어떤 도메인에서 사용할 수 있는지 정할 수 있다.
  • setMaxAge 로 쿠키의 만료 시간을 정할 수 있다.

3.getCookie

@GetMapping("/get-cookie")
    public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
        System.out.println("value = " + value);

        return "getCookie : " + value;
    }
  • Cookie 정보를 가져오려면 @CookieValue 어노테이션으로 해당 쿠키의 Name 값을 넣어줘야 한다.

🎫 Session 생성과 조회

1. Session 생성

  @GetMapping("/create-session")
    public String createSession(HttpServletRequest req) {
        HttpSession session = req.getSession(true); // true -> 세션 생성 또는 이미 존재한다면 세션 반환
        session.setAttribute(AUTHORIZATION_HEADER, "Moon Auth");
        return "createSession";
    }

Session 이 생성될 때는 ServletReqeust 를 활용한다. 서버에 해당 내용이 저장되어야 하기 때문.

  • 생성할 땐 .getSession 으로 일단 ServletRequest 에 세션이 있는지 확인하고 없다면 생성한다!
  • 세션 정보에 Cookie 처럼 Name : Value 가 들어감.

2. Session 조회

@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
	HttpSession session = req.getSession(false);

	String value = (String) session.getAttribute(AUTHORIZATION_HEADER);
    System.out.println("value = " + value);
    return "getSession : " + value;
    }
  • request에 생성된 세션이 있는지 찾기 위해 .getSession에 false 를 넣으면 세션이 존재하지 않을 때는 null 값을 반환한다.
  • session.getAttribute(세션 Name값) 을 넣으면 값이 String 으로 나온다.

🔑 JWT 생성 및 조회

JWT 생성 ▶︎ 해당 토큰 쿠키에 담기

1. JWT 생성

일단, AuthController 에서 시작한다.

1) AuthController 의 createJwt 메서드

@GetMapping("/create-jwt")
public String createJwt(HttpServletResponse res) {
	// Jwt 생성
	String token = jwtUtil.createToken("Moon", UserRoleEnum.USER);

	// Jwt 쿠키 저장
    jwtUtil.addJwtToCookie(token, res);

	return "createJwt : " + token;
}
  • JWT 또한 클라이언트가 해당 정보를 Cookie에 담아 가지고 있어야 하기에 Response 로 시작한다.
  • UserRoleEnum.USER 은 Enum 값으로 ROLE_USER 를 반환한다.

2. Create Token

JwtUtil 에서 실행되는 createToken 은 다음과 같다.

@Component
public class JwtUtil {

    // Header Key 값
    public static final String AUTHORIZATION_HEADER = "Authorization";

    // 사용자 권한 값의 Key
    public static final String AUTHORIZATION_KEY = "auth";

    // Token 식별자
    public static final String BEARER_PREFIX = "Bearer ";

    // Token 만료 시간
    public static final Long TOKEN_TIME = 60 * 60 * 1000L;

    @Value("${jwt.secret.key}")
    private String secretKey;

    private Key key;

    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 로그 설정
    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");


    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // 토큰 생성
    public String createToken(String username, UserRoleEnum roleEnum) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username) // 사용자 식별자값
                        .claim(AUTHORIZATION_KEY, roleEnum) // 사용자 권한 키 밸류
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }

@PostConstruct?

  1. 클래스가 초기화된 직후, 즉 의존성 주입이 완료된 직후에 호출된다.
  2. 한 번만 호출된다. Spring Bean이 생성되고 나서, 객체가 생성된 직후 호출됨.
  3. 초기화 작업을 수행

따라서 JwtUtil 클래스에 대한 의존성 주입이 완료된 후에 init() 메서드가 실행되며 key 에 미리 설정한 secretKey 를 활용하여 Base64로 디코딩한 값을 넣는다.
해당 키는 한번만 수행되어 초기화 되기에 매번 새롭게 생성되지 않는다.

그리고 이제 createToken 메서드를 살펴보면,
BEARER_PREFIX String 값을 맨 앞에 넣으면서 시작한다. (암묵적 규칙이라함)
Jwts 라는 클래스에 한개씩 담기 시작하면 된다.

  • setSubject -> 사용자 식별 ID 를 담는다.
  • claim -> payLoad 즉 데이터가 담기는 부분
  • setExpiration -> 토큰 만료 시간 설정
  • setIssuedAt(date) -> 토큰 발급일
  • signWith -> 암호화할 키와 알고리즘 설정
  • compact -> 생성!

3. 토큰 쿠키에 담기

public void addJwtToCookie(String token, HttpServletResponse res) {
	try {
            token = URLEncoder.encode(token, "UTF-8").replaceAll("\\+", "%20");

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token);
            cookie.setPath("/");

            //Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage());
        }
    }
  • 쿠키 생성할 때와 같이 쿠키 객체에 Name : Value 형태로 토큰을 넣으면 된다.

4. 토큰 가져오기

  • getJwt API
@GetMapping("/get-jwt") 
public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
        // JWT 토큰 substring
        String token = jwtUtil.substringToken(tokenValue);

        // 토큰 검증
        if(!jwtUtil.validateToken(token)){
            throw new IllegalArgumentException("Token Error");
        }

        // 토큰에서 사용자 정보 가져오기
        Claims info = jwtUtil.getUserInfoFromToken(token);
        // 사용자 username
        String username = info.getSubject();
        System.out.println("username = " + username);
        // 사용자 권한
        String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
        System.out.println("authority = " + authority);

        return "getJwt : " + username + ", " + authority;
    }
  • 관련 메서드
// JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            logger.error("유효하지 않은 JWT");
        } catch (ExpiredJwtException e) {
            logger.error("만료된 토큰");
        } catch (UnsupportedJwtException e) {
            logger.error("지원되지 않는 토큰");
        } catch (IllegalArgumentException e) {
            logger.error("잘못된 토큰");
        }
        return false;
    }

    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

0개의 댓글