본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.




패스트캠퍼스의 '9개 도메인 프로젝트로 끝내는 백엔드 웹 개발 (Java/Spring) 초격차 패키지 Online' 강의 수강 30일차!
오늘은 커뮤니티 피드 서비스의 로그인 기능을 구현하는 작업을 진행하였다.
jwt 라이브러리에서 제공하는 SecretKey 클래스를 사용하였다.
private final SecretKey key;
public TokenProvider(String secretKey) {
this.key = Keys.hmacShaKeyFor(secretKey.getBytes());
}
public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException {
if (bytes == null) {
throw new InvalidKeyException("SecretKey byte array cannot be null.");
}
int bitLength = bytes.length * 8;
//Purposefully ordered higher to lower to ensure the strongest key possible can be generated.
if (bitLength >= 512) {
return new SecretKeySpec(bytes, "HmacSHA512");
} else if (bitLength >= 384) {
return new SecretKeySpec(bytes, "HmacSHA384");
} else if (bitLength >= 256) {
return new SecretKeySpec(bytes, "HmacSHA256");
}
String msg = "The specified key byte array is " + bitLength + " bits which " +
"is not secure enough for any JWT HMAC-SHA algorithm. The JWT " +
"JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " +
"size >= 256 bits (the key size must be greater than or equal to the hash " +
"output size). Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() " +
"or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA " +
"algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.";
throw new WeakKeyException(msg);
}
위 코드는 jwt에서 제공하는 hmacShaKeyFor 라는 메소드로, 비밀키를 생성하는 역할을 한다.
코드에서 확인할 수 있다시피 최소 256비트 이상이어야 비밀키 생성이 가능하고, 그것보다 짧다면 보안상의 문제가 생길 수 있어 WeakKeyException을 발생시키게 된다.
컨트롤러 메서드에 로그인한 사용자 정보를 자동으로 주입해주기 위한 메소드 Resolver를 만들었다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthPrincipal {
}
먼저, 사용할 어노테이션을 직접 생성하였다.
public class AuthPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
private final TokenProvider tokenProvider;
public AuthPrincipalArgumentResolver(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthPrincipal.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
try {
String authToken = webRequest.getHeader("Authorization");
if (authToken == null || authToken.split(" ").length != 2) {
throw new IllegalArgumentException();
}
// Bearer 토큰값
String token = authToken.split(" ")[1];
Long userId = tokenProvider.getUserId(token);
String role = tokenProvider.getUserRole(token);
return new UserPrincipal(userId, role);
} catch (Exception e) {
throw new IllegalArgumentException("올바르지 않은 토큰입니다.");
}
}
}
@AuthPrincipal이라는 커스텀 어노테이션이 붙은 파라미터에 대해 JWT 토큰을 파싱해서 사용자 정보를 넣어주는 로직을 구현하였다.
클라이언트가 요청을 보낼 때, 'Authorization: Bearer asefasdflads(토큰값)' 과 같은 형식으로 http 헤더를 보내게 된다.
여기에서 Bearer라는 접두사를 제외하고 토큰값만 가져오기 위해 split()을 사용하였다.
HandlerMethodArgumentResolver는 Spring MVC에서 컨트롤러 메서드의 인자를 커스터마이징해서 주입할 수 있게 해주는 인터페이스로, 사용자 인증 정보나 기타 공통 파라미터를 자동으로 바인딩하는 데 유용하게 사용된다.
토큰 기반의 로그인은 뭔가 구현하기 어려울 것 같은 느낌이 있었는데, 실제로 해보니까 오히려 세션 기반 로그인 기능을 구현할 때보다 훨씬 구조적으로 구현을 할 수 있었던 것 같다.