지금 로그인 상태를 확인하는 방법이 꽤나 괴랄하다.
- 세션 유지 방식
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
이 설정은 인증된 사용자가 있을 경우 세션을 생성해서 인증 상태를 유지하겠다는 의미. 즉, 로그인한 사용자는 세션을 통해 계속 인증 상태를 유지할 수 있음.
- 인증 상태 확인
- Spring Security는
SecurityContextHolder
와Authentication
객체를 사용해 인증 상태를 자동으로 확인. - 예를 들어,
/api/**
경로에 접근하려면 사용자가 인증되어야 하도록 설정되어 있고, 인증되지 않은 경우에는 자동으로customAuthenticationEntryPoint
가 호출되서401 Unauthorized
를 반환.
- Spring Security는
- 결론
지금 코드에서는 "직접적으로 로그인 상태를 확인"하려고 하는 게 아니라, Spring Security의 인증 체계를 설정하고 활용하는 코드라고 보면 돼. 세션을 사용해서 인증 상태를 유지하고, Spring Security가 그 상태를 관리하도록 맡긴 설계.
근데 이런 방식은 좀 이상하다. 그러니까 그냥 일반적이지는 않다.
그래서 저번에 한번 다뤄봤던 (제대로 다루지는 못했지만) JWT를 제대로 도입할 예정.
// JWT
implementation ("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly ("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly ("io.jsonwebtoken:jjwt-jackson:0.11.5")
우선 의존성부터 주입하자.
그리고 이제
해당 파일들의 코드가 생성 및 수정되었음.
소셜 로그인이라 소셜로그인을 할 때의 서비스에서 토큰 생성을 하게끔 로직을 짰고,
리프래시 토큰은 Redis에 저장하고 사용하게끔 함.
그리고 로그아웃은 memberController
에 있었기 때문에 거기서 세션 무효화 방식에서 리프레시 토큰 삭제하는 방식으로 수정함.
그리고 하는김에
Yml
파일도 나눠서 제대로 돌아가게끔 수정함.
그럼 이제 프론트에서 헤더로 제대로 넘기는지 보면 됨.
일단 프론트에서 인증상태를 확인하기 위해서 api/member/me
라는 백엔드 엔드포인트에 계속 요청을 보내고 있었는데 AuthProvider
에서 해당 로직을 지우고
useEffect(() => {
const token = localStorage.getItem("accessToken");
if (token) {
try {
const decoded = jwtDecode(token); // JWT 디코딩
if (Date.now() < decoded.exp * 1000) { // 토큰 만료 여부 확인
setIsAuthenticated(true);
setUser(decoded); // JWT 페이로드 정보를 사용자 정보로 설정
console.log("JWT decoded user data:", decoded);
} else {
console.warn("Access token expired");
setIsAuthenticated(false);
setUser(null);
}
} catch (error) {
console.error("Invalid JWT token:", error);
setIsAuthenticated(false);
setUser(null);
}
} else {
console.log("No access token found");
setIsAuthenticated(false);
setUser(null);
}
setIsAuthLoading(false); // 로딩 완료
}, []);
이렇게 바꿔줌.
아 그전에 npm install jwt-decode@3.1.2
으로 설치 해줬음 라이브러리.
이제 테스트 해보자.
백엔드에서는 생성을 하는데,,
프론트에서 못가져오는듯.
Redis에서도 정상적으로 저장되어있는거 확인했고,,
근데 로그아웃해보니까 삭제가 안돼서 알아보니, MemberController
에서 토큰 삭제 로직을 작성해놨었음. 해당 요청 경로는 시큐리티가 가로채서 CustomLogoutHandler
로 삭제로직을 옮겨준 뒤에 시큐리티 필터체인에 넣어줌.
그랬더니
우히히
순환참조 얘는 진짜 잊을만 하면 나오네.
@Lazy
어노테이션으로 해결하려고 했는데 해결이 안돼서,
그래서 걍 SecurityConfig
에서
@Bean
public CustomLogoutHandler customLogoutHandler(OAuth2AuthorizedClientRepository authorizedClientRepository,
RedisRefreshTokenService redisRefreshTokenService) {
return new CustomLogoutHandler(authorizedClientRepository, redisRefreshTokenService);
}
@Bean
으로 등록해서 SecurityConfig
에서 직접 생성하지 않게 만들어줌.
아이..
CustomLogoutHandler
클래스에서 @Component
제거하고 실행.
잘되고 생성 잘 하고,
삭제도 함.
근데 레디스에는 남아있네..?
오우 로그를 조금 더 자세히 찍어보고 어떤 문제가 있는지 살펴봐야될듯.
생성할 때 뭔가 잘못된듯..
많은 헛짓거리 끝에..
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/react/dashboard" element={<GoogleAuthHandler />} />
<Route path="/react/main" element={<MainPage/>}/> {/* 메인 페이지로 라우팅 */}
여기서 깡통 메인 페이지 하나 만들어서
import React from "react";
import "./css/GoogleLoginButton.css";
import {motion} from "framer-motion";
const GoogleLoginButton = () => {
const googleLogin = () => {
console.log("%c[INFO] Google 로그인 시도 중...", "color: blue");
const backendUrl =
process.env.REACT_APP_BACKEND_URL || "http://localhost:7777";
window.location.href = ${backendUrl}/oauth2/authorization/google; // Google OAuth2 로그인 경로
};
return (
<motion.button
onClick={googleLogin}
className="google-login-button"
whileHover={{scale: 1.1}} // 호버 시 확대
whileTap={{scale: 0.9}} // 클릭 시 축소
>
<div className="bc-logo">
<img
src="https://developers.google.com/identity/images/g-logo.png"
className="google-logo"
alt="Google logo"
/>
</div>
<span className="login-text">Sign in with Google</span>
</motion.button>
);
};
export default GoogleLoginButton;
여기서 구글 로그인 이후에
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
String email = oauthUser.getAttribute("email");
if (email != null) {
String accessToken = jwtUtil.generateToken(email);
String refreshToken = jwtUtil.generateRefreshToken(email);
log.info("Generated Access Token: {}", accessToken);
log.info("Generated Refresh Token: {}", refreshToken);
// React 메인 페이지로 리디렉션 (쿼리 파라미터에 토큰 추가)
response.sendRedirect("/react/dashboard?accessToken=" + accessToken + "&refreshToken=" + refreshToken);
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Email not found");
}
}
이렇게 백엔드에서 url에 토큰을 태워 보내고,
// GoogleAuthHandler.js
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
const GoogleAuthHandler = () => {
const navigate = useNavigate();
useEffect(() => {
const queryParams = new URLSearchParams(window.location.search);
const accessToken = queryParams.get("accessToken");
const refreshToken = queryParams.get("refreshToken");
if (accessToken && refreshToken) {
// 로컬스토리지에 저장
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
console.log("Tokens saved successfully");
// 토큰 저장 후 메인 페이지로 리다이렉트
navigate("/react/main");
} else {
console.error("No tokens found in query params");
navigate("/login"); // 인증 실패 시 로그인 페이지로 리다이렉트
}
}, [navigate]);
return <div>Authenticating...</div>; // 로딩 화면 또는 처리 중 메시지
};
export default GoogleAuthHandler;
여기서 그거 까서 저장해서
로그인 성공함.
그럼 이제 로그아웃시에 토큰을 태워 보내서 확인하게 하고, 백엔드에서 토큰을 소멸시키고, 로그아웃 성공하는것 보고, Redis
에서도 지워지는지 확인해보면 된다.
// 로그아웃 함수
const logout = async () => {
console.log("Logging out...");
// 로컬 스토리지에서 토큰 가져오기
const accessToken = localStorage.getItem("accessToken");
try {
if (accessToken) {
// 서버 로그아웃 요청
const response = await fetch('/api/member/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`, // 토큰 포함
'Content-Type': 'application/json',
},
});
if (response.ok) {
console.log("Server logout successful.");
} else {
console.warn("Server logout failed:", response.status);
}
} else {
console.warn("No access token found in localStorage.");
}
} catch (error) {
console.error("Error during logout request:", error);
}
// 상태 초기화 및 클라이언트 측 토큰 제거
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
setIsAuthenticated(false);
setUser(null);
};
로그아웃 함수도 새로 고쳤다.
꽤나 오래 걸렸는데 로그도 다 찍어가면서 로그아웃 시도만 몇번을 해봤는지 모르겠다.
프론트에서는 로그아웃 처리가 되는데 백엔드에서 어째선지 JWT 필터에 걸리지가 않아서 제대로 redis
에 있는 정보와, 또 리프레시 토큰이 제대로 삭제되지 않는 일이 일어났었음.
그래서 진짜 별의 별 짓 다 하다가.
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager(http.getSharedObject
(AuthenticationConfiguration.class)), jwtUtil), LogoutFilter.class)
이거 한 줄 추가해서 로직 제대로 작동함..
시큐리티에서 JwtAuthenticationFilter
에 무조건 LogoutFilter
가 걸리게 해줬음..
진짜 힘들었다.
여튼 다른 위치 로직 같은건 생각외로 망가지지 않았음. 인증 로직 분리해두길 진짜 너무 잘 한듯..
앞으로 이제 할 것은 JWT 리프레시 토큰으로 토큰 새로 발급 받아서 프론트에 업데이트 하는 로직인데 이건 이미 아까 만들면서 백엔드에 /refresh
경로 만들어둬서 금방 하지 않을까? 여튼 오늘은 여기까지.
'Coding History > Team Project' 카테고리의 다른 글
팀플) 디테일 수정 사항 (0) | 2024.12.05 |
---|---|
팀플) 오늘 한 것.. (JWT 완전 도입 완료, 세세한 수정, 상세 페이지 CRUD) (3) | 2024.12.03 |
팀플) 캘린더에 유성우 일정 업데이트. (1) | 2024.11.28 |
팀플) 캘린더 DB 저장. (0) | 2024.11.27 |
팀플) Google Calendar API 연결 (0) | 2024.11.26 |