JWT - nhnacademy-be10-WannaB/wannab-wiki GitHub Wiki
Http์ ํน์ง
- ๋ฌด์ํ์ฑ : ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์ ์ด์ ์์ฒญ ์ํ๋ฅผ ๊ธฐ์ตํ์ง ์์ โ ๊ฐ ์์ฒญ์ ๋ ๋ฆฝ์ ์
- ๋น์ฐ๊ฒฐ์ฑ : ํด๋ผ์ด์ธํธ(์์ฒญ) โ ์๋ฒ(์๋ต) โ ์ฐ๊ฒฐ ๋๊น
์์ ๊ฐ์ ํน์ง์ผ๋ก ๊ฐ ์์ฒญ๋ง๋ค ์ฌ์ฉ์๋ฅผ ๊ฒ์ฆํ ํ์๊ฐ ์์!!
์ด์ ์ ์ฌ์ฉํ ์ธ์ ๋ฐฉ์
- ๋ก๊ทธ์ธ ์ ์ธ์ ID๊ฐ ์์ฑ๋์ด DB์ ์ ์ฅ๋จ
- ์ดํ ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ ๋๋ง๋ค ํด๋ผ์ด์ธํธ ์ฟ ํค์ ์๋ ์ธ์ ID์ DB์ ์ธ์ ID๋ฅผ ๊ฒ์ฆ
- ๊ฒ์ฆ์ด ์๋ฃ๋ ํ(filter) ์์ฒญ์ฒ๋ฆฌ
๋ฌธ์ ์
- ์ธ์ ์ด ๋ง์์ง์๋ก ์๋ฒ ๋ฉ๋ชจ๋ฆฌ or db ๋ถํ ์ฆ๊ฐ
- ์ํ ํ์ฅ์ ๋ฌธ์ ๋ฐ์(์๋ฒ ๋ถ์ฐ ๊ตฌ์กฐ) : ์ธ์ ์ ๋ณด๊ฐ ํ ์๋ฒ์๋ง ์กด์ฌ โ ์ธ์ฆ ์ค๋ฅ ๋ฐ์ ๊ฐ๋ฅ์ฑ ์์
- ์ธ์ ํ์ทจ? ์ฟ ํค ํ์ทจ? โ ๊ณ์ ๋์ฉ
jwt๋ ์๋ฒ๊ฐ ์ธ์ ์ ์ ์ฅํ์ง ์์๋ ๋๋๋กํ๋ ๋ฐฉ์
๋์๊ณผ์
- ๋ก๊ทธ์ธ ์ฑ๊ณต ์ : ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์๊ฒ jwt๋ฅผ ์์ฑํ์ฌ ์ ๋ฌ
- ํด๋ผ์ด์ธํธ๋ jwt๋ฅผ localStorage ๋๋ ์ฟ ํค์ ์ ์ฅ
- ์ดํ ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ด๋ ์์ฒญ์๋ http header์ jwt๋ฅผ ๋ถ์ด์ ๊ฐ
- ์๋ฒ๋ ์์ฒญ๋ง๋ค 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 |
์ฃผ์ํ ์
- payload์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅ โ ๋ฏผ๊ฐํ ์ ๋ณด๋ ์ ์ฅX
jwt๋ ์ํธํ๊ฐ ์๋ base64๋ก ์ธ์ฝ๋ฉ๋ ๋ฌธ์์ด โ ๋์ฝ๋ฉ ๊ฐ๋ฅ : payload ๋ด์ฉ์ ์์๋ผ ๊ฐ๋ฅ์ฑ์ด ์์
- jwt๋ฅผ localStorage์ ์ ์ฅํ๋ฉด xss์ ์ทจ์ฝํจ(์๋ฐ ์คํฌ๋ฆฝํธ๋ก localStorage๋ฅผ ๊ฑด๋๋ฆด ์ ์๋๋ฏํจ)
๊ทธ๋์ ์ฟ ํค์ ์ ์ฅํ๊ณ CSRF ๋๋น์ฑ ์ผ๋ก http-only, secure ์ต์ ์ ์ฌ์ฉํ๋๋ก ํจ
++ localStorage๊ฐ ์๋ ์ฟ ํค์ ์ ์ฅํ๋ฉด ์ง์ ํค๋๋ฅผ ์ค์ ํ ํ์์์ด ์๋์ผ๋ก ์์ฒญ์ ๋ถ์ด๊ฐ ^^>
ใดโ ์ฟ ํค๊ฐ ์๋์ผ๋ก ์์ฒญ์ ๋ถ์ด๊ฐ๋๋ก ๋๋ฉด ๊ฐ rest api ์๋ฒ์์ ํ์ฑํ๋ ์ฝ๋๊ฐ ์ค๋ณต๋จ
ใดโ jwt ํ ํฐ์ gateway์์ ๋์ฝ๋ฉ โ ํ์ฑ โ header์ ๋ถ์
SSR(์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง)์์๋ ์ฃผ๋ก HttpOnly + Secure + ์ฟ ํค๋ก jwtํ ํฐ์ ์ ์ฅ
httponly ์ต์ ์ด ์์ผ๋ฉด js์์๋ ์ฟ ํค๋ฅผ ์ฝ์ด์ฌ ์ ์์ โ credential ์ต์ ์ ์ฌ์ฉํ์ฌ ์ง์ ์ฟ ํค์ ์๋ Jwt ํ ํฐ์ ํ์ธํ ์๋์์ง๋ง js์์ ๋น๋๊ธฐ๋ก ๋ณด๋ด๋ ์์ฒญ์ ๋ํด ์ฟ ํค๋ฅผ ์๋์ ์กํ ์ ์๊ฒ ํด์ค
access token : ์ธ์ฆ๋ ์ฌ์ฉ์์์ ์ฆ๋ช ํ๋ ๊ฒ(๋ง๋ฃ๊ธฐ๊ฐ์ด ์ง๋์ง ์์์ ๊ฒฝ์ฐ)
refresh token : ์ฌ์ฉ์๊ฐ ์ฌ ๋ก๊ทธ์ธ ํ๋ ๋ฒ๊ฑฐ๋ก์์ ์์ ๊ธฐ ์ํด access token์ด ๋ง๋ฃ๋๊ณ refresh token์ด ๋ง๋ฃ๋์ง ์์์ ๋ access token์ ์ฌ๋ฐ๊ธ ํ๋๋ฐ ์ฌ์ฉ๋๋ ๊ฒ
์ด ์น๊ตฌ๋ก ์์ฒญํ 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์์ ๊ธฐ์กด ์์ฒญ ๊ธฐ์ตํด์ ๋ค์ ์์ฒญํ๊ธฐ(์ด๋ป๊ฒํ์ง..)
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();
}
}
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));