From 41b4b4e9d6cf3d1750fe7902c7e10d886abeff84 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: Fri, 24 Jan 2025 20:20:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[feat/#17]=20=EB=82=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 26 +++++++ .../domain/member/domain/Member.java | 4 +- .../dto/response/MemberGetResponse.java | 18 +++++ .../global/annotation/AuthUser.java | 14 ++++ .../AuthenticationArgumentResolver.java | 53 +++++++++++++ .../global/config/WebConfig.java | 19 +++-- .../global/config/swagger/SwaggerConfig.java | 28 +++++++ .../controller/MemberControllerTest.java | 78 +++++++++++++++++++ ...ion-test.yml => application-mysqltest.yml} | 0 src/test/resources/auth-test-data.sql | 4 +- 10 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/moplus/moplus_server/domain/member/controller/MemberController.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/member/dto/response/MemberGetResponse.java create mode 100644 src/main/java/com/moplus/moplus_server/global/annotation/AuthUser.java create mode 100644 src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java create mode 100644 src/main/java/com/moplus/moplus_server/global/config/swagger/SwaggerConfig.java create mode 100644 src/test/java/com/moplus/moplus_server/domain/member/controller/MemberControllerTest.java rename src/test/resources/{application-test.yml => application-mysqltest.yml} (100%) diff --git a/src/main/java/com/moplus/moplus_server/domain/member/controller/MemberController.java b/src/main/java/com/moplus/moplus_server/domain/member/controller/MemberController.java new file mode 100644 index 0000000..c23ddc7 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/member/controller/MemberController.java @@ -0,0 +1,26 @@ +package com.moplus.moplus_server.domain.member.controller; + +import com.moplus.moplus_server.domain.member.domain.Member; +import com.moplus.moplus_server.domain.member.dto.response.MemberGetResponse; +import com.moplus.moplus_server.global.annotation.AuthUser; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/member") +@RequiredArgsConstructor +public class MemberController { + + @GetMapping("me") + @Operation(summary = "내 정보 조회", description = "jwt accessToken을 통해 내 정보를 조회합니다.") + public ResponseEntity getMyInfo( + @AuthUser Member member + ) { + return ResponseEntity.ok(MemberGetResponse.of(member)); + } +} + 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 289a78d..fb000bd 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 @@ -23,6 +23,7 @@ public class Member extends BaseEntity { @Column(name = "member_id") private Long id; + private String name; private String email; private String password; @@ -30,7 +31,8 @@ public class Member extends BaseEntity { private MemberRole role; @Builder - public Member(String nickname, String email, String password, MemberRole role) { + public Member(String name, String email, String password, MemberRole role) { + this.name = name; this.email = email; this.password = password; this.role = role; diff --git a/src/main/java/com/moplus/moplus_server/domain/member/dto/response/MemberGetResponse.java b/src/main/java/com/moplus/moplus_server/domain/member/dto/response/MemberGetResponse.java new file mode 100644 index 0000000..0585dac --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/member/dto/response/MemberGetResponse.java @@ -0,0 +1,18 @@ +package com.moplus.moplus_server.domain.member.dto.response; + + +import com.moplus.moplus_server.domain.member.domain.Member; + +public record MemberGetResponse( + Long id, + String name, + String email +) { + public static MemberGetResponse of(Member member) { + return new MemberGetResponse( + member.getId(), + member.getName(), + member.getEmail() + ); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/annotation/AuthUser.java b/src/main/java/com/moplus/moplus_server/global/annotation/AuthUser.java new file mode 100644 index 0000000..5dd8a36 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/annotation/AuthUser.java @@ -0,0 +1,14 @@ +package com.moplus.moplus_server.global.annotation; + +import io.swagger.v3.oas.annotations.Parameter; +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) +@Parameter(hidden = true) +public @interface AuthUser { + +} diff --git a/src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java b/src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java new file mode 100644 index 0000000..2b20910 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java @@ -0,0 +1,53 @@ +package com.moplus.moplus_server.global.annotation; + +import com.moplus.moplus_server.domain.member.domain.Member; +import com.moplus.moplus_server.domain.member.repository.MemberRepository; +import com.moplus.moplus_server.global.error.exception.BusinessException; +import com.moplus.moplus_server.global.error.exception.ErrorCode; +import com.moplus.moplus_server.global.error.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver { + + private final MemberRepository memberRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + final boolean isUserAuthAnnotation = parameter.getParameterAnnotation(AuthUser.class) != null; + final boolean isMemberClass = parameter.getParameterType().equals(Member.class); + return isUserAuthAnnotation && isMemberClass; + } + + @Override + public Member resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + return getCurrentMember(); + } + + private Member getCurrentMember() { + return memberRepository + .findById(getCurrentMemberId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND)); + } + + private Long getCurrentMemberId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new BusinessException(ErrorCode.AUTH_NOT_FOUND); + } + + Member principal = (Member) authentication.getPrincipal(); + return principal.getId(); + } +} diff --git a/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java b/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java index 51117e6..1663f45 100644 --- a/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java +++ b/src/main/java/com/moplus/moplus_server/global/config/WebConfig.java @@ -1,9 +1,12 @@ package com.moplus.moplus_server.global.config; +import com.moplus.moplus_server.global.annotation.AuthenticationArgumentResolver; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -13,15 +16,21 @@ @EnableTransactionManagement public class WebConfig implements WebMvcConfigurer { + private final AuthenticationArgumentResolver authenticationArgumentResolver; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("https://dev.mopl.kr","http://dev.mopl.kr", "http://localhost:8080", "https://www.mopl.kr", "http" - + "://localhost:3000", "https://dev-web.mopl.kr", "http://dev-web.mopl.kr") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); + .allowedOrigins("https://dev.mopl.kr", "http://dev.mopl.kr", "http://localhost:8080", + "https://www.mopl.kr", "http" + + "://localhost:3000", "https://dev-web.mopl.kr", "http://dev-web.mopl.kr") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); } + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(authenticationArgumentResolver); + } } diff --git a/src/main/java/com/moplus/moplus_server/global/config/swagger/SwaggerConfig.java b/src/main/java/com/moplus/moplus_server/global/config/swagger/SwaggerConfig.java new file mode 100644 index 0000000..f532118 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/global/config/swagger/SwaggerConfig.java @@ -0,0 +1,28 @@ +package com.moplus.moplus_server.global.config.swagger; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("Bearer"); + } + + @Bean + public OpenAPI openAPI() { + return new OpenAPI().addSecurityItem(new SecurityRequirement().addList("JWT")) + .components(new Components().addSecuritySchemes("JWT", createAPIKeyScheme())) + .info(new Info().title("모플 API 명세서") + .description("모플 API 명세서 입니다") + .version("v0.0.1")); + } +} diff --git a/src/test/java/com/moplus/moplus_server/domain/member/controller/MemberControllerTest.java b/src/test/java/com/moplus/moplus_server/domain/member/controller/MemberControllerTest.java new file mode 100644 index 0000000..9f0defd --- /dev/null +++ b/src/test/java/com/moplus/moplus_server/domain/member/controller/MemberControllerTest.java @@ -0,0 +1,78 @@ +package com.moplus.moplus_server.domain.member.controller; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.http.HttpHeaders; +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 MemberControllerTest { + + @Nested + class 어드민_로그인 { + + @Autowired + private JwtProperties jwtProperties; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext context; + + private String validToken; + private Key key; + + @BeforeEach + public void setMockMvc() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()).build(); + + key = Keys.hmacShaKeyFor(jwtProperties.accessTokenSecret().getBytes()); + Date issuedAt = new Date(); // 3 hour ago + Date expiredAt = new Date(issuedAt.getTime() + jwtProperties.accessTokenExpirationMilliTime()); + validToken = Jwts.builder() + .setIssuer(jwtProperties.issuer()) + .setSubject("1") + .claim("role", "ROLE_USER") + .setIssuedAt(issuedAt) + .setExpiration(expiredAt) + .signWith(key) + .compact(); + } + + @Test + void 성공() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/member/me") + .contentType("application/json") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + validToken)) + .andExpect(status().isOk()) // 200 응답 확인 + .andExpect(jsonPath("$.id").exists()) // MemberGetResponse의 필드 확인 + .andExpect(jsonPath("$.name").exists()) + .andExpect(jsonPath("$.email").exists()); + } + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-mysqltest.yml similarity index 100% rename from src/test/resources/application-test.yml rename to src/test/resources/application-mysqltest.yml diff --git a/src/test/resources/auth-test-data.sql b/src/test/resources/auth-test-data.sql index f757a22..595162a 100644 --- a/src/test/resources/auth-test-data.sql +++ b/src/test/resources/auth-test-data.sql @@ -1,4 +1,4 @@ -INSERT INTO member (deleted, created_at, update_at, member_id, email, password, role) +INSERT INTO member (deleted, created_at, update_at, member_id, email, password, name, role) VALUES (false, '2024-07-24 21:27:20.000000', '2024-07-24 21:27:21.000000', 1, 'admin@example.com', - 'password123', 'ADMIN'); + 'password123', '홍길동', 'ADMIN'); From a416fbde0182f120fdf8213a9514281346420aaf 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, 28 Jan 2025 17:28:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[fix/#17]=20jwtAuthenticationProvider?= =?UTF-8?q?=EC=99=80=20authenticationArgurentResolver=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthenticationArgumentResolver.java | 16 ++++++---------- .../global/error/exception/ErrorCode.java | 1 + 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java b/src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java index 2b20910..3952759 100644 --- a/src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java +++ b/src/main/java/com/moplus/moplus_server/global/annotation/AuthenticationArgumentResolver.java @@ -4,7 +4,6 @@ import com.moplus.moplus_server.domain.member.repository.MemberRepository; import com.moplus.moplus_server.global.error.exception.BusinessException; import com.moplus.moplus_server.global.error.exception.ErrorCode; -import com.moplus.moplus_server.global.error.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.security.core.Authentication; @@ -35,19 +34,16 @@ public Member resolveArgument(MethodParameter parameter, ModelAndViewContainer m } private Member getCurrentMember() { - return memberRepository - .findById(getCurrentMemberId()) - .orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND)); - } - - private Long getCurrentMemberId() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - + if (authentication == null || !authentication.isAuthenticated()) { throw new BusinessException(ErrorCode.AUTH_NOT_FOUND); } + Object principal = authentication.getPrincipal(); - Member principal = (Member) authentication.getPrincipal(); - return principal.getId(); + if (!(principal instanceof Member)) { + throw new BusinessException(ErrorCode.INVALID_PRINCIPAL); + } + return (Member) principal; } } 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 31735e8..b8c892f 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 @@ -21,6 +21,7 @@ public enum ErrorCode { TOKEN_SUBJECT_FORMAT_ERROR(HttpStatus.UNAUTHORIZED, "Subject 값에 Long 타입이 아닌 다른 타입이 들어있습니다."), AT_EXPIRED_AND_RT_NOT_FOUND(HttpStatus.UNAUTHORIZED, "AT는 만료되었고 RT는 비어있습니다."), RT_NOT_FOUND(HttpStatus.UNAUTHORIZED, "RT가 비어있습니다"), + INVALID_PRINCIPAL(HttpStatus.UNAUTHORIZED, "잘못된 Principal입니다"), //모의고사 PRACTICE_TEST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 모의고사를 찾을 수 없습니다"),