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
Expand Up @@ -23,15 +23,13 @@ public class CategoryController {

// CREATE: 카테고리 생성
@PostMapping
@PreAuthorize("hasAuthority(T(org.myteam.server.member.domain.MemberRole).ADMIN.name())")
public ResponseEntity<ResponseDto<CategoryResponse>> createCategory(@Valid @RequestBody CategorySaveRequest categorySaveRequest) {
CategoryResponse response = categoryService.create(categorySaveRequest);
return ResponseEntity.ok(new ResponseDto<>(SUCCESS.name(), "카테고리 생성 성공", response));
}

// UPDATE: 카테고리 수정
@PutMapping("/{id}")
@PreAuthorize("hasAuthority(T(org.myteam.server.member.domain.MemberRole).ADMIN.name())")
public ResponseEntity<ResponseDto<CategoryResponse>> updateCategory(@PathVariable Long id,
@Valid @RequestBody CategoryUpdateRequest categoryUpdateRequest) {
CategoryResponse response = categoryService.update(id, categoryUpdateRequest);
Expand All @@ -40,7 +38,6 @@ public ResponseEntity<ResponseDto<CategoryResponse>> updateCategory(@PathVariabl

// DELETE: 카테고리 삭제
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority(T(org.myteam.server.member.domain.MemberRole).ADMIN.name())")
public ResponseEntity<ResponseDto<Void>> deleteCategory(@PathVariable Long id) {
categoryService.delete(id);
return ResponseEntity.ok(new ResponseDto<>(SUCCESS.name(), "카테고리 삭제 성공", null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
Expand Down Expand Up @@ -97,14 +98,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
);

http
.addFilterBefore(
.addFilterAt(
new JwtAuthenticationFilter(authenticationManager(), jwtProvider, refreshJpaRepository),
UsernamePasswordAuthenticationFilter.class
) // 로그인 인증 필터
.addFilterAfter(
new TokenAuthenticationFilter(jwtProvider),
JwtAuthenticationFilter.class
)
.addFilterAfter(
new JwtAuthenticationFilter(authenticationManager(), jwtProvider, refreshJpaRepository),
TokenAuthenticationFilter.class
); // 회원 로그인 필터
); // JWT 토큰 검증 필터

// cors 설정
http
Expand All @@ -113,13 +114,24 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
// 경로별 인가 작업
http
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/h2-console").permitAll() // H2 콘솔 접근 허용
.requestMatchers(TOKEN_REISSUE_PATH).permitAll() // 토큰 재발급
.requestMatchers("/api/admin/**").hasAnyRole(MemberRole.ADMIN.name())
.requestMatchers("/api/members/role").permitAll() // 유저 권한 변경 허용
.requestMatchers("/api/members/get-token/{email}").permitAll() // 테스트용 토큰 발급용
.anyRequest().permitAll() // 나머지 요청은 모두 허용
authorizeRequests
.requestMatchers("/upload/**").permitAll() // 정적 자원 접근 허용
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll()

.requestMatchers("/h2-console").permitAll() // H2 콘솔 접근 허용
.requestMatchers("/api/members/get-token/**").permitAll() // 테스트용 토큰 발급용

.requestMatchers("/api/admin/**").hasAnyAuthority(MemberRole.ADMIN.name())
.requestMatchers(HttpMethod.POST, "/api/me/create").permitAll()
.requestMatchers(HttpMethod.GET, "/api/categories/**").permitAll()
.requestMatchers(HttpMethod.PUT, "/api/categories/**").hasAnyAuthority(MemberRole.ADMIN.name())
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAnyAuthority(MemberRole.ADMIN.name())
.requestMatchers(HttpMethod.POST, "/api/categories").hasAnyAuthority(MemberRole.ADMIN.name())

.requestMatchers(TOKEN_REISSUE_PATH).permitAll() // 토큰 재발급
.requestMatchers("/api/members/role").permitAll() // 유저 권한 변경 허용

.anyRequest().authenticated() // 나머지 요청은 모두 허용
);

http
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR

if (status.equals(PENDING.name())) {
log.warn("PENDING 상태인 경우 로그인이 불가능합니다");
sendErrorResponse(response, HttpStatus.FORBIDDEN, "PENDING 상태인 경우 로그인이 불가능합니다");
sendErrorResponse(response, HttpStatus.LOCKED, "PENDING 상태인 경우 로그인이 불가능합니다");
return;
} else if (status.equals(INACTIVE.name())) {
log.warn("INACTIVE 상태인 경우 로그인이 불가능합니다");
Expand Down Expand Up @@ -113,6 +113,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR

log.debug("print accessToken: {}", accessToken);
log.debug("print refreshToken: {}", refreshToken);
log.debug("print role: {}", role);

//Refresh 토큰 저장
addRefreshEntity(publicId, refreshToken, Duration.ofHours(24));
Expand All @@ -122,7 +123,6 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR
response.addCookie(createCookie(REFRESH_TOKEN_KEY, cookieValue, LOGOUT_PATH, 24 * 60 * 60, true));
response.setStatus(HttpStatus.OK.value());


// frontUrl += "?" + ACCESS_TOKEN_KEY + "=" + ("Bearer%20" + accessToken);
// frontUrl += "&" + REFRESH_TOKEN_KEY + "=" + ("Bearer%20" + refreshToken);
// response.sendRedirect(frontUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,26 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
log.info("TokenAuthenticationFilter 토큰을 검사중");
log.info("Token Authenticate Filter 토큰을 검사중");
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
String accessToken = jwtProvider.getAccessToken(authorizationHeader);

log.info("accessToken : " + accessToken);
if (StringUtils.isNotBlank(accessToken)) {
if (jwtProvider.validToken(accessToken)) {
try {
if (!jwtProvider.validToken(accessToken)) {
log.warn("인증되지 않은 토큰입니다");
filterChain.doFilter(request, response);
return;
}

String accessCategory = jwtProvider.getCategory(accessToken);

if (!accessCategory.equals(TOKEN_CATEGORY_ACCESS)) {
// RestControllerAdvice 로 에러가 전달 되지 않아 여기서 에러 처리함
log.warn("잘못된 토큰 유형입니다.");
sendErrorResponse(response, INVALID_TOKEN_TYPE.getStatus(), "잘못된 토큰 유형");
if (!TOKEN_CATEGORY_ACCESS.equals(accessCategory)) {
log.warn("잘못된 토큰 유형입니다");
filterChain.doFilter(request, response);
return;
}

// 토큰에서 username과 role 획득
UUID publicId = jwtProvider.getPublicId(accessToken);
String role = jwtProvider.getRole(accessToken);
String status = jwtProvider.getStatus(accessToken);
Expand All @@ -60,53 +62,23 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
log.info("role : " + role);
log.info("status : " + status);

// Member 를 생성하여 값 set
Member member = Member.builder()
.publicId(publicId)
.role(MemberRole.valueOf(role))
.status(MemberStatus.valueOf(status))
.build();

CustomUserDetails customUserDetails = new CustomUserDetails(member);

Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(authToken);
log.info("security Context 에 정보 저장이 완료되었습니다.");
} else {
try {
if (jwtProvider.isExpired(accessToken)) {
// RestControllerAdvice 로 에러가 전달 되지 않아 여기서 에러 처리함
log.warn("토큰이 만료되었습니다.");
sendErrorResponse(response, ACCESS_TOKEN_EXPIRED.getStatus(), "만료된 토큰");
return;
}
} catch (JwtException | IllegalArgumentException e) {
// RestControllerAdvice 로 에러가 전달 되지 않아 여기서 에러 처리함
log.error("잘못된 JWT 토큰 형식 또는 그 외 에러 : {}", e.getMessage());
sendErrorResponse(response, INVALID_ACCESS_TOKEN.getStatus(), "잘못된 JWT 토큰 형식 또는 그 외 에러");
return;
}
// RestControllerAdvice 로 에러가 전달 되지 않아 여기서 에러 처리함
log.warn("인증되지 않은 토큰입니다.");
sendErrorResponse(response, INVALID_ACCESS_TOKEN.getStatus(), "인증되지 않은 토큰");
log.info("SecurityContext 에 인증 정보 저장 완료");
} catch (JwtException e) {
log.error("JWT 처리 중 오류 발생: {}", e.getMessage());
filterChain.doFilter(request, response);
return;
}
}
filterChain.doFilter(request, response);
}

/**
* 공통 에러 응답 처리 메서드
*
* @param response HttpServletResponse
* @param httpStatus HTTP 상태 오브젝트
* @param message 메시지
* @throws IOException
*/
private void sendErrorResponse(HttpServletResponse response, HttpStatus httpStatus, String message) throws IOException {
response.setStatus(httpStatus.value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(String.format("{\"message\":\"%s\",\"status\":\"%s\"}", message, httpStatus.name()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,9 @@ public ResponseEntity<?> updateStatus(@RequestBody @Valid MemberStatusUpdateRequ
log.info("email : {}" , response.getEmail());

// 서비스 호출
MemberStatus memberStatus = memberStatusUpdateRequest.getStatus(); // 변경 상태
String targetEmail = response.getEmail(); // 상태를 변경할 대상 이메일
String extractedEmail = memberStatusUpdateRequest.getEmail(); // 토큰에서 추출된 이메일
String targetEmail = response.getEmail(); // 변경을 시도하는 유저의 이메일 (본인 또는 관리자)

memberService.updateStatus(extractedEmail, targetEmail, memberStatus);
memberService.updateStatus(targetEmail, memberStatusUpdateRequest);

return ResponseEntity.ok(new ResponseDto<>(SUCCESS.name(), "회원 상태가 성공적으로 변경되었습니다.", null));
}
Expand All @@ -82,11 +80,19 @@ public ResponseEntity<?> updateRole(@RequestBody @Valid MemberRoleUpdateRequest
return new ResponseEntity<>(new ResponseDto<>(SUCCESS.name(), "권한 변경 성공", response), HttpStatus.OK);
}

@GetMapping("/get-token/{email}")
@GetMapping("/get-token/admin/{email}")
public ResponseEntity<?> getAdminToken(@PathVariable String email) {
log.info("getToken 메서드가 실행되었습니다.");
MemberResponse response = memberService.getByEmail(email);
String encode = TOKEN_PREFIX + jwtProvider.generateToken(TOKEN_CATEGORY_ACCESS, Duration.ofHours(10), response.getPublicId(), MemberRole.ADMIN.name(), response.getStatus().name());
return new ResponseEntity<>(new ResponseDto<>(SUCCESS.name(), "토큰 조회 성공", encode), HttpStatus.OK);
}

@GetMapping("/get-token/user/{email}")
public ResponseEntity<?> getToken(@PathVariable String email) {
log.info("getToken 메서드가 실행되었습니다.");
MemberResponse response = memberService.getByEmail(email);
String encode = TOKEN_PREFIX + jwtProvider.generateToken(TOKEN_CATEGORY_ACCESS, Duration.ofHours(6), response.getPublicId(), MemberRole.USER.name(), response.getStatus().name());
String encode = TOKEN_PREFIX + jwtProvider.generateToken(TOKEN_CATEGORY_ACCESS, Duration.ofHours(10), response.getPublicId(), MemberRole.USER.name(), response.getStatus().name());
return new ResponseEntity<>(new ResponseDto<>(SUCCESS.name(), "토큰 조회 성공", encode), HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.myteam.server.global.exception.PlayHiveException;

import static org.myteam.server.global.exception.ErrorCode.INVALID_PARAMETER;

@AllArgsConstructor
@Getter
Expand All @@ -18,6 +15,6 @@ public static GenderType fromValue(String value) {
return gender;
}
}
throw new PlayHiveException(INVALID_PARAMETER, "Invalid gender value: " + value);
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ public class MemberDeleteRequest {

@NotBlank
// @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$", message = "비밀번호는 영문+숫자 조합 4 ~ 10자 이내로 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{8,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 10자 이내로 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{4,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 15자 이내로 입력해주세요")
private String password; // 비밀번호
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class MemberSaveRequest {

@NotBlank
// @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$", message = "비밀번호는 영문+숫자 조합 4 ~ 10자 이내로 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{8,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 10자 이내로 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{4,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 15자 이내로 입력해주세요")
private String password; // 비밀번호

@NotBlank
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class MemberUpdateRequest {
@Column(name = "birth_date")
private LocalDate birthdate;

@Pattern(regexp = "^(MALE|FEMALE)$", message = "성별은 MALE, FEMALE 중 하나여야 합니다.")
@Pattern(regexp = "^(?i)(MALE|FEMALE)$", message = "성별은 MALE, FEMALE 중 하나여야 합니다.")
private String gender;

private MemberStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
@NoArgsConstructor
public class PasswordChangeRequest {
@NotNull
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{8,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 10자 이내로 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{4,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 15자 이내로 입력해주세요")
private String password;

@NotNull
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{8,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 10자 이내로 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{4,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 15자 이내로 입력해주세요")
private String newPassword;

@NotNull
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{8,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 10자 이내로 입력해주세요")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{4,15}$", message = "비밀번호는 영문, 숫자 조합 4 ~ 15자 이내로 입력해주세요")
private String confirmPassword;

@Builder
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/myteam/server/member/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.myteam.server.member.dto.PasswordChangeRequest;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.myteam.server.member.domain.MemberRole.ADMIN;
import static org.myteam.server.member.domain.MemberRole.USER;
import static org.myteam.server.member.domain.MemberStatus.PENDING;
import static org.myteam.server.member.domain.MemberType.LOCAL;
Expand Down Expand Up @@ -126,6 +127,10 @@ public boolean verifyOwnEmail(String email) {
return email.equals(this.email);
}

public boolean isAdmin() {
return this.role.equals(ADMIN);
}

public boolean validatePassword(String inputPassword, PasswordEncoder bCryptPasswordEncoder) {
// 입력된 평문 패스워드와 이미 암호화된 패스워드를 비교
boolean isValid = bCryptPasswordEncoder.matches(inputPassword, this.password);
Expand Down
Loading