Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.cakey.exception.handler;
package com.cakey.common.exception.handler;

import com.cakey.cake.exception.CakeyApiBaseException;
import com.cakey.rescode.ErrorBaseCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.cakey.common.filter;

import com.cakey.Constants;
import com.cakey.common.response.BaseResponse;
import com.cakey.rescode.ErrorBaseCode;
import com.cakey.rescode.ErrorCode;
import com.cakey.user.exception.UserUnAuthorizedException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.io.PrintWriter;

@Slf4j
public class ExceptionHandlerFilter extends OncePerRequestFilter {

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
try {
filterChain.doFilter(request, response);
} catch (UserUnAuthorizedException e) {
handleUnauthorizedException(response, e);
} catch (Exception ee) {
handleException(response, ee);
}
}

private void handleUnauthorizedException(HttpServletResponse response, Exception e) throws IOException {
UserUnAuthorizedException ue = (UserUnAuthorizedException) e;
ErrorCode errorCode = ue.getErrorCode();
HttpStatus httpStatus = errorCode.getHttpStatus();
setResponse(response, httpStatus, errorCode);
}

private void handleException(HttpServletResponse response, Exception e) throws IOException {
log.error("-------------- Exception Handler Filter ------------ \n" + e.getCause() + e.getMessage());
log.error("\n ---------------------------------------------------------");
setResponse(response, HttpStatus.INTERNAL_SERVER_ERROR, ErrorBaseCode.INTERNAL_SERVER_ERROR);
}

private void setResponse(HttpServletResponse response, HttpStatus httpStatus, ErrorCode errorBaseCode) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(Constants.CHARACTER_TYPE);
response.setStatus(httpStatus.value());
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(BaseResponse.of(errorBaseCode)));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.cakey.common.filter;

import com.cakey.Constants;
import com.cakey.exception.AuthExpiredJwtException;
import com.cakey.exception.AuthWrongJwtException;
import com.cakey.jwt.auth.JwtProvider;
import com.cakey.jwt.auth.UserAuthentication;
import com.cakey.rescode.ErrorBaseCode;
import com.cakey.user.exception.UserUnAuthorizedException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
Expand All @@ -13,14 +17,15 @@

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.apache.tomcat.util.http.parser.Authorization;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@RequiredArgsConstructor
@Slf4j
public class OptionalAuthenticationFilter extends OncePerRequestFilter { //로그인 상관 X
private final JwtProvider jwtProvider;

Expand All @@ -45,7 +50,7 @@ public class OptionalAuthenticationFilter extends OncePerRequestFilter { //로
"/api/v1/store/*/size",
"/api/v1/store/*/information",
"/api/v1/store/*/kakaoLink",
"api/v1/user/login"
"/api/v1/user/login"
);

@Override
Expand All @@ -60,39 +65,41 @@ protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
) {

String accessToken = request.getHeader(Constants.AUTHORIZATION);

if ("Bearer: ".equals(accessToken)) {
accessToken = null;
if (accessToken != null) { ///Authorization 헤더 왔을 때
if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) {
accessToken = accessToken.substring(Constants.BEARER.length());
} else {
throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT); ///액세스토큰 비어있거나 Bearer로 시작안할때
}

} else if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) {
/// "Bearer: "로 시작하는 경우
accessToken = accessToken.substring(Constants.BEARER.length()).trim();
try {
final long userId = jwtProvider.getUserIdFromSubject(accessToken);
SecurityContextHolder
.getContext()
.setAuthentication(new UserAuthentication(userId, null, null));
filterChain.doFilter(request, response);

/// 접두사 제거 후 내용이 없으면 null 처리
if (accessToken.isEmpty()) {
accessToken = null;
} catch (AuthExpiredJwtException e) {
throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_AT_EXPIRED);
} catch (AuthWrongJwtException e) {
throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT);
} catch (Exception e) {
log.error("-------UNAUTHORIZED ERROR LOG -----------\n" + e.getMessage(), e);
throw new UserUnAuthorizedException(ErrorBaseCode.INTERNAL_SERVER_ERROR);
}
} else {
// 유효하지 않은 경우 null 처리
accessToken = null;
}

if (accessToken != null) {
final long userId = jwtProvider.getUserIdFromSubject(accessToken);
SecurityContextHolder
.getContext()
.setAuthentication(new UserAuthentication(userId, null, null));
} else {
} else { ///Authorization 헤더 안왔을 때
SecurityContextHolder
.getContext()
.setAuthentication(new UserAuthentication(null, null, null));
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("-------UNAUTHORIZED ERROR LOG ----------- \n " + e.getMessage(), e);
throw new UserUnAuthorizedException(ErrorBaseCode.INTERNAL_SERVER_ERROR); }
}

filterChain.doFilter(request, response);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

import com.cakey.Constants;
import com.cakey.common.response.ApiResponseUtil;
import com.cakey.exception.AuthExpiredJwtException;
import com.cakey.exception.AuthWrongJwtException;
import com.cakey.jwt.auth.JwtProvider;
import com.cakey.jwt.auth.UserAuthentication;
import com.cakey.rescode.ErrorBaseCode;
import com.cakey.user.exception.UserBadRequestException;
import com.cakey.user.exception.UserErrorCode;
import com.cakey.user.exception.UserUnAuthorizedException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
Expand All @@ -30,7 +35,6 @@
@Slf4j
public class RequiredAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider; //로그인 필수
private final ObjectMapper objectMapper;

// 필터를 건너뛸 API 경로 목록
private static final List<String> EXCLUDED_PATHS = List.of(
Expand All @@ -52,8 +56,7 @@ public class RequiredAuthenticationFilter extends OncePerRequestFilter {
"/api/v1/store/*/size",
"/api/v1/store/*/information",
"/api/v1/store/*/kakaoLink",
"api/v1/user/login"

"/api/v1/user/login"
);

@Override
Expand All @@ -68,42 +71,28 @@ protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
try {
String accessToken = request.getHeader(Constants.AUTHORIZATION);
) {
String accessToken = request.getHeader(Constants.AUTHORIZATION);

if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) {
accessToken = accessToken.substring(Constants.BEARER.length());
} else {
throw new Exception();
}
if (StringUtils.hasText(accessToken) && accessToken.startsWith(Constants.BEARER)) {
accessToken = accessToken.substring(Constants.BEARER.length());
} else {
throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT); ///액세스토큰 비어있거나 Bearer로 시작안할때
}

try {
final long userId = jwtProvider.getUserIdFromSubject(accessToken);

SecurityContextHolder
.getContext()
.setAuthentication(new UserAuthentication(userId, null, null));

filterChain.doFilter(request, response); // 다음 필터로 요청 전달
filterChain.doFilter(request, response);
} catch (AuthExpiredJwtException e) {
throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_AT_EXPIRED);
} catch (AuthWrongJwtException e) {
throw new UserUnAuthorizedException(ErrorBaseCode.UNAUTHORIZED_WRONG_AT);
} catch (Exception e) {
log.error("--------------------쿠키 에러------------------------");
log.error(e.getMessage());

// 예외 발생 시 JSON 응답 생성
final ErrorBaseCode errorCode = ErrorBaseCode.UNAUTHORIZED;

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(Constants.CHARACTER_TYPE);
response.setStatus(errorCode.getHttpStatus().value()); // HTTP 상태 코드 401 설정

log.error("--------------------토큰 없음------------------------"); //todo: 추후 삭제(테스트용)
// `ApiResponseUtil.failure`를 이용해 응답 작성
final PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(
ApiResponseUtil.failure(errorCode).getBody()
));
writer.flush();
return; // 체인 호출 중단
log.error("-------UNAUTHORIZED ERROR LOG -----------\n" + e.getMessage(), e);
throw new UserUnAuthorizedException(ErrorBaseCode.INTERNAL_SERVER_ERROR);
}
}
}
}
6 changes: 2 additions & 4 deletions cakey-api/src/main/java/com/cakey/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package com.cakey.config;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import com.cakey.common.filter.ExceptionHandlerFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

@Bean
Expand All @@ -24,6 +21,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new ExceptionHandlerFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests( auth -> auth.anyRequest().permitAll())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import com.cakey.common.response.ApiResponseUtil;
import com.cakey.common.response.BaseResponse;
import com.cakey.rescode.SuccessCode;
import com.cakey.user.dto.JwtReissueRes;
import com.cakey.user.dto.LoginSuccessRes;
import com.cakey.user.service.UserService;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
Expand Down Expand Up @@ -53,15 +55,16 @@ public ResponseEntity<BaseResponse<?>> logout(
userService.logout(userId, response);
return ApiResponseUtil.success(SuccessCode.OK);
}


// //jwt 재발급
// @GetMapping("/reissue")
// public ResponseEntity<BaseResponse<?>> jwtReissue(
// @CookieValue(name = "refreshToken") Cookie cookie
// ) {
// final String refreshToken = cookie.getValue();
// final LoginSuccessRes loginSuccessRes = userService.jwtReissue(refreshToken);
// return ApiResponseUtil.success(SuccessCode.OK, loginSuccessRes);
// }
//jwt 재발급
@PatchMapping("/reissue")
public ResponseEntity<BaseResponse<?>> jwtReissue(
@RequestHeader(value = "userId") final long userId,
@CookieValue(name = "refreshToken") final Cookie cookie,
final HttpServletResponse response

) {
final JwtReissueRes jwtReissueRes = userService.jwtReissue(userId, cookie.getValue(), response);
return ApiResponseUtil.success(SuccessCode.OK, jwtReissueRes);
}
}
9 changes: 9 additions & 0 deletions cakey-api/src/main/java/com/cakey/user/dto/JwtReissueRes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.cakey.user.dto;

public record JwtReissueRes(
String accessToken
) {
public static JwtReissueRes of(final String accessToken) {
return new JwtReissueRes(accessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
@RequiredArgsConstructor
public enum UserErrorCode implements ErrorCode {

/**
* 400 Bad Request
*/

/**
* 404 Not Found
Expand All @@ -18,7 +21,8 @@ public enum UserErrorCode implements ErrorCode {
/**
* 500 Server Internal Error
*/
KAKAO_LOGIN_FAILED(HttpStatus.BAD_REQUEST, 50030, "카카오 로그인에 실패하였습니다"),
KAKAO_LOGIN_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 50030, "카카오 로그인에 실패하였습니다"),
USER_RT_CACHE_NOT_FOUNT(HttpStatus.INTERNAL_SERVER_ERROR, 50031, "서버 내부 캐시에 저장된 리프레시 토큰이 없습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.cakey.user.exception;

import com.cakey.rescode.ErrorCode;
import org.springframework.http.HttpStatus;

public class UserUnAuthorizedException extends UserApiBaseException {
public UserUnAuthorizedException(final ErrorCode errorCode) {
super(errorCode);
}

@Override
HttpStatus getStatus() {
return HttpStatus.UNAUTHORIZED;
}
}
Loading