- FilterConfig
- 인증에 사용할 AuthorizationFilter를 Filter에 삽입
@Bean
public FilterRegistrationBean<AuthorizationFilter> authorizationFilter() {
FilterRegistrationBean<AuthorizationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new AuthorizationFilter());
registrationBean.addUrlPatterns("/s/*"); // 모든 요청에 적용
registrationBean.setOrder(2); // 필터 순서 설정
return registrationBean;
}
- AuthorizationFilter
- 요청의 Header에서 ‘Authorization’의 Token을 확인
- 토큰이 없거나, 만료기간이 지났거나, 프로토콜을 준수하지 않았거나, 값이 일치하지 않을 경우 오류 발생
- Status : 401 (Unauthorized)
package shop.mtcoding.blog._core.filter;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import shop.mtcoding.blog._core.util.JwtUtil;
import shop.mtcoding.blog._core.util.Resp;
import shop.mtcoding.blog.user.User;
import java.io.IOException;
import java.io.PrintWriter;
public class AuthorizationFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String accessToken = request.getHeader("Authorization");
try {
if (accessToken == null || accessToken.isBlank()) throw new RuntimeException("토큰을 전달해주세요");
if (!accessToken.startsWith("Bearer ")) throw new RuntimeException("Bearer 프로토콜 지켜야지 짜식아");
accessToken = accessToken.replace("Bearer ", "");
User user = JwtUtil.verify(accessToken);
// 토큰 재검증 회피를 위한 임시 저장용 session
HttpSession session = request.getSession();
session.setAttribute("sessionUser", user);
chain.doFilter(request, response);
} catch (TokenExpiredException e1) {
e1.printStackTrace();
exResponse(response, "토큰이 만료되었습니다");
} catch (JWTDecodeException | SignatureVerificationException e2) {
e2.printStackTrace();
exResponse(response, "토큰 검증에 실패했어요");
} catch (RuntimeException e3) {
e3.printStackTrace();
exResponse(response, e3.getMessage());
}
}
private void exResponse(HttpServletResponse response, String msg) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(401);
PrintWriter out = response.getWriter();
Resp<?> resp = Resp.fail(401, msg);
String responseBody = new ObjectMapper().writeValueAsString(resp);
out.println(responseBody);
}
}
- GlobalExceptionHandler
- REST API → 기존 Exception을 전부 삭제하고, 데이터를 전송하는 ExceptionApi만 남김
- 실제 상태 코드도 바꿔서 돌려줘야 하므로, ResponseEntity 사용
- body, status code를 받음
package shop.mtcoding.blog._core.error;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import shop.mtcoding.blog._core.error.ex.ExceptionApi400;
import shop.mtcoding.blog._core.error.ex.ExceptionApi401;
import shop.mtcoding.blog._core.error.ex.ExceptionApi403;
import shop.mtcoding.blog._core.error.ex.ExceptionApi404;
import shop.mtcoding.blog._core.util.Resp;
@RestControllerAdvice // @ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ExceptionApi400.class)
public ResponseEntity<?> exApi400(ExceptionApi400 e) {
Resp<?> resp = Resp.fail(400, e.getMessage());
return new ResponseEntity<>(resp, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(ExceptionApi401.class)
public ResponseEntity<?> exApi401(ExceptionApi401 e) {
Resp<?> resp = Resp.fail(401, e.getMessage());
return new ResponseEntity<>(resp, HttpStatus.UNAUTHORIZED);
}
@ExceptionHandler(ExceptionApi403.class)
public ResponseEntity<?> exApi403(ExceptionApi403 e) {
Resp<?> resp = Resp.fail(403, e.getMessage());
return new ResponseEntity<>(resp, HttpStatus.FORBIDDEN);
}
@ExceptionHandler(ExceptionApi404.class)
public ResponseEntity<?> exApi404(ExceptionApi404 e) {
Resp<?> resp = Resp.fail(404, e.getMessage());
return new ResponseEntity<>(resp, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> exUnKnown(Exception e) {
System.out.println("관리자님 보세요 : " + e.getMessage()); // 로그를 파일에 기록해서 나중에 봐야함
Resp<?> resp = Resp.fail(500, "관리자에게 문의하세요");
return new ResponseEntity<>(resp, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
- JwtUtil
- JWT 생성 함수 : create(User)
- withSubject : 토큰 사용처 (제목)
- withExpiresAt : 유효 시간 (LocalDate 타입)
- withClaim : 포함할 내용 (Payload, 여러 개 추가 가능)
- sign : 암호화 기법을 고르고, 암호화에 사용할 Secret Key 작성
- JWT 인증 함수 : verify(JWT)
- JWT.require : Secret Key를 포함한 암호화 기법을 사용하여 전달 받은 JWT를 복호화
- 복호화한 JWT에 담겨있는 정보만을 가지고 Fake User를 만들어 넘겨줘야 함
- Token은 개인 정보를 담아선 안됨 (누구나 열람이 가능)
package shop.mtcoding.blog._core.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import shop.mtcoding.blog.user.User;
import java.util.Date;
public class JwtUtil {
public static String create(User user) {
String jwt = JWT.create()
.withSubject("blog")
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
.withClaim("id", user.getId())
.withClaim("username", user.getUsername())
.sign(Algorithm.HMAC512("metacoding"));
return jwt;
}
public static User verify(String jwt) {
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512("metacoding")).build().verify(jwt);
int id = decodedJWT.getClaim("id").asInt();
String username = decodedJWT.getClaim("username").asString();
return User.builder()
.id(id)
.username(username)
.build();
}
}
- UserService
public UserResponse.TokenDTO 로그인(UserRequest.LoginDTO loginDTO) {
User userPS = userRepository.findByUsername(loginDTO.getUsername())
.orElseThrow(() -> new ExceptionApi401("유저네임 혹은 비밀번호가 틀렸습니다"));
boolean isSame = BCrypt.checkpw(loginDTO.getPassword(), userPS.getPassword());
if (!isSame) throw new ExceptionApi401("유저네임 혹은 비밀번호가 틀렸습니다.");
// 토큰 생성
String accessToken = JwtUtil.create(userPS);
return UserResponse.TokenDTO.builder().accessToken(accessToken).build();
}
- 각 Service
- 인증이 필요한 경우의 인터셉터 교체 (인증은 필터에서 진행)
결과
- 로그인을 하여 새 토큰 발행 후 댓글 삭제

성공

실패
- 토큰이 없을 때

- 토큰 프로토콜이 올바르지 않을 때

Share article