From 14e7f473b62ebb500dc695e71a404220b4265c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:32:23 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[feat/#9]=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20provider?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI.yml | 38 ++++++ build.gradle | 9 ++ .../MoplusServerApplication.java | 3 - .../auth/controller/AuthController.java | 39 ++++++ .../auth/dto/request/AdminLoginRequest.java | 7 ++ .../domain/member/domain/Member.java | 38 ++++++ .../domain/member/domain/MemberRole.java | 21 ++++ .../member/repository/MemberRepository.java | 9 ++ .../domain/member/service/MemberService.java | 21 ++++ .../config/properties/PropertiesConfig.java | 13 ++ .../config/security/SecurityConfig.java | 112 ++++++++++++++++++ .../global/error/exception/ErrorCode.java | 3 + .../global/properties/jwt/JwtProperties.java | 21 ++++ .../global/security/AuthConstants.java | 13 ++ .../global/security/JwtUtil.java | 55 +++++++++ .../EmailPasswordAuthenticationFilter.java | 37 ++++++ .../handler/EmailPasswordSuccessHandler.java | 29 +++++ .../EmailPasswordAuthenticationProvider.java | 43 +++++++ .../TransactionalPracticeService.java | 29 ----- src/main/resources/application-local.yml | 4 - src/main/resources/application-security.yml | 6 + src/main/resources/application.yml | 7 +- .../auth/controller/AuthControllerTest.java | 70 +++++++++++ src/test/resources/application-h2test.yml | 33 ++++++ src/test/resources/auth-test-data.sql | 4 + 25 files changed, 627 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/CI.yml create mode 100644 src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthController.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/auth/dto/request/AdminLoginRequest.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/member/domain/MemberRole.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java create mode 100644 src/main/java/com/moplus/moplus_server/global/config/properties/PropertiesConfig.java create mode 100644 src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java create mode 100644 src/main/java/com/moplus/moplus_server/global/properties/jwt/JwtProperties.java create mode 100644 src/main/java/com/moplus/moplus_server/global/security/AuthConstants.java create mode 100644 src/main/java/com/moplus/moplus_server/global/security/JwtUtil.java create mode 100644 src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java create mode 100644 src/main/java/com/moplus/moplus_server/global/security/handler/EmailPasswordSuccessHandler.java create mode 100644 src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java delete mode 100644 src/main/java/com/moplus/moplus_server/transactional/TransactionalPracticeService.java create mode 100644 src/main/resources/application-security.yml create mode 100644 src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java create mode 100644 src/test/resources/application-h2test.yml create mode 100644 src/test/resources/auth-test-data.sql diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..c9608e6 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,38 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle + +name: Gradle Package + +on: + pull_request: + branches: + - "master" + - "develop" + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + + - name: build and test + run: | + chmod +x gradlew + ./gradlew build diff --git a/build.gradle b/build.gradle index bdbfc70..96a2e6a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,10 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + //db-h2 implementation 'com.h2database:h2' testImplementation 'com.h2database:h2' @@ -44,6 +48,11 @@ dependencies { //s3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + } tasks.named('test') { diff --git a/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java b/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java index da5ce8f..f4f7565 100644 --- a/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java +++ b/src/main/java/com/moplus/moplus_server/MoplusServerApplication.java @@ -1,14 +1,11 @@ package com.moplus.moplus_server; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.servers.Server; import java.util.Arrays; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -@OpenAPIDefinition(servers = {@Server(url = "https://dev.mopl.kr", description = "Default Server URL")}) @SpringBootApplication @EnableJpaAuditing public class MoplusServerApplication { diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthController.java b/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthController.java new file mode 100644 index 0000000..9a3957e --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/auth/controller/AuthController.java @@ -0,0 +1,39 @@ +package com.moplus.moplus_server.domain.auth.controller; + +import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +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 +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + + @Operation(summary = "어드민 로그인", description = "아아디 패스워드 로그인 후 토큰 발급합니다.") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "로그인 성공", + headers = { + @Header(name = "Authorization", description = "Access Token", schema = @Schema(type = "string")), + @Header(name = "RefreshToken", description = "Refresh Token", schema = @Schema(type = "string")) + } + ), + @ApiResponse(responseCode = "400", description = "잘못된 요청"), + @ApiResponse(responseCode = "401", description = "인증 실패") + }) + @PostMapping("/admin/login") + public void adminLogin( + @RequestBody AdminLoginRequest request + ) { + // 실제 처리는 Security 필터에서 이루어지며, 이 메서드는 Swagger 명세용입니다. + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/auth/dto/request/AdminLoginRequest.java b/src/main/java/com/moplus/moplus_server/domain/auth/dto/request/AdminLoginRequest.java new file mode 100644 index 0000000..861df83 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/auth/dto/request/AdminLoginRequest.java @@ -0,0 +1,7 @@ +package com.moplus.moplus_server.domain.auth.dto.request; + +public record AdminLoginRequest( + String email, + String password +) { +} diff --git a/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java b/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java new file mode 100644 index 0000000..13579cd --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java @@ -0,0 +1,38 @@ +package com.moplus.moplus_server.domain.member.domain; + +import com.moplus.moplus_server.global.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long id; + + private String email; + private String password; + + @Enumerated(EnumType.STRING) + private MemberRole role; + + @Builder + public Member(String nickname, String email, String password, MemberRole role) { + this.email = email; + this.password = password; + this.role = role; + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/member/domain/MemberRole.java b/src/main/java/com/moplus/moplus_server/domain/member/domain/MemberRole.java new file mode 100644 index 0000000..3f38166 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/member/domain/MemberRole.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.domain.member.domain; + +import java.util.Arrays; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MemberRole { + USER("ROLE_USER"), + ADMIN("ROLE_ADMIN"); + + private final String value; + + public static MemberRole findByKey(String value) { + return Arrays.stream(MemberRole.values()) + .filter(role -> role.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No role with key: " + value)); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java b/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java new file mode 100644 index 0000000..bdfe25c --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java @@ -0,0 +1,9 @@ +package com.moplus.moplus_server.domain.member.repository; + +import com.moplus.moplus_server.domain.member.domain.Member; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java b/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java new file mode 100644 index 0000000..3fb39dc --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.domain.member.service; + +import com.moplus.moplus_server.domain.member.domain.Member; +import com.moplus.moplus_server.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + @Transactional(readOnly = true) + public Member getMemberByEmail(String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("해당 이메일로 가입된 회원이 없습니다.")); + } + +} diff --git a/src/main/java/com/moplus/moplus_server/global/config/properties/PropertiesConfig.java b/src/main/java/com/moplus/moplus_server/global/config/properties/PropertiesConfig.java new file mode 100644 index 0000000..f616ece --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/config/properties/PropertiesConfig.java @@ -0,0 +1,13 @@ +package com.moplus.moplus_server.global.config.properties; + +import com.moplus.moplus_server.global.properties.jwt.JwtProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@EnableConfigurationProperties({ + JwtProperties.class +}) +@Configuration +public class PropertiesConfig { + +} diff --git a/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java b/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java new file mode 100644 index 0000000..292a2e0 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java @@ -0,0 +1,112 @@ +package com.moplus.moplus_server.global.config.security; + +import com.moplus.moplus_server.domain.member.service.MemberService; +import com.moplus.moplus_server.global.security.filter.EmailPasswordAuthenticationFilter; +import com.moplus.moplus_server.global.security.handler.EmailPasswordSuccessHandler; +import com.moplus.moplus_server.global.security.provider.EmailPasswordAuthenticationProvider; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +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.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final MemberService memberService; + private final EmailPasswordSuccessHandler emailPasswordSuccessHandler; + private final AuthenticationConfiguration authenticationConfiguration; + + private String[] allowUrls = {"/", "/favicon.ico", "/swagger-ui/**", "/v3/**"}; + + @Value("${cors-allowed-origins}") + private List corsAllowedOrigins; + + @Bean + public WebSecurityCustomizer configure() { + // filter 안타게 무시 + return (web) -> web.ignoring().requestMatchers(allowUrls); + } + + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .cors(customizer -> customizer.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(request -> request + .requestMatchers(allowUrls).permitAll() + .anyRequest().authenticated()); + + http + .exceptionHandling(exception -> + exception.authenticationEntryPoint((request, response, authException) -> + response.setStatus(HttpStatus.UNAUTHORIZED.value()))); // 인증,인가가 되지 않은 요청 시 발생시 + + http + .addFilterAt(emailPasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); +// .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) +// .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public EmailPasswordAuthenticationFilter emailPasswordAuthenticationFilter() throws Exception { + EmailPasswordAuthenticationFilter emailPasswordAuthenticationFilter = new EmailPasswordAuthenticationFilter( + authenticationManager(authenticationConfiguration)); + emailPasswordAuthenticationFilter.setFilterProcessesUrl("/api/v1/auth/admin/login"); + emailPasswordAuthenticationFilter.setAuthenticationSuccessHandler(emailPasswordSuccessHandler); + emailPasswordAuthenticationFilter.afterPropertiesSet(); + return emailPasswordAuthenticationFilter; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + ProviderManager providerManager = (ProviderManager) authenticationConfiguration.getAuthenticationManager(); + providerManager.getProviders().add(emailPasswordAuthenticationProvider()); + return configuration.getAuthenticationManager(); + } + + @Bean + public EmailPasswordAuthenticationProvider emailPasswordAuthenticationProvider() { + return new EmailPasswordAuthenticationProvider(memberService, bCryptPasswordEncoder()); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(corsAllowedOrigins); + configuration.addAllowedMethod("*"); + configuration.setAllowedHeaders(List.of("*")); // 허용할 헤더 + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); // 모든 경로에 적용 + return source; + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java index fb9d7e3..2b572be 100644 --- a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java @@ -23,6 +23,9 @@ public enum ErrorCode { //이미지 IMAGE_FILE_EXTENSION_NOT_FOUND(HttpStatus.NOT_FOUND, "지원하지 않는 이미지 확장자입니다"), IMAGE_FILE_NOT_FOUND_IN_S3(HttpStatus.NOT_FOUND, "S3에 해당 이미지 파일을 찾을 수 없습니다"), + + //회원 + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다"), ; diff --git a/src/main/java/com/moplus/moplus_server/global/properties/jwt/JwtProperties.java b/src/main/java/com/moplus/moplus_server/global/properties/jwt/JwtProperties.java new file mode 100644 index 0000000..bd71a2e --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/properties/jwt/JwtProperties.java @@ -0,0 +1,21 @@ +package com.moplus.moplus_server.global.properties.jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtProperties( + String accessTokenSecret, + String refreshTokenSecret, + Long accessTokenExpirationTime, + Long refreshTokenExpirationTime, + String issuer +) { + + public Long accessTokenExpirationMilliTime() { + return accessTokenExpirationTime * 1000; + } + + public Long refreshTokenExpirationMilliTime() { + return refreshTokenExpirationTime * 1000; + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/security/AuthConstants.java b/src/main/java/com/moplus/moplus_server/global/security/AuthConstants.java new file mode 100644 index 0000000..312195b --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/security/AuthConstants.java @@ -0,0 +1,13 @@ +package com.moplus.moplus_server.global.security; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class AuthConstants { + + public static final String AUTH_HEADER = "Authorization"; + public static final String TOKEN_TYPE = "BEARER"; + public static final String REFRESH_TOKEN_HEADER = "RefreshToken"; + +} diff --git a/src/main/java/com/moplus/moplus_server/global/security/JwtUtil.java b/src/main/java/com/moplus/moplus_server/global/security/JwtUtil.java new file mode 100644 index 0000000..fdaf629 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/security/JwtUtil.java @@ -0,0 +1,55 @@ +package com.moplus.moplus_server.global.security; + +import com.moplus.moplus_server.domain.member.domain.Member; +import com.moplus.moplus_server.global.properties.jwt.JwtProperties; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtUtil { + + public static final String TOKEN_ROLE_NAME = "role"; + private final JwtProperties jwtProperties; + + public String generateAccessToken(Member member) { + Date issuedAt = new Date(); + Date expiredAt = new Date(issuedAt.getTime() + jwtProperties.accessTokenExpirationMilliTime()); + return Jwts.builder() + .setIssuer(jwtProperties.issuer()) + .setSubject(member.getId().toString()) + .claim(TOKEN_ROLE_NAME, member.getRole().getValue()) + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .signWith(getAccessTokenKey()) + .compact(); + } + + public String generateRefreshToken(Member member) { + Date issuedAt = new Date(); + Date expiredAt = new Date(issuedAt.getTime() + jwtProperties.refreshTokenExpirationMilliTime()); + + return Jwts.builder() + .setIssuer(jwtProperties.issuer()) + .setSubject(member.getId().toString()) + .claim(TOKEN_ROLE_NAME, member.getRole().getValue()) + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .signWith(getRefreshTokenKey()) + .compact(); + } + + private Key getAccessTokenKey() { + return Keys.hmacShaKeyFor(jwtProperties.accessTokenSecret().getBytes()); + } + + private Key getRefreshTokenKey() { + return Keys.hmacShaKeyFor(jwtProperties.refreshTokenSecret().getBytes()); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java b/src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java new file mode 100644 index 0000000..9c23e26 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java @@ -0,0 +1,37 @@ +package com.moplus.moplus_server.global.security.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class EmailPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + public EmailPasswordAuthenticationFilter(final AuthenticationManager authenticationManager) { + super.setAuthenticationManager(authenticationManager); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException { + final UsernamePasswordAuthenticationToken authRequest; + try { + final AdminLoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(), + AdminLoginRequest.class); + authRequest = UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.email(), + loginRequest.password()); + } catch (IOException exception) { + throw new NotFoundException(ErrorCode.MEMBER_NOT_FOUND); + } + setDetails(request, authRequest); + return this.getAuthenticationManager().authenticate(authRequest); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/security/handler/EmailPasswordSuccessHandler.java b/src/main/java/com/moplus/moplus_server/global/security/handler/EmailPasswordSuccessHandler.java new file mode 100644 index 0000000..a7cdaba --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/security/handler/EmailPasswordSuccessHandler.java @@ -0,0 +1,29 @@ +package com.moplus.moplus_server.global.security.handler; + +import com.moplus.moplus_server.domain.member.domain.Member; +import com.moplus.moplus_server.global.security.AuthConstants; +import com.moplus.moplus_server.global.security.JwtUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EmailPasswordSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + + private final JwtUtil jwtUtil; + + @Override + public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, + final Authentication authentication) { + Member member = (Member) authentication.getPrincipal(); + String accessToken = jwtUtil.generateAccessToken(member); + String refreshToken = jwtUtil.generateRefreshToken(member); + response.addHeader(AuthConstants.AUTH_HEADER, AuthConstants.TOKEN_TYPE + " " + accessToken); + response.addHeader(AuthConstants.REFRESH_TOKEN_HEADER, AuthConstants.TOKEN_TYPE + " " + refreshToken); + } + +} diff --git a/src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java b/src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java new file mode 100644 index 0000000..0012386 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java @@ -0,0 +1,43 @@ +package com.moplus.moplus_server.global.security.provider; + +import com.moplus.moplus_server.domain.member.domain.Member; +import com.moplus.moplus_server.domain.member.service.MemberService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@RequiredArgsConstructor +public class EmailPasswordAuthenticationProvider implements AuthenticationProvider { + + private final MemberService memberService; + private final BCryptPasswordEncoder passwordEncoder; + + @Override + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { + final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; + final String memberEmail = token.getName(); + final String memberPassword = (String) token.getCredentials(); + + Member member = memberService.getMemberByEmail(memberEmail); + if (!memberPassword.equals(member.getPassword())) { + throw new BadCredentialsException(member.getEmail() + "Invalid password"); + } + + return UsernamePasswordAuthenticationToken.authenticated( + member, + memberPassword, + List.of(new SimpleGrantedAuthority(member.getRole().getValue()) + )); + } + + @Override + public boolean supports(Class authentication) { + return authentication.equals(UsernamePasswordAuthenticationToken.class); + } +} diff --git a/src/main/java/com/moplus/moplus_server/transactional/TransactionalPracticeService.java b/src/main/java/com/moplus/moplus_server/transactional/TransactionalPracticeService.java deleted file mode 100644 index 801e578..0000000 --- a/src/main/java/com/moplus/moplus_server/transactional/TransactionalPracticeService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.moplus.moplus_server.transactional; - -import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.practiceTest.service.client.PracticeTestService; - -public class TransactionalPracticeService { - - private final PracticeTestService practiceTestService; - - public TransactionalPracticeService(PracticeTestService practiceTestService) { - this.practiceTestService = practiceTestService; - } - - public void updateViewCount(Long practiceTestId) { - startTransaction(); - - practiceTestService.updateViewCount(practiceTestId); - - endTransaction(); - } - - private void startTransaction() { - // start transaction - } - - private void endTransaction() { - // end transaction - } -} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 5667c0f..72180bd 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -10,7 +10,3 @@ spring: use_sql_comments: true hibernate: ddl-auto: update - -logging: - level: - root: ERROR diff --git a/src/main/resources/application-security.yml b/src/main/resources/application-security.yml new file mode 100644 index 0000000..266da82 --- /dev/null +++ b/src/main/resources/application-security.yml @@ -0,0 +1,6 @@ +jwt: + access-token-secret: ${JWT_ACCESS_TOKEN_SECRET:} + refresh-token-secret: ${JWT_REFRESH_TOKEN_SECRET:} + access-token-expiration-time: ${JWT_ACCESS_TOKEN_EXPIRATION_TIME:36000} #10시간 + refresh-token-expiration-time: ${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800} #7일 + issuer: ${JWT_ISSUER:} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7be66c3..c5ca596 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,10 +2,15 @@ spring: profiles: active: ${profile} group: - local: "local" + local: "local, datasource" dev: "dev" prod: "prod" include: - aws + - security mvc: ignore-default-favicon: true + +cors-allowed-origins: + http://localhost:8080, + http://localhost:3000, \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java new file mode 100644 index 0000000..9354676 --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java @@ -0,0 +1,70 @@ +package com.moplus.moplus_server.domain.auth.controller; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +@Transactional +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("h2test") +@Sql({"/auth-test-data.sql"}) +class AuthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private WebApplicationContext context; + + @BeforeEach + public void setMockMvc() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()).build(); + } + + @Test + void 어드민_로그인_성공() throws Exception { + + AdminLoginRequest request = new AdminLoginRequest("admin@example.com", "password123"); // DTO 객체 생성 + String requestBody = objectMapper.writeValueAsString(request); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") + .contentType("application/json") + .content(requestBody)) + .andExpect(status().isOk()) // 200 응답 확인 + .andExpect(header().exists("Authorization")) + .andExpect(header().exists("RefreshToken")); + + } + + @Test + void 어드민_로그인_실패() throws Exception { + + AdminLoginRequest request = new AdminLoginRequest("admin@example.com", "wrong123"); // DTO 객체 생성 + String requestBody = objectMapper.writeValueAsString(request); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") + .contentType("application/json") + .content(requestBody)) + .andExpect(status().isUnauthorized()); // 401 응답 확인 + } +} \ No newline at end of file diff --git a/src/test/resources/application-h2test.yml b/src/test/resources/application-h2test.yml new file mode 100644 index 0000000..f1bf6e6 --- /dev/null +++ b/src/test/resources/application-h2test.yml @@ -0,0 +1,33 @@ +spring: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + + datasource: + url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MYSQL + +jwt: + access-token-secret: 0eFd7h2PLQ5tH7v3jBcXFr6L8hYh5u3g1kFxWrZ0dJc= + refresh-token-secret: q8aV4Mf4r7l5u9OxC7ZtVx2qY2eDz9Tw5uDl9JQ6SJI= + access-token-expiration-time: 3600 #1시간 + refresh-token-expiration-time: 604800 #7일 + issuer: test + +cloud: + aws: + s3: + bucket: test + signature-version: AWS4-HMAC-SHA256 + credentials: + access-key: 0eFd7h2PLQ5tH7v3jBcXFr6L8hYh5u3g1kFxWrZ0dJc= + secret-key: q8aV4Mf4r7l5u9OxC7ZtVx2qY2eDz9Tw5uDl9JQ6SJI= + region: + static: test + auto: false + stack: + auto: false + +cors-allowed-origins: + http://localhost:8080, + http://localhost:3000, diff --git a/src/test/resources/auth-test-data.sql b/src/test/resources/auth-test-data.sql new file mode 100644 index 0000000..f757a22 --- /dev/null +++ b/src/test/resources/auth-test-data.sql @@ -0,0 +1,4 @@ +INSERT INTO member (deleted, created_at, update_at, member_id, email, password, role) +VALUES (false, '2024-07-24 21:27:20.000000', '2024-07-24 21:27:21.000000', 1, 'admin@example.com', + 'password123', 'ADMIN'); + From c8b6a3532839d41194fcc9a3c044903b05670052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:45:48 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[fix]=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20DB=20h2=20=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OptimisticLockPracticeTestFacadeTest.java | 54 ------------------- .../client/PracticeTestServiceTest.java | 14 +---- .../client/ProblemServiceConcurrencyTest.java | 9 ++-- 3 files changed, 5 insertions(+), 72 deletions(-) delete mode 100644 src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/OptimisticLockPracticeTestFacadeTest.java diff --git a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/OptimisticLockPracticeTestFacadeTest.java b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/OptimisticLockPracticeTestFacadeTest.java deleted file mode 100644 index 487cd63..0000000 --- a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/OptimisticLockPracticeTestFacadeTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.moplus.moplus_server.domain.practiceTest.service.client; - -import static org.junit.jupiter.api.Assertions.*; - -import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.practiceTest.repository.PracticeTestRepository; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles("test") -class OptimisticLockPracticeTestFacadeTest { - @Autowired - private OptimisticLockPracticeTestFacade optimisticLockPracticeTestFacade; - - @Autowired - private PracticeTestRepository practiceTestRepository; - - @BeforeEach - void setup() { - PracticeTest practiceTest = new PracticeTest(); - practiceTestRepository.save(practiceTest); - } - - @Test - public void 동시에_조회수가_정상적으로_업데이트_되어야한다() throws InterruptedException { - Long practiceTestId = 1L; - int threadCount = 100; - ExecutorService executorService = Executors.newFixedThreadPool(36); - CountDownLatch countDownLatch = new CountDownLatch(threadCount); - - for (int i = 0; i < threadCount; i++) { - executorService.submit(() -> { - try { - optimisticLockPracticeTestFacade.updateViewCount(practiceTestId); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - countDownLatch.countDown(); - } - }); - } - countDownLatch.await(); - - PracticeTest practiceTest = practiceTestRepository.findById(practiceTestId).orElseThrow(); - assertEquals(threadCount, practiceTest.getViewCount()); - } -} \ No newline at end of file diff --git a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java index 092c189..6558a15 100644 --- a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/PracticeTestServiceTest.java @@ -1,31 +1,21 @@ package com.moplus.moplus_server.domain.practiceTest.service.client; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.practiceTest.domain.Problem; import com.moplus.moplus_server.domain.practiceTest.repository.PracticeTestRepository; import com.moplus.moplus_server.domain.practiceTest.repository.ProblemRepository; -import jakarta.transaction.Transactional; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.jdbc.Sql; @SpringBootTest -@ActiveProfiles("test") +@ActiveProfiles("h2test") class PracticeTestServiceTest { @Autowired diff --git a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java index eb9936b..b8483f7 100644 --- a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java @@ -2,10 +2,8 @@ import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest; import com.moplus.moplus_server.domain.practiceTest.domain.Problem; -import com.moplus.moplus_server.domain.practiceTest.dto.client.response.ProblemGetResponse; import com.moplus.moplus_server.domain.practiceTest.repository.PracticeTestRepository; import com.moplus.moplus_server.domain.practiceTest.repository.ProblemRepository; -import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -17,7 +15,7 @@ import org.springframework.test.context.ActiveProfiles; @SpringBootTest -@ActiveProfiles("test") +@ActiveProfiles("h2test") public class ProblemServiceConcurrencyTest { @Autowired @@ -58,9 +56,8 @@ public void testConcurrentUpdateCorrectRateAndCount() throws InterruptedExceptio CountDownLatch countDownLatch = new CountDownLatch(threadCount); Problem problem = problemRepository.findById(1L).orElseThrow(); - for (int i = 0; i < threadCount; i++) { - if(i % 2 == 0){ + if (i % 2 == 0) { executorService.submit(() -> { try { practiceTestService.updateViewCount(practiceTestId); @@ -82,7 +79,7 @@ public void testConcurrentUpdateCorrectRateAndCount() throws InterruptedExceptio PracticeTest practiceTest = practiceTestRepository.findById(practiceTestId).orElseThrow(); - Assertions.assertEquals(threadCount/2, practiceTest.getViewCount()); + Assertions.assertEquals(threadCount / 2, practiceTest.getViewCount()); } } From 24d496d22638f714bbae3ecb6136d0d91bcf665e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:47:45 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[fix]=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/ProblemServiceConcurrencyTest.java | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java diff --git a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java b/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java deleted file mode 100644 index b8483f7..0000000 --- a/src/test/java/com/moplus/moplus_server/domain/practiceTest/service/client/ProblemServiceConcurrencyTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.moplus.moplus_server.domain.practiceTest.service.client; - -import com.moplus.moplus_server.domain.practiceTest.domain.PracticeTest; -import com.moplus.moplus_server.domain.practiceTest.domain.Problem; -import com.moplus.moplus_server.domain.practiceTest.repository.PracticeTestRepository; -import com.moplus.moplus_server.domain.practiceTest.repository.ProblemRepository; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles("h2test") -public class ProblemServiceConcurrencyTest { - - @Autowired - private PracticeTestService practiceTestService; - - @Autowired - private PracticeTestRepository practiceTestRepository; - - @Autowired - private ProblemRepository problemRepository; - - @Autowired - private ProblemService problemService; - - @BeforeEach - void setup() { - PracticeTest practiceTest = new PracticeTest(); - practiceTestRepository.save(practiceTest); - PracticeTest entity = practiceTestRepository.findById(practiceTest.getId()).orElseThrow(); - - Problem problem = Problem.builder() - .problemNumber("1") - .answer("42") - .point(5) - .incorrectNum(10L) - .practiceTest(entity) // Assume we have a PracticeTest entity linked here - .correctRate(0.5) - .build(); - problemRepository.save(problem); - } - - @Test - public void testConcurrentUpdateCorrectRateAndCount() throws InterruptedException { - Long practiceTestId = 1L; - Long problemId = 1L; - int threadCount = 100; - ExecutorService executorService = Executors.newFixedThreadPool(36); - CountDownLatch countDownLatch = new CountDownLatch(threadCount); - Problem problem = problemRepository.findById(1L).orElseThrow(); - - for (int i = 0; i < threadCount; i++) { - if (i % 2 == 0) { - executorService.submit(() -> { - try { - practiceTestService.updateViewCount(practiceTestId); - } finally { - countDownLatch.countDown(); - } - }); - } else { - executorService.submit(() -> { - try { - problemService.updateCorrectRate(practiceTestId, "1", 0.7); - } finally { - countDownLatch.countDown(); - } - }); - } - } - countDownLatch.await(); - - PracticeTest practiceTest = practiceTestRepository.findById(practiceTestId).orElseThrow(); - - Assertions.assertEquals(threadCount / 2, practiceTest.getViewCount()); - } - -} From 44e718d0c8811a8ce5860ab244812c9420ecb10c Mon Sep 17 00:00:00 2001 From: sejoon Date: Wed, 15 Jan 2025 16:55:44 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[fix/#9]=20=EB=8B=A8=EC=88=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=A1=B0=ED=9A=8C=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20repository=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/repository/MemberRepository.java | 6 ++++++ .../moplus_server/domain/member/service/MemberService.java | 4 +--- .../moplus_server/global/error/exception/ErrorCode.java | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java b/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java index bdfe25c..9eb0168 100644 --- a/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/member/repository/MemberRepository.java @@ -1,9 +1,15 @@ package com.moplus.moplus_server.domain.member.repository; import com.moplus.moplus_server.domain.member.domain.Member; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); + + default Member findByEmailOrThrow(String email) { + return findByEmail(email).orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + } } diff --git a/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java b/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java index 3fb39dc..a16f50b 100644 --- a/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java +++ b/src/main/java/com/moplus/moplus_server/domain/member/service/MemberService.java @@ -14,8 +14,6 @@ public class MemberService { @Transactional(readOnly = true) public Member getMemberByEmail(String email) { - return memberRepository.findByEmail(email) - .orElseThrow(() -> new IllegalArgumentException("해당 이메일로 가입된 회원이 없습니다.")); + return memberRepository.findByEmailOrThrow(email); } - } diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java index 2b572be..bc30576 100644 --- a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java @@ -5,7 +5,11 @@ @Getter public enum ErrorCode { + + //공통 INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류, 관리자에게 문의하세요"), + INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "잘못된 입력 값입니다"), + BAD_CREDENTIALS(HttpStatus.UNAUTHORIZED, "잘못된 인증 정보입니다"), //모의고사 PRACTICE_TEST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 모의고사를 찾을 수 없습니다"), From 88c2033afd3370fb0745da96bed8b30dd2d894c5 Mon Sep 17 00:00:00 2001 From: sejoon Date: Wed, 15 Jan 2025 16:57:08 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[fix/#9]=20parsing=20=EC=98=88=EC=99=B8,=20?= =?UTF-8?q?NotFound=20=EC=98=88=EC=99=B8=20BadCredentialsException?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../domain/member/domain/Member.java | 4 + .../config/security/SecurityConfig.java | 2 +- .../EmailPasswordAuthenticationFilter.java | 4 +- .../EmailPasswordAuthenticationProvider.java | 25 ++++-- .../auth/controller/AuthControllerTest.java | 84 ++++++++++++++----- 6 files changed, 95 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index 96a2e6a..47c4de6 100644 --- a/build.gradle +++ b/build.gradle @@ -53,6 +53,9 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // validator + implementation 'commons-validator:commons-validator:1.7' + } tasks.named('test') { diff --git a/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java b/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java index 13579cd..289a78d 100644 --- a/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java +++ b/src/main/java/com/moplus/moplus_server/domain/member/domain/Member.java @@ -35,4 +35,8 @@ public Member(String nickname, String email, String password, MemberRole role) { this.password = password; this.role = role; } + + public boolean isMatchingPassword(String password) { + return this.password.equals(password); + } } diff --git a/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java b/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java index 292a2e0..64d3142 100644 --- a/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java +++ b/src/main/java/com/moplus/moplus_server/global/config/security/SecurityConfig.java @@ -89,7 +89,7 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c @Bean public EmailPasswordAuthenticationProvider emailPasswordAuthenticationProvider() { - return new EmailPasswordAuthenticationProvider(memberService, bCryptPasswordEncoder()); + return new EmailPasswordAuthenticationProvider(memberService); } @Bean diff --git a/src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java b/src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java index 9c23e26..310ad74 100644 --- a/src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java +++ b/src/main/java/com/moplus/moplus_server/global/security/filter/EmailPasswordAuthenticationFilter.java @@ -3,11 +3,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest; import com.moplus.moplus_server.global.error.exception.ErrorCode; -import com.moplus.moplus_server.global.error.exception.NotFoundException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; 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.AuthenticationException; @@ -29,7 +29,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ authRequest = UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.email(), loginRequest.password()); } catch (IOException exception) { - throw new NotFoundException(ErrorCode.MEMBER_NOT_FOUND); + throw new BadCredentialsException(ErrorCode.INVALID_INPUT_VALUE.getMessage()); } setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); diff --git a/src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java b/src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java index 0012386..a284656 100644 --- a/src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java +++ b/src/main/java/com/moplus/moplus_server/global/security/provider/EmailPasswordAuthenticationProvider.java @@ -2,21 +2,27 @@ import com.moplus.moplus_server.domain.member.domain.Member; import com.moplus.moplus_server.domain.member.service.MemberService; +import com.moplus.moplus_server.global.error.exception.ErrorCode; import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.commons.validator.routines.EmailValidator; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @RequiredArgsConstructor public class EmailPasswordAuthenticationProvider implements AuthenticationProvider { private final MemberService memberService; - private final BCryptPasswordEncoder passwordEncoder; + + private static void validateEmail(String memberEmail) { + if (!EmailValidator.getInstance().isValid(memberEmail)) { + throw new BadCredentialsException(ErrorCode.BAD_CREDENTIALS.getMessage()); + } + } @Override public Authentication authenticate(final Authentication authentication) throws AuthenticationException { @@ -24,9 +30,10 @@ public Authentication authenticate(final Authentication authentication) throws A final String memberEmail = token.getName(); final String memberPassword = (String) token.getCredentials(); - Member member = memberService.getMemberByEmail(memberEmail); - if (!memberPassword.equals(member.getPassword())) { - throw new BadCredentialsException(member.getEmail() + "Invalid password"); + validateEmail(memberEmail); + final Member member = getMemberByEmail(memberEmail); + if (!member.isMatchingPassword(memberPassword)) { + throw new BadCredentialsException(ErrorCode.BAD_CREDENTIALS.getMessage()); } return UsernamePasswordAuthenticationToken.authenticated( @@ -36,6 +43,14 @@ public Authentication authenticate(final Authentication authentication) throws A )); } + private Member getMemberByEmail(String memberEmail) { + try { + return memberService.getMemberByEmail(memberEmail); + } catch (Exception e) { + throw new BadCredentialsException(ErrorCode.BAD_CREDENTIALS.getMessage()); + } + } + @Override public boolean supports(Class authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); diff --git a/src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java index 9354676..95ee4e7 100644 --- a/src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/moplus/moplus_server/domain/auth/controller/AuthControllerTest.java @@ -7,7 +7,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -41,30 +44,73 @@ public void setMockMvc() { .apply(springSecurity()).build(); } - @Test - void 어드민_로그인_성공() throws Exception { + @Nested + class 어드민_로그인 { - AdminLoginRequest request = new AdminLoginRequest("admin@example.com", "password123"); // DTO 객체 생성 - String requestBody = objectMapper.writeValueAsString(request); + @Test + void 성공() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") - .contentType("application/json") - .content(requestBody)) - .andExpect(status().isOk()) // 200 응답 확인 - .andExpect(header().exists("Authorization")) - .andExpect(header().exists("RefreshToken")); + AdminLoginRequest request = new AdminLoginRequest("admin@example.com", "password123"); + String requestBody = objectMapper.writeValueAsString(request); - } + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") + .contentType("application/json") + .content(requestBody)) + .andExpect(status().isOk()) // 200 응답 확인 + .andExpect(header().exists("Authorization")) + .andExpect(header().exists("RefreshToken")); + + } + + @Test + void 잘못된_요청_본문() throws Exception { + + record TempRecord(String data) { + } + + TempRecord request = new TempRecord("임시 테스트 요청 본문"); + String requestBody = objectMapper.writeValueAsString(request); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") + .contentType("application/json") + .content(requestBody)) + .andExpect(status().isUnauthorized()); + } + + @ParameterizedTest + @ValueSource(strings = { + "plainaddress", // 이메일 형식이 아님 + "@missingusername.com", // 사용자명 없음 + "username@.com", // 도메인 이름 없음 + "username@com", // 잘못된 도메인 + "username@domain..com", // 연속된 점 + "username@domain,com", // 쉼표 포함 + "username@domain space.com", // 공백 포함 + "username@domain.com space", // 공백 포함 + "username@domain#com", // 특수문자 포함 + "" // 빈 문자열 + }) + void 잘못된_이메일_양식(String email) throws Exception { + + AdminLoginRequest request = new AdminLoginRequest(email, "password123"); + String requestBody = objectMapper.writeValueAsString(request); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") + .contentType("application/json") + .content(requestBody)) + .andExpect(status().isUnauthorized()); // 401 응답 확인 + } - @Test - void 어드민_로그인_실패() throws Exception { + @Test + void 실패() throws Exception { - AdminLoginRequest request = new AdminLoginRequest("admin@example.com", "wrong123"); // DTO 객체 생성 - String requestBody = objectMapper.writeValueAsString(request); + AdminLoginRequest request = new AdminLoginRequest("admin@example.com", "wrong123"); + String requestBody = objectMapper.writeValueAsString(request); - mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") - .contentType("application/json") - .content(requestBody)) - .andExpect(status().isUnauthorized()); // 401 응답 확인 + mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/auth/admin/login") + .contentType("application/json") + .content(requestBody)) + .andExpect(status().isUnauthorized()); // 401 응답 확인 + } } } \ No newline at end of file