패스트캠퍼스 챌린지

패스트캠퍼스 환급 챌린지 30일차 : 9개 도메인 프로젝트로 끝내는 백엔드 웹 개발 (Java/Spring) 초격차 패키지 Online 강의 후기

ssoonearth 2025. 4. 30. 13:48

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

https://abit.ly/lisbva

 

 

테스트 코드 구현 작업 화면

 

 

 

패스트캠퍼스의 '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에서 컨트롤러 메서드의 인자를 커스터마이징해서 주입할 수 있게 해주는 인터페이스로, 사용자 인증 정보나 기타 공통 파라미터를 자동으로 바인딩하는 데 유용하게 사용된다.

 

 

 

토큰 기반의 로그인은 뭔가 구현하기 어려울 것 같은 느낌이 있었는데, 실제로 해보니까 오히려 세션 기반 로그인 기능을 구현할 때보다 훨씬 구조적으로 구현을 할 수 있었던 것 같다.