JSON Web Token
์ต๊ทผ ์์คํ ์์ ๋ฐ์ก๋๋ ํน์ ์ด๋ฉ์ผ์ ํฌํจ๋๋ ๋งํฌ๋ฅผ ํตํด์ ์ฌ์ฉ์๊ฐ ๊ด๋ จ๋ ํ์ด์ง์ ์ ๊ทผํ ์ ์๋๋ก ํด๋ฌ๋ผ๋ ์๊ตฌ์ฌํญ์ด ์์์ต๋๋ค. ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ์๊ฐ ์ด๋ฉ์ผ์ ํตํด์ ์์คํ ์ ์ ์ํ๋ ๊ฒฝ์ฐ์๋ ์์ ์ ๊ณ์ ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ฌ์ฉํ์ฌ ์์คํ ์ ์ธ์ฆํ๊ธฐ ์ ์ธ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ๋ฐ๋ผ์, ์ด๋ฉ์ผ์ ํฌํจ๋ ๋งํฌ๋ฅผ ํตํด์ ์ ๊ทผํ ๋ ์ฌ์ฉ์ ์ธ์ฆ์ ์ผ์์ ์ผ๋ก ์ ๊ณตํ๋ ๋ฐฉ์์ ๋์ ํด์ผํฉ๋๋ค.
Token-based Authentication
ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ ๋งค์ปค๋์ฆ์ ์ฌ์ฉ์ ๊ณ์ ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํ์ง ์์๋ ์์คํ ์ ์ด์ฉํ ์ ์๋ ๊ถํ์ ์ ๊ณตํ๊ธฐ ์ํ ๋ฐฉ์์ ๋๋ค. ์ฌ๊ธฐ์ ํ ํฐ์ด๋ผํจ์ ์์คํ ์ด ์ธ์ํ ์ ์๋ ๋ฌธ์์ด ๋ฐ์ดํฐ๋ฅผ ๋งํฉ๋๋ค. ํด๋ํฐ ๋ฌธ์ ์ธ์ฆ์ด๋ ์ด๋ฉ์ผ๋ก ๋ฐ์ก๋๋ ์ธ์ฆ์ฝ๋ ๋๋ ๋งํฌ๋ฅผ ํ ํฐ์ด๋ผ ๋ถ๋ฅผ ์ ์์ต๋๋ค.
ํ์ฌ ์์คํ ์ Spring Security OAuth๋ฅผ ํตํด ํด๋ผ์ด์ธํธ ํฌ๋ ๋ด์ ๊ธฐ๋ฐ์ ํ ํฐ์ผ๋ก OpenAPI๋ฅผ ์ฌ์ฉํ ์ ์๋๋ก ์ ๊ณตํ๊ณ ์๋๋ฐ JWT๊ฐ ์๋ JdbcTokenStore๋ฅผ ์ฌ์ฉํ๊ณ ์๊ธฐ์ SecureRandomBytesKeyGenerator๋ก ๋ง๋ค์ด์ง๋ ์ก์ธ์ค ํ ํฐ์ ์ฌ์ฉํ๊ฒ ๋์ด์์ต๋๋ค. ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ๋ฅผ ๋ณด๋ฉด ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ ๋งค์ปค๋์ฆ์ด ์๋ ํ ํฐ ๊ธฐ๋ฐ์ผ๋ก JWT๋ฅผ ๋ฐ๊ธํ๊ณ ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ํต์ ํ ๋ JWT๋ฅผ ์์ฒญ ํค๋์ ํฌํจ์์ผ ์์คํ ์ ๋ํ ๊ถํ ๋ฐ ์ธ๊ฐ๋ฅผ ์ํํ ์ ์๋๋ก ๊ตฌ์ฑํ๋ ๊ฒ ๊ฐ์ต๋๋ค.
JWT
RFC7519๋ก ์ ์๋์ด์๋ JWT(JSON Web Token)์ ์ฌ์ฉ์์ ์ ์์ ํ์ธํ ์ ์๋ ์ ๋ณด๋ฅผ Base64 ์ธ์ฝ๋ฉ์ผ๋ก ํํํ๋ฉด์ ํ์ด๋ก๋์ ๋ํ ์ ์์๋ช ์ ํตํด์ ์ธ์ฆ ์์คํ ์์ ๋ฐ๊ธํ ํ ํฐ์ ์ ๋ขฐํ ์ ์๋์ง ๊ฒ์ฆํ์ฌ ํ ํฐ์ ํฌํจ๋ ์ ๋ณด๋ฅผ ํ ๋๋ก ๊ถํ ๋ฐ ์ธ๊ฐ๋ฅผ ์ ์ฉํ ์ ์์ต๋๋ค. JWT์ ๋ํด์๋ ์๋์ ๋งํฌ๋ฅผ ํตํด์ ๊ฐ๋จํ๊ฒ ์ดํดํ ์ ์์ต๋๋ค.
URI Query String
JSON Web Token (JWT) is a compact claims representation format intended for space constrained environments such as HTTP Authorization headers and URI query parameters.
์ด๋ฉ์ผ์ ํฌํจ๋๋ ๋งํฌ๋ GET ์์ฒญ์ ์ํํ๋ URL(URI + Query String)์ด๋ฏ๋ก ์ผ๋ฐ์ ์ผ๋ก HTTP ํต์ ์ Authorization ๋๋ X-Auth-Token ํค๋์ ํ ํฐ์ ํฌํจํ์ฌ ์ ๋ฌํ ์ ์์ต๋๋ค. JWT๋ ๊ทธ ์์ฒด๋ก Base64 URL Safe๋ก ์ธ์ฝ๋ฉ๋๋ฏ๋ก ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ์ ํฌํจํ์ฌ ์ ๋ฌํ ์ ์์ต๋๋ค.
Token Parameter
์ด๋ฉ์ผ์ ํฌํจ๋๋ ๋งํฌ์ ํ ํฐ ์ ๋ณด๊ฐ ์ด๋ป๊ฒ ๋ด๊ฒจ์ง๋์ง ๋๊ฐ์ง ์์๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ฒซ๋ฒ์งธ๋ ์ด๋ฉ์ผ๊ณผ ์ธ์ฆ์ ์ํ ํ ํฐ์ด Path Variable ํํ๋ก URI์ ํฌํจ๋๋ ๋ฐฉ์์ด๋ฉฐ, ๋๋ฒ์งธ๋ URI์ ๋ํ ์ฟผ๋ฆฌ ์คํธ๋ง์ผ๋ก ํ ํฐ ํ๋ผ๋ฏธํฐ๊ฐ ํฌํจ๋จ์ ๋ณด์ฌ์ค๋๋ค.
- http://daily-devblog.com/api/regist/certify/kdevkr@gmail.com/$TOKEN
- https://careers.kakao.com/applicant/checkEmail?token=$TOKEN&jobOfferId=P-1
๋๋ฒ์งธ์ฒ๋ผ ์ด๋ฉ์ผ์ ํฌํจ๋ ๋งํฌ๋ฅผ ๋์ค์ ํด๋ฆญํ๋๋ผ๋ ์์คํ ์์ ์ธ์ํ ์ ์๋ ํ ํฐ์ธ์ง ๋ง๋ฃ๋์๋์ง๋ฅผ ํ๋จํ์ฌ ์์คํ ์ ๋ํ ์ธ๊ฐ๋ฅผ ํ๋จํ๊ฒ ๋ฉ๋๋ค. ์ฌ๊ธฐ์ ํ์ธํ ์ ์๋ฏ์ด ์ด๋ฉ์ผ์๋ ์์คํ ์์ ๋ฐ๊ธํ ํ ํฐ์ด ๋งํฌ์ ํฌํจ๋์ด ์ ์ง๋๋ฏ๋ก ๋ณด์ ์ ๋ง๋ฃ ์๊ฐ์ ์ต์ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
Implementation Sample
์ด๋ฉ์ผ ๋งํฌ์ ํฌํจ๋ ํ ํฐ ํ๋ผ๋ฏธํฐ์ ๋ฐ๋ผ ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ ๋งค์ปค๋์ฆ์ ์ํํ๊ณ ์ฌ์ฉ์ ์ธ์ฆ์ ์ํํ ์ ์๋ ๋ฐฉ์์ ์ ์ฉํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค. ๊ฐ๋จํ ์ํ ํํ๋ก ๋ง๋ค๊ณ ์ผ๋ จ์ ํ๋ก์ธ์ค๋ฅผ ์ดํดํ ์ ์๋๋ก ๊ณต์ ํ๊ณ ์ํจ์ด๋ ์ฝ๋๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ๊ธฐ์๋ ๋ฌธ์ ์ ์ด ์์ ์ ์์ต๋๋ค.
Dependencies
์ฐ์ ์คํ๋ง ์ํ๋ฆฌํฐ ๊ธฐ๋ฐ์ ํ๊ฒฝ์ด๋ผ๋ ๊ฒ์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ฉฐ JWT ๋ฐ๊ธ๊ณผ ๊ฒ์ฆ์ ์ํด์ ์ฌ์ฉํ Java JWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ JWT ๋ฐ๊ธ ์ ์๋ช ํ ํค ํ์ด๋ฅผ ๋ถ๋ฌ์ฌ ์ ์๋๋ก Bouncy Castle๋ฅผ ์์กด์ฑ์ ์ถ๊ฐํฉ๋๋ค.
dependencies {
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.bouncycastle:bcprov-jdk18on:1.71'
}
Generate EC Key Pair
๋ณธ ์ํ์์๋ Cryptographic Algorithms for Digital Signatures and MACs ๋ชฉ๋ก ์ค์์ ES256์ด๋ผ๋ ์๋ช ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ๋ JWT ํ ํฐ์ ๋ฐ๊ธํ๊ธฐ ์ํด P-256 ๊ณก์ ์ ์ฌ์ฉํ๋ EC ํค ํ์ด๋ฅผ ์์ฑํฉ๋๋ค.
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem
openssl ec -in ec-private.pem -pubout -out ec-public.pem
openssl pkcs8 -topk8 -inform pem -in ec-private.pem -outform pem -nocrypt -out ec-private.pkcs8
Generate JWT
JWT ๋ฐ๊ธ์ ์ํ EC ํค ํ์ด๋ฅผ ์ค๋นํ์ผ๋ฏ๋ก ํด๋์คํจ์ค๋ก๋ถํฐ ํค ํ์ด๋ฅผ ๋ถ๋ฌ์ค๊ณ ํ ํฐ ๋ฐ๊ธ์ ์ํ ์ ํธ ํด๋์ค๋ฅผ ์์ฑํฉ๋๋ค.
package com.example.crypto.util;
import io.jsonwebtoken.SignatureAlgorithm;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class KeyUtil {
private KeyUtil() {
}
private static final Map<String, Key> signingKeyStore = new ConcurrentHashMap<>();
private static final Map<String, Key> parseKeyStore = new ConcurrentHashMap<>();
public static Key parseKey(SignatureAlgorithm signatureAlgorithm) {
Key key = null;
if (parseKeyStore.containsKey(signatureAlgorithm.name())) {
key = parseKeyStore.get(signatureAlgorithm.name());
}
if (key != null) return key;
try {
String content;
PemReader pemReader;
X509EncodedKeySpec spec;
KeyFactory kf;
switch (signatureAlgorithm) {
case HS256:
content = StreamUtils.copyToString(new ClassPathResource("jwt/hs256/secret.key").getInputStream(), StandardCharsets.UTF_8);
key = new SecretKeySpec(DatatypeConverter.parseBase64Binary(content), signatureAlgorithm.getJcaName());
break;
case RS256:
content = StreamUtils.copyToString(new ClassPathResource("jwt/rs256/rsa-public.pem").getInputStream(), StandardCharsets.UTF_8);
pemReader = new PemReader(new StringReader(content));
spec = new X509EncodedKeySpec(pemReader.readPemObject().getContent());
kf = KeyFactory.getInstance("RSA");
key = kf.generatePublic(spec);
break;
case ES256:
content = StreamUtils.copyToString(new ClassPathResource("jwt/es256/ec-public.pem").getInputStream(), StandardCharsets.UTF_8);
pemReader = new PemReader(new StringReader(content));
spec = new X509EncodedKeySpec(pemReader.readPemObject().getContent());
kf = KeyFactory.getInstance("EC"); // Elliptic Curve
key = kf.generatePublic(spec);
break;
default:
throw new UnsupportedOperationException("Only support HS256, RS256, ES256");
}
} catch (Exception e) {
e.printStackTrace();
}
parseKeyStore.put(signatureAlgorithm.name(), key);
return key;
}
public static Key signingKey(SignatureAlgorithm signatureAlgorithm) {
Key key = null;
if (signingKeyStore.containsKey(signatureAlgorithm.name())) {
key = signingKeyStore.get(signatureAlgorithm.name());
}
if (key != null) return key;
try {
String content;
PemReader pemReader;
PKCS8EncodedKeySpec spec;
KeyFactory kf;
switch (signatureAlgorithm) {
case HS256:
content = StreamUtils.copyToString(new ClassPathResource("jwt/hs256/secret.key").getInputStream(), StandardCharsets.UTF_8);
key = new SecretKeySpec(DatatypeConverter.parseBase64Binary(content), signatureAlgorithm.getJcaName());
break;
case RS256:
content = StreamUtils.copyToString(new ClassPathResource("jwt/rs256/rsa-private.pem").getInputStream(), StandardCharsets.UTF_8);
pemReader = new PemReader(new StringReader(content));
spec = new PKCS8EncodedKeySpec(pemReader.readPemObject().getContent());
kf = KeyFactory.getInstance("RSA");
key = kf.generatePrivate(spec);
break;
case ES256:
content = StreamUtils.copyToString(new ClassPathResource("jwt/es256/ec-private.pkcs8").getInputStream(), StandardCharsets.UTF_8);
pemReader = new PemReader(new StringReader(content));
spec = new PKCS8EncodedKeySpec(pemReader.readPemObject().getContent());
kf = KeyFactory.getInstance("EC"); // Elliptic Curve
key = kf.generatePrivate(spec);
break;
default:
throw new UnsupportedOperationException("Only support HS256, RS256, ES256");
}
} catch (Exception e) {
e.printStackTrace();
}
signingKeyStore.put(signatureAlgorithm.name(), key);
return key;
}
}
package com.example.crypto.util;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.jsonwebtoken.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.time.ZonedDateTime;
import java.util.*;
public class JwtUtil {
private static final String REGEX = "^[A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+/=]*$";
private static final Gson gson = new Gson();
private static final Type hashMapType = new TypeToken<HashMap<String, Object>>() {
}.getType();
private JwtUtil() {
}
public static boolean isJwt(String token) {
return token != null && !token.isBlank() && token.matches(REGEX);
}
public static boolean isValid(String token) {
if (!isJwt(token)) return false;
return isValid(token, alg(token));
}
public static boolean isValid(String token, String alg) {
Key publicKey = KeyUtil.signingKey(SignatureAlgorithm.forName(alg));
JwtParser parser = Jwts.parserBuilder().setSigningKey(publicKey).build();
return parser.isSigned(token);
}
private static String alg(String token) {
if (!isJwt(token)) return null;
String[] chunks = token.split("\\.");
String header = new String(Base64.getUrlDecoder().decode(chunks[0]), StandardCharsets.UTF_8);
Map<String, Object> headerMap = gson.fromJson(header, hashMapType);
return (String) headerMap.get("alg");
}
public static String generate(String subject, long expires, Map<String, Object> claims, SignatureAlgorithm signatureAlgorithm) {
long issuedAt = ZonedDateTime.now(TimeZone.getTimeZone("UTC").toZoneId()).toInstant().toEpochMilli();
long expiration = issuedAt + expires;
try {
Key signingKey = KeyUtil.signingKey(signatureAlgorithm);
if (signingKey == null) throw new JwtException("Not found signingKey for " + signatureAlgorithm.name());
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setIssuer("JWT Sample Issuer")
.setSubject(subject)
.setIssuedAt(new Date(issuedAt))
.setExpiration(new Date(expiration))
.signWith(signingKey, signatureAlgorithm)
.addClaims(claims)
.compact();
} catch (Exception e) {
e.printStackTrace();
}
throw new UnsupportedJwtException("Cannot generate jwt");
}
public static Jws<Claims> parseClaims(String token) {
return parseClaims(token, alg(token));
}
public static Jws<Claims> parseClaims(String token, String alg) {
Key publicKey = KeyUtil.signingKey(SignatureAlgorithm.forName(alg));
JwtParser parser = Jwts.parserBuilder().setSigningKey(publicKey).build();
return parser.parseClaimsJws(token);
}
}
Add Token Filter in SecurityFilterChain
์์คํ ์์ ๋ฐ๊ธ๋ ํ ํฐ์ด ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋์์ ๋ JWT๋ฅผ ๊ฒ์ฆํ๊ณ ์ฌ์ฉ์ ์ธ์ฆ์ ์ฒ๋ฆฌํ ์ ์๋ ํํฐ๋ฅผ ์์ฑํ๊ณ ์คํ๋ง ์ํ๋ฆฌํฐ ํํฐ ์ฒด์ธ์ ๋ฑ๋กํฉ๋๋ค.
TokenAuthenticationFilter.java
package com.example.crypto.filter;
import com.example.crypto.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private static final String ADDITIONAL_URI_PATTERN = "/users/{username}";
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final UserDetailsService userDetailsService;
public TokenAuthenticationFilter(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getParameter("token");
if (JwtUtil.isJwt(token)) {
String requestURI = request.getRequestURI();
String base64RequestURI = Base64.getUrlEncoder().encodeToString(requestURI.getBytes(StandardCharsets.UTF_8));
String redirectLoginUrl = "/login?redirect=" + base64RequestURI;
boolean isValidToken;
try {
isValidToken = JwtUtil.isValid(token);
if (isValidToken) {
Jws<Claims> claims = JwtUtil.parseClaims(token);
Claims body = claims.getBody();
String subject = body.getSubject();
if (pathMatcher.match(ADDITIONAL_URI_PATTERN, requestURI)) {
isValidToken = isValidWithVariables(body, requestURI);
}
if (isValidToken) {
UserDetails userDetails = userDetailsService.loadUserByUsername(subject);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
response.sendRedirect(requestURI);
}
}
} catch (Exception e) {
if (e instanceof JwtException) {
e.printStackTrace();
}
isValidToken = false;
}
if (!isValidToken) {
// NOTE: If token expired or invalid, redirect for login page.
response.sendRedirect(redirectLoginUrl);
}
} else {
filterChain.doFilter(request, response);
}
}
private boolean isValidWithVariables(Claims body, String requestURI) {
Map<String, String> variables = pathMatcher.extractUriTemplateVariables(ADDITIONAL_URI_PATTERN, requestURI);
String userId = body.get("username", String.class);
return variables.containsKey("username") && variables.get("username").equals(userId);
}
}
SecurityConfig.java
๊ฐ๋จํ ์ํ์ด๋ฏ๋ก ์ธ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ์๋ฅผ ๋ฑ๋กํ๋ ์คํ๋ง ์ํ๋ฆฌํฐ ํ๊ฒฝ์ ๊ตฌ์ฑํฉ๋๋ค.
package com.example.crypto.config;
import com.example.crypto.filter.TokenAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
// https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/in-memory.html
@Bean
public UserDetailsService users() {
UserDetails mambo = User.builder()
.username("mambo")
.password("{noop}1234")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(mambo);
}
// https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-httpsecurity
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, TokenAuthenticationFilter tokenAuthenticationFilter) throws Exception {
http.formLogin()
.and().authorizeRequests().antMatchers("/users/{username}/**").authenticated()
.and().addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Generate JWT Test
ํ ์คํธ ์ฝ๋๋ฅผ ํตํด์ ํ ํฐ์ ๋ฐ๊ธํด๋ณด๊ณ ์ด๋ฉ์ผ์ ํฌํจ๋ ๋งํฌ์ฒ๋ผ ํ ํฐ ํ๋ผ๋ฏธํฐ์ ํ ํฐ์ ํฌํจํ์ฌ ๋ธ๋ผ์ฐ์ ์ฃผ์์ ์ง์ ์ ๋ ฅํด๋ด ๋๋ค.
@Slf4j
class JwtUtilTest {
@Test
void generateToken() {
Assertions.assertDoesNotThrow(() -> {
String subject = "mambo";
long expires = Duration.ofMinutes(3L).toMillis();
Map<String, Object> claims = new HashMap<>();
claims.put("username", "mambo");
String jwt = JwtUtil.generate(subject, expires, claims, SignatureAlgorithm.ES256);
log.info("\nmambo.kr:8080/users/mambo?token={}", jwt);
});
}
}
ํ ํฐ์ด ๋ฐ๊ธ๋๊ณ ๋์ 3๋ถ ์ด๋ด์๋ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๊ฐ ๋จ์ ํ์ธํ ์ ์์ง๋ง ๋ง๋ฃ๋ ์ดํ๋ผ๋ฉด ์ฌ๋ฐ๋ฅด๊ฒ ์๋ช ๋ ํ ํฐ์ผ์ง๋ผ๋ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋๋ ์ ๋จ์ ํ์ธํ ์ ์์ต๋๋ค.
๋ณธ ๊ธ์์๋ JWS์ผ๋ก๋ง ๊ตฌ์ฑ๋ JWT ํ ํฐ์ ๋ฐ๊ธํ์ฌ ์ ๋ขฐํ ์ ์๋ ๋ฐ๊ธ์๋ก๋ถํฐ ์๋ช ๋ ๊ฒ์์ ์ฆ๋ช ํ๋ ๊ฒ์ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์ ์ธ์ฆ์ ์ํํ์ต๋๋ค. ๋ค๋ง, JWT๋ ์ํธํ๋์ง ์์ ์ํ๋ก ์ด๋ฉ์ผ๊ณผ ๊ฐ์ ๊ณณ์ ๋ ธ์ถ๋์ด์์ผ๋ฏ๋ก ์งง์ ๋ง๋ฃ ์๊ฐ์ ๋์ด ์ ํ ์๊ฐ๋ด์๋ง ์ธ์ฆํ ์ ์๋๋ก ํ์์ต๋๋ค.
์ข ๋ ์๊ฐ์ด ์ฃผ์ด์ง๋ค๋ฉด JWS์ ํจ๊ป JWE๋ก ํด๋ ์ ํ์ด๋ก๋๊ฐ ์ํธํ๋ JWT ํ ํฐ์ ๋ฐ๊ธํด๋ณผ ์ ์๋ ๊ฒ๋ ์์๋ด์ผํ ๊ฒ ๊ฐ์ต๋๋ค. ๋ณธ ๊ธ์์ ์ฌ์ฉํ๋ jjwt ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ JWE๋ฅผ ์ง์ํ์ง ์์ผ๋ฏ๋ก Nimbus JOSE + JWT๋ก ๋์ฒดํด์ผ ํฉ๋๋ค.