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,44 @@
package com.yeogido.backend.domain.auth.controller;

import com.yeogido.backend.domain.auth.dto.AuthResDTO;
import com.yeogido.backend.domain.auth.service.JwtTokenProvider;
import com.yeogido.backend.domain.user.entity.User;
import com.yeogido.backend.domain.user.exception.UserErrorCode;
import com.yeogido.backend.domain.user.repository.UserRepository;
import com.yeogido.backend.global.common.code.SuccessCode;
import com.yeogido.backend.global.common.response.ApiResponse;
import com.yeogido.backend.global.config.LocalDataInitializer;
import com.yeogido.backend.global.exception.GeneralException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "로컬 테스트 API", description = "local 프로필에서만 사용하는 개발 환경 전용 테스트 API")
@Profile("local")
@RestController
@RequestMapping("/api/v1/auth/test")
@RequiredArgsConstructor
public class LocalAuthTestController {

private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;

@Operation(summary = "로컬 테스트용 토큰 발급 API", description = "local 프로필에서만 사용할 수 있는 테스트 사용자 Access/Refresh Token을 발급합니다.")
@PostMapping("/token")
public ApiResponse<AuthResDTO.Token> issueTestToken() {
User user = userRepository.findByEmail(LocalDataInitializer.TEST_USER_EMAIL)
.orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND));

AuthResDTO.Token response = new AuthResDTO.Token(
user.getId(),
jwtTokenProvider.issueAccessToken(user),
jwtTokenProvider.issueRefreshToken(user)
);

return ApiResponse.onSuccess(SuccessCode.OK, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public enum AuthErrorCode implements ErrorCode {
HttpStatus.CONFLICT,
"AUTH4091",
"이미 가입된 소셜 계정입니다."
),

LOGIN_REQUIRED(
HttpStatus.UNAUTHORIZED,
"AUTH4011",
"로그인이 필요합니다."
);

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.yeogido.backend.domain.auth.security;

import com.yeogido.backend.domain.user.enums.UserRole;

public record AuthUser(
Long userId,
UserRole role
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.yeogido.backend.domain.auth.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yeogido.backend.domain.auth.exception.AuthErrorCode;
import com.yeogido.backend.domain.auth.service.JwtTokenProvider;
import com.yeogido.backend.global.common.response.ApiResponse;
import com.yeogido.backend.global.exception.GeneralException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String BEARER_PREFIX = "Bearer ";

private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String token = resolveToken(request);

if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return;
}

try {
AuthUser authUser = jwtTokenProvider.parseAccessToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
authUser,
null,
List.of(new SimpleGrantedAuthority("ROLE_" + authUser.role().name()))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (GeneralException e) {
SecurityContextHolder.clearContext();
writeUnauthorizedResponse(response);
}
}

private String resolveToken(HttpServletRequest request) {
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

if (!StringUtils.hasText(authorization) || !authorization.startsWith(BEARER_PREFIX)) {
return null;
}

return authorization.substring(BEARER_PREFIX.length());
}

private void writeUnauthorizedResponse(HttpServletResponse response) throws IOException {
response.setStatus(AuthErrorCode.LOGIN_REQUIRED.getHttpStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
objectMapper.writeValue(response.getWriter(), ApiResponse.onFailure(AuthErrorCode.LOGIN_REQUIRED));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.yeogido.backend.domain.auth.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yeogido.backend.domain.auth.dto.AuthResDTO;
import com.yeogido.backend.domain.auth.exception.AuthErrorCode;
import com.yeogido.backend.domain.auth.security.AuthUser;
import com.yeogido.backend.domain.user.entity.User;
import com.yeogido.backend.domain.user.enums.UserRole;
import com.yeogido.backend.global.exception.GeneralErrorCode;
import com.yeogido.backend.global.exception.GeneralException;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -38,11 +42,40 @@ public class JwtTokenProvider {
public AuthResDTO.Token issueToken(User user) {
return new AuthResDTO.Token(
user.getId(),
createToken(user, TOKEN_TYPE_ACCESS, Instant.now().plus(accessTokenExpirationMinutes, ChronoUnit.MINUTES)),
createToken(user, TOKEN_TYPE_REFRESH, Instant.now().plus(refreshTokenExpirationDays, ChronoUnit.DAYS))
issueAccessToken(user),
issueRefreshToken(user)
);
}

public String issueAccessToken(User user) {
return createToken(user, TOKEN_TYPE_ACCESS, Instant.now().plus(accessTokenExpirationMinutes, ChronoUnit.MINUTES));
}

public String issueRefreshToken(User user) {
return createToken(user, TOKEN_TYPE_REFRESH, Instant.now().plus(refreshTokenExpirationDays, ChronoUnit.DAYS));
}

public AuthUser parseAccessToken(String token) {
Map<String, Object> payload = parseToken(token);

if (!TOKEN_TYPE_ACCESS.equals(payload.get("type"))) {
throw new GeneralException(AuthErrorCode.LOGIN_REQUIRED);
}

Number userId = (Number) payload.get("userId");
String role = (String) payload.get("role");

if (userId == null || role == null) {
throw new GeneralException(AuthErrorCode.LOGIN_REQUIRED);
}

try {
return new AuthUser(userId.longValue(), UserRole.valueOf(role));
} catch (IllegalArgumentException e) {
throw new GeneralException(AuthErrorCode.LOGIN_REQUIRED);
}
}

private String createToken(User user, String tokenType, Instant expiresAt) {
try {
String header = encode(objectMapper.writeValueAsString(Map.of(
Expand All @@ -64,12 +97,45 @@ private String createToken(User user, String tokenType, Instant expiresAt) {
}
}

private Map<String, Object> parseToken(String token) {
try {
String[] tokenParts = token.split("\\.");

if (tokenParts.length != 3) {
throw new GeneralException(AuthErrorCode.LOGIN_REQUIRED);
}

String unsignedToken = tokenParts[0] + "." + tokenParts[1];
if (!sign(unsignedToken).equals(tokenParts[2])) {
throw new GeneralException(AuthErrorCode.LOGIN_REQUIRED);
}

Map<String, Object> payload = objectMapper.readValue(
decode(tokenParts[1]),
new TypeReference<>() {}
);

Number expiresAt = (Number) payload.get("exp");
if (expiresAt == null || expiresAt.longValue() < Instant.now().getEpochSecond()) {
throw new GeneralException(AuthErrorCode.LOGIN_REQUIRED);
}

return payload;
} catch (JsonProcessingException | IllegalArgumentException | ClassCastException e) {
throw new GeneralException(AuthErrorCode.LOGIN_REQUIRED);
}
}

private String encode(String value) {
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(value.getBytes(StandardCharsets.UTF_8));
}

private String decode(String value) {
return new String(Base64.getUrlDecoder().decode(value), StandardCharsets.UTF_8);
}

private String sign(String value) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
import com.yeogido.backend.global.common.code.SuccessCode;
import com.yeogido.backend.global.common.response.ApiResponse;
import com.yeogido.backend.global.common.response.CursorResponse;
import com.yeogido.backend.domain.auth.security.AuthUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -39,9 +40,9 @@ public ApiResponse<CursorResponse<UserResDTO.LikedResponse>> getLikedList(
@Operation(summary = "내 프로필 조회 API", description = "로그인한 사용자의 프로필 및 계정 정보를 조회합니다.")
@GetMapping("/me")
public ApiResponse<UserResDTO.Profile> getMyPage(
@RequestHeader(value = "Authorization", required = false) String authorization
@AuthenticationPrincipal AuthUser authUser
) {
UserResDTO.Profile response = userService.getMyPage(authorization);
UserResDTO.Profile response = userService.getMyPage(authUser.userId());
return ApiResponse.onSuccess(SuccessCode.OK, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.yeogido.backend.domain.user.converter;

import com.yeogido.backend.domain.user.dto.UserResDTO;
import com.yeogido.backend.domain.user.entity.User;

public class UserConverter {

private UserConverter() {
}

public static UserResDTO.Profile toProfile(User user) {
return new UserResDTO.Profile(
user.getId(),
user.getEmail(),
user.getNickname(),
user.getRegion().getName(),
user.getRole(),
user.getProfileImage()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ public enum UserErrorCode implements ErrorCode {
HttpStatus.CONFLICT,
"USER4091",
"이미 가입된 이메일입니다."
),

USER_NOT_FOUND(
HttpStatus.NOT_FOUND,
"USER4041",
"존재하지 않는 회원입니다."
);

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.yeogido.backend.domain.user.repository;

import com.yeogido.backend.domain.user.entity.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

boolean existsByEmail(String email);

Optional<User> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ CursorResponse<UserResDTO.LikedResponse> getLikedList(
Integer size
);

UserResDTO.Profile getMyPage(String authorization);
UserResDTO.Profile getMyPage(Long userId);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package com.yeogido.backend.domain.user.service;

import com.yeogido.backend.domain.user.converter.UserConverter;
import com.yeogido.backend.domain.user.dto.UserResDTO;
import com.yeogido.backend.domain.user.enums.UserRole;
import com.yeogido.backend.domain.user.entity.User;
import com.yeogido.backend.domain.user.exception.UserErrorCode;
import com.yeogido.backend.domain.user.repository.UserRepository;
import com.yeogido.backend.global.common.response.CursorResponse;
import com.yeogido.backend.global.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{

private final UserRepository userRepository;

@Override
public CursorResponse<UserResDTO.LikedResponse> getLikedList(
String category,
Expand All @@ -20,15 +27,12 @@ public CursorResponse<UserResDTO.LikedResponse> getLikedList(
}

@Override
public UserResDTO.Profile getMyPage(String authorization) {
return new UserResDTO.Profile(
1L,
"abc@example.com",
"홍길동",
"서울",
UserRole.USER,
"https://s3.ap-northeast-2.amazonaws.com/.../profile.jpg"
);
@Transactional(readOnly = true)
public UserResDTO.Profile getMyPage(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(UserErrorCode.USER_NOT_FOUND));

return UserConverter.toProfile(user);
}

}
Loading
Loading