🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [61]일차
🚀61일차에는 JWT 관련하여 인증되지 않은 사용자 접근 시 예외발생을 실습할 수 있었다.
학습 목표 : AccessToken이 없는 사용자의 경우 인증되지 않은 것으로 예외를 별도로 처리할 수 있어야함
학습 과정 : 회고를 통해 작성
Spring Security 인증
- 인증시 Spring Security에 맡기지 않고 JWT를 이용해서 인증을 구현했음
- 나머지 CORS나 CSRF 설정은 Spring Security가 자동으로 하도록 설정 (ex. disable())
GrantedAuthority
- 권한을 추상화해놓은 인터페이스
- 이 인터페이스를 구현한 객체가 SimpleGrantedAuthority
이는 권한 하나가 추상화된 객체를 의미
public class SimpleGrantedAuthority implements GrantedAuthority {
private final String role; // 역할(Role)을 저장하는 필드
public SimpleGrantedAuthority(String role) {
this.role = role;
}
@Override
public String getAuthority() {
return this.role; // 역할(Role) 반환
}
}
- SimpleGrantedAuthority의 구조로 구현되어있음
OncePerRequestFilter
- 요청 당 한번만 실행되도록 보장하는 필터
- AuthenticationFilter는 한번만 필터로 인증되면되므로 OncePerRequestFilter를 이용
예외 추가
- enum 형태로 Jwt의 예외들을 Enum 타입으로 가지고 있게함
UNKNOWN_ERROR("UNKNOWN_ERROR", "알 수 없는 오류"),
NOT_FOUND_TOKEN("NOT_FOUND_TOKEN", "Headers에 토큰 형식의 값 찾을 수 없음"),
INVALID_TOKEN("INVALID_TOKEN", "유효하지 않은 토큰"),
EXPIRED_TOKEN("EXPIRED_TOKEN", "기간이 만료된 토큰"),
UNSUPPORTED_TOKEN("UNSUPPORTED_TOKEN", "지원하지 않는 토큰");
private final String code; //값이 변하지 않도록 final
private final String message;
JwtExceptionCode(String code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() { //예외 메시지를 출력할 때 더 유용한 정보 제공.
return String.format("[%s] %s", code, message);
}
JwtAuthenticationFilter - 예외 추가
if(StringUtils.hasText(token)){
try{
Authentication authentication = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}catch (ExpiredJwtException e){ // 예외 추가 (JwtExceptionEnum으로부터)
request.setAttribute("exception", JwtExceptionCode.EXPIRED_TOKEN.getCode());
log.error("Expired Token : {}",token,e);
SecurityContextHolder.clearContext();
throw new BadCredentialsException("Expired token exception", e);
}
// ... 다른 예외들
}catch (Exception e){
log.error("JWT Filter - Internal Error: {}", token, e);
SecurityContextHolder.clearContext();
throw new BadCredentialsException("JWT Filter - Internal Error");
}
}
- 유효성 검사 중 발생할 수 있는 예외를 처리하며, 각 예외에 따라 다른 동작을 수행
- 만료된 토큰(ExpiredJwtException) : 토큰이 만료되었음을 클라이언트에게 알림
- 지원하지 않는 토큰(UnsupportedJwtException): 지원하지 않는 형식의 토큰임을 클라이언트에게 알림
- 잘못된 형식의 토큰(MalformedJwtException): 토큰의 형식이 잘못되었음을 클라이언트에게 알림
- 토큰을 찾을 수 없음(IllegalArgumentException): 요청에서 토큰을 찾을 수 없음을 클라이언트에게 알림
- 기타 예외(Exception): 이 외 내부 오류로 인해 토큰을 처리할 수 없음을 클라이언트에게 알림
CustomAuthenticationEntryPoint
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String exception = (String)request.getAttribute("exception"); // Filter에서 "exception"으로 사용했던 것을 꺼내옴
}
}
- AuthenticationEntryPoint 인터페이스
➡️시큐리티에서 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할때 동작하는 인터페이스 - CustomAuthenticationEntryPoint 클래스
➡️AuthenticationEntryPoint의 구현체로 사용자가 인증되지 않았을때 어떻게 응답할지를 정의하는 클래스
⚠️반드시 필요한 것은 아님 - (String)request.getAttribute("exception");
➡️Filter에서 request.setAttribute(”exception”, …) 처럼 Key-Value로 적용했던 것을 꺼내옴
catch (ExpiredJwtException e){ // 예외 추가 (JwtExceptionEnum으로부터)
request.setAttribute("exception", JwtExceptionCode.EXPIRED_TOKEN.getCode());
log.error("Expired Token : {}",token,e);
SecurityContextHolder.clearContext();
throw new BadCredentialsException("Expired token exception", e);
- Filter의 setAttribute
CustomAuthenticationEntryPoint - isRestRequest() 메소드 추가
private boolean isRestRequest(HttpServletRequest request) {
String requestedWithHeader = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equals(requestedWithHeader) || request.getRequestURI().startsWith("/api/");
}
- 이 메소드는 지금 요청이 Rest인지 알아내는 것 (isRestRequest)
- request.getHeader(”X-Requested-With”);
➡️모든 요청은 request에 담김 (=요청정보)
➡️그 request로부터 Header정보를 꺼낸 것 (getHeader()) - return “XMLHttpRequest”.equals(requestedWithHeader)
➡️Ajax요청이 들어올때는 XMLHttpRequest 객체가 Header에 Value로 들어오게됨
Ajax로 요청했다는 것은 Rest로 요청했다는 것
Ajax로 요청이 들어올때는 Page로 요청이 들어오지 않기 때문
얻어낸 Header정보가 이 객체와 같다고 검사하는 부분
따라서 이 정보가 존재하면 Ajax가 호출했음을 알 수 있음 - Ajax : 자바스크립트에서 비동기통신을 담당하는 것
- 정리하자면
“X-Requested-With”는 요청방식을 알려주는 Key 값
XMLHttpRequest는 그 Key값에 따른 Value값 - request.getRequestURI().startsWith("/api/");
/api/ 로 URL이 시작되는 것을 가져오도록 설정해주고 있음
❓헤더 정보란
- 이처럼 Authorization, Cookie 같은 헤더 정보를 의미
▶️실습 - 페이지 요청이 들어왔을때 처리
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String exception = (String)request.getAttribute("exception");
if(isRestRequest(request)){
// Rest로 요청이 들어왔을때 수행
}else{
// page로 요청이 들어왔을때 수행
handlePageResponse(request, response, exception);
}
}
// 페이지 요청 중 예외가 발생했다면 로그를 남기고 /loginform에 필수적으로 리다이렉트
private void handlePageResponse(HttpServletRequest request, HttpServletResponse response, String exception) throws IOException {
log.error("Page Request - Commence Get Exception : {}", exception);
if (exception != null) {
// 추가적인 페이지 요청에 대한 예외 처리 로직을 여기에 추가 가능
}
response.sendRedirect("/loginform");
}
- response.sendRedirect(”/loginform”)
➡️localhost:8080/mypage로 하면 403 error가 발생했지만
이러한 예외처리를 통해서 loginform.html 페이지로 리다이렉트되도록 구현 가능
else{...} 문에서 구현된 handlePageResponse 메소드에 인자를 넣어보냄
반환받은 값에 따라 리다이렉트 수행
SecurityConfig - EntryPoint 적용
http
// 다른 설정들 ...
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customAuthenticationEntryPoint));
- .exceptionHandling을 추가하여 authenticationEntryPoint()에 구현한 CustomAuthenticationEntryPoint를 보냄
▶️실습 - 쿠키 삭제 후 인증이 되지 않았을 경우 (localhost:8080/api/info)
- custom으로 정의한 예외에 따라 JSON형태로 예외를 발생시킬 수 있다.
UserApiController - RefreshToken 메소드 추가
@PostMapping("refreshToken")
public ResponseEntity<?> requestRefresh(){
//...
}
- 이 메소드는 Refresh Token을 받으면 내 DB에 해당 Refresh Token가 있는지 검사 후
존재할 경우에만 accessToken을 재발급해서 보냄 - Refresh Token을 갖고있다가 Access Token이 만료되기 전 Refresh Token을 요청하게 될 것
= 이때 이러한 Refresh Token 요청은 내부적으로 호출할 것 (사용자가 직접 호출하지 않음) - AccessToken을 발급할때 RefreshToken도 발급하도록 구현할 수 있지만
매번 발급이 되면 서버에 부하가 생길 수 있기때문에 적절하게 발급시기를 정해주면 된다.
1. AccessToken 발급 시 RefreshToken도 발급한다거나
2. RefreshToken은 만료기간이 끝났을 경우에만 발급한다는 등 - 일반적인 페이지일 경우 둘다 만료되었을 경우에 사이트에서 다시 인증하라는 페이지가 발생할 것
- 웹과 앱에서의 토큰 관리 방식이 조금은 다를 것
- 웹 : 공유될 수 있는 반면
- 앱 : 개인의 기기에서 작동하기때문에 작동방식이 다를 수 있음
🚀회고 결과 :
이번 회고에서는 JWT에서의 예외처리 방법을 실습해볼 수 있었다. 개념에 대해서는 학습하면서 부족했었는데 회고를 통해 정리할 수 있었다.
- Refresh Token 발급 시기
- EntryPoint
느낀 점 :
Spring Security부분은 중요하기도 하고 복잡한 부분이 많아서 다시 한 번은 공부해야될 것 같다.
향후 계획 :
- 이번 주 학습 내용 복습!
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_63일차_"Swagger" (0) | 2025.03.11 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_62일차_"OAuth2 연동" (0) | 2025.03.10 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_60일차_"Jwt Authentication" (0) | 2025.03.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_58일차_"Spring Security와 DB연결" (0) | 2025.03.04 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_57일차_"Spring Security - 권한, 쿠키" (0) | 2025.02.28 |