
무작정 request Interceptor로 구현하였는데..
이거 매번 컨트롤러마다@AuthenticationPrincipal를 붙여야하는 불상사가 일어나고 있었다...
사실 내가 체크하려는 로직은 로그인여부와 같이 간단한 로직이니 굳이 비지니스 로직에서 검사하는 것이 아닌 스프링 전역에서 유지하는 Filter 를 사용하는 것이 더 좋을 것 같다는 생각이 들었다..
(아니 근데 사실 그러면 request interceptor는 언제 쓰지...?
즉 로그인 여부는 스프링과 관련이 없으니... HTTP 선에서 처리하는 것이 더 깔끔할 것 같았다..
내가 생각한 것은
- 클라이언트가 서버로 HTTP 요청을 보낸다.
- Filter가 요청을 가로채서 요청을 처리하거나 검증한다!!
- 필터가 자신의 역할을 마치고, 요청을 다음 단계(다음 필터 또는 서블릿)로 넘기기 위해 chain.doFilter(request, response)를 호출한다.
- 다음 필터가 없으면 서블릿이나 컨트롤러가 요청을 처리한다!
아무튼 그래서 Interceptor -> Filter 로 변경해보자.....
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilter {
private final JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = resolveToken(httpRequest);
if (token != null) {
try {
jwtUtil.extractAllClaims(token); // 토큰 검증
Authentication authentication = jwtUtil.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication); // context 홀더에 넣기
} catch (RuntimeException e) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않은 JWT 토큰입니다: " + e.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
filter 클래스 구현!
interceptor와 사실상 거의 다 비슷하다.
public class SecurityUtils {
public static UserAdapter getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return (UserAdapter) authentication.getPrincipal();
}
throw new UnauthorizedException("인증되지 않은 사용자입니다.");
}
}
디테일 하게 하기로 한김에 adapter에서 현재 유저를 찾아올 수 있는 getCurrentUser를 일단 생성 추후에 더 필요한 것이 있으면 생성하면 될 것 같다!
public enum ErrorMessage {
UNAUTHORIZED("인증되지 않은 사용자입니다."),
USER_NOT_FOUND("사용자가 존재하지 않습니다."),
BAD_REQUEST("잘못된 요청입니다.");
private final String message;
ErrorMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
Enum으로 ErrorMSG도 간단하게 만들었는데 이건 나중에 회의 후 수정하도록 하겠다.
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException() {
super(ErrorMessage.UNAUTHORIZED.getMessage());
}
}
UnauthorizedException
인증되지 않은 예외에 대해 메시지를 던져주기
하지만 보면 알겠지만 무수한 try/ catch늪에 빠져있다.
그래서 어처피 공통된 예외이니 글로벌 익셉션 핸들러를 사용해서 전역적으로 예외를 처리하도록 하겠다
아~ 관점지향적이다~
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("오류가 발생했습니다: " + e.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorMessage.BAD_REQUEST.getMessage() + ": " + e.getMessage());
}
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<String> handleUnauthorizedException(UnauthorizedException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(e.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<String> handleAccessDeniedException(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("접근이 거부되었습니다: " + e.getMessage());
}
}
간단하게 큼지막한 에러들만 모아두었다!
오.. 이제 진짜 뭔가 개발하는 거 같아서 재미가 들리기 시작했다!! 추후에 에러메시지들도 추가해서 수정하도록 하겠다!!
@Controller
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
private final UserService userService;
private final PasswordTokenRepository tokenRepository;
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
@PostMapping("/join")
public ResponseEntity<User> join(@RequestBody User user) {
try {
User savedUser = userService.joinUser(user);
return ResponseEntity.ok(savedUser);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/readProfile")
public ResponseEntity<?> readUserProfile() {
UserAdapter userAdapter = SecurityUtils.getCurrentUser();
if (userAdapter == null) {
throw new UnauthorizedException();
}
UserDTO userDTO = userService.readUser(userAdapter);
return ResponseEntity.ok(userDTO);
}
@PutMapping("/update")
public ResponseEntity<?> updateProfile(@RequestBody UserDTO userDTO) {
UserAdapter userAdapter = SecurityUtils.getCurrentUser();
if (userAdapter == null) {
throw new UnauthorizedException();
}
User user = userService.updateUser(userAdapter, userDTO);
return ResponseEntity.ok(user);
}
@DeleteMapping("/delete")
public ResponseEntity<?> deleteUser() {
UserAdapter userAdapter = SecurityUtils.getCurrentUser();
if (userAdapter == null) {
throw new UnauthorizedException();
}
userService.deleteUser(userAdapter);
return ResponseEntity.ok().build();
}
@PutMapping("/changeTempPassword")
public ResponseEntity<?> verifyTempPassword(@RequestParam("token") String token,
@RequestBody PasswordDTO passwordDTO) {
PasswordToken passwordToken = tokenRepository.findByToken(token)
.orElseThrow(() -> new RuntimeException("유효하지 않은 토큰임"));
if (passwordToken.getExpiryDate().isBefore(LocalDateTime.now())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("토큰이 만료되었음");
}
User user = passwordToken.getUser();
String newPassword = passwordDTO.getNewPassword();
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
tokenRepository.delete(passwordToken);
return ResponseEntity.ok("비밀번호 변경 ok");
}
@PutMapping("/changePassword")
public ResponseEntity<?> changePassword(@RequestBody PasswordDTO passwordDTO) {
UserAdapter userAdapter = SecurityUtils.getCurrentUser();
if (userAdapter == null) {
throw new UnauthorizedException();
}
userService.updatePassword(userAdapter, passwordDTO);
return ResponseEntity.ok("비밀번호가 변경되었습니다.");
}
}
하아.. 아름답다 아름다워...
확실히 코드가 우아?하게 줄었다!!
아직 예외처리 안한 것들도 많기는 한데 그건 다음 편에서 전부 예외를 잡아 볼 생각이다!! 일단은 filter 적용부터~
이제
@AuthenticationPrincipal UserAdapter userAdapter로 가져오는 것과
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
로 수동적으로 가져오는 것중 어떤 것을 택해야 할지 고민하였다.
가독성 부분과 편리함 부분에서는 위에 어노테이션이 편하긴 한데,,, 후자는 조금 더 유연한 처리를 할 수가 있다.
이미 userDeatails를 구현하기도 하였고, 무작정 자동화보다는 조금 더 유연하게 에러를 한번 잡아보고 싶어서 수동적으로 하는 방법을 선택하였다.
에러가 없으면 섭하지
io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 0
자꾸 jwt토큰을 못 찾아오고 있었다.
흠.. 에러 메시지를 보면 .으로 구별된것을 못찾고 있는 것 같은데...
wow... 내가 초반에 개발한다고 타입을 token으로 직접 주입받고 있어서 당연히 토큰 넣는데다가 userAdpater를 냅다 집어넣고 있었다... 이 간단한걸!!!!!
그래도 점점 디버깅하는 실력이 조금씩 늘고 있다!!