Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
33b16b8
[Refactor] #158 관리자용 리프레시토큰 발급 메서드 생성
JJUYAAA Jan 4, 2026
879fe81
[Feat] #158 관리자용 로그인 응답 DTO 생성
JJUYAAA Jan 4, 2026
91c6b73
[Feat] #158 관리자용 토큰 발급 서비스 코드 생성
JJUYAAA Jan 4, 2026
d6ca881
[Refactor] #158 관리자용 토큰 발급 관련 환경변수 추가
JJUYAAA Jan 4, 2026
b719b48
[Refactor] #158 관리자용 토큰 발급 서비스 - AuthServiceFacade에 추가
JJUYAAA Jan 4, 2026
6415657
[Refactor] #158 Redis에 관리자용 토큰 저장하는 로직 추가
JJUYAAA Jan 4, 2026
848cc4c
[Refactor] #158 관리자용 토큰 발급 API - Controller에 추가
JJUYAAA Jan 4, 2026
243f444
[Test] #158 application-test.yml에 관리자용 토큰 발급 관련 환경변수 추가
JJUYAAA Jan 4, 2026
cb8d305
[Test] #158 TestInitializer에 특정한 ID의 유저 생성하는 메서드 추가
JJUYAAA Jan 4, 2026
dca8923
[Test] #158 관리자용 토큰 발급 서비스 테스트코드 작성
JJUYAAA Jan 4, 2026
ca4909e
[Test] #158 AuthControllerTest에 관리자용 토큰 발급 API 테스트 추가
JJUYAAA Jan 4, 2026
ecf97e3
[Test] #158 AuthControllerTest 보강 - 401 반환 테스트
JJUYAAA Jan 4, 2026
274e51a
[Refactor] #158 Role-ADMIN 추가
JJUYAAA Jan 10, 2026
284159f
[Refactor] #158 관리자용 refreshToken 생성 메서드 삭제, 관리자용 accessToken 생성 메서드 추가
JJUYAAA Jan 10, 2026
51f4224
[Refactor] #158 관리자용 로그인 ResponseDTO 수정
JJUYAAA Jan 10, 2026
53d27a8
[Refactor] #158 관리자용 로그인 서비스 코드 수정 - refreshToken 제거
JJUYAAA Jan 10, 2026
7f4d66a
[Refactor] #158 RedisRefreshTokenRepository 수정 - refreshToken 제거
JJUYAAA Jan 10, 2026
a8f2b1c
[Refactor] #158 AdminAllowlistFilter 추가
JJUYAAA Jan 10, 2026
40de914
[Test] #158 AdminLoginServiceImplTest 수정
JJUYAAA Jan 10, 2026
3b12825
[Test] #158 AuthControllerTest 수정
JJUYAAA Jan 10, 2026
64b4e4b
[Refactor] #158 Image 업로드 API - PreAuthorize 수정
JJUYAAA Jan 10, 2026
c25b0f4
[Refactor] #158 ExceptionTranslationFilter 뒤에 AdminAllowListFilter를 추가
JJUYAAA Jan 11, 2026
f8ef5cb
Merge pull request #161 from FindYou-Kuit/refactor/#158-admin-login
JJUYAAA Jan 11, 2026
7dc1e6c
[Fix] #166 breed 필드의 길이를 20 -> 50으로 증가
ksg1227 Jan 19, 2026
10d7475
[Fix] #166 품종 필드 칼럼 길이 수정 스크립트 작성
ksg1227 Jan 19, 2026
0d3a81b
Merge pull request #167 from FindYou-Kuit/fix/#166-extend-breed-length
ksg1227 Jan 19, 2026
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
Expand Up @@ -3,21 +3,22 @@
import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest;
import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse;
import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse;
import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse;
import com.kuit.findyou.domain.auth.service.AuthServiceFacade;
import com.kuit.findyou.global.common.annotation.CustomExceptionDescription;
import com.kuit.findyou.global.common.exception.CustomException;
import com.kuit.findyou.global.common.response.BaseResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.UNAUTHORIZED;
import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.*;

@Tag(name = "Login", description = "로그인 관련 API")
Expand All @@ -28,6 +29,9 @@
public class AuthController {
private final AuthServiceFacade authServiceFacade;

@Value("${admin.api.key}")
private String adminApiKey;

@Operation(
summary = "카카오 로그인 API",
description = "카카오 사용자 식별자를 이용해서 유저 정보와 엑세스 토큰을 얻을 수 있습니다. 가입된 회원인지 여부를 반환합니다."
Expand Down Expand Up @@ -58,4 +62,18 @@ public BaseResponse<GuestLoginResponse> guestLogin(@RequestBody GuestLoginReques
public BaseResponse<ReissueTokenResponse> reissueToken(@RequestBody ReissueTokenRequest request){
return BaseResponse.ok(authServiceFacade.reissueToken(request));
}

@Operation(
summary = "서비스 계정 로그인 API (내부 자동화용)",
description = "내부 자동화/관리용 서비스 계정 토큰을 발급합니다. X-ADMIN-KEY 헤더가 필요합니다."
)
@PostMapping("/login/admin")
public BaseResponse<AdminLoginResponse> adminLogin(
@RequestHeader(value = "X-ADMIN-KEY", required = false) String adminKey
) {
if (adminKey == null || adminKey.isBlank() || !adminApiKey.equals(adminKey)) {
throw new CustomException(UNAUTHORIZED);
}
return BaseResponse.ok(authServiceFacade.adminLogin());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.kuit.findyou.domain.auth.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "관리자 로그인 응답 DTO")
public record AdminLoginResponse(
@Schema(description = "관리자 유저 식별자")
Long userId,
@Schema(description = "엑세스 토큰")
String accessToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.kuit.findyou.domain.auth.service;

import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse;

public interface AdminLoginService {
AdminLoginResponse adminLogin();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.kuit.findyou.domain.auth.service;

import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse;
import com.kuit.findyou.domain.user.model.Role;
import com.kuit.findyou.domain.user.model.User;
import com.kuit.findyou.domain.user.repository.UserRepository;
import com.kuit.findyou.global.common.exception.CustomException;
import com.kuit.findyou.global.jwt.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.USER_NOT_FOUND;

@RequiredArgsConstructor
@Service
public class AdminLoginServiceImpl implements AdminLoginService{
private final JwtUtil jwtUtil;
private final UserRepository userRepository;

@Value("${admin.admin-user-id}")
private Long adminUserId;

@Value("${admin.access-ttl-ms}")
private Long adminAccessTtlMs;

@Override
public AdminLoginResponse adminLogin() {
User user = userRepository.findById(adminUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));

String accessToken = jwtUtil.createAccessJwt(user.getId(), Role.ADMIN, adminAccessTtlMs);

return new AdminLoginResponse(user.getId(), accessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse;
import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest;
import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest;
import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse;
import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse;
import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse;
import lombok.RequiredArgsConstructor;
Expand All @@ -14,6 +15,7 @@
public class AuthServiceFacade {
private final LoginService loginService;
private final ReissueTokenService reissueTokenService;
private final AdminLoginService adminLoginService;

public KakaoLoginResponse kakaoLogin(KakaoLoginRequest request) {
return loginService.kakaoLogin(request);
Expand All @@ -26,4 +28,8 @@ public GuestLoginResponse guestLogin(GuestLoginRequest request) {
public ReissueTokenResponse reissueToken(ReissueTokenRequest request) {
return reissueTokenService.reissueToken(request);
}

public AdminLoginResponse adminLogin() {
return adminLoginService.adminLogin();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class ImageController {

@Operation(summary = "신고글 이미지 업로드 API", description = "멀티파트 이미지 업로드 후 CDN URL 리스트 반환")
@CustomExceptionDescription(IMAGE_UPLOAD)
@PreAuthorize("hasRole('ROLE_USER')")
@PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
@PostMapping(value = "/upload", consumes = MULTIPART_FORM_DATA_VALUE)
public BaseResponse<ReportImageResponse> uploadImages(@RequestPart(value = "files", required = false) List<MultipartFile> files, @LoginUserId Long userId) {
List<String> urls = imageUploadService.uploadImages(files);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public abstract class Report extends BaseEntity {
@Column(name = "id", nullable = false)
private Long id;

@Column(name = "breed", length = 20, nullable = false)
@Column(name = "breed", length = 50, nullable = false)
private String breed;

@Column(name = "species", length = 100, nullable = false)
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/kuit/findyou/domain/user/model/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@Getter
public enum Role {

USER("회원"), GUEST("비회원");
USER("회원"), GUEST("비회원"), ADMIN("관리자");

private final String value;

Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/kuit/findyou/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.kuit.findyou.global.config;

import com.kuit.findyou.global.jwt.filter.AdminAllowlistFilter;
import com.kuit.findyou.global.jwt.security.CustomAccessDeniedHandler;
import com.kuit.findyou.global.jwt.security.CustomAuthenticationEntryPoint;
import com.kuit.findyou.global.jwt.filter.JwtAuthenticationFilter;
import com.kuit.findyou.global.logging.MDCLoggingFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
Expand All @@ -13,6 +15,7 @@
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.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


Expand All @@ -24,6 +27,7 @@ public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final AdminAllowlistFilter adminAllowlistFilter;

// MDCLoggingFilter 명시적 빈 등록
@Bean
Expand Down Expand Up @@ -74,6 +78,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.addFilterBefore(mdcLoggingFilter(), JwtAuthenticationFilter.class);

http.addFilterAfter(adminAllowlistFilter, ExceptionTranslationFilter.class);

// 토큰 검증 예외 처리 추가
http
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(customAuthenticationEntryPoint)
Expand All @@ -85,4 +91,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

return http.build();
}
@Bean
public FilterRegistrationBean<AdminAllowlistFilter> adminAllowlistFilterRegistration(AdminAllowlistFilter filter) {
FilterRegistrationBean<AdminAllowlistFilter> bean = new FilterRegistrationBean<>(filter);
bean.setEnabled(false);
return bean;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.kuit.findyou.global.jwt.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

@Component
@RequiredArgsConstructor
public class AdminAllowlistFilter extends OncePerRequestFilter {

private final AntPathMatcher matcher = new AntPathMatcher();

// ADMIN에게 허용되는 API
private static final List<Allow> ADMIN_ALLOWLIST = List.of(
new Allow(HttpMethod.GET.name(), "/api/v2/reports/protecting-reports/random-s3"),
new Allow(HttpMethod.GET.name(), "/api/v2/reports/missing-reports/random-s3"),
new Allow(HttpMethod.POST.name(), "/api/v2/images/upload")
);

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

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth != null && auth.isAuthenticated()) {
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));

if (isAdmin) {
String method = request.getMethod();
String path = request.getRequestURI();

boolean allowed = ADMIN_ALLOWLIST.stream()
.anyMatch(a -> a.method.equals(method) && matcher.match(a.pathPattern, path));

if (!allowed) {
throw new AccessDeniedException("ADMIN은 허용된 API만 호출할 수 있습니다.");
}
}
}

filterChain.doFilter(request, response);
}

private static class Allow {
final String method;
final String pathPattern;

private Allow(String method, String pathPattern) {
this.method = method;
this.pathPattern = pathPattern;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
BaseErrorResponse body = new BaseErrorResponse(FORBIDDEN);
String message = accessDeniedException.getMessage();
BaseErrorResponse body = (message == null || message.isBlank())
? new BaseErrorResponse(FORBIDDEN)
: new BaseErrorResponse(FORBIDDEN, message);
String json = objectMapper.writeValueAsString(body);

response.setStatus(FORBIDDEN.getCode());
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ public String createAccessJwt(Long userId, Role role) {
.compact();
}

public String createAccessJwt(Long userId, Role role, long expireMs) {
return Jwts.builder()
.claim(JwtClaimKey.USER_ID.getKey(), userId)
.claim(JwtClaimKey.ROLE.getKey(), role.name())
.claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.ACCESS_TOKEN)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expireMs))
.signWith(secretKey)
.compact();
}

public String createRefreshJwt(Long userId) {
return Jwts.builder()
.id(UUID.randomUUID().toString())
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,9 @@ management:
exposure:
include: health, info, prometheus


admin:
admin-user-id: ${ADMIN_USER_ID}
access-ttl-ms: ${ADMIN_ACCESS_TTL_MS}
api:
key: ${ADMIN_API_KEY}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- V9__alter_reports_breed_column.sql
-- reports 테이블의 breed 컬럼 길이를 20에서 50으로 변경

ALTER TABLE reports MODIFY COLUMN breed VARCHAR(50) NOT NULL;
Loading