Skip to content
Merged
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
@@ -0,0 +1,52 @@
package com.back.domain.user.controller;

import com.back.domain.user.service.UserAuthService;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "UserAuth", description = "사용자 인증 API")
@Slf4j
@RestController
@RequestMapping("/api/user/auth")
@RequiredArgsConstructor
public class UserAuthController {

private final UserAuthService userAuthService;

//400 Bad Request: 클라이언트가 잘못된 요청을 보냄 (형식 오류)
//401 Unauthorized: 인증 실패 (토큰 없음/만료/유효하지 않음)
//404 Not Found: 리소스를 찾을 수 없음
@Operation(summary = "토큰 갱신", description = "리프레시 토큰으로 새로운 액세스 토큰을 발급")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "토큰 갱신 성공"),
@ApiResponse(responseCode = "401", description = "토큰이 유효하지 않거나 만료됨")
})
@PostMapping("/refresh")
public RsData<Void> refreshToken(HttpServletRequest request, HttpServletResponse response) {
boolean success = userAuthService.refreshTokens(request, response);

if (success) {
return RsData.of(200, "토큰이 성공적으로 갱신되었습니다.");
} else {
return RsData.of(401, "토큰 갱신에 실패했습니다. 다시 로그인해주세요.");
}
}

@Operation(summary = "로그아웃", description = "현재 세션을 종료하고 토큰을 무효화")
@ApiResponse(responseCode = "200", description = "로그아웃 성공")
@PostMapping("/logout")
public RsData<Void> logout(HttpServletRequest request, HttpServletResponse response) {
userAuthService.logout(request, response);
return RsData.of(200, "로그아웃되었습니다.");
}
}
139 changes: 139 additions & 0 deletions src/main/java/com/back/domain/user/service/UserAuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.back.domain.user.service;

import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.ServiceException;
import com.back.global.jwt.JwtUtil;
import com.back.global.jwt.refreshToken.entity.RefreshToken;
import com.back.global.jwt.refreshToken.repository.RefreshTokenRepository;
import com.back.global.jwt.refreshToken.service.RefreshTokenService;
import com.back.global.rsData.RsData;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserAuthService {

private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final RefreshTokenService refreshTokenService;
private final RefreshTokenRepository refreshTokenRepository;

//OAuth 관련

public User joinSocial(String oauthId, String email, String nickname){
userRepository.findByOauthId(oauthId)
.ifPresent(user -> {
throw new ServiceException(409, "이미 존재하는 계정입니다.");
});

// 고유한 닉네임 생성
String uniqueNickname = generateUniqueNickname(nickname);

User user = User.builder()
.email(email)
.nickname(uniqueNickname)
.abvDegree(0.0)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.role("USER")
.oauthId(oauthId)
.build();

return userRepository.save(user);
}

@Transactional
public RsData<User> findOrCreateOAuthUser(String oauthId, String email, String nickname) {
Optional<User> existingUser = userRepository.findByOauthId(oauthId);

if (existingUser.isPresent()) {
// 기존 사용자 업데이트 (이메일만 업데이트)
User user = existingUser.get();
user.setEmail(email);
return RsData.of(200, "회원 정보가 업데이트 되었습니다", user); //더티체킹
} else {
User newUser = joinSocial(oauthId, email, nickname);
return RsData.of(201, "사용자가 생성되었습니다", newUser);
}
}

public String generateUniqueNickname(String baseNickname) {
// null이거나 빈 문자열인 경우 기본값 설정
if (baseNickname == null || baseNickname.trim().isEmpty()) {
baseNickname = "User";
}

String nickname = baseNickname;
int counter = 1;

// 중복 체크 및 고유한 닉네임 생성
while (userRepository.findByNickname(nickname).isPresent()) {
nickname = baseNickname + counter;
counter++;
}

return nickname;
}

// 리프레시 토큰 관련

public void issueTokens(HttpServletResponse response, Long userId, String email) {
String accessToken = jwtUtil.generateAccessToken(userId, email);
String refreshToken = refreshTokenService.generateRefreshToken(userId, email);

jwtUtil.addAccessTokenToCookie(response, accessToken);
jwtUtil.addRefreshTokenToCookie(response, refreshToken);
}

public boolean refreshTokens(HttpServletRequest request, HttpServletResponse response) {
try {
String oldRefreshToken = jwtUtil.getRefreshTokenFromCookie(request);

if (oldRefreshToken == null || !refreshTokenService.validateToken(oldRefreshToken)) {
return false;
}

Optional<RefreshToken> tokenData = refreshTokenRepository.findByToken(oldRefreshToken);
if (tokenData.isEmpty()) {
return false;
}

RefreshToken refreshTokenEntity = tokenData.get();
Long userId = refreshTokenEntity.getUserId();
String email = refreshTokenEntity.getEmail();

String newRefreshToken = refreshTokenService.rotateToken(oldRefreshToken);
String newAccessToken = jwtUtil.generateAccessToken(userId, email);

jwtUtil.addAccessTokenToCookie(response, newAccessToken);
jwtUtil.addRefreshTokenToCookie(response, newRefreshToken);

return true;
} catch (Exception e) {
log.error("토큰 갱신 중 오류 발생: {}", e.getMessage());
return false;
}
}

//토큰 끊기면서 OAuth 자동 로그아웃
public void logout(HttpServletRequest request, HttpServletResponse response) {
String refreshToken = jwtUtil.getRefreshTokenFromCookie(request);

if (refreshToken != null) {
refreshTokenService.revokeToken(refreshToken);
}

jwtUtil.removeAccessTokenCookie(response);
jwtUtil.removeRefreshTokenCookie(response);
}
}
58 changes: 0 additions & 58 deletions src/main/java/com/back/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@

import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.ServiceException;
import com.back.global.rsData.RsData;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class UserService {
Expand All @@ -24,59 +19,6 @@ public User findById(Long id) {
}


public User joinSocial(String oauthId, String email, String nickname){
userRepository.findByOauthId(oauthId)
.ifPresent(user -> {
throw new ServiceException(409, "이미 존재하는 계정입니다.");
});

// 고유한 닉네임 생성
String uniqueNickname = generateUniqueNickname(nickname);

User user = User.builder()
.email(email)
.nickname(uniqueNickname)
.abvDegree(0.0)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.role("USER")
.oauthId(oauthId)
.build();

return userRepository.save(user);
}

@Transactional
public RsData<User> findOrCreateOAuthUser(String oauthId, String email, String nickname) {
Optional<User> existingUser = userRepository.findByOauthId(oauthId);

if (existingUser.isPresent()) {
// 기존 사용자 업데이트 (이메일만 업데이트)
User user = existingUser.get();
user.setEmail(email);
return RsData.of(200, "회원 정보가 업데이트 되었습니다", user); //더티체킹
} else {
User newUser = joinSocial(oauthId, email, nickname);
return RsData.of(201, "사용자가 생성되었습니다", newUser);
}
}

public String generateUniqueNickname(String baseNickname) {
// null이거나 빈 문자열인 경우 기본값 설정
if (baseNickname == null || baseNickname.trim().isEmpty()) {
baseNickname = "User";
}

String nickname = baseNickname;
int counter = 1;

// 중복 체크 및 고유한 닉네임 생성
while (userRepository.findByNickname(nickname).isPresent()) {
nickname = baseNickname + counter;
counter++;
}

return nickname;
}

}
33 changes: 32 additions & 1 deletion src/main/java/com/back/global/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void removeAccessTokenCookie(HttpServletResponse response) {
response.addCookie(cookie);
}

public boolean validateToken(String token) {
public boolean validateAccessToken(String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
Expand Down Expand Up @@ -110,4 +110,35 @@ private Claims parseToken(String token) {
.getPayload();
}

public void addRefreshTokenToCookie(HttpServletResponse response, String refreshToken) {
Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 30);
response.addCookie(cookie);
}

public String getRefreshTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (REFRESH_TOKEN_COOKIE_NAME.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}

public void removeRefreshTokenCookie(HttpServletResponse response) {
Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, null);
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -21,16 +23,18 @@
import java.io.IOException;
import java.util.Map;

@Slf4j
@Component
@RequiredArgsConstructor
public class CustomAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final Rq rq;

@Value("${custom.accessToken.expirationSeconds}")
private int accessTokenExpiration;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.debug("Processing request for " + request.getRequestURI());

try {
work(request, response, filterChain);
} catch (ServiceException e) {
Expand Down Expand Up @@ -90,7 +94,7 @@ private void work(HttpServletRequest request, HttpServletResponse response, Filt

// accessToken 검증
if (isAccessTokenExists) {
if (jwtUtil.validateToken(accessToken)) {
if (jwtUtil.validateAccessToken(accessToken)) {
Long userId = jwtUtil.getUserIdFromToken(accessToken);
String email = jwtUtil.getEmailFromToken(accessToken);
String nickname = jwtUtil.getNicknameFromToken(accessToken);
Expand All @@ -113,7 +117,7 @@ private void work(HttpServletRequest request, HttpServletResponse response, Filt
// accessToken이 만료됐으면 새로 발급
if (isAccessTokenExists && !isAccessTokenValid) {
String newAccessToken = jwtUtil.generateAccessToken(user.getId(), user.getEmail());
rq.setCrossDomainCookie("accessToken", newAccessToken, 60 * 20);
rq.setCrossDomainCookie("accessToken", newAccessToken, accessTokenExpiration);
}

// SecurityContext에 인증 정보 저장
Expand Down
Loading