From 9f908e6a95a3a3ad46846cdd2092aead416434b3 Mon Sep 17 00:00:00 2001 From: Haewon Lee Date: Sat, 24 Jan 2026 01:02:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85,?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 8 ++ build.gradle | 6 +- .../Team3BackendApplication.java | 2 + .../auth/controller/AuthController.java | 84 +++++++++++++++++++ .../domain/auth/dto/request/LoginRequest.java | 16 ++++ .../auth/dto/request/SignupRequest.java | 20 +++++ .../auth/dto/response/AuthResponse.java | 19 +++++ .../domain/auth/exception/AuthErrorCode.java | 28 +++++++ .../domain/auth/exception/AuthException.java | 8 ++ .../auth/exception/AuthSuccessCode.java | 20 +++++ .../domain/auth/service/AuthService.java | 80 ++++++++++++++++++ .../domain/user/entity/User.java | 2 +- .../user/repository/UserRepository.java | 12 +++ .../apiPayload/code/GeneralErrorCode.java | 3 + .../global/config/CorsConfig.java | 1 + .../global/config/SecurityConfig.java | 26 ++++-- .../global/security/CurrentUser.java | 14 ++++ .../CustomAuthenticationEntryPoint.java | 41 +++++++++ .../global/security/CustomUserDetails.java | 61 ++++++++++++++ .../security/CustomUserDetailsService.java | 27 ++++++ 20 files changed, 471 insertions(+), 7 deletions(-) create mode 100644 .env create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/controller/AuthController.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/LoginRequest.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/SignupRequest.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/response/AuthResponse.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthErrorCode.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthException.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthSuccessCode.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/auth/service/AuthService.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/domain/user/repository/UserRepository.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/global/security/CurrentUser.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/global/security/CustomAuthenticationEntryPoint.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetails.java create mode 100644 src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetailsService.java diff --git a/.env b/.env new file mode 100644 index 0000000..5e6d4ad --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +SPRING_PROFILES_ACTIVE=local +# prod에서 local로 변경 +LOCAL_DB_URL=jdbc:mysql://localhost:3306/cokerthon-local?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&createDatabaseIfNotExist=true +LOCAL_DB_USERNAME=root +LOCAL_DB_PASSWORD=dlgodnjs! +PROD_DB_URL=jdbc:mysql://cokerthon-db.clgeccwgurgu.ap-northeast-2.rds.amazonaws.com:3306/cokathon?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +PROD_DB_USERNAME=admin +PROD_DB_PASSWORD=cokerthonpassword \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0718a5e..0ff41af 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webmvc' + implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' @@ -37,7 +38,10 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // Swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14' + + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' } tasks.named('test') { diff --git a/src/main/java/com/cokerthon/Team3_Backend/Team3BackendApplication.java b/src/main/java/com/cokerthon/Team3_Backend/Team3BackendApplication.java index 10d1588..c99de86 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/Team3BackendApplication.java +++ b/src/main/java/com/cokerthon/Team3_Backend/Team3BackendApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class Team3BackendApplication { diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/controller/AuthController.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..25ae0cc --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/controller/AuthController.java @@ -0,0 +1,84 @@ +package com.cokerthon.Team3_Backend.domain.auth.controller; + +import com.cokerthon.Team3_Backend.domain.auth.dto.request.LoginRequest; +import com.cokerthon.Team3_Backend.domain.auth.dto.request.SignupRequest; +import com.cokerthon.Team3_Backend.domain.auth.dto.response.AuthResponse; +import com.cokerthon.Team3_Backend.domain.auth.exception.AuthSuccessCode; +import com.cokerthon.Team3_Backend.domain.auth.service.AuthService; +import com.cokerthon.Team3_Backend.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +@Tag(name = "Auth API") +public class AuthController { + + private final AuthService authService; + + @Operation(summary = "회원가입 API", description = "닉네임, 아이디, 비밀번호를 입력받아 회원가입합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "회원가입 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 존재하는 닉네임 또는 아이디") + }) + @PostMapping("/signup") + public ApiResponse signUp(@Valid @RequestBody SignupRequest request) { + + AuthResponse response = authService.signUp(request); + return ApiResponse.onSuccess(AuthSuccessCode.SIGNUP_SUCCESS, response); + } + + @Operation(summary = "로그인 API", description = "아이디와 비밀번호로 로그인합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "로그인 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "비밀번호 불일치") + }) + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody LoginRequest request, HttpServletRequest httpRequest) { + + AuthResponse response = authService.login(request); + + // 세션 생성 + HttpSession session = httpRequest.getSession(true); + SecurityContext securityContext = SecurityContextHolder.getContext(); + session.setAttribute( + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, + securityContext + ); + + return ApiResponse.onSuccess(AuthSuccessCode.LOGIN_SUCCESS, response); + } + + @Operation(summary = "로그아웃 API", description = "현재 사용자의 세션을 만료시키고 쿠키를 삭제합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "로그아웃 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증되지 않은 사용자") + }) + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request, HttpServletResponse response) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // 인증 정보 있으면 로그아웃 처리 + if (authentication != null) { + new SecurityContextLogoutHandler().logout(request, response, authentication); + } + return ApiResponse.onSuccess(AuthSuccessCode.LOGOUT_SUCCESS, null); + } + +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/LoginRequest.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/LoginRequest.java new file mode 100644 index 0000000..ee6b901 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/LoginRequest.java @@ -0,0 +1,16 @@ +package com.cokerthon.Team3_Backend.domain.auth.dto.request; + + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @Schema(description = "로그인 아이디", example = "test") + @NotBlank(message = "아이디는 필수입니다.") + String loginId, + + @Schema(description = "비밀번호", example = "password") + @NotBlank(message = "비밀번호는 필수입니다.") + String password +) { +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/SignupRequest.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/SignupRequest.java new file mode 100644 index 0000000..8bbe902 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/request/SignupRequest.java @@ -0,0 +1,20 @@ +package com.cokerthon.Team3_Backend.domain.auth.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record SignupRequest( + + @Schema(description = "닉네임", example = "닉네임") + @NotBlank(message = "닉네임은 필수입니다.") + String nickname, + + @Schema(description = "로그인 아이디", example = "test") + @NotBlank(message = "아이디는 필수입니다.") + String loginId, + + @Schema(description = "비밀번호", example = "password") + @NotBlank(message = "비밀번호는 필수입니다.") + String password +) { +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/response/AuthResponse.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/response/AuthResponse.java new file mode 100644 index 0000000..ebf32f8 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/dto/response/AuthResponse.java @@ -0,0 +1,19 @@ +package com.cokerthon.Team3_Backend.domain.auth.dto.response; + +import com.cokerthon.Team3_Backend.domain.user.entity.User; +import lombok.Builder; + +@Builder +public record AuthResponse( + Long id, + String nickname, + String loginId +){ + public static AuthResponse from(User user) { + return new AuthResponse( + user.getId(), + user.getNickname(), + user.getLoginId() + ); + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthErrorCode.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthErrorCode.java new file mode 100644 index 0000000..8124bb4 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthErrorCode.java @@ -0,0 +1,28 @@ +package com.cokerthon.Team3_Backend.domain.auth.exception; + +import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum AuthErrorCode implements BaseErrorCode { + + + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH-001", "비밀번호가 일치하지 않습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH-002", "사용자를 찾을 수 없습니다."), + DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "AUTH-003", "이미 존재하는 닉네임입니다."), + DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "AUTH-004", "이미 존재하는 아이디입니다."), + + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public HttpStatus getStatus() { + return this.httpStatus; + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthException.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthException.java new file mode 100644 index 0000000..99359a4 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthException.java @@ -0,0 +1,8 @@ +package com.cokerthon.Team3_Backend.domain.auth.exception; + +import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseErrorCode; +import com.cokerthon.Team3_Backend.global.exception.GeneralException; + +public class AuthException extends GeneralException { + public AuthException(BaseErrorCode code) { super(code); } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthSuccessCode.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthSuccessCode.java new file mode 100644 index 0000000..6222102 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/exception/AuthSuccessCode.java @@ -0,0 +1,20 @@ +package com.cokerthon.Team3_Backend.domain.auth.exception; + +import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum AuthSuccessCode implements BaseSuccessCode { + + SIGNUP_SUCCESS(HttpStatus.OK, "SIGNUP200_1", "회원가입에 성공했습니다."), + LOGIN_SUCCESS(HttpStatus.OK, "LOGIN200_1", "로그인에 성공했습니다."), + LOGOUT_SUCCESS(HttpStatus.OK, "LOGOUT200_1", "로그아웃에 성공했습니다.") + + ; + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/auth/service/AuthService.java b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/service/AuthService.java new file mode 100644 index 0000000..5689caa --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/auth/service/AuthService.java @@ -0,0 +1,80 @@ +package com.cokerthon.Team3_Backend.domain.auth.service; + +import com.cokerthon.Team3_Backend.domain.auth.dto.request.LoginRequest; +import com.cokerthon.Team3_Backend.domain.auth.dto.request.SignupRequest; +import com.cokerthon.Team3_Backend.domain.auth.dto.response.AuthResponse; +import com.cokerthon.Team3_Backend.domain.auth.exception.AuthErrorCode; +import com.cokerthon.Team3_Backend.domain.auth.exception.AuthException; +import com.cokerthon.Team3_Backend.domain.user.entity.User; +import com.cokerthon.Team3_Backend.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + + @Transactional + public AuthResponse signUp(SignupRequest request) { + + // 닉네임 중복 확인 + if (userRepository.findByNickname(request.nickname()).isPresent()) { + throw new AuthException(AuthErrorCode.DUPLICATE_NICKNAME); + } + + // 아이디 중복 확인 + if (userRepository.findByLoginId(request.loginId()).isPresent()) { + throw new AuthException(AuthErrorCode.DUPLICATE_LOGIN_ID); + } + + String encodedPassword = passwordEncoder.encode(request.password()); + + // 4. User 엔티티 생성 및 저장 + User user = User.builder() + .loginId(request.loginId()) + .password(encodedPassword) + .nickname(request.nickname()) + .build(); + + User newUser = userRepository.save(user); + return AuthResponse.from(user); + } + + public AuthResponse login(LoginRequest request) { + + User user = userRepository.findByLoginId(request.loginId()) + .orElseThrow(() -> new AuthException(AuthErrorCode.NOT_FOUND)); + + // 시큐리티에게 로그인 검사 요청 + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + request.loginId(), + request.password() + ); + + try { + // 비밀번호 확인 + Authentication authentication = authenticationManager.authenticate(token); + + // 세션 확인 + SecurityContextHolder.getContext().setAuthentication(authentication); + return AuthResponse.from(user); + } catch (BadCredentialsException e) { + throw new AuthException(AuthErrorCode.UNAUTHORIZED); + } + + + } + +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/user/entity/User.java b/src/main/java/com/cokerthon/Team3_Backend/domain/user/entity/User.java index dfdeda8..f9d2f02 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/user/entity/User.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/user/entity/User.java @@ -31,6 +31,6 @@ public class User extends BaseEntity { private String password; /** 닉네임 */ - @Column(nullable = false, length = 256) + @Column(nullable = false, length = 256, unique = true) private String nickname; } diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/user/repository/UserRepository.java b/src/main/java/com/cokerthon/Team3_Backend/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..883bc2d --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/user/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.cokerthon.Team3_Backend.domain.user.repository; + +import com.cokerthon.Team3_Backend.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findByLoginId(String loginId); + + Optional findByNickname(String nickname); } diff --git a/src/main/java/com/cokerthon/Team3_Backend/global/apiPayload/code/GeneralErrorCode.java b/src/main/java/com/cokerthon/Team3_Backend/global/apiPayload/code/GeneralErrorCode.java index 7a21f0a..414c2fa 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/global/apiPayload/code/GeneralErrorCode.java +++ b/src/main/java/com/cokerthon/Team3_Backend/global/apiPayload/code/GeneralErrorCode.java @@ -13,6 +13,9 @@ public enum GeneralErrorCode implements BaseErrorCode { BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400_1", "잘못된 요청입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "COMMON401_1", + "인증이 필요합니다."), NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404_1", "요청한 리소스를 찾을 수 없습니다."), diff --git a/src/main/java/com/cokerthon/Team3_Backend/global/config/CorsConfig.java b/src/main/java/com/cokerthon/Team3_Backend/global/config/CorsConfig.java index 7ed5f95..f92e1e6 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/global/config/CorsConfig.java +++ b/src/main/java/com/cokerthon/Team3_Backend/global/config/CorsConfig.java @@ -43,4 +43,5 @@ public CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", config); return source; } + } \ No newline at end of file diff --git a/src/main/java/com/cokerthon/Team3_Backend/global/config/SecurityConfig.java b/src/main/java/com/cokerthon/Team3_Backend/global/config/SecurityConfig.java index 9f64ba9..4b8ac5e 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/global/config/SecurityConfig.java +++ b/src/main/java/com/cokerthon/Team3_Backend/global/config/SecurityConfig.java @@ -1,8 +1,11 @@ package com.cokerthon.Team3_Backend.global.config; +import com.cokerthon.Team3_Backend.global.security.CustomAuthenticationEntryPoint; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -17,18 +20,26 @@ public class SecurityConfig { private final CorsConfigurationSource corsConfigurationSource; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource)) - .csrf(AbstractHttpConfigurer::disable) - .formLogin(AbstractHttpConfigurer::disable) - .httpBasic(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) // CSRF 보호 비활성화 + .formLogin(AbstractHttpConfigurer::disable) // 기본 폼 로그인 비활성화 + .httpBasic(AbstractHttpConfigurer::disable) // HTTP Basic 인증 비활성화 .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() - ); + .requestMatchers("/api/auth/login", "/api/auth/signup").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() + //.anyRequest().permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(customAuthenticationEntryPoint) + ); + return http.build(); } @@ -37,4 +48,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } } \ No newline at end of file diff --git a/src/main/java/com/cokerthon/Team3_Backend/global/security/CurrentUser.java b/src/main/java/com/cokerthon/Team3_Backend/global/security/CurrentUser.java new file mode 100644 index 0000000..a7f1ee6 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/global/security/CurrentUser.java @@ -0,0 +1,14 @@ +package com.cokerthon.Team3_Backend.global.security; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@AuthenticationPrincipal(expression = "user") +public @interface CurrentUser { +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomAuthenticationEntryPoint.java b/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..1e6a789 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,41 @@ +package com.cokerthon.Team3_Backend.global.security; + +import com.cokerthon.Team3_Backend.global.apiPayload.ApiResponse; +import com.cokerthon.Team3_Backend.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) + throws IOException { + log.error("Unauthorized error: {}", authException.getMessage()); + + ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + + // JSON으로 변환, 전송 + objectMapper.writeValue(response.getWriter(), errorResponse); + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetails.java b/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetails.java new file mode 100644 index 0000000..1610062 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetails.java @@ -0,0 +1,61 @@ +package com.cokerthon.Team3_Backend.global.security; + +import com.cokerthon.Team3_Backend.domain.user.entity.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +public class CustomUserDetails implements UserDetails { + + private final User user; + + public CustomUserDetails(User user) { + this.user = user; + } + + public User getUser() { + return user; + } + + public Long getUserId() { + return user.getId(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getLoginId(); + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetailsService.java b/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetailsService.java new file mode 100644 index 0000000..4dc7144 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/global/security/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.cokerthon.Team3_Backend.global.security; + +import com.cokerthon.Team3_Backend.domain.auth.exception.AuthErrorCode; +import com.cokerthon.Team3_Backend.domain.auth.exception.AuthException; +import com.cokerthon.Team3_Backend.domain.user.entity.User; +import com.cokerthon.Team3_Backend.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String loginId) { + + User user = userRepository.findByLoginId(loginId) + .orElseThrow(() -> new AuthException(AuthErrorCode.NOT_FOUND)); + + return new CustomUserDetails(user); + } + +}