feat: 비밀번호 변경 기능 (PUT /api/auth/password)
CI / build (push) Failing after 14m46s

현재 비번 검증 후 BCrypt 교체. 소셜 계정/동일 비번 거부.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-05-31 18:46:42 +09:00
parent 43d1b53d01
commit eb6b9830ce
6 changed files with 59 additions and 1 deletions
@@ -3,6 +3,7 @@ package com.sb.web.auth.controller;
import com.sb.web.auth.dto.LoginRequest;
import com.sb.web.auth.dto.LoginResponse;
import com.sb.web.auth.dto.MemberResponse;
import com.sb.web.auth.dto.PasswordChangeRequest;
import com.sb.web.auth.dto.SessionUser;
import com.sb.web.auth.dto.SignupRequest;
import com.sb.web.auth.service.AuthService;
@@ -48,4 +49,12 @@ public class AuthController {
public SessionUser me(@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return current;
}
@PutMapping("/password")
public ResponseEntity<Void> changePassword(
@Valid @RequestBody PasswordChangeRequest req,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
authService.changePassword(current.getId(), req);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,19 @@
package com.sb.web.auth.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 비밀번호 변경 요청 (로그인 사용자 본인).
*/
@Data
public class PasswordChangeRequest {
@NotBlank(message = "현재 비밀번호를 입력하세요.")
private String currentPassword;
@NotBlank(message = "새 비밀번호를 입력하세요.")
@Size(min = 8, max = 64, message = "비밀번호는 8~64자여야 합니다.")
private String newPassword;
}
@@ -20,4 +20,6 @@ public interface MemberMapper {
int countByLoginId(@Param("loginId") String loginId);
int insert(Member member);
int updatePassword(@Param("id") Long id, @Param("password") String password);
}
@@ -4,6 +4,7 @@ import com.sb.web.auth.domain.Member;
import com.sb.web.auth.dto.LoginRequest;
import com.sb.web.auth.dto.LoginResponse;
import com.sb.web.auth.dto.MemberResponse;
import com.sb.web.auth.dto.PasswordChangeRequest;
import com.sb.web.auth.dto.SessionUser;
import com.sb.web.auth.dto.SignupRequest;
import com.sb.web.common.exception.ApiException;
@@ -99,4 +100,26 @@ public class AuthService {
redisTemplate.delete(SESSION_PREFIX + token);
}
}
/**
* 비밀번호 변경 (본인). 현재 비밀번호 검증 후 새 비밀번호로 교체.
*/
@Transactional
public void changePassword(Long memberId, PasswordChangeRequest req) {
Member member = memberMapper.findById(memberId);
if (member == null) {
throw new ApiException(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다.");
}
if (!"LOCAL".equals(member.getProvider()) || member.getPassword() == null) {
throw new ApiException(HttpStatus.BAD_REQUEST, "소셜 로그인 계정은 비밀번호를 변경할 수 없습니다.");
}
if (!passwordEncoder.matches(req.getCurrentPassword(), member.getPassword())) {
throw new ApiException(HttpStatus.BAD_REQUEST, "현재 비밀번호가 올바르지 않습니다.");
}
if (passwordEncoder.matches(req.getNewPassword(), member.getPassword())) {
throw new ApiException(HttpStatus.BAD_REQUEST, "새 비밀번호가 기존 비밀번호와 동일합니다.");
}
memberMapper.updatePassword(memberId, passwordEncoder.encode(req.getNewPassword()));
log.info("[password] changed for {}", member.getLoginId());
}
}
@@ -23,7 +23,8 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// 인증: 로그인 필요한 경로 (관리자 경로 포함 — SessionUser 세팅용으로 먼저 실행)
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/auth/me", "/api/auth/logout", "/api/board/**", "/api/admin/**", "/api/account/**");
.addPathPatterns("/api/auth/me", "/api/auth/logout", "/api/auth/password",
"/api/board/**", "/api/admin/**", "/api/account/**");
// 인가: 관리자 전용 경로 (authInterceptor 다음에 실행되어 role 검사)
registry.addInterceptor(adminInterceptor)
.addPathPatterns("/api/admin/**");