From ff8c982b69fdef61949be864c8799a9bd3f29943 Mon Sep 17 00:00:00 2001 From: DH CHOI Date: Fri, 23 Jun 2023 16:13:14 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feature:=20Swagger=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- instagram-api/build.gradle | 3 +- .../global/config/SwaggerConfig.java | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 instagram-api/src/main/java/clone/instagram/global/config/SwaggerConfig.java diff --git a/instagram-api/build.gradle b/instagram-api/build.gradle index d29aeac..b7b5226 100644 --- a/instagram-api/build.gradle +++ b/instagram-api/build.gradle @@ -3,8 +3,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' - implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' + implementation "io.springfox:springfox-boot-starter:3.0.0" // implementation 'org.springframework.boot:spring-boot-starter-security' implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' diff --git a/instagram-api/src/main/java/clone/instagram/global/config/SwaggerConfig.java b/instagram-api/src/main/java/clone/instagram/global/config/SwaggerConfig.java new file mode 100644 index 0000000..65c749c --- /dev/null +++ b/instagram-api/src/main/java/clone/instagram/global/config/SwaggerConfig.java @@ -0,0 +1,78 @@ +package clone.instagram.global.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.web.bind.annotation.RestController; + +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.builders.ResponseBuilder; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.AuthorizationScope; +import springfox.documentation.service.Response; +import springfox.documentation.service.SecurityReference; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; + +@Configuration +public class SwaggerConfig { + + @Bean + public Docket api() { + final List responseMessages = new ArrayList<>(); + responseMessages.add(new ResponseBuilder().code("405") + .description("G002 - 허용되지 않은 HTTP method 입니다.").build()); + responseMessages.add(new ResponseBuilder().code("500") + .description("G001 - 내부 서버 오류입니다.").build()); + + return new Docket(DocumentationType.SWAGGER_2) + .useDefaultResponseMessages(false) + .globalResponses(HttpMethod.POST, responseMessages) + .globalResponses(HttpMethod.GET, responseMessages) + .globalResponses(HttpMethod.DELETE, responseMessages) + .globalResponses(HttpMethod.PUT, responseMessages) + .apiInfo(apiInfo()) + .securityContexts(List.of(securityContext())) + .securitySchemes(List.of(apiKey())) + .select() + .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Instagram's API Docs") + .version("1.0") + .description("API 명세서") + .build(); + } + + private ApiKey apiKey() { + return new ApiKey("JWT", "Authorization", "header"); + } + + private SecurityContext securityContext() { + return SecurityContext.builder() + .securityReferences(defaultAuth()) + .build(); + } + + private List defaultAuth() { + AuthorizationScope authorizationScope = + new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = + new AuthorizationScope[1]; + + authorizationScopes[0] = authorizationScope; + return List.of(new SecurityReference("JWT", authorizationScopes)); + } + +} From d6e6d9f1b27e775cf461efca948c543bb254b0d1 Mon Sep 17 00:00:00 2001 From: DH CHOI Date: Fri, 23 Jun 2023 19:54:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Feature:=20RegisterRequest=EC=97=90=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/RegisterController.java | 5 ++-- .../member/request/RegisterRequest.java | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/instagram-api/src/main/java/clone/instagram/member/controller/RegisterController.java b/instagram-api/src/main/java/clone/instagram/member/controller/RegisterController.java index 2fe8478..5662bbd 100644 --- a/instagram-api/src/main/java/clone/instagram/member/controller/RegisterController.java +++ b/instagram-api/src/main/java/clone/instagram/member/controller/RegisterController.java @@ -2,6 +2,8 @@ import static clone.instagram.result.ResultCode.*; +import javax.validation.Valid; + import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; @@ -20,7 +22,6 @@ import lombok.RequiredArgsConstructor; @Api(tags = "멤버 인증 API") -@Validated @RestController @RequiredArgsConstructor public class RegisterController { @@ -38,7 +39,7 @@ public class RegisterController { + "M007 - 인증 이메일 전송을 먼저 해야합니다.") }) @PostMapping(value = "/accounts") - public ResponseEntity register(@RequestBody RegisterRequest registerRequest) { + public ResponseEntity register(@RequestBody @Valid RegisterRequest registerRequest) { final RegisterUseCase.Command command = mapRequestToCommand(registerRequest); return toResponseEntity(registerUseCase.invoke(command)); } diff --git a/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java b/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java index 41c13d0..253f3f9 100644 --- a/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java +++ b/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java @@ -1,5 +1,11 @@ package clone.instagram.member.request; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModelProperty; import lombok.Getter; import lombok.NoArgsConstructor; @@ -7,9 +13,26 @@ @NoArgsConstructor public class RegisterRequest { + @ApiModelProperty(value = "유저네임", example = "dlwlrma", required = true) + @NotBlank(message = "username을 입력해주세요") + @Size(min = 4, max = 12, message = "사용자 이름은 4문자 이상 12문자 이하여야 합니다") + @Pattern(regexp = "^[0-9a-zA-Z]+$", message = "username엔 대소문자, 숫자만 사용할 수 있습니다.") private String username; + + @ApiModelProperty(value = "이름", example = "이지금", required = true) + @NotBlank(message = "이름을 입력해주세요") + @Size(min = 2, max = 12, message = "이름은 2문자 이상 12문자 이하여야 합니다") private String password; + + @ApiModelProperty(value = "비밀번호", example = "a12341234", required = true) + @NotBlank(message = "비밀번호를 입력해주세요") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", message = "비밀번호는 8자 이상, 최소 하나의 문자와 숫자가 필요합니다") + @Size(max = 20, message = "비밀번호는 20문자 이하여야 합니다") private String name; + + @ApiModelProperty(value = "이메일", example = "aaa@gmail.com", required = true) + @NotBlank(message = "이메일을 입력해주세요") + @Email(message = "이메일의 형식이 맞지 않습니다") private String email; } From 1d1d013ec0dedc67ec635e69341d6e4f14a05766 Mon Sep 17 00:00:00 2001 From: DH CHOI Date: Fri, 23 Jun 2023 20:47:35 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Feature:=20Security=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- instagram-api/build.gradle | 2 +- .../global/config/SecurityConfig.java | 65 +++++++++++++++++++ .../src/main/resources/application.yml | 4 +- 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 instagram-api/src/main/java/clone/instagram/global/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index 8a46b45..f6b47fb 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.5.4' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.7.5' classpath 'io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE' } } diff --git a/instagram-api/build.gradle b/instagram-api/build.gradle index b7b5226..af98f1d 100644 --- a/instagram-api/build.gradle +++ b/instagram-api/build.gradle @@ -4,7 +4,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation "io.springfox:springfox-boot-starter:3.0.0" -// implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' diff --git a/instagram-api/src/main/java/clone/instagram/global/config/SecurityConfig.java b/instagram-api/src/main/java/clone/instagram/global/config/SecurityConfig.java new file mode 100644 index 0000000..7827141 --- /dev/null +++ b/instagram-api/src/main/java/clone/instagram/global/config/SecurityConfig.java @@ -0,0 +1,65 @@ +package clone.instagram.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class SecurityConfig { + + private static final String[] AUTH_WHITELIST_SWAGGER = {"/v2/api-docs", "/configuration/ui", "/swagger-resources", + "/swagger-resources/**", "/configuration/security", "/swagger-ui.html", "/webjars/**", "/swagger-ui/**"}; + private static final String[] AUTH_WHITELIST_STATIC = {"/static/css/**", "/static/js/**", "*.ico"}; + private static final String[] AUTH_WHITELIST = {"/accounts"}; + + @Bean + public CorsConfigurationSource configurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.addAllowedOriginPattern("*"); + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + http.logout().disable() + .formLogin().disable() + .httpBasic().disable(); + + http.cors() + .configurationSource(configurationSource()) + .and() + .csrf() + .disable() + .authorizeRequests() + .requestMatchers(CorsUtils::isPreFlightRequest) + .permitAll() + .antMatchers(AUTH_WHITELIST) + .permitAll() + .antMatchers(AUTH_WHITELIST_STATIC) + .permitAll() + .antMatchers(AUTH_WHITELIST_SWAGGER) + .permitAll() + .anyRequest().hasAuthority("ROLE_USER"); + + return http.build(); + } + +} diff --git a/instagram-api/src/main/resources/application.yml b/instagram-api/src/main/resources/application.yml index 11b3cbc..d3bd47b 100644 --- a/instagram-api/src/main/resources/application.yml +++ b/instagram-api/src/main/resources/application.yml @@ -1,7 +1,9 @@ spring: profiles: include: adapter, data, application, local - + mvc: + pathmatch: + matching-strategy: ant_path_matcher auth: whitelist: api: /login, /login/recovery, /accounts, /**/without,\ From dfce0b8050bfb4a981d5cdd72120ef345a6ec279 Mon Sep 17 00:00:00 2001 From: DH CHOI Date: Fri, 23 Jun 2023 20:48:01 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Fix:=20=EC=9E=98=EB=AA=BB=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=EB=90=9C=20Swagger,=20=EA=B2=80=EC=A6=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instagram/member/request/RegisterRequest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java b/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java index 253f3f9..0ff6e0c 100644 --- a/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java +++ b/instagram-api/src/main/java/clone/instagram/member/request/RegisterRequest.java @@ -19,15 +19,15 @@ public class RegisterRequest { @Pattern(regexp = "^[0-9a-zA-Z]+$", message = "username엔 대소문자, 숫자만 사용할 수 있습니다.") private String username; - @ApiModelProperty(value = "이름", example = "이지금", required = true) - @NotBlank(message = "이름을 입력해주세요") - @Size(min = 2, max = 12, message = "이름은 2문자 이상 12문자 이하여야 합니다") - private String password; - @ApiModelProperty(value = "비밀번호", example = "a12341234", required = true) @NotBlank(message = "비밀번호를 입력해주세요") @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", message = "비밀번호는 8자 이상, 최소 하나의 문자와 숫자가 필요합니다") @Size(max = 20, message = "비밀번호는 20문자 이하여야 합니다") + private String password; + + @ApiModelProperty(value = "이름", example = "이지금", required = true) + @NotBlank(message = "이름을 입력해주세요") + @Size(min = 2, max = 12, message = "이름은 2문자 이상 12문자 이하여야 합니다") private String name; @ApiModelProperty(value = "이메일", example = "aaa@gmail.com", required = true)