Skip to content

Commit 47ed43f

Browse files
authored
[FEAT] 서포터즈용 토큰 발급 API 추가 (#396)
* feat: 관리자 인증 API 경로 변경 및 Springdoc 설정 업데이트 * feat: 서포터 전용 토큰 발급 기능 추가 및 Discord 메시지 전송 로직 개선 * feat: 서포터 역할 추가 및 관련 API 접근 권한 수정 * fix: createToken 메서드에 null 및 유효하지 않은 만료 시간 검증 추가 * feat: 서포터 토큰 만료 시간을 10분에서 1일로 변경
1 parent 23c795e commit 47ed43f

23 files changed

Lines changed: 236 additions & 87 deletions

src/main/java/org/ject/support/common/security/config/SecurityConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.beans.factory.annotation.Value;
99
import org.springframework.context.annotation.Bean;
1010
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.http.HttpMethod;
1112
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
1213
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
1314
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -70,6 +71,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
7071
})
7172
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
7273
.authorizeHttpRequests(request -> request
74+
.requestMatchers("/admin-auth/**").permitAll()
75+
.requestMatchers(HttpMethod.GET, "/admin/**")
76+
.hasAnyRole("ADMIN", "SUPPORTER")
77+
.requestMatchers("/admin/**").hasRole("ADMIN")
7378
.anyRequest().permitAll()
7479
)
7580
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

src/main/java/org/ject/support/common/security/jwt/JwtAuthenticationFilter.java

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
package org.ject.support.common.security.jwt;
22

33
import jakarta.servlet.FilterChain;
4-
import jakarta.servlet.ServletException;
54
import jakarta.servlet.http.HttpServletRequest;
65
import jakarta.servlet.http.HttpServletResponse;
7-
import java.io.IOException;
8-
import java.util.Collections;
9-
import java.util.List;
10-
11-
import io.jsonwebtoken.Claims;
12-
import io.jsonwebtoken.Jwts;
136
import lombok.RequiredArgsConstructor;
147
import lombok.extern.slf4j.Slf4j;
15-
8+
import org.ject.support.common.exception.GlobalErrorCode;
9+
import org.ject.support.common.exception.GlobalException;
1610
import org.ject.support.common.security.CustomUserDetails;
11+
import org.ject.support.domain.auth.exception.AuthErrorCode;
12+
import org.ject.support.domain.auth.exception.AuthException;
1713
import org.ject.support.domain.member.Role;
1814
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1915
import org.springframework.security.core.Authentication;
@@ -23,6 +19,9 @@
2319
import org.springframework.stereotype.Component;
2420
import org.springframework.web.filter.OncePerRequestFilter;
2521

22+
import java.util.Collections;
23+
import java.util.List;
24+
2625
/**
2726
* JWT 토큰 기반의 인증을 처리하는 필터
2827
*
@@ -39,38 +38,42 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
3938
private final JwtTokenProvider jwtTokenProvider;
4039

4140
@Override
42-
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
43-
throws ServletException, IOException {
41+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
4442

4543
String accessToken = jwtTokenProvider.resolveAccessToken(request);
4644

47-
if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) {
48-
try {
45+
try {
46+
if (accessToken != null) {
47+
if (!jwtTokenProvider.validateToken(accessToken)) {
48+
throw new AuthException(AuthErrorCode.INVALID_TOKEN);
49+
}
4950
Authentication auth = jwtTokenProvider.getAuthenticationByToken(accessToken);
5051
SecurityContextHolder.getContext().setAuthentication(auth);
5152
chain.doFilter(request, response);
5253
return;
53-
} catch (Exception e) {
54-
log.error("엑세스 토큰 인증 실패: {}", e.getMessage());
5554
}
56-
}
5755

58-
String verificationToken = jwtTokenProvider.resolveVerificationToken(request);
59-
if (verificationToken != null && jwtTokenProvider.validateToken(verificationToken)) {
60-
try {
56+
String verificationToken = jwtTokenProvider.resolveVerificationToken(request);
57+
if (verificationToken != null) {
58+
if (!jwtTokenProvider.validateToken(verificationToken)) {
59+
throw new AuthException(AuthErrorCode.INVALID_TOKEN);
60+
}
6161
// verification 토큰에서 이메일 추출
6262
String email = jwtTokenProvider.extractEmailFromVerificationToken(verificationToken);
6363
Authentication auth = createVerificationAuthentication(email);
6464
SecurityContextHolder.getContext().setAuthentication(auth);
6565
chain.doFilter(request, response);
6666
return;
67-
} catch (Exception e) {
68-
log.error("검증 토큰 인증 실패: {}", e.getMessage());
6967
}
70-
}
7168

72-
// 두 토큰 모두 없거나 유효하지 않으면, 인증 없이 진행
73-
chain.doFilter(request, response);
69+
// 두 토큰 모두 없으면 인증 없이 진행 (익명 요청)
70+
chain.doFilter(request, response);
71+
72+
} catch (Exception e) {
73+
log.error("JWT 인증 처리 중 에러 발생", e);
74+
SecurityContextHolder.clearContext();
75+
throw new GlobalException(GlobalErrorCode.INVALID_ACCESS_TOKEN);
76+
}
7477
}
7578

7679
private Authentication createVerificationAuthentication(String email) {

src/main/java/org/ject/support/common/security/jwt/JwtTokenProvider.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import jakarta.annotation.PostConstruct;
1111
import jakarta.servlet.http.Cookie;
1212
import jakarta.servlet.http.HttpServletRequest;
13-
import java.security.Key;
14-
import java.util.Date;
1513
import lombok.Getter;
1614
import lombok.RequiredArgsConstructor;
1715
import org.ject.support.common.exception.GlobalException;
@@ -23,6 +21,9 @@
2321
import org.springframework.security.core.Authentication;
2422
import org.springframework.stereotype.Component;
2523

24+
import java.security.Key;
25+
import java.util.Date;
26+
2627
import static org.ject.support.common.exception.GlobalErrorCode.AUTHENTICATION_REQUIRED;
2728

2829
@Component
@@ -62,6 +63,24 @@ public String createAccessToken(Authentication authentication, Long memberId) {
6263
.compact();
6364
}
6465

66+
public String createToken(Claims claims, long expirationMillis) {
67+
if (claims == null) {
68+
throw new IllegalArgumentException("Claims cannot be null");
69+
}
70+
if (expirationMillis <= 0) {
71+
throw new IllegalArgumentException("Expiration time must be positive");
72+
}
73+
Date now = new Date();
74+
Date expireDate = new Date(now.getTime() + expirationMillis);
75+
76+
return Jwts.builder()
77+
.setClaims(claims)
78+
.setIssuedAt(now)
79+
.setExpiration(expireDate)
80+
.signWith(secretKey)
81+
.compact();
82+
}
83+
6584
/**
6685
* Refresh 토큰 생성
6786
*/

src/main/java/org/ject/support/common/springdoc/SpringdocConfig.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,21 @@ public OpenAPI openAPI() {
4848
public GroupedOpenApi coreApi() {
4949
return GroupedOpenApi.builder()
5050
.group("Core API")
51-
.pathsToExclude("/admin/**")
51+
.pathsToExclude(
52+
"/admin/**",
53+
"/admin-auth/**"
54+
)
5255
.build();
5356
}
5457

5558
@Bean
5659
public GroupedOpenApi adminApi() {
5760
return GroupedOpenApi.builder()
5861
.group("Admin API")
59-
.pathsToMatch("/admin/**")
62+
.pathsToMatch(
63+
"/admin/**",
64+
"/admin-auth/**"
65+
)
6066
.build();
6167
}
6268
}

src/main/java/org/ject/support/domain/admin/controller/AdminAuthController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@
1717

1818
@RestController
1919
@RequiredArgsConstructor
20-
@RequestMapping("/admin")
20+
@RequestMapping("/admin-auth")
2121
public class AdminAuthController implements AdminAuthApiSpec {
2222

2323
private final AdminAuthService adminAuthService;
2424
private final CustomSuccessHandler customSuccessHandler;
2525

26-
@PostMapping("/auth/codes")
26+
@PostMapping("/code")
2727
public AdminAuthSendResponse sendAdminAuthCode(@RequestBody @Valid AdminAuthSendRequest request) {
2828
String email = adminAuthService.sendAdminAuthCode(request.email());
2929
return AdminAuthSendResponse.builder()
3030
.email(email)
3131
.build();
3232
}
3333

34-
@PostMapping("/auth/codes/verify")
34+
@PostMapping("/verify-code")
3535
public boolean verifyAdminAuthCode(
3636
@RequestBody @Valid AdminVerifyRequest request,
3737
HttpServletRequest httpRequest,

src/main/java/org/ject/support/domain/admin/controller/AdminTempApplyController.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import org.springframework.data.domain.Page;
1010
import org.springframework.data.domain.Pageable;
1111
import org.springframework.data.web.PageableDefault;
12-
import org.springframework.security.access.prepost.PreAuthorize;
1312
import org.springframework.web.bind.annotation.DeleteMapping;
1413
import org.springframework.web.bind.annotation.GetMapping;
1514
import org.springframework.web.bind.annotation.PathVariable;
@@ -25,25 +24,21 @@ public class AdminTempApplyController implements AdminTempApplyApiSpec {
2524
private final AdminTempApplyService adminTempApplyService;
2625

2726
@GetMapping("/{tempApplyId}")
28-
@PreAuthorize("hasRole('ROLE_ADMIN')")
2927
public TempApplyDetailResponse getTempApplyDetail(@PathVariable("tempApplyId") Long tempApplyId) {
3028
return adminTempApplyService.getTempApplyDetail(tempApplyId);
3129
}
3230

3331
@GetMapping("/count")
34-
@PreAuthorize("hasRole('ROLE_ADMIN')")
3532
public TempSavedApplyCountResponse getTempSavedApplyCount() {
3633
return adminTempApplyService.getTempSavedApplyCount();
3734
}
3835

3936
@DeleteMapping("/{tempApplyId}")
40-
@PreAuthorize("hasRole('ROLE_ADMIN')")
4137
public void deleteTempApply(@PathVariable final Long tempApplyId) {
4238
adminTempApplyService.deleteTempApply(tempApplyId);
4339
}
4440

4541
@GetMapping()
46-
@PreAuthorize("hasRole('ROLE_ADMIN')")
4742
public Page<TempSavedApplyResponse> getTempApplies(@RequestParam(required = false) JobFamily jobFamily,
4843
@PageableDefault(size = 15) Pageable pageable) {
4944
return adminTempApplyService.getTempApplies(jobFamily, pageable);

src/main/java/org/ject/support/domain/admin/controller/ApplyPassController.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import lombok.RequiredArgsConstructor;
44
import org.ject.support.domain.admin.dto.ApplyPassRequest;
55
import org.ject.support.domain.admin.service.ApplyPassService;
6-
import org.springframework.security.access.prepost.PreAuthorize;
76
import org.springframework.web.bind.annotation.PostMapping;
87
import org.springframework.web.bind.annotation.RequestBody;
98
import org.springframework.web.bind.annotation.RequestMapping;
@@ -18,7 +17,6 @@ public class ApplyPassController implements ApplyPassApiSpec {
1817

1918
@Override
2019
@PostMapping
21-
@PreAuthorize("hasRole('ROLE_ADMIN')")
2220
public int passApply(@RequestBody ApplyPassRequest request) {
2321
return applyPassService.passApply(request.applyIds());
2422
}

src/main/java/org/ject/support/domain/admin/controller/MemberManagementController.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22

33
import jakarta.validation.Valid;
44
import lombok.RequiredArgsConstructor;
5-
import org.ject.support.domain.admin.service.MemberManagementService;
6-
import org.ject.support.domain.member.JobFamily;
7-
import org.ject.support.domain.member.Role;
85
import org.ject.support.domain.admin.dto.MemberBulkDeleteRequest;
96
import org.ject.support.domain.admin.dto.MemberDetailResponse;
107
import org.ject.support.domain.admin.dto.MemberEditRequest;
118
import org.ject.support.domain.admin.dto.MemberRegisterRequest;
129
import org.ject.support.domain.admin.dto.MemberResponse;
10+
import org.ject.support.domain.admin.service.MemberManagementService;
11+
import org.ject.support.domain.member.JobFamily;
12+
import org.ject.support.domain.member.Role;
1313
import org.springframework.data.domain.Page;
1414
import org.springframework.data.domain.Pageable;
1515
import org.springframework.data.web.PageableDefault;
16-
import org.springframework.security.access.prepost.PreAuthorize;
1716
import org.springframework.web.bind.annotation.DeleteMapping;
1817
import org.springframework.web.bind.annotation.GetMapping;
1918
import org.springframework.web.bind.annotation.PathVariable;
@@ -33,7 +32,6 @@ public class MemberManagementController implements MemberManagementApiSpec {
3332

3433
@Override
3534
@GetMapping
36-
@PreAuthorize("hasRole('ROLE_ADMIN')")
3735
public Page<MemberResponse> findMembers(@RequestParam final Role role,
3836
@RequestParam(required = false) final JobFamily jobFamily,
3937
@RequestParam(required = false) final Long semesterId,
@@ -43,36 +41,31 @@ public Page<MemberResponse> findMembers(@RequestParam final Role role,
4341

4442
@Override
4543
@GetMapping("/{memberId}")
46-
@PreAuthorize("hasRole('ROLE_ADMIN')")
4744
public MemberDetailResponse findMemberDetail(@PathVariable("memberId") final Long memberId) {
4845
return memberManagementService.findMemberDetail(memberId);
4946
}
5047

5148
@Override
5249
@PostMapping
53-
@PreAuthorize("hasRole('ROLE_ADMIN')")
5450
public void registerMember(@RequestBody @Valid final MemberRegisterRequest request) {
5551
memberManagementService.registerMember(request);
5652
}
5753

5854
@Override
5955
@PutMapping("/{memberId}")
60-
@PreAuthorize("hasRole('ROLE_ADMIN')")
6156
public void editMember(@PathVariable("memberId") final Long memberId,
6257
@RequestBody @Valid final MemberEditRequest request) {
6358
memberManagementService.editMember(memberId, request);
6459
}
6560

6661
@Override
6762
@DeleteMapping("/{memberId}")
68-
@PreAuthorize("hasRole('ROLE_ADMIN')")
6963
public void deleteMember(@PathVariable("memberId") final Long memberId) {
7064
memberManagementService.deleteMember(memberId);
7165
}
7266

7367
@Override
7468
@DeleteMapping
75-
@PreAuthorize("hasRole('ROLE_ADMIN')")
7669
public int deleteMembers(@RequestBody @Valid final MemberBulkDeleteRequest request) {
7770
return memberManagementService.deleteMembers(request.memberIds());
7871
}

src/main/java/org/ject/support/domain/admin/controller/SubmittedApplyController.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.springframework.data.domain.Page;
1313
import org.springframework.data.domain.Pageable;
1414
import org.springframework.data.web.PageableDefault;
15-
import org.springframework.security.access.prepost.PreAuthorize;
1615
import org.springframework.web.bind.annotation.DeleteMapping;
1716
import org.springframework.web.bind.annotation.GetMapping;
1817
import org.springframework.web.bind.annotation.PathVariable;
@@ -31,44 +30,38 @@ public class SubmittedApplyController implements SubmittedApplyApiSpec {
3130

3231
@Override
3332
@GetMapping("/count")
34-
@PreAuthorize("hasRole('ROLE_ADMIN')")
3533
public SubmittedApplyCountResponse getSubmittedApplyCount() {
3634
return submittedApplyService.countSubmittedApply();
3735
}
3836

3937
@Override
4038
@GetMapping
41-
@PreAuthorize("hasRole('ROLE_ADMIN')")
4239
public Page<SubmittedApplyResponse> findSubmittedApplies(@RequestParam(required = false) final JobFamily jobFamily,
4340
@PageableDefault(size = 15) final Pageable pageable) {
4441
return submittedApplyService.findSubmittedApplies(jobFamily, pageable);
4542
}
4643

4744
@Override
4845
@GetMapping("/{applyId}")
49-
@PreAuthorize("hasRole('ROLE_ADMIN')")
5046
public SubmittedApplyDetailResponse findSubmittedApplyDetail(@PathVariable("applyId") final Long applyId) {
5147
return submittedApplyService.findSubmittedApplyDetail(applyId);
5248
}
5349

5450
@Override
5551
@PutMapping("/{applyId}")
56-
@PreAuthorize("hasRole('ROLE_ADMIN')")
5752
public void editSubmittedApply(@PathVariable("applyId") final Long applyId,
5853
@RequestBody @Valid final SubmittedApplyEditRequest request) {
5954
submittedApplyService.updateSubmittedApply(applyId, request);
6055
}
6156

6257
@Override
6358
@DeleteMapping("/{applyId}")
64-
@PreAuthorize("hasRole('ROLE_ADMIN')")
6559
public void deleteSubmittedApply(@PathVariable("applyId") final Long applyId) {
6660
submittedApplyService.deleteSubmittedApply(applyId);
6761
}
6862

6963
@Override
7064
@DeleteMapping
71-
@PreAuthorize("hasRole('ROLE_ADMIN')")
7265
public int deleteSubmittedApplies(@RequestBody @Valid final SubmittedApplyBulkDeleteRequest request) {
7366
return submittedApplyService.deleteSubmittedApplies(request.applyIds());
7467
}

src/main/java/org/ject/support/domain/admin/service/AdminAuthService.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public String sendAdminAuthCode(String email) {
4646

4747
if (discordRateLimiter.tryConsume(1)) {
4848
redisTemplate.opsForValue().set(key, authCode, Duration.ofSeconds(ADMIN_LOGIN_AUTH_CODE_EXPIRATION));
49-
discordComponent.sendAdminLoginMessage(makeAdminLoginMessage(member.getEmail(), authCode))
49+
discordComponent.sendAdminLoginMessage(member.getEmail(), authCode)
5050
.doOnError(e -> log.error("Discord 전송 실패: {}", member.getEmail(), e))
5151
.subscribe();
5252
} else {
@@ -109,12 +109,4 @@ private int getFailCount(String failCountKey) {
109109
String failCountStr = redisTemplate.opsForValue().get(failCountKey);
110110
return failCountStr == null ? 0 : Integer.parseInt(failCountStr);
111111
}
112-
113-
private String makeAdminLoginMessage(String email, String code) {
114-
return """
115-
관리자 로그인 인증 코드 요청
116-
관리자 이메일: %s
117-
인증 코드 : %s
118-
""".formatted(email, code);
119-
}
120112
}

0 commit comments

Comments
 (0)