Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mod] 토큰 전달 방식 변경 #192

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
Open
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,7 +1,6 @@
package com.example.eatmate.global.auth.jwt;

import java.io.IOException;
import java.util.Arrays;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
Expand All @@ -18,23 +17,23 @@

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* "/login" 이외의 URI 요청이 왔을 때 처리하는 필터
*
* JWT 인증 필터 (헤더 기반으로 변경)
*
* 1. RefreshToken이 있으면 DB에서 검증 후 AccessToken + RefreshToken 재발급 (RTR 방식)
* 2. RefreshToken이 없으면 AccessToken을 검증하여 인증 처리
*/
@RequiredArgsConstructor
@Slf4j
@Component
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {

private static final String NO_CHECK_URL = "/login"; // 로그인 요청은 필터 제외
private static final String NO_CHECK_URL = "/login"; // "/login" 요청은 필터 제외

private final JwtService jwtService;
private final MemberRepository memberRepository;
Expand All @@ -44,58 +43,62 @@ public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String requestURI = request.getRequestURI();
log.info("Processing request URI: {}", request.getRequestURI());

// "/login" 요청은 필터 제외
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response); // /login 요청은 필터 제외
filterChain.doFilter(request, response);
return;
}
if (request.getRequestURI().startsWith("/ws/chat")) {
log.info("WebSocket 핸드셰이크 요청 감지: 필터를 통과시킴 : {}", request.getRequestURI());
// ✅ OAuth2 로그인 요청은 JWT 인증 제외
if (requestURI.startsWith("/api/auth/google")) {
log.info("OAuth2 로그인 요청 감지, JWT 인증 필터 적용 제외: {}", requestURI);
filterChain.doFilter(request, response);
return;
}

// 요청에서 쿠키 추출 및 디버깅 로그
log.info("Request Cookies: {}", Arrays.toString(request.getCookies()));
// WebSocket 요청 필터 제외
if (request.getRequestURI().startsWith("/ws/chat")) {
log.info("WebSocket 핸드셰이크 요청 감지: 필터를 통과시킴 : {}", request.getRequestURI());
filterChain.doFilter(request, response);
return;
}

// Refresh Token 처리
String refreshToken = jwtService.extractRefreshTokenFromCookie(request)
// Refresh Token 검사
String refreshToken = jwtService.extractRefreshToken(request)
.filter(jwtService::isTokenValid)
.orElse(null);

if (refreshToken != null) {
log.info("Valid RefreshToken found. Processing token renewal...");
handleRefreshToken(response, refreshToken);
return; // Refresh Token을 재발급 후 인증은 처리하지 않음
}

// Access Token 처리
try {
handleAccessToken(request, response, filterChain);
} catch (Exception e) {
log.error("Error while handling Access Token: {}", e.getMessage(), e);
sendErrorResponse(response, "Invalid Access Token");
}
// RefreshToken이 없으면 Access Token 검사
checkAccessTokenAndAuthenticate(request, response, filterChain);
}

/**
* Refresh Token 처리 및 Access Token 재발급
* [리프레시 토큰 확인 후 Access Token + Refresh Token 재발급]
*/
private void handleRefreshToken(HttpServletResponse response, String refreshToken) {
memberRepository.findByRefreshToken(refreshToken)
.ifPresentOrElse(
member -> {
// 새 Access Token & Refresh Token 발급
String newAccessToken = jwtService.createAccessToken(member.getEmail(), member.getRole().name(),
member.getRole() == Role.USER ? member.getGender().name() : null);
String newRefreshToken = jwtService.createRefreshToken();

// DB에 새 Refresh Token 저장
member.updateRefreshToken(newRefreshToken);
memberRepository.saveAndFlush(member);

// 쿠키에 토큰 설정
setTokenInCookie(response, "AccessToken", newAccessToken,
jwtService.getAccessTokenExpirationPeriod());
setTokenInCookie(response, "RefreshToken", newRefreshToken,
jwtService.getRefreshTokenExpirationPeriod());
// 응답 헤더에 토큰 추가
jwtService.sendAccessAndRefreshToken(response, newAccessToken, newRefreshToken);

log.info("Refresh Token 유효. Access Token 및 Refresh Token 재발급 완료.");
},
Expand All @@ -104,29 +107,31 @@ private void handleRefreshToken(HttpServletResponse response, String refreshToke
}

/**
* Access Token 처리SecurityContext 설정
* [Access Token 검사인증 수행]
*/
private void handleAccessToken(HttpServletRequest request, HttpServletResponse response,
private void checkAccessTokenAndAuthenticate(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
jwtService.extractAccessTokenFromCookie(request)

jwtService.extractAccessToken(request)
.filter(jwtService::isTokenValid) // AccessToken 유효성 검증
.flatMap(jwtService::extractEmail) // 유효한 AccessToken에서 Email 추출
.flatMap(memberRepository::findByEmail) // 이메일로 Member 조회
.ifPresentOrElse(
member -> {
log.info("Authentication successful for user: {}", member.getEmail());
setAuthentication(member); // 인증 정보 SecurityContext에 저장
saveAuthentication(member); // SecurityContext에 인증 정보 저장
},
() -> log.warn("Failed to authenticate user. Either token is invalid or member not found.")
);

// 필터 체인 계속 진행
filterChain.doFilter(request, response);
}

/**
* 인증 정보 SecurityContext에 저장
* [SecurityContext에 인증 정보 저장]
*/
private void setAuthentication(Member member) {
private void saveAuthentication(Member member) {
UserDetails userDetails = org.springframework.security.core.userdetails.User.builder()
.username(member.getEmail())
.password("") // 비밀번호는 사용하지 않으므로 빈 문자열
Expand All @@ -141,7 +146,7 @@ private void setAuthentication(Member member) {
}

/**
* 에러 응답 전송
* [에러 응답 전송]
*/
private void sendErrorResponse(HttpServletResponse response, String message) {
try {
Expand All @@ -152,17 +157,4 @@ private void sendErrorResponse(HttpServletResponse response, String message) {
log.error("에러 응답 전송 중 오류 발생", e);
}
}

/**
* 쿠키에 토큰 설정
*/
private void setTokenInCookie(HttpServletResponse response, String name, String token, long maxAge) {
Cookie cookie = new Cookie(name, token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge((int)(maxAge / 1000)); // maxAge는 초 단위로 설정
response.addCookie(cookie);
log.info("{} 쿠키 설정 완료", name);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.example.eatmate.global.auth.jwt;

import java.util.Map;
import java.util.Optional;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
Expand All @@ -12,7 +11,6 @@

import com.example.eatmate.app.domain.member.domain.repository.MemberRepository;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -28,34 +26,50 @@ public class JwtHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {

if (request instanceof ServletServerHttpRequest) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest)request).getServletRequest();

log.info("WebSocket 요청 감지: {}", servletRequest.getRequestURI());

//요청된 쿠키 확인
if (servletRequest.getCookies() != null) {
for (Cookie cookie : servletRequest.getCookies()) {
log.info("쿠키 확인: {} = {}", cookie.getName(), cookie.getValue());
// [1] Authorization 헤더에서 토큰 추출
String authHeader = servletRequest.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7); // "Bearer " 제거

// [2] 토큰 유효성 검사 및 사용자 정보 추출
if (jwtService.isTokenValid(token)) {
jwtService.extractEmail(token)
.flatMap(memberRepository::findByEmail)
.ifPresentOrElse(member -> {
log.info("WebSocket 인증 성공! 사용자: {}", member.getEmail());
attributes.put("userDetails", member); // WebSocket 세션에 사용자 정보 저장
}, () -> {
log.warn("WebSocket 인증 실패: 토큰에서 이메일 추출 실패 또는 회원 미존재");
});
} else {
log.warn("WebSocket 인증 실패: 토큰 유효하지 않음");
}
} else {
log.warn("WebSocket 요청에 쿠키 없음!");
log.warn("WebSocket 요청에 Authorization 헤더가 없거나 Bearer 스키마가 아님");
}

//쿠키에서 AccessToken 추출
Optional<String> tokenOptional = jwtService.extractAccessTokenFromCookie(servletRequest);

tokenOptional
.filter(jwtService::isTokenValid)
.flatMap(jwtService::extractEmail)
.flatMap(memberRepository::findByEmail)
.ifPresentOrElse(member -> {
log.info("WebSocket 인증 성공! 사용자: {}", member.getEmail());
attributes.put("userDetails", member); //WebSocket 세션에 사용자 정보 저장
}, () -> {
log.warn("WebSocket 인증 실패: JWT 없음 또는 유효하지 않음");
});
// [기존 쿠키 기반 로직 제거 혹은 주석 처리]
// if (servletRequest.getCookies() != null) {
// for (Cookie cookie : servletRequest.getCookies()) {
// log.info("쿠키 확인: {} = {}", cookie.getName(), cookie.getValue());
// }
// } else {
// log.warn("WebSocket 요청에 쿠키 없음!");
// }
// Optional<String> tokenOptional = jwtService.extractAccessToken(servletRequest);
// tokenOptional
// .filter(jwtService::isTokenValid)
// .flatMap(jwtService::extractEmail)
// .flatMap(memberRepository::findByEmail)
// .ifPresentOrElse(...);
}

return true;
}

Expand Down
Loading