Spring 프로젝트 시작하기 - 보안 ) Jwt Token 쿠키 검증

지난시간엔 Jwt Token을 발급해보았다.

 

그렇다면 실제 API를 던졌을때 Token을 확인하고 실제 유저인지 판별해보는 로직을 만들어보겠다.

 

CookieParser

@Slf4j
public class CookieParser {
	/**
	 * 쿠키에서 특정 key값을 가진 value를 가져온다.
	 * @param cookies 쿠키에 담긴 key,value 목록
	 * @param key key
	 * @return value or exception
	 */
	public static String getCookieValue(Cookie[] cookies, String key) {
		try (Stream<Cookie> stream = Arrays.stream(cookies)) {
			return stream.filter(cookie -> cookie.getName().equals(key))
					.map(Cookie::getValue)
					.findAny()
					.orElseThrow(() -> {
						log.error("getCookieValue() : 유효하지 않은 쿠키");
						return new BadRequestException(ServerResponse.EXPIRE_TOKEN);
					});
		} catch (Exception e) {
			throw e;
		}
	}
}

특정한 key값을 파라미터로 받아, Cookie에 해당 키값의 데이터가 있는지 확인하는 로직이다.

 

JwtProvider

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {
    private JwtParser jwtParser;

    private final MemberRepository memberRepository;

    @Value("${jwt.access.name}")
    private String TOKEN_COOKIENAME;

    @Value("${jwt.access.secret-key}")
    private String accessTokenSecret;

    @Value("${jwt.access.expiration-time}")
    private long accessExpirationTime;

    /**
     * JWT Signature 유효성 검증을 위한 Parser 설정
     * 시크릿 키 : config
     */
    @PostConstruct
    public void setJwtParser() {
        jwtParser = Jwts.parser().setSigningKey(accessTokenSecret.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * jwt access token 을 secret key 로 복호화를 진행하고 유저에 대한
     * 유효성을 검증한다.
     *
     * @param request
     */
    public JwtTokenDto checkAccessToken(HttpServletRequest request) {
        // 쿠키 정보를 가져온다.
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            // 쿠키가 존재하지 않을시 오류 리턴
            throw new BadRequestException(ServerResponse.NO_ACCESS_TOKEN);
        }
        try {
            // TOKEN_COOKIENAME으로 저장된 Token을 가져온다.
            String accessToken = CookieParser.getCookieValue(cookies, TOKEN_COOKIENAME);
            Claims body = jwtParser.parseClaimsJws(accessToken).getBody();

            // PayLoad 정보로 사용자 유효성 검증
            JwtTokenDto tokenDto = JwtTokenDto.builder()
                    .cIdx(Long.parseLong(body.get("cIdx").toString()))
                    .uId(body.get("uId").toString())
                    .uIdx(Long.parseLong(body.get("uIdx").toString()))
                    .build();
                    
            MemberEntity user = memberRepository.findMemberEntityByUserIdx(tokenDto.getUIdx());
            if (user == null) {
                throw new BadRequestException(ServerResponse.INVALID_TOKEN);
            }
            
            return tokenDto;
        } catch (Exception e) {
            throw new BadRequestException(ServerResponse.INVALID_TOKEN);
        }
    }
	... 생략 ...
}

Token을 판별하는 로직도 생성한다.

1. setJwtParser를 정의하고

2. Token을 파싱한다.

3. Token에 있는 정보로 실제 존재하는 유저인지 데이터를 확인한다.

4. 존재하지 않으면 오류를 리턴한다.

 

이제 API가 지나가는 길에 판별하는 로직을 거치고 가도록 셋팅을 해주면 된다.

 

TokenAuthInterceptor

@Slf4j
@Component
public class TokenAuthInterceptor implements HandlerInterceptor {
    public static final String USER_ID = "X-USER-ID";
    private final JwtProvider jwtProvider;

    public TokenAuthInterceptor(JwtProvider jwtProvider) {
        this.jwtProvider = jwtProvider;
    }

    /**
     * 컨트롤러에 도착하기 직전 정보를 반환한다.
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //어노테이션 체크 - Controller에 @WithOutAuth 어노테이션이 있는지 확인
        try {
            JwtTokenDto tokenUser = jwtProvider.checkAccessToken(request);
            request.setAttribute("cIdx", tokenUser.getCIdx());
            request.setAttribute("uIdx", tokenUser.getUIdx());
            request.setAttribute("uId", tokenUser.getUId());
            MDC.put(USER_ID, tokenUser.getUId()); // 로그 출력시에 USER_ID값을 사용할 수 있도록 저장한다.
        } catch (Exception e) {
            throw new BadRequestException(ServerResponse.EXPIRE_TOKEN);
        }
        return true;
    }
}

HandlerInterceptor를 상속해서 preHandle을 오버라이드하면 Controller에서 로직을 처리하기 전에 Token을 검증할 수 있다.

request에 넣어서 Controller단에서도 Token 정보를 쓸수 있도록 해주었다.

 

WebConfiguration

@Configuration
@RequiredArgsConstructor
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenAuthInterceptor);
    }
}

이제 TokenAuthInterceptor를 쓸수 있도록 WebConfiguration을 만들어서 addInterceptor 해주면 끝이다.

 

테스트

Cookie가 없는 상태로 API를 호출했을때, 토큰이 없다는 오류를 정상적으로 리턴하는것이 확인된다.