AngelPlayer`s Diary

 

앞선 포스트에서 Google OAuth와 SpringBoot, Next.js로 간단히 소셜 로그인과 방명록 기능을 구현해보았습니다.

 

기존 구현에서는 로그인한 사용자를 인증하기 위해 Session 방식을 사용하였습니다.

 

하지만 최근에는 

 

 

이전 포스트의 소스코드 : 세션방식
Google OAuth 로그인 → Spring Security 세션에 OAuth2User 저장
브라우저는 JSESSIONID 쿠키로 로그인 유지
컨트롤러에서 @AuthenticationPrincipal OAuth2User principal 사용

 


앞으로 개선할 방향 : JWT 방식
OAuth 로그인 성공 시
서버가 Access Token / Refresh Token(JWT) 를 생성
이 둘을 HttpOnly 쿠키(예: ACCESS_TOKEN, REFRESH_TOKEN) 로 내려줌

프론트는 그냥 credentials: 'include' 그대로 사용 → 쿠키 자동 전송
이후 API 호출

JSESSIONID 대신 ACCESS_TOKEN 쿠키를 읽어 검증
토큰이 유효하면 SecurityContext 에 Authentication 넣어줌
컨트롤러는 @AuthenticationPrincipal 로 사용자 정보 사용

세션은 더 이상 인증에 쓰이지 않음

 

 

 

백엔드 수정 사항

com/tistory/angelplayer/oauth
    ├── OauthApplication.java                      # 스프링 부트 메인 클래스
    │
    ├── config
    │    ├── SecurityConfig.java                   # ★ 시큐리티 설정 (OAuth2 + JWT + CORS)
    │    ├── JwtTokenProvider.java                 # ★ JWT 생성/검증 유틸
    │    └── JwtAuthenticationFilter.java          # ★ 요청마다 AccessToken 검증 필터
    │
    ├── auth
    │    ├── controller
    │    │      └── AuthController.java            # ★ /me, /refresh, /logout API
    │    │
    │    ├── handler
    │    │      └── OAuth2LoginSuccessHandler.java # ★ 구글 로그인 성공 → JWT 발급 & 쿠키 저장
    │    │
    │    ├── entity
    │    │      └── RefreshToken.java              # ★ 사용자별 RefreshToken 저장 엔티티
    │    │
    │    ├── repository
    │    │      └── RefreshTokenRepository.java    # ★ RefreshToken JPA 리포지토리
    │    │
    │    └── service
    │           └── AuthService.java               # ★ RefreshToken 저장/검증 & 토큰 재발급 로직
    │
    └── guestbook
         ├── controller
         │      └── GuestbookController.java       # 방명록 API (JWT 인증 필요)
         ├── dto
         │      └── GuestbookRequestDto.java
         ├── entity
         │      └── GuestbookEntry.java
         ├── repository
         │      └── GuestbookEntryRepository.java
         └── service
                └── GuestbookService.java

이번에 변경될 새로운 파일 구조입니다.

 

 

 

1) build.gardle

dependencies {
    // ... 기존 내용

    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

JWT 서명용 키와 유효시간을 설정합니다.

JwtTokenProvider 가 이 값을 읽어서 토큰을 생성/검증합니다.

 

 

 

 

2) application.yml 에 JWT 설정 값 추가

jwt:
  secret: "ZmFrZV9zZWNyZXRfZmFrZV9zZWNyZXRfZmFrZV9zZWNyZXQ="  # 32바이트 이상 Base64 문자열
  access-token-validity-in-seconds: 3600       # Access Token 1시간
  refresh-token-validity-in-seconds: 604800    # Refresh Token 7일

jwt:는 spring:과 동급입니다.

 

하위에 넣으시면 안됩니다.

 

 

 

 

3) JwtTokenProvider.java (JWT 생성/검증 유틸)

package com.tistory.angelplayer.oauth.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Collections;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.access-token-validity-in-seconds}")
    private long accessTokenValidityInSeconds;

    @Value("${jwt.refresh-token-validity-in-seconds}")
    private long refreshTokenValidityInSeconds;

    private Key key;

    @PostConstruct
    public void init() {
        // application.yml의 Base64 secret 문자열을 디코딩해서 HMAC 키 생성
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    // AccessToken 생성 (email, name claim 포함)
    public String createAccessToken(String email, String name) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + accessTokenValidityInSeconds * 1000);

        return Jwts.builder()
                .setSubject(email)          // 토큰 주체: 이메일
                .claim("name", name)        // 클레임에 이름 저장 (필요 시 사용)
                .setIssuedAt(now)           // 발급 시각
                .setExpiration(expiry)      // 만료 시각
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    // RefreshToken 생성
    public String createRefreshToken(String email) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + refreshTokenValidityInSeconds * 1000);

        return Jwts.builder()
                .setSubject(email)
                .setIssuedAt(now)
                .setExpiration(expiry)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    // 토큰 유효성 검증 (서명 + 만료 체크)
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            // 서명 오류, 만료, 형식 오류 등 → 전부 false 처리
            return false;
        }
    }

    // 토큰에서 Claims 꺼내기 (이메일, name 등 필요하면 사용)
    public Claims getClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    // 토큰에서 Authentication 객체 생성
    public Authentication getAuthentication(String token) {
        Claims claims = getClaims(token);
        String email = claims.getSubject();

        // 현재는 ROLE_USER 하나만 부여
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");

        User principal = new User(
                email,                               // username
                "",                                  // password (사용 안 함)
                Collections.singletonList(authority) // 권한 리스트
        );

        return new UsernamePasswordAuthenticationToken(
                principal,
                token,                               // credentials 자리에 토큰 자체
                principal.getAuthorities()
        );
    }

    public long getAccessTokenValidityInSeconds() {
        return accessTokenValidityInSeconds;
    }

    public long getRefreshTokenValidityInSeconds() {
        return refreshTokenValidityInSeconds;
    }
}

AccessToken / RefreshToken 생성

토큰의 유효성 검증

토큰에서 Authentication 객체 만들기 (email 기반 UserDetails)

 

 

 

4) JwtAuthenticationFilter.java (요청마다 AccessToken 인증)

// com/tistory/angelplayer/oauth/config/JwtAuthenticationFilter.java

package com.tistory.angelplayer.oauth.config;

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 org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

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

        String token = resolveToken(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    // 쿠키(ACCESS_TOKEN) 또는 Authorization 헤더에서 토큰 추출
    private String resolveToken(HttpServletRequest request) {
        // 1) 쿠키 우선
        if (request.getCookies() != null) {
            for (Cookie cookie : request.getCookies()) {
                if ("ACCESS_TOKEN".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }

        // 2) Authorization: Bearer 토큰도 지원 (필요시)
        String bearer = request.getHeader("Authorization");
        if (bearer != null && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }

        return null;
    }
}

 

매 HTTP 요청마다 쿠키의 ACCESS_TOKEN 또는 Authorization 헤더에서 토큰을 읽고 검증

유효하면 SecurityContext 에 Authentication 세팅

세션 없이 JWT만으로 인증을 처리

 

 

 

5. RefreshToken.java (RefreshToken 엔티티)

// com/tistory/angelplayer/oauth/auth/entity/RefreshToken.java

package com.tistory.angelplayer.oauth.auth.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.Instant;

@Entity
@Table(name = "refresh_tokens")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RefreshToken {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 이메일을 사용자 식별자처럼 사용 (별도 User 엔티티 없으므로)
    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false, length = 512)
    private String token;

    @Column(nullable = false)
    private Instant expiry;
}

사용자(email)별 현재 유효한 RefreshToken 을 DB에 저장

재발급 시 저장된 토큰과 비교하여 탈취/중복 사용 방지

 

 

 

6) RefreshTokenRepository.java (리포지토리)

package com.tistory.angelplayer.oauth.auth.repository;

import com.tistory.angelplayer.oauth.auth.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

    Optional<RefreshToken> findByEmail(String email);

    Optional<RefreshToken> findByToken(String token);

    void deleteByEmail(String email);
}

이메일 기준으로 RefreshToken을 조회/저장/삭제

 

 

 

7) AuthService.java (RefreshToken 관리 + 재발급 로직)

package com.tistory.angelplayer.oauth.auth.service;

import com.tistory.angelplayer.oauth.auth.entity.RefreshToken;
import com.tistory.angelplayer.oauth.auth.repository.RefreshTokenRepository;
import com.tistory.angelplayer.oauth.config.JwtTokenProvider;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class AuthService {

    private final JwtTokenProvider jwtTokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;

    // 로그인 성공 시 RefreshToken 저장/업데이트
    @Transactional
    public void saveRefreshToken(String email, String refreshToken) {
        Instant expiry = Instant.now()
                .plusSeconds(jwtTokenProvider.getRefreshTokenValidityInSeconds());

        refreshTokenRepository.findByEmail(email)
                .ifPresentOrElse(
                        entity -> {
                            entity.setToken(refreshToken);
                            entity.setExpiry(expiry);
                        },
                        () -> {
                            RefreshToken newToken = RefreshToken.builder()
                                    .email(email)
                                    .token(refreshToken)
                                    .expiry(expiry)
                                    .build();
                            refreshTokenRepository.save(newToken);
                        }
                );
    }

    // 로그아웃 시 RefreshToken 삭제
    @Transactional
    public void deleteRefreshToken(String email) {
        refreshTokenRepository.deleteByEmail(email);
    }

    // RefreshToken으로 AccessToken + RefreshToken 재발급
    @Transactional
    public Map<String, String> reissueTokens(String refreshToken) {
        // 1) 토큰 형식/만료 검증
        if (!jwtTokenProvider.validateToken(refreshToken)) {
            throw new IllegalArgumentException("유효하지 않거나 만료된 RefreshToken 입니다.");
        }

        Claims claims = jwtTokenProvider.getClaims(refreshToken);
        String email = claims.getSubject();

        // 2) DB에 저장된 RefreshToken과 일치하는지 확인
        RefreshToken saved = refreshTokenRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException("저장된 RefreshToken이 없습니다."));

        if (!saved.getToken().equals(refreshToken)) {
            throw new IllegalArgumentException("RefreshToken이 일치하지 않습니다.");
        }

        if (saved.getExpiry().isBefore(Instant.now())) {
            throw new IllegalArgumentException("RefreshToken이 만료되었습니다.");
        }

        // 3) 새 AccessToken + RefreshToken 생성
        String newAccessToken = jwtTokenProvider.createAccessToken(email, (String) claims.get("name"));
        String newRefreshToken = jwtTokenProvider.createRefreshToken(email);

        // 4) DB에 RefreshToken 업데이트
        saveRefreshToken(email, newRefreshToken);

        return Map.of(
                "accessToken", newAccessToken,
                "refreshToken", newRefreshToken
        );
    }
}

로그인 성공 시 RefreshToken 저장/업데이트

전달받은 RefreshToken 검증 후, 새 AccessToken + RefreshToken 발급

로그아웃 시 RefreshToken 삭제

 

 

 

8) OAuth2LoginSuccessHandler.java (구글 로그인 성공 → JWT + 쿠키)

package com.tistory.angelplayer.oauth.auth.handler;

import com.tistory.angelplayer.oauth.auth.service.AuthService;
import com.tistory.angelplayer.oauth.config.JwtTokenProvider;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {

    private final JwtTokenProvider jwtTokenProvider;
    private final AuthService authService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws IOException, ServletException {

        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        Map<String, Object> attrs = oAuth2User.getAttributes();

        String email = (String) attrs.get("email");
        String name  = (String) attrs.get("name");

        // 1) Access / Refresh Token 생성
        String accessToken  = jwtTokenProvider.createAccessToken(email, name);
        String refreshToken = jwtTokenProvider.createRefreshToken(email);

        // 2) RefreshToken DB 저장
        authService.saveRefreshToken(email, refreshToken);

        // 3) 쿠키에 저장 (HttpOnly)
        addCookie(response, "ACCESS_TOKEN", accessToken,
                (int) jwtTokenProvider.getAccessTokenValidityInSeconds());
        addCookie(response, "REFRESH_TOKEN", refreshToken,
                (int) jwtTokenProvider.getRefreshTokenValidityInSeconds());

        // 4) 프론트엔드 페이지로 리다이렉트
        response.sendRedirect("http://localhost:3000/guestbook");
    }

    private void addCookie(HttpServletResponse response,
                           String name,
                           String value,
                           int maxAge) {

        Cookie cookie = new Cookie(name, value);
        cookie.setHttpOnly(true);  // JS에서 접근 불가 → XSS 방지
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        // 개발 환경에서는 http, 운영에서는 https + Secure 설정 권장
        // cookie.setSecure(true);

        response.addCookie(cookie);
    }
}

Google OAuth2 로그인 성공 시 아래 기능을 수행

- 구글 프로필에서 email, name 추출
- AccessToken + RefreshToken 생성
- RefreshToken DB 저장
- 두 토큰을 HttpOnly 쿠키 로 브라우저에 전달
- 프론트 /guestbook 페이지로 리다이렉트

 

 

 

9) SecurityConfig.java (시큐리티 설정 변경)

package com.tistory.angelplayer.oauth.config;

import com.tistory.angelplayer.oauth.auth.handler.OAuth2LoginSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .csrf(csrf -> csrf.disable())
                .cors(Customizer.withDefaults())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/", "/oauth2/**", "/login/**").permitAll()
                        .requestMatchers("/api/auth/**").permitAll() // /me, /refresh, /logout
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth -> oauth
                        .successHandler(oAuth2LoginSuccessHandler)
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .deleteCookies("ACCESS_TOKEN", "REFRESH_TOKEN")
                        .logoutSuccessUrl("http://localhost:3000")
                );

        // JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가
        http.addFilterBefore(
                new JwtAuthenticationFilter(jwtTokenProvider),
                UsernamePasswordAuthenticationFilter.class
        );

        return http.build();
    }

    // CORS: 프론트 개발용 설정
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true); // 쿠키 포함 허용

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

세션을 사용하지 않고 STATELESS 로 설정

OAuth2 로그인 성공 시 OAuth2LoginSuccessHandler 사용

JwtAuthenticationFilter 를 필터 체인에 추가

CORS 설정 (localhost:3000 허용)

/api/auth/**, /oauth2/** 등 일부 엔드포인트는 인증 없이 허용

 

 

 

10) AuthController.java (me + refresh + logout API)

package com.tistory.angelplayer.oauth.auth.controller;

import com.tistory.angelplayer.oauth.auth.service.AuthService;
import com.tistory.angelplayer.oauth.config.JwtTokenProvider;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final JwtTokenProvider jwtTokenProvider;
    private final AuthService authService;

    // 1) 현재 로그인 정보 조회
    @GetMapping("/me")
    public Map<String, Object> me(@AuthenticationPrincipal UserDetails user) {
        if (user == null) {
            return Map.of("authenticated", false);
        }
        return Map.of(
                "authenticated", true,
                "email", user.getUsername()
        );
    }

    // 2) RefreshToken으로 AccessToken + RefreshToken 재발급
    @PostMapping("/refresh")
    public Map<String, Object> refresh(HttpServletRequest request,
                                       HttpServletResponse response) {

        String refreshToken = extractCookie(request, "REFRESH_TOKEN");
        if (refreshToken == null) {
            throw new IllegalArgumentException("RefreshToken 쿠키가 없습니다.");
        }

        Map<String, String> tokens = authService.reissueTokens(refreshToken);

        // 쿠키 재설정
        addCookie(response, "ACCESS_TOKEN", tokens.get("accessToken"),
                (int) jwtTokenProvider.getAccessTokenValidityInSeconds());
        addCookie(response, "REFRESH_TOKEN", tokens.get("refreshToken"),
                (int) jwtTokenProvider.getRefreshTokenValidityInSeconds());

        // 응답 바디에는 간단히 ok 정도만 내려도 되고, 토큰을 넣어도 됩니다.
        return Map.of(
                "status", "ok"
        );
    }

    // 3) 로그아웃 (RefreshToken DB 삭제 + 쿠키 삭제)
    @PostMapping("/logout")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void logout(@AuthenticationPrincipal UserDetails user,
                       HttpServletResponse response) {

        if (user != null) {
            authService.deleteRefreshToken(user.getUsername());
        }

        // 쿠키 삭제
        deleteCookie(response, "ACCESS_TOKEN");
        deleteCookie(response, "REFRESH_TOKEN");
    }

    // ====== 유틸 메서드 ======

    private String extractCookie(HttpServletRequest request, String name) {
        if (request.getCookies() == null) return null;
        for (Cookie cookie : request.getCookies()) {
            if (name.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
        return null;
    }

    private void addCookie(HttpServletResponse response,
                           String name,
                           String value,
                           int maxAge) {

        Cookie cookie = new Cookie(name, value);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }

    private void deleteCookie(HttpServletResponse response, String name) {
        Cookie cookie = new Cookie(name, "");
        cookie.setPath("/");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }
}

/api/auth/me
- 현재 로그인 정보(이메일) 확인

/api/auth/refresh
- RefreshToken 쿠키를 읽어 새 Access/Refresh 토큰 발급
- 쿠키 갱신

/api/auth/logout
- RefreshToken DB 삭제 + 쿠키 삭제

 

 

 

11) GuestbookController.java (JWT 기반으로 사용자 정보 받기)

package com.tistory.angelplayer.oauth.guestbook.controller;

import com.tistory.angelplayer.oauth.guestbook.dto.GuestbookRequestDto;
import com.tistory.angelplayer.oauth.guestbook.entity.GuestbookEntry;
import com.tistory.angelplayer.oauth.guestbook.service.GuestbookService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@RestController
@RequestMapping("/api/guestbook")
@RequiredArgsConstructor
public class GuestbookController {

    private final GuestbookService guestbookService;

    @GetMapping
    public List<GuestbookEntry> list() {
        return guestbookService.getAll();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public GuestbookEntry create(@AuthenticationPrincipal UserDetails user,
                                 @RequestBody GuestbookRequestDto dto) {
        if (user == null) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
        }

        if (dto.getMessage() == null || dto.getMessage().trim().isEmpty()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "메시지를 입력하세요.");
        }

        String email = user.getUsername();
        String name = "익명"; // JWT claim에서 name을 꺼내고 싶으면 추가 작업 가능

        return guestbookService.createEntry(email, name, dto);
    }
}

@AuthenticationPrincipal UserDetails user 를 사용하여 JWT에서 나온 사용자 이메일을 이용해 글 작성자 정보 설정

 

 

 

12) GuestbookService.java

package com.tistory.angelplayer.oauth.guestbook.service;

import com.tistory.angelplayer.oauth.guestbook.dto.GuestbookRequestDto;
import com.tistory.angelplayer.oauth.guestbook.entity.GuestbookEntry;
import com.tistory.angelplayer.oauth.guestbook.repository.GuestbookEntryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
public class GuestbookService {

    private final GuestbookEntryRepository guestbookEntryRepository;

    /**
     * 방명록 전체 목록 조회
     */
    public List<GuestbookEntry> getAll() {
        // 단순 전체 조회. 필요하면 정렬 조건 추가 가능
        return guestbookEntryRepository.findAll();
    }

    /**
     * 방명록 글 등록
     * @param email 작성자 이메일 (JWT에서 추출)
     * @param name  작성자 이름(또는 닉네임)
     * @param dto   요청 DTO (message 등)
     */
    public GuestbookEntry createEntry(String email, String name, GuestbookRequestDto dto) {
        GuestbookEntry entry = new GuestbookEntry();
        entry.setUserEmail(email);
        entry.setUserName(name);
        entry.setMessage(dto.getMessage());
        entry.setCreatedAt(LocalDateTime.from(Instant.now()));

        return guestbookEntryRepository.save(entry);
    }
}

.getAll() 추가

 

 

 

Front-end의 경우 기존 소스코드를 그대로 유지해도 새롭게 변경된 JWT 기능이 정상적으로 동작합니다.

 

 

 

 

 

공유하기

facebook twitter kakaoTalk kakaostory naver band