제목에 대한 내용들을 처음 코드로 구현한다.
따라서 느끼는 점들과 새롭게 알게된 내용들을 따끈따끈하게 기록하려 한다.
우선, AuthController 를 통해 Cookie, Session, JWT 를 생성 및 조회 할 수 있도록 관리하고
JwtUtil 에서 JWT 에 관한 기능 수행을 담당하도록 구현한 코드로 연습한다.
각 기능별로 어떤 궁금증이 있었고, 어떻게 결론을 내렸는지 사고의 흐름대로 작성할 예정이다.
@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 아님 ? 이런 생각이 들었다.
그럼 createCookie 가 실행되고 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 객체를 만들기 위해선 Name : Value 형태로 값이 들어가야 생성된다.@GetMapping("/get-cookie")
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
System.out.println("value = " + value);
return "getCookie : " + value;
}
@CookieValue 어노테이션으로 해당 쿠키의 Name 값을 넣어줘야 한다. @GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
HttpSession session = req.getSession(true); // true -> 세션 생성 또는 이미 존재한다면 세션 반환
session.setAttribute(AUTHORIZATION_HEADER, "Moon Auth");
return "createSession";
}
Session 이 생성될 때는 ServletReqeust 를 활용한다. 서버에 해당 내용이 저장되어야 하기 때문.
@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;
}
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;
}
UserRoleEnum.USER 은 Enum 값으로 ROLE_USER 를 반환한다.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();
}
따라서 JwtUtil 클래스에 대한 의존성 주입이 완료된 후에 init() 메서드가 실행되며 key 에 미리 설정한 secretKey 를 활용하여 Base64로 디코딩한 값을 넣는다.
해당 키는 한번만 수행되어 초기화 되기에 매번 새롭게 생성되지 않는다.
그리고 이제 createToken 메서드를 살펴보면,
BEARER_PREFIX String 값을 맨 앞에 넣으면서 시작한다. (암묵적 규칙이라함)
Jwts 라는 클래스에 한개씩 담기 시작하면 된다.
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());
}
}
@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();
}