앞선 포스트에서 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 기능이 정상적으로 동작합니다.
| [Redis] Windows에 Redis 설치하기 (0) | 2025.12.20 |
|---|---|
| [OAuth] 01 Google OAuth + SpringBoot + Next로 간편 로그인 구현 (0) | 2025.12.01 |
| [URL] 구매한 도메인 주소 이전하기 (Cafe24 -> 가비아) (0) | 2024.11.07 |
| [SEO] 웹사이트 구축 후 검색 엔진 최적화 설정 (Feat. Next.js) (6) | 2024.10.16 |
| [Vercel] 도메인 주소 변경하기 (Feat. Cafe24) (0) | 2024.07.20 |