JWT - nhnacademy-be10-WannaB/wannab-wiki GitHub Wiki

Http์˜ ํŠน์ง•

  1. ๋ฌด์ƒํƒœ์„ฑ : ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์˜ ์ด์ „ ์š”์ฒญ ์ƒํƒœ๋ฅผ ๊ธฐ์–ตํ•˜์ง€ ์•Š์Œ โ†’ ๊ฐ ์š”์ฒญ์€ ๋…๋ฆฝ์ ์ž„
  2. ๋น„์—ฐ๊ฒฐ์„ฑ : ํด๋ผ์ด์–ธํŠธ(์š”์ฒญ) โ†’ ์„œ๋ฒ„(์‘๋‹ต) โ†’ ์—ฐ๊ฒฐ ๋Š๊น€

์œ„์˜ ๊ฐ™์€ ํŠน์ง•์œผ๋กœ ๊ฐ ์š”์ฒญ๋งˆ๋‹ค ์‚ฌ์šฉ์ž๋ฅผ ๊ฒ€์ฆํ•  ํ•„์š”๊ฐ€ ์žˆ์Œ!!

์ด์ „์— ์‚ฌ์šฉํ•œ ์„ธ์…˜ ๋ฐฉ์‹

  1. ๋กœ๊ทธ์ธ ์‹œ ์„ธ์…˜ID๊ฐ€ ์ƒ์„ฑ๋˜์–ด DB์— ์ €์žฅ๋จ
  2. ์ดํ›„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•  ๋•Œ๋งˆ๋‹ค ํด๋ผ์ด์–ธํŠธ ์ฟ ํ‚ค์— ์žˆ๋Š” ์„ธ์…˜ID์™€ DB์˜ ์„ธ์…˜ID๋ฅผ ๊ฒ€์ฆ
  3. ๊ฒ€์ฆ์ด ์™„๋ฃŒ๋œ ํ›„(filter) ์š”์ฒญ์ฒ˜๋ฆฌ

๋ฌธ์ œ์ 

  1. ์„ธ์…˜์ด ๋งŽ์•„์งˆ์ˆ˜๋ก ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ or db ๋ถ€ํ•˜ ์ฆ๊ฐ€
  2. ์ˆ˜ํ‰ ํ™•์žฅ์— ๋ฌธ์ œ ๋ฐœ์ƒ(์„œ๋ฒ„ ๋ถ„์‚ฐ ๊ตฌ์กฐ) : ์„ธ์…˜ ์ •๋ณด๊ฐ€ ํ•œ ์„œ๋ฒ„์—๋งŒ ์กด์žฌ โ†’ ์ธ์ฆ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ
  3. ์„ธ์…˜ ํƒˆ์ทจ? ์ฟ ํ‚ค ํƒˆ์ทจ? โ†’ ๊ณ„์ • ๋„์šฉ

JWT(Java Web Token)

jwt๋Š” ์„œ๋ฒ„๊ฐ€ ์„ธ์…˜์„ ์ €์žฅํ•˜์ง€ ์•Š์•„๋„ ๋˜๋„๋กํ•˜๋Š” ๋ฐฉ์‹

๋™์ž‘๊ณผ์ •

  1. ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ : ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ jwt๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ „๋‹ฌ
  2. ํด๋ผ์ด์–ธํŠธ๋Š” jwt๋ฅผ localStorage ๋˜๋Š” ์ฟ ํ‚ค์— ์ €์žฅ
  3. ์ดํ›„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋‚ด๋Š” ์š”์ฒญ์—๋Š” http header์— jwt๋ฅผ ๋ถ™์–ด์„œ ๊ฐ
  4. ์„œ๋ฒ„๋Š” ์š”์ฒญ๋งˆ๋‹ค jwt๋งŒ ๊ฒ€์‚ฌํ•ด์„œ ์‚ฌ์šฉ์ž๋ฅผ ํ™•์ธ? ๊ฒ€์ฆ?ํ•œ๋‹ค

jwt์˜ ๊ตฌ์กฐ

Header - ํ† ํฐ ํƒ€์ž…, signature ์•Œ๊ณ ๋ฆฌ์ฆ˜

Payload - ์‚ฌ์šฉ์ž ์ •๋ณด(userId, role)

Header:  { "alg": "HS256", "typ": "JWT" }
Payload: { "userId": 123, "role": "admin" }

Signature - Base64๋กœ ์ธ์ฝ”๋”ฉ๋œ header, payload์™€ ๋น„๋ฐ€ํ‚ค๋ฅผ ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์ƒ์„ฑ

message = base64UrlEncode(Header) + "." + base64UrlEncode(Payload)
Signature = HMAC-SHA256(message, secret_key)

Signature ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ข…๋ฅ˜

๋Œ€์นญํ‚ค ๊ธฐ๋ฐ˜(HMAC) - ๊ฐ„๋‹จํ•˜๊ณ  ๋น ๋ฆ„, ์†Œ๊ทœ๋ชจ ์„œ๋น„์Šค๋‚˜ ์„œ๋ฒ„ ๊ฐ„ ์‹ ๋ขฐ๊ฐ€ ์žˆ๋Š” ๊ตฌ์กฐ์—์„œ ์ ํ•ฉ

์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ค๋ช…
HS256 HMAC + SHA-256 (๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ)
HS384 HMAC + SHA-384
HS512 HMAC + SHA-512

๋น„๋Œ€์นญํ‚ค ๊ธฐ๋ฐ˜(RSA, ECDSA) - ์„œ๋ช…์ž์™€ ๊ฒ€์ฆ์ž ๋ถ„๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ํ™˜๊ฒฝ

์•Œ๊ณ ๋ฆฌ์ฆ˜ ์„ค๋ช…
RS256 RSA + SHA-256 (๊ณต๊ฐœํ‚ค/๊ฐœ์ธํ‚ค ๋ฐฉ์‹)
RS384 RSA + SHA-384
RS512 RSA + SHA-512
ES256 ECDSA + SHA-256 (ํƒ€์›๊ณก์„  ์•”ํ˜ธ๋ฐฉ์‹)
ES384 ECDSA + SHA-384
ES512 ECDSA + SHA-512

์ฃผ์˜ํ•  ์ 

  1. payload์— ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅ โ†’ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ €์žฅX

jwt๋Š” ์•”ํ˜ธํ™”๊ฐ€ ์•„๋‹Œ base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด โ†’ ๋””์ฝ”๋”ฉ ๊ฐ€๋Šฅ : payload ๋‚ด์šฉ์„ ์•Œ์•„๋‚ผ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Œ

  1. jwt๋ฅผ localStorage์— ์ €์žฅํ•˜๋ฉด xss์— ์ทจ์•ฝํ•จ(์ž๋ฐ” ์Šคํฌ๋ฆฝํŠธ๋กœ localStorage๋ฅผ ๊ฑด๋“œ๋ฆด ์ˆ˜ ์žˆ๋Š”๋“ฏํ•จ)

๊ทธ๋ž˜์„œ ์ฟ ํ‚ค์— ์ €์žฅํ•˜๊ณ  CSRF ๋Œ€๋น„์ฑ…์œผ๋กœ http-only, secure ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ•จ

++ localStorage๊ฐ€ ์•„๋‹Œ ์ฟ ํ‚ค์— ์ €์žฅํ•˜๋ฉด ์ง์ ‘ ํ—ค๋”๋ฅผ ์„ค์ •ํ•  ํ•„์š”์—†์ด ์ž๋™์œผ๋กœ ์š”์ฒญ์— ๋ถ™์–ด๊ฐ ^^>

ใ„ดโ†’ ์ฟ ํ‚ค๊ฐ€ ์ž๋™์œผ๋กœ ์š”์ฒญ์— ๋ถ™์–ด๊ฐ€๋„๋ก ๋‘๋ฉด ๊ฐ rest api ์„œ๋ฒ„์—์„œ ํŒŒ์‹ฑํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋จ

ใ„ดโ†’ jwt ํ† ํฐ์„ gateway์—์„œ ๋””์ฝ”๋”ฉ โ†’ ํŒŒ์‹ฑ โ†’ header์— ๋ถ™์ž„

SSR(์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง)์—์„œ๋Š” ์ฃผ๋กœ HttpOnly + Secure + ์ฟ ํ‚ค๋กœ jwtํ† ํฐ์„ ์ €์žฅ

httponly ์˜ต์…˜์ด ์žˆ์œผ๋ฉด js์—์„œ๋Š” ์ฟ ํ‚ค๋ฅผ ์ฝ์–ด์˜ฌ ์ˆ˜ ์—†์Œ โ†’ credential ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ง์ ‘ ์ฟ ํ‚ค์— ์žˆ๋Š” Jwt ํ† ํฐ์„ ํ™•์ธํ•  ์ˆ˜๋Š”์—†์ง€๋งŒ js์—์„œ ๋น„๋™๊ธฐ๋กœ ๋ณด๋‚ด๋Š” ์š”์ฒญ์— ๋Œ€ํ•ด ์ฟ ํ‚ค๋ฅผ ์ž๋™์ „์†กํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ

access, refresh Token

access token : ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์ž„์„ ์ฆ๋ช…ํ•˜๋Š” ๊ฒƒ(๋งŒ๋ฃŒ๊ธฐ๊ฐ„์ด ์ง€๋‚˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ)

refresh token : ์‚ฌ์šฉ์ž๊ฐ€ ์žฌ ๋กœ๊ทธ์ธ ํ•˜๋Š” ๋ฒˆ๊ฑฐ๋กœ์›€์„ ์—†์• ๊ธฐ ์œ„ํ•ด access token์ด ๋งŒ๋ฃŒ๋˜๊ณ  refresh token์ด ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์•˜์„ ๋•Œ access token์„ ์žฌ๋ฐœ๊ธ‰ ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ

FeignClient

์ด ์นœ๊ตฌ๋กœ ์š”์ฒญํ•œ response์˜ ์ฟ ํ‚ค๋Š” client์—๊ฒŒ ๊ฐ€์ง€ ์•Š์Œ โ†’ ์™œ? ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์•„๋‹˜

jwtํ† ํฐ์„ ์ฟ ํ‚ค๋ฅผ ์ €์žฅํ•˜๋Š” ๊ณณ์€ api๋ฅผ ํ˜ธ์ถœํ•˜๋Š” front ์„œ๋ฒ„!

ํ† ํฐ ๋ฐœ๊ธ‰ : client โ†’ front โ†’ gateway โ†’ user api(ํ† ํฐ ๋ฐœ๊ธ‰) โ†’ gateway โ†’ front(์ฟ ํ‚ค์ €์žฅ) โ†’ client(๋ธŒ๋ผ์šฐ์ €)

ํ† ํฐ ๊ฒ€์ฆ : client โ†’ front(ํ—ค๋”์— ํ† ํฐ ์ €์žฅ) โ†’ gateway(ํ† ํฐ ๊ฒ€์ฆ) โ†’ api

ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ : client โ†’ front โ†’ gateway(์žฌ๋ฐœ๊ธ‰ ์˜ˆ์™ธ์ฒ˜๋ฆฌ) โ†’ front(์˜ˆ์™ธ์ฒ˜๋ฆฌ ์žฌ๋ฐœ๊ธ‰ api ์š”์ฒญ) โ†’ gateway โ†’ api(ํ† ํฐ ๋ฐœ๊ธ‰) โ†’ gateway โ†’ front(์ฟ ํ‚ค์— ํ† ํฐ ์ €์žฅ) โ†’ client(๋ธŒ๋ผ์šฐ์ € ์ฟ ํ‚ค ์žฌ๋ฐœ๊ธ‰ ์™„๋ฃŒ) โ†’ js์—์„œ ๊ธฐ์กด ์š”์ฒญ ๊ธฐ์–ตํ•ด์„œ ๋‹ค์‹œ ์š”์ฒญํ•˜๊ธฐ(์–ด๋–ป๊ฒŒํ•˜์ง€..)

ํ† ํฐ ์ €์žฅ

jwt ์œ ํ‹ธ ํด๋ž˜์Šค

package com.example.jwt.demo_jwt;

import static java.security.KeyRep.Type.SECRET;

import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Date;

public class JwtUtil { private static final String SECRET = "P3C+yJU3dv7iXM2umvApxy7NTkfm2BL6V3GBIPTbe/Q="; private static final Key SECRET_KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));

public static String createAccessToken(String username) {
    return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000)) // 15๋ถ„
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
            .compact();
}

public static String createRefreshToken(String username) {
    return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)) // 7์ผ
            .claim("type", "refresh") // ์ถ”๊ฐ€ ๊ตฌ๋ถ„์ž claim
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
            .compact();
}

// ํ† ํฐ ๊ฒ€์ฆ ๋ฐ ํŒŒ์‹ฑ
public static String validateAndGetUsername(String token) {
    return Jwts.parser()
            .setSigningKey(SECRET_KEY)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
}

}

๋กœ๊ทธ์ธ ์ปจํŠธ๋กค๋Ÿฌ jwt ์ฟ ํ‚ค์„ค์ •

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;

@RestController public class AuthController {

@PostMapping("/login")
public String login(@RequestParam String username,
                    @RequestParam String password,
                    HttpServletResponse response) {

    // ์‹ค์ œ๋กœ๋Š” username/password๋ฅผ DB์—์„œ ๊ฒ€์ฆํ•ด์•ผ ํ•จ
    if ("user".equals(username) && "1234".equals(password)) {
        String jwt = JwtUtil.generateToken(username);

        Cookie cookie = new Cookie("access_token", jwt);
        cookie.setHttpOnly(true);           // ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€
        cookie.setSecure(true);             // HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ์ „์†ก
        cookie.setPath("/");                // ์ „์ฒด ๊ฒฝ๋กœ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
        cookie.setMaxAge(60 * 60);          // 1์‹œ๊ฐ„ ์œ ํšจ

        response.addCookie(cookie);

        return "๋กœ๊ทธ์ธ ์„ฑ๊ณต";
    } else {
        return "์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ์˜ค๋ฅ˜";
    }
}

}

์„œ๋ฒ„ ์„ค์ • ์ฃผ์˜

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("<http://localhost:3000>") // ํ”„๋ก ํŠธ ์ฃผ์†Œ
                .allowedMethods("*")
                .allowCredentials(true); // -> ํ—ˆ์šฉ ํ•ด์ค˜์•ผํ•จ
    }
}

์ฟ ํ‚ค ์ž๋™ ์ „์†ก ์„ค์ •

axios.post("<https://yourdomain.com/login>", {
  username: "user",
  password: "1234"
}, {
  withCredentials: true // -> ์„ค์ • 
});

์˜์กด์„ฑ

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
        </dependency>

๋น„๋ฐ€ํ‚ค ์„ค์ • - HS256(๋„๋ฆฌ ์‚ฌ์šฉ๋œ๋‹ค๊ณ  ํ•จ)

byte[] keyBytes = new byte[byteLength];
new SecureRandom().nextBytes(keyBytes);
return Base64.getEncoder().encodeToString(32); // 32๋ฐ”์ดํŠธ = 256๋น„ํŠธ

private static final String SECRET = "P3C+yJU3dv7iXM2umvApxy7NTkfm2BL6V3GBIPTbe/Q="; private static final Key SECRET_KEY = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));

โš ๏ธ **GitHub.com Fallback** โš ๏ธ