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 d29aeac..af98f1d 100644 --- a/instagram-api/build.gradle +++ b/instagram-api/build.gradle @@ -3,9 +3,8 @@ 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 'org.springframework.boot:spring-boot-starter-security' + 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' 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/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)); + } + +} 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..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 @@ -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 = "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) + @NotBlank(message = "이메일을 입력해주세요") + @Email(message = "이메일의 형식이 맞지 않습니다") private String email; } 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,\