Project/Project01.MailBuddy

일반 폼로그인(세션) → JWT 연동 프로세스

develop_mii 2025. 11. 27. 23:09
한 줄 요약
  • 예전:
    • Spring Security formLogin() + HttpSession 기반 인증
    • 서버가 세션을 들고 있고, 브라우저는 JSESSIONID 쿠키로만 인증
    • 프론트/백이 한 몸(같은 도메인 기준)일 때 편하지만, S3 + EC2 분리, JWT 기반 SPA에는 안 어울림
  • 지금:
    • /api/auth/login 에서 JWT 발급 → 프론트(localStorage)에 저장
    • 이후 모든 요청에 Authorization: Bearer <JWT> 헤더로 인증
    • 서버는 완전 stateless(세션 안 들고 있음)
    • 구글 OAuth는 “연동용”으로만 사용, 연동 결과는 User 엔티티에 저장해서 JWT 유저랑 연결

 


백엔드에서 어떻게 바뀌었는지

 

# 로그인 방식: 폼 로그인 → JWT 로그인

 

(1) 예전: formLogin + HttpSession

  • SecurityConfig 구조
http
  .formLogin(form -> form
      .loginPage("/login")
      .defaultSuccessUrl("/", true)
  )
  .logout(...)
  .sessionManagement(session ->
      session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
  );
  • 유저가 /login 으로 폼 제출 → 스프링 시큐리티가 직접 로그인 처리 →
    성공하면 서버가 세션(JSESSIONID) 을 만들고 쿠키로 내려줌.
  • 이후 요청마다 쿠키(JSESSIONID)가 자동으로 붙고, 서버는 세션 저장소에서 Authentication 찾아서 사용.

 

👉 특징

  • 서버가 “누가 로그인 중인지” 상태를 들고 있음 (stateful).
  • 프론트는 JWT 같은 거 몰라도 되고, 쿠키만 있으면 됨.
  • 단점: 프론트/백 분리(S3 + EC2), 도메인 다름, CORS, SameSite, HTTPS 등 문제가 복잡해짐.

 

(2) 지금: /api/auth/login + JWT 발급

  • AuthController
@PostMapping("/login")
public ResponseEntity<?> login(
        @RequestBody @Valid LoginRequestDto req,
        BindingResult result
) {
    // 1) DTO 검증
    if (result.hasErrors()) { ... }

    try {
        // 2) AuthenticationManager로 아이디/비번 검증
        UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword());

        Authentication authentication = authenticationManager.authenticate(authToken);

        // 3) DB에서 유저 조회
        User user = userRepository.findByUsername(authentication.getName())
                .orElseThrow(...);

        // 4) JWT 발급 (username + role)
        String token = jwtUtil.generateToken(user.getUsername(), user.getUserRole().name());

        // 5) 프론트로 내려줄 DTO
        LoginResponseDto responseDto = new LoginResponseDto(
                user.getUsername(),
                user.getName(),
                user.getBirth(),
                user.getUserRole().name(),
                token
        );

        return ResponseEntity.ok(responseDto);
    } catch (BadCredentialsException ex) {
        return ResponseEntity.status(401)
                .body(Map.of("error", "아이디 또는 비밀번호가 올바르지 않습니다."));
    }
}

 

 

핵심 차이점:

  • 로그인 성공 시 세션을 만드는 대신
    JWT 문자열을 발급해서 JSON 응답으로 내려줌.
  • 그 JWT 안에는 sub = username, role 클레임, 만료시간 등이 들어 있음.

👉 이 JWT는 서버가 들고 있는 상태가 아니라, 브라우저가 들고 다니는 “증명서” 같은 것.

 

 

# SecurityConfig: 세션 → stateless + JWT 필터

  • securityConfig
http
    .csrf(csrf -> csrf.disable())
    .cors(Customizer.withDefaults())

    // ✅ JWT 기준으로 stateless
    .sessionManagement(session ->
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    )

    .authorizeHttpRequests(auth -> auth
        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()

        // 공개 API
        .requestMatchers("/", "/index.html", "/login", "/error").permitAll()
        .requestMatchers("/api/auth/login", "/api/auth/signup").permitAll()

        // OAuth2 시작/콜백
        .requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll()

        // 권한 분리
        .requestMatchers("/api/admin/**").hasRole("ADMIN")
        .requestMatchers("/api/user/**").hasRole("USER")

        // 그 외는 모두 JWT 인증 필요
        .anyRequest().authenticated()
    )

    // ✅ 폼 로그인은 완전 비활성화
    .formLogin(form -> form.disable())

    // ✅ OAuth2 로그인은 "구글 연동용"으로만 사용
    .oauth2Login(oauth2 -> oauth2
        .loginPage("/login").permitAll()
        .authorizationEndpoint(authz -> authz
                .authorizationRequestResolver(customOAuth2AuthorizationRequestResolver())
        )
        .userInfoEndpoint(userInfo -> userInfo
                .userService(customOAuth2UserService)
        )
        .failureHandler(oauth2LoginFailureHandler)
        .defaultSuccessUrl("http://프론트도메인/schedule", true)
    );
    
    ...
    
    
    // ✅ JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

 

 

  • JwtAuthenticationFilter 동작
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserSecurityService userSecurityService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;

        // 1) Authorization 헤더에서 JWT 꺼내기
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
            try {
                username = jwtUtil.extractUsername(token);
            } catch (Exception e) {
                // 토큰 파싱 실패 → 그냥 통과 (컨트롤러에서 401 나게)
            }
        }

        // 2) 아직 인증 안 돼 있고, username이 있으면
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userSecurityService.loadUserByUsername(username);
            if (jwtUtil.validateToken(token, userDetails)) {
                // 3) 토큰이 유효하면 Authentication 구성해서 SecurityContext에 넣기
                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());

                authToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );

                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

 

 

👉 결과적으로:

  • 일반 요청 흐름
    1. 프론트가 Authorization: Bearer xxx.yyy.zzz 헤더와 함께 요청
    2. JwtAuthenticationFilter가 토큰 파싱 → username/role 확인
    3. 유효하면 SecurityContextHolder에 Authentication 세팅
    4. 컨트롤러에서 Authentication authentication 파라미터로 그대로 사용 가능
  • 서버는 세션에 유저정보를 저장하지 않음
    → 같은 유저 요청이 여러 서버로 분산돼도 상관없는 구조(stateless)

 

 

/api/auth/me – 로그인된 유저 정보 조회

 

@GetMapping("/me")
public ResponseEntity<?> me(Authentication authentication) {
    if (authentication == null || !authentication.isAuthenticated()) {
        return ResponseEntity.status(401).build();
    }

    String username = authentication.getName();
    User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));

    String googleEmail = user.getGoogleEmail();
    boolean googleLinked = (googleEmail != null && !googleEmail.isBlank());

    Map<String, Object> body = new HashMap<>();
    body.put("username", user.getUsername());
    body.put("name", user.getName());
    body.put("birth", user.getBirth());
    body.put("userRole", user.getUserRole().name());
    body.put("googleEmail", googleEmail);
    body.put("googleLinked", googleLinked);

    return ResponseEntity.ok(body);
}

 

 

  • 여기서 authentication 은 JWT 필터가 만들어준 Authentication.
  • 이 엔드포인트는 프론트에서:
    • 앱 처음 로드할 때
    • 새로고침 후
    • OAuth2 연동 성공 후
      “지금 누가 로그인되어 있는지, 구글연동 되어 있는지” 확인하는 용도.

 

구글 OAuth 연동 – 세션 대신 DB + JWT 유저에 연결

 

(1) OAuth2AuthorizationRequestResolver 커스터마이징

@Bean
public OAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver() {
    DefaultOAuth2AuthorizationRequestResolver defaultResolver =
            new DefaultOAuth2AuthorizationRequestResolver(
                    clientRegistrationRepository,
                    "/oauth2/authorization"
            );

    return new OAuth2AuthorizationRequestResolver() {

        @Override
        public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
            saveJwtTokenToSession(request);
            OAuth2AuthorizationRequest authorizationRequest = defaultResolver.resolve(request);
            return customize(authorizationRequest);
        }

        @Override
        public OAuth2AuthorizationRequest resolve(HttpServletRequest request,
                                                  String clientRegistrationId) {
            saveJwtTokenToSession(request);
            OAuth2AuthorizationRequest authorizationRequest =
                    defaultResolver.resolve(request, clientRegistrationId);
            return customize(authorizationRequest);
        }

        private void saveJwtTokenToSession(HttpServletRequest request) {
            String jwt = request.getParameter("jwt_token");
            if (jwt != null && !jwt.isEmpty()) {
                request.getSession(true).setAttribute("LINK_JWT", jwt);
            }
        }

        private OAuth2AuthorizationRequest customize(OAuth2AuthorizationRequest authorizationRequest) {
            if (authorizationRequest == null) {
                return null;
            }
            Map<String, Object> additionalParameters =
                    new HashMap<>(authorizationRequest.getAdditionalParameters());
            additionalParameters.put("prompt", "select_account");

            return OAuth2AuthorizationRequest.from(authorizationRequest)
                    .additionalParameters(additionalParameters)
                    .build();
        }
    };
}

 

 

  • 프론트에서  /oauth2/authorization/google?jwt_token=<현재로그인사용자의JWT>로 보내면 
  • 여기서 jwt_token을 세션에 LINK_JWT로 저장.
  • OAuth2 로그인 플로우가 끝난 후 CustomOAuth2UserService에서 이 LINK_JWT를 보고
    • 이 JWT가 가리키는 User 엔티티를 찾고
    • 구글 이메일 + 액세스 토큰을 그 User에 저장
    • 중복연동, 다른 계정에 이미 연동된 구글 계정이면 OAuth2AuthenticationException 던지고, OAuth2LoginFailureHandler에서 에러 페이지로 리다이렉트.

=> 여기서 **HttpSession은 오직 “구글 OAuth 플로우 동안만 임시로 사용하는 용도”

      로그인/인증은 여전히 JWT가 담당.

 

 

GmailController – 세션 대신 User 엔티티 + JWT Authentication 사용

 

@GetMapping("/messages/save-top10")
public ResponseEntity<?> saveTop10Messages(Authentication authentication) {
    User user = getUserWithGoogleToken(authentication);
    String accessToken = user.getGoogleAccessToken();

    int top10 = 10;
    String mailListTop10Response = gmailWebClient.get()
            .uri(uriBuilder -> uriBuilder
                    .path("/users/me/messages")
                    .queryParam("maxResults", top10)
                    .build())
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
            .retrieve()
            .bodyToMono(String.class)
            .block();

    // ... 파싱 + Gmail 엔티티 저장 ...
}

 

getUserWithGoogleToken(authentication):

private User getUserWithGoogleToken(Authentication authentication) {
    if (authentication == null || !authentication.isAuthenticated()) {
        throw new UsernameNotFoundException("인증되지 않은 사용자입니다.");
    }

    User user = userRepository.findByUsername(authentication.getName())
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));

    if (user.getGoogleEmail() == null || user.getGoogleAccessToken() == null) {
        throw new UsernameNotFoundException("구글 계정이 연동되어 있지 않거나 액세스 토큰이 없습니다.");
    }

    return user;
}

 

 

👉 중요한 변화:

  • 예전에는 HttpSession이나 OAuth2AuthenticationToken에서 구글 토큰을 꺼내 쓰려 했다면,
  • 지금은 User 엔티티에 저장된 googleAccessToken을 사용.
  • 어떤 요청이든 JWT로 유저 식별 → DB의 User에서 구글 토큰 꺼냄 구조.

 


프론트에서 어떻게 바뀌었는지

 

axios 설정: 쿠키 → Authorization 헤더

예전에는 withCredentials: true + 세션 쿠키 의존

 

변경 axiosConfig.js

import axios from "axios";
import { getToken } from "./tokenHelper";

const api = axios.create({
  baseURL: "백엔드 주소 (로컬/EC2)",
  withCredentials: true, // (필요하면 유지)
});

api.interceptors.request.use((config) => {
  const token = getToken();

  const skipAuthUrls = ["/api/auth/me2"]; // 세션 기반일 때 썼던 것, 지금은 거의 안 쓰거나 삭제 예정
  const url = config.url || "";
  const shouldSkip = skipAuthUrls.some((u) => url.startsWith(u));

  if (token && !shouldSkip) {
    config.headers = config.headers || {};
    config.headers.Authorization = `Bearer ${token}`; // ✅ JWT 붙이기
  }

  return config;
});

export default api;

👉 모든 보호된 API 호출은 JWT를 헤더에 실어서 보냄.

 

 

로그인 페이지 – form submit → /api/auth/login (JWT 받기)

const handleLogin = async () => {
  try {
    const res = await api.post("/api/auth/login", {
      username,
      password,
    });

    // res.data = LoginResponseDto
    const { token, username, name, birth, userRole } = res.data;

    // 1) JWT 저장
    localStorage.setItem("token", token);

    // 2) UserContext 또는 상위 App 상태 업데이트
    //    setUser(username), setUserRole(userRole), setBirth(birth)

    // 3) 홈이나 일정 페이지로 이동
    navigate("/schedule");
  } catch (e) {
    // 401이면 "아이디 또는 비밀번호가 올바르지 않습니다." 표시
  }
};

 

 

UserContext – 전역으로 유저 / 토큰 / 구글연동 상태 관리

export const UserContext = createContext(null);

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [userRole, setUserRole] = useState(null);
  const [token, setToken] = useState(getToken());
  const [googleLinked, setGoogleLinked] = useState(false);
  const [googleEmail, setGoogleEmail] = useState(null);
  const [authLoading, setAuthLoading] = useState(true);

  const fetchMe = async () => {
    const jwt = getToken();
    if (!jwt) {
      setUser(null);
      setUserRole(null);
      setGoogleLinked(false);
      setGoogleEmail(null);
      setAuthLoading(false);
      return;
    }

    try {
      const res = await api.get("/api/auth/me");
      const data = res.data;

      setUser(data.username);
      setUserRole(data.userRole);
      setGoogleLinked(!!data.googleLinked);
      setGoogleEmail(data.googleEmail || null);
    } catch (e) {
      setUser(null);
      setUserRole(null);
      setGoogleLinked(false);
      setGoogleEmail(null);
    } finally {
      setAuthLoading(false);
    }
  };

  useEffect(() => {
    fetchMe();
  }, []);

  const handleLoginSuccess = (loginResponseDto) => {
    const jwt = loginResponseDto.token;
    localStorage.setItem("token", jwt);
    setToken(jwt);
    fetchMe(); // 로그인 후 사용자 정보/구글연동 상태 재조회
  };

  const logout = () => {
    localStorage.removeItem("token");
    setToken(null);
    setUser(null);
    setUserRole(null);
    setGoogleLinked(false);
    setGoogleEmail(null);
  };

  const value = {
    user,
    userRole,
    token,
    googleLinked,
    googleEmail,
    authLoading,
    login: handleLoginSuccess,
    logout,
    refetchUser: fetchMe,
  };

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

 

👉 이걸 쓰면

 

  • 앱이 처음 로딩될 때 /api/auth/me 한 번 호출해서
    • 로그인 상태인지,
    • 어떤 유저인지,
    • 구글 연동 여부까지 전역으로 관리.
  • Navbar, Schedule, CalendarNav 등에서 같은 UserContext 값 공유.

 


폼로그인 → JWT 전환의 효과

 

 

  • 프론트/백 완전 분리에 적합
    • 프론트: S3 정적 호스팅, 백엔드: EC2
    • 도메인이 달라도 JWT로 인증이 가능
  • 서버 stateless → 확장성↑
    • 세션 공유/클러스터링 없이 서버를 여러 대로 늘릴 수 있음
  • React SPA와 자연스럽게 연동
    • 로그인부터 라우팅, 에러 처리까지 프론트에서 주도
    • 백엔드는 깔끔한 REST API 역할만 수행
  • 구글 연동, Gmail, AI 요약 기능 확장에 유리
    • JWT로 “누구인지” 항상 알 수 있고
    • User 엔티티에 외부 서비스 토큰/이메일을 안전하게 저장
  • 보안/구조적으로 명확해짐
    • 인증: JWT + Security 필터
    • 인가(권한): UserRole + hasRole("ADMIN"/"USER")
    • 외부 연동: OAuth2는 “연동용”으로만 사용

 

전체적인 흐름 파악하기

 

# 로그인 흐름

[React 로그인 페이지]
    ↓ (1. username/password 로 POST)
[백엔드 /api/auth/login]
    ↓ (2. AuthenticationManager로 계정 검증)
[JwtUtil]

1) React에서 POST /api/auth/login (JSON: { username, password })
2) Spring Security AuthenticationManager가 아이디/비번 검증
3) 성공 시:
   - User 엔티티 조회 (role, name, birth 등)
   - JwtUtil.generateToken(username, role) 로 JWT 생성
4) 백엔드 → 프론트:
   - JSON 응답: { username, name, birth, userRole, token }
5) 프론트:
   - token을 localStorage에 저장
   - UserContext / App state에 user, userRole 세팅
   - /schedule 같은 페이지로 라우팅
6) 이후 모든 API 호출 시:
   - Authorization: Bearer <JWT> 헤더를 붙여서 백엔드 호출
  • 서버는 로그인 상태를 저장하지 않고, 매 요청마다 토큰만 검증 → stateless

 

# JWT필터 요청 인증 처리 흐름

[React / axios]
    ↓ (1. Authorization: Bearer <JWT> 헤더 포함)
[Spring Security Filter Chain]
    ↓ (2. JwtAuthenticationFilter)
[컨트롤러 (@RestController)]

1) axios가 보호된 API 호출:
   - 예: GET /api/auth/me
   - 헤더: Authorization: Bearer xxx.yyy.zzz

2) JwtAuthenticationFilter:
   - Authorization 헤더에서 JWT 추출
   - JwtUtil.extractUsername(token) 로 username 파싱
   - JwtUtil.validateToken(token, userDetails) 로 유효성 검증

3) 토큰 유효 시:
   - UserSecurityService.loadUserByUsername(username)
   - UsernamePasswordAuthenticationToken 생성
   - SecurityContextHolder에 Authentication 저장

4) 컨트롤러 진입:
   - 파라미터로 Authentication 주입 가능
   - 예: public ResponseEntity<?> me(Authentication authentication)

 

 

# 현재 로그인 유저 조회 (/api/auth/me)

[React (앱 처음 로딩 / 새로고침 시)]
    ↓ (1. GET /api/auth/me)
[JwtAuthenticationFilter]
    ↓ (2. Authentication 설정)
[AuthController.me]

1) React가 localStorage에 남아있는 JWT를 확인
2) 있으면 axios로 GET /api/auth/me 호출
3) JwtAuthenticationFilter에서 토큰 검증 → Authentication 세팅
4) AuthController.me(Authentication auth):

   - auth.getName() 으로 username 확인
   - UserRepository.findByUsername(username)
   - 사용자 정보 + googleEmail + googleLinked 계산
   - JSON 응답: { username, name, birth, userRole, googleEmail, googleLinked }

5) React UserContext:
   - user, userRole, googleLinked, googleEmail 상태 세팅
   - Navbar, Schedule, CalendarNav 등에서 공통으로 사용

 

  • 앱이 켜질 때마다 “지금 누가 로그인 되어 있는가 + 구글 연동 여부”를 하나의 API로 해결
  • 세션 없이 JWT만으로 유저 상태를 전역 관리하는 기반 제공

 

 

# 구글 연동(OAuth) 전체 흐름

[1. 프론트에서 연동 버튼 클릭]
Navbar.handleGoogleLogin()
    ↓
- 로그인 안 되어 있으면: /login 으로 이동
- 로그인 되어 있고 token 존재:
  const redirectUrl =
    `${API_BASE_URL}/oauth2/authorization/google?jwt_token=${token}`;
  window.location.href = redirectUrl;

[2. 백엔드: OAuth2AuthorizationRequestResolver]
    ↓
- 요청 파라미터 jwt_token 읽어서
  HttpSession에 LINK_JWT 로 저장
- 구글 OAuth2 authorization 요청 생성
- 추가로 prompt=select_account 설정

[3. 구글 로그인/계정 선택 화면]
    ↓
- 유저가 구글 계정 선택 & 동의

[4. 백엔드: CustomOAuth2UserService]
    ↓
- 세션에서 LINK_JWT 꺼냄
- JWT 검증 → 우리 서비스 User 찾기
- 구글에서 받은 이메일, 액세스 토큰을
  User 엔티티(googleEmail, googleAccessToken)에 저장
- 이미 다른 계정에 연동된 이메일이면
  OAuth2AuthenticationException 던져서 실패 처리

[5. OAuth2LoginSuccess / defaultSuccessUrl]
    ↓
- 성공 시 프론트 /schedule 등으로 리다이렉트
- 이후 /api/auth/me 호출 시 googleLinked = true

 

  • **로그인 수단이 아니라 “연동 수단”**으로 구글 OAuth를 사용
  • JWT로 이미 로그인된 MailBuddy 계정과 구글 계정을 1:1로 매칭
  • 구글 액세스 토큰은 User 엔티티에 암호화(Jasypt)하여 저장

 

# Gmail 가져오기 & AI 요약 흐름

Gmail 저장

[1. 사용자가 캘린더 상단에서 버튼 클릭]
CalendarNav.handleSaveEmails()
    ↓
- GET /api/gmail/messages/save-top10

[2. GmailController.saveTop10Messages(Authentication auth)]
    ↓
- getUserWithGoogleToken(auth):
    - JWT 기반 Authentication에서 username 가져오기
    - UserRepository로 User 조회
    - googleEmail, googleAccessToken 있는지 확인
- gmailWebClient 로 구글 Gmail API 호출:
    - /users/me/messages?maxResults=10
    - 각 메시지 상세 조회 → 발신자, 제목, 날짜, 본문 파싱
    - Base64 디코딩, HTML/텍스트 추출
    - EmailTimeParser로 한국 시간 변환
    - Gmail 엔티티로 DB에 저장 (messageId로 중복 방지)
- 결과: "N개 메일을 DB에 저장 완료했습니다." 응답

 

 

AI 요약

[1. 사용자가 "AI 요약하기" 버튼 클릭]
CalendarNav.handleSummarize()
    ↓
- POST /api/summarize

[2. Summary 관련 서비스]
    ↓
- JWT Authentication 기반으로 User 찾기
- 해당 User의 Gmail 엔티티 목록 조회
- 각 메일 본문 + 프롬프트로 Mistral API 호출
- JSON 형태의 SummaryDto(title, eventDate, time, place, notes ...) 생성
- Summary를 DB의 일정 엔티티로 저장/업데이트

[3. 프론트]
    ↓
- AI 요약 완료 후 loadSummaries() 호출
- 달력/메모 영역에서 요약된 일정 표시
  • 모든 Gmail/요약 기능은 항상 JWT → User 엔티티 → 구글 토큰/메일 

 

 

 

 


📌  전체 정리

 

로그인하면 /api/auth/login에서 서버가 세션을 만들지 않고 → JWT를 발급해서 프론트가 localStorage에 저장

이후 모든 API 요청은 Authorization: Bearer <JWT> 헤더로 인증하고,

스프링 시큐리티의 JwtAuthenticationFilter가 토큰을 검증해 SecurityContext에 Authentication을 세팅

서버는 세션 상태를 갖지 않는 구조가 되었고, /api/auth/me를 통해 현재 로그인한 사용자 정보와 구글 연동 여부를 언제든지 조회할 가능

구글 로그인은  로그인 수단이 아니라, JWT로 인증된 기존 사용자 계정에 구글 이메일과 액세스 토큰을 연결하는 “연동” 용도로만 사용 → 연동 정보는 User 엔티티에 저장됩니다.

프론트에서는 React의 UserContext로 user, userRole, token, googleLinked를 전역 관리하고, axios 인터셉터에서 JWT를 자동으로 붙여 모든 보호된 API를 호출합니다.

이 덕분에 로그인 UI, 구글 연동 버튼, Gmail/AI 요약 기능 노출 여부를 모두 프론트에서 제어할 수 있고, 서버는 세션 없는 REST API 서버로 깔끔하게 정리되었습니다.