Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<MemberGetResponse> getMyInfo(
@AuthUser Member member
) {
return ResponseEntity.ok(MemberGetResponse.of(member));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ public class Member extends BaseEntity {
@Column(name = "member_id")
private Long id;

private String name;
private String email;
private String password;

@Enumerated(EnumType.STRING)
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 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() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || !authentication.isAuthenticated()) {
throw new BusinessException(ErrorCode.AUTH_NOT_FOUND);
}
Object principal = authentication.getPrincipal();

if (!(principal instanceof Member)) {
throw new BusinessException(ErrorCode.INVALID_PRINCIPAL);
}
return (Member) principal;
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authenticationArgumentResolver);
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, "해당 모의고사를 찾을 수 없습니다"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
4 changes: 2 additions & 2 deletions src/test/resources/auth-test-data.sql
Original file line number Diff line number Diff line change
@@ -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');

Loading