로그인 이후 생성된 JWT 토큰을 검증하는 필터를 제작합니다.
해당 필터는 토큰이 있는지> 토큰의 프로토콜이 잘 지켜졌는지 등을 검사하고,
브라우저의 요청을 허가해주는 필터입니다.
JwtAuthorizationFilter
는 Filter를 상속 받고 있습니다.생성을 하면
doFilter를 만들어줘야 합니다.
해당 doFilter는 사용자가 토큰 검증이 필요한 요청을 할 때, 실행이 됩니다.
doFilter는 ServletRequest랑 Response, FilterChain을 받아서 사용합니다.
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("debug: JWT Authorization Filter 작동됨.............");
// 1. 서블릿 리퀘스트 + 리스폰스 HttpServlet으로 변경필요
// 2. 토큰 리퀘스트에서 꺼내기
// 3. 꺼낸 토큰 null인지 확인하기
// 4. 토큰의 형식이 올바른지 확인하기
// 5. 정상적인 토큰이면 jwtUtil의 검증기능 사용하기
// 6. FilterChain에서 doFilter를 사용하기
}
위의 절차대로 필터가 진행됩니다.
1번, 2번 구현
1번과 2번을 구현하겠습니다.
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("debug: JWT Authorization Filter 작동됨.............");
// 1. 서블릿 리퀘스트 + 리스폰스 HttpServlet으로 변경필요
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 2. 토큰 리퀘스트에서 꺼내기
String jwt = request.getHeader("Authorization");
// 3. 꺼낸 토큰 null인지 확인하기
// 4. 토큰의 형식이 올바른지 확인하기
// 5. 정상적인 토큰이면 jwtUtil의 검증기능 사용하기
// 6. FilterChain에서 doFilter를 사용하기
}
- Servlet들을 HttpServlet으로 변경시켜줍니다.
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
- 요청 헤더에 담겨져 있는 토큰을 꺼냅니다.
String jwt = request.getHeader("Authorization");
이후 꺼낸 토큰 String jwt를 가지고 검증하는 절차를 구현해야 합니다.
3번, 4번 검증 구현
3번과 4번을 구현하겠습니다.
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("debug: JWT Authorization Filter 작동됨.............");
// 1. 서블릿 리퀘스트 + 리스폰스 HttpServlet으로 변경필요
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 2. 토큰 리퀘스트에서 꺼내기
String jwt = request.getHeader("Authorization");
// 3. 꺼낸 토큰 null인지 확인하기
if (jwt == null){
onError(response,"토큰없음..");
return;
}
// 4. 토큰의 형식이 올바른지 확인하기
if (!jwt.startsWith("Bearer ")){
onError(response,"프로토콜 잘못됨");
return;
}
// 5. 정상적인 토큰이면 jwtUtil의 검증기능 사용하기
// 6. FilterChain에서 doFilter를 사용하기
}
3번과 4번을 검증할 때 잘못된 상태의 경우 Response로 오류가 있다는 것을 보내줘야 하기 때문에
onError를 만들어줘서 응답을 보냅니다.
private void onError(HttpServletResponse response, String msg){
try {
String responseBody = new ObjectMapper().writeValueAsString(Resp.fail(msg));
response.setStatus(401);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(responseBody);
}catch (Exception e){
e.printStackTrace();
}
}
onError는 HttpServletResponse를 사용하여
오류 status와 응답 보낼 contentType을 지정해서 보내 줄 수 있습니다.
onError를 구현한 이유
이렇게 구현한 이유로는
해당 필터는 spring내부에서 진행되는 것이 아닌
Spring에 진입하기 전에 필터가 작동되어
Exception을 직접 구현해주어야 하기 때문입니다.
5. 정상적인 토큰이면 jwtUtil의 검증 기능 구현하기
이제 정상적인 토큰일 경우에 처리하는 기능을 구현합니다.
해당 작성 중인 필터 클래스와 별개로
정상적인 토큰을 검증하는 기능은 JwtUtil을 통해 구현합니다.
JwtUtil
public LoginUser verify(String jwt)
throws SignatureVerificationException, TokenExpiredException, JWTDecodeException {
jwt = jwt.replace("Bearer ", "");
// JWT를 검증한 후, 검증이 완료되면, header, payload를 base64로 복호화함.
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(secret))
.build().verify(jwt);
int id = decodedJWT.getClaim("id").asInt();
String username = decodedJWT.getClaim("username").asString();
return new LoginUser(id, username, jwt);
}
토큰을 확인하고, 토큰 안에 있는 사용자id, username, jwt를 LoginUser 객체로 만들어 줍니다.
이 LoginUser를 session에 담아 줘야합니다.
JwtAuthorizationFilter
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("debug: JWT Authorization Filter 작동됨.............");
// 1. 서블릿 리퀘스트 + 리스폰스 HttpServlet으로 변경필요
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 2. 토큰 리퀘스트에서 꺼내기
String jwt = request.getHeader("Authorization");
// 3. 꺼낸 토큰 null인지 확인하기
if (jwt == null){
onError(response,"토큰없음..");
return;
}
// 4. 토큰의 형식이 올바른지 확인하기
if (!jwt.startsWith("Bearer ")){
onError(response,"프로토콜 잘못됨");
return;
}
// 5. 정상적인 토큰이면 jwtUtil의 검증기능 사용하기
LoginUser loginUser = jwtUtil.verify(jwt);
// 5-2. 검증된 토큰의 사용자 정보를 Session에 넣어주기
HttpSession httpSession = request.getSession();
httpSession.setAttribute("sessionUser", loginUser);
// 6. FilterChain에서 doFilter를 사용하기
chain.doFilter(request,response);
}
이제 만들어진 Filter를 필터Config에 등록을 해주면
필터가 만들어지고, 작동이 됩니다.
FilterConfig
필터Config에 등록
package com.metacoding.restserver._core.config;
import com.metacoding.restserver._core.filter.CorsFilter;
import com.metacoding.restserver._core.filter.JwtAuthorizationFilter;
import com.metacoding.restserver._core.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class FilterConfig {
private final JwtUtil jwtUtil;
@Bean
public FilterRegistrationBean<JwtAuthorizationFilter> jwtAuthorizationFilter() {
System.out.println("jwtAuthorizationFilter ON~~~~~~~~~~~");
FilterRegistrationBean<JwtAuthorizationFilter> bean = new FilterRegistrationBean<>(new JwtAuthorizationFilter(jwtUtil));
bean.addUrlPatterns("/api/*"); // * 하나만 써야됨.
bean.setOrder(1); // 낮은 번호부터 실행됨.
return bean;
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
System.out.println("corsFilter ON~~~~~~~~~~~");
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter());
bean.addUrlPatterns("/*"); // * 하나만 써야됨.
bean.setOrder(0); // 낮은 번호부터 실행됨.
return bean;
}
}
전체 코드
JwtAuthorizationFilter
package com.metacoding.restserver._core.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.metacoding.restserver._core.auth.LoginUser;
import com.metacoding.restserver._core.util.JwtUtil;
import com.metacoding.restserver._core.util.Resp;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional;
@RequiredArgsConstructor
public class JwtAuthorizationFilter implements Filter {
// ObjectMapper objectMapper = new ObjectMapper();
private final JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("debug: JWT Authorization Filter 작동됨.............");
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String jwt = request.getHeader("Authorization");
if (jwt == null){
onError(response,"토큰없음..");
return;
}
if (!jwt.startsWith("Bearer ")){
onError(response,"프로토콜 잘못됨");
return;
}
LoginUser loginUser = jwtUtil.verify(jwt);
HttpSession httpSession = request.getSession();
httpSession.setAttribute("sessionUser", loginUser);
chain.doFilter(request,response);
}
private void onError(HttpServletResponse response, String msg){
try {
String responseBody = new ObjectMapper().writeValueAsString(Resp.fail(msg));
response.setStatus(401);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(responseBody);
}catch (Exception e){
e.printStackTrace();
}
}
}
JwtUtil
package com.metacoding.restserver._core.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.metacoding.restserver._core.auth.LoginUser;
import com.metacoding.restserver.user.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Component
public class JwtUtil {
// 컴퍼넌트 스캔시에 @Value가 발동하고, 해당 값은 application.properties에서 가져온다.
@Value("${var.jwt.secret}")
private String secret;
public final Long EXPIRATION_TIME = 1000*60*60*24*2L;
public String create(User user) {
String jwt = JWT.create()
.withSubject("title")
.withClaim("id", user.getId())
.withClaim("username", user.getUsername())
.withExpiresAt(Instant.now().plusMillis(EXPIRATION_TIME))
.sign(Algorithm.HMAC512(secret));
return "Bearer " + jwt;
}
public LoginUser verify(String jwt)
throws SignatureVerificationException, TokenExpiredException, JWTDecodeException {
jwt = jwt.replace("Bearer ", "");
// JWT를 검증한 후, 검증이 완료되면, header, payload를 base64로 복호화함.
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(secret))
.build().verify(jwt);
int id = decodedJWT.getClaim("id").asInt();
String username = decodedJWT.getClaim("username").asString();
return new LoginUser(id, username, jwt);
}
}
Share article