From e663724567d86e787fa0124a319ec11bc0c5f60d Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Tue, 16 Apr 2019 17:04:23 +0900 Subject: [PATCH 01/18] Create README.md --- src/README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/README.md diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..badbe85 --- /dev/null +++ b/src/README.md @@ -0,0 +1,65 @@ +# Spring Rest Api 만들기 프로젝트 + +### 0. 개요 +- SpringBoot2 framework 기반에서 RESTful api 서비스를 Step by Step으로 만들어 나가는 프로젝트 +- daddyprogrammer.org에서 연재 및 소스 Github 등록 + - https://daddyprogrammer.org/post/series/springboot2%EB%A1%9C-rest-api-%EB%A7%8C%EB%93%A4%EA%B8%B0/ + +### 1. 개발환경 +- Java 8~11 +- SpringBoot 2.x +- SpringSecurity 5.x +- JPA, H2 +- Intellij Community + +### 2. 프로젝트 실행 +- H2 database 설치 + - https://www.h2database.com/html/download.html +- intellij lombok 플러그인 설치 + - Preferences -> Plugins -> Browse repositories... -> search lombok -> Install "IntelliJ Lombok plugin" +- Enable annotation processing + - Preferences - Annotation Procesors - Enable annotation processing 체크 +- build.gradle에 lombok 추가(Git을 받은경우 이미 추가되어있음) + - compileOnly 'org.projectlombok:lombok:1.16.16' +- 실행 + - Run -> SpringBootApiApplication +- Swagger + - http://localhost:8080/swagger-ui.html + +### 3. 목차 +- SpringBoot2로 Rest api 만들기(1) – Intellij Community에서 프로젝트생성 + - Document + - https://daddyprogrammer.org/post/19/spring-boot1-start-intellij/ +- SpringBoot2로 Rest api 만들기(2) – HelloWorld + - Document + - https://daddyprogrammer.org/post/41/spring-boot2-helloworld/ +- SpringBoot2로 Rest api 만들기(3) – H2 Database 연동 + - Document + - https://daddyprogrammer.org/post/152/spring-boot2-h2-database-intergrate/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/h2 +- SpringBoot2로 Rest api 만들기(4) – Swagger API 문서 자동화 + - Document + - https://daddyprogrammer.org/post/313/swagger-api-doc/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/swagger +- SpringBoot2로 Rest api 만들기(5) – API 인터페이스 및 결과 데이터 구조 설계 + - Document + - https://daddyprogrammer.org/post/404/spring-boot2-5-design-api-interface-and-data-structure/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/api-structure +- SpringBoot2로 Rest api 만들기(6) – ControllerAdvice를 이용한 Exception처리 + - Document + - https://daddyprogrammer.org/post/446/spring-boot2-5-exception-handling/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/controller-advice +- SpringBoot2로 Rest api 만들기(7) – MessageSource를 이용한 Exception 처리 + - Document + - https://daddyprogrammer.org/post/499/springboot2-message-exception-handling-with-controlleradvice/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/messagesource +- SpringBoot2로 Rest api 만들기(8) – SpringSecurity를 이용한 인증 및 권한부여 + - Document + - https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/security \ No newline at end of file From b5157aa38125e1dd8897c6ee22c0f5c2d36f4838 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Tue, 16 Apr 2019 17:05:35 +0900 Subject: [PATCH 02/18] Create README.md --- src/README.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/README.md => README.md (100%) diff --git a/src/README.md b/README.md similarity index 100% rename from src/README.md rename to README.md From 02a4b3b7ea7dd567941449ee328ada62db7c8f06 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Wed, 17 Apr 2019 10:57:10 +0900 Subject: [PATCH 03/18] Modify sonarqube analysis --- build.gradle | 1 + .../java/com/rest/api/advice/ExceptionAdvice.java | 2 +- .../com/rest/api/config/MessageConfiguration.java | 4 +--- .../config/security/CustomAccessDeniedHandler.java | 14 +++++--------- .../com/rest/api/controller/HelloController.java | 8 +++++--- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 8eff071..ccfd7b6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' + id "org.sonarqube" version "2.7" } apply plugin: 'io.spring.dependency-management' diff --git a/src/main/java/com/rest/api/advice/ExceptionAdvice.java b/src/main/java/com/rest/api/advice/ExceptionAdvice.java index c0b475a..7ed7d40 100644 --- a/src/main/java/com/rest/api/advice/ExceptionAdvice.java +++ b/src/main/java/com/rest/api/advice/ExceptionAdvice.java @@ -51,7 +51,7 @@ public class ExceptionAdvice { @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) - public CommonResult AccessDeniedException(HttpServletRequest request, AccessDeniedException e) { + public CommonResult accessDeniedException(HttpServletRequest request, AccessDeniedException e) { return responseService.getFailResult(Integer.valueOf(getMessage("accessDenied.code")), getMessage("accessDenied.msg")); } diff --git a/src/main/java/com/rest/api/config/MessageConfiguration.java b/src/main/java/com/rest/api/config/MessageConfiguration.java index ce36020..bff452a 100644 --- a/src/main/java/com/rest/api/config/MessageConfiguration.java +++ b/src/main/java/com/rest/api/config/MessageConfiguration.java @@ -1,6 +1,5 @@ package com.rest.api.config; -import lombok.extern.slf4j.Slf4j; import net.rakugakibox.util.YamlResourceBundle; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; @@ -14,7 +13,6 @@ import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.SessionLocaleResolver; import java.util.Locale; -import java.util.MissingResourceException; import java.util.ResourceBundle; @Configuration @@ -56,7 +54,7 @@ public class MessageConfiguration implements WebMvcConfigurer { // locale 정보에 따라 다른 yml 파일을 읽도록 처리 private static class YamlMessageSource extends ResourceBundleMessageSource { @Override - protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { + protected ResourceBundle doGetBundle(String basename, Locale locale) { return ResourceBundle.getBundle(basename, locale, YamlResourceBundle.Control.INSTANCE); } } diff --git a/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java b/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java index eaaa3cb..8b30ba8 100644 --- a/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java +++ b/src/main/java/com/rest/api/config/security/CustomAccessDeniedHandler.java @@ -1,8 +1,6 @@ package com.rest.api.config.security; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; @@ -17,12 +15,10 @@ import java.io.IOException; @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { - private static final Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class); - - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException { - RequestDispatcher dispatcher = request.getRequestDispatcher("/exception/accessdenied"); - dispatcher.forward(request, response); - } + RequestDispatcher dispatcher = request.getRequestDispatcher("/exception/accessdenied"); + dispatcher.forward(request, response); + } } diff --git a/src/main/java/com/rest/api/controller/HelloController.java b/src/main/java/com/rest/api/controller/HelloController.java index 22f68dd..f8241a0 100644 --- a/src/main/java/com/rest/api/controller/HelloController.java +++ b/src/main/java/com/rest/api/controller/HelloController.java @@ -9,6 +9,8 @@ import org.springframework.web.bind.annotation.ResponseBody; @Controller public class HelloController { + private static final String HELLO = "helloworld"; + @Setter @Getter public static class Hello { @@ -18,19 +20,19 @@ public class HelloController { @GetMapping(value = "/helloworld/string") @ResponseBody public String helloworldString() { - return "helloworld"; + return HELLO; } @GetMapping(value = "/helloworld/json") @ResponseBody public Hello helloworldJson() { Hello hello = new Hello(); - hello.message = "helloworld"; + hello.message = HELLO; return hello; } @GetMapping(value = "/helloworld/page") public String helloworld() { - return "helloworld"; + return HELLO; } } From e4d5cf3a7700dc9bab46c069d7fb2ecc6b5c65b5 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Wed, 17 Apr 2019 19:24:09 +0900 Subject: [PATCH 04/18] Spring Unit Test --- build.gradle | 1 + .../security/SecurityConfiguration.java | 3 +- .../api/controller/v1/SignController.java | 11 +- .../api/controller/v1/UserController.java | 4 +- src/main/java/com/rest/api/entity/User.java | 2 +- .../com/rest/api/service/ResponseService.java | 2 +- .../api/SpringRestApiApplicationTests.java | 16 --- .../api/controller/HelloControllerTest.java | 52 ++++++++ .../api/controller/v1/SignControllerTest.java | 89 +++++++++++++ .../api/controller/v1/UserControllerTest.java | 118 ++++++++++++++++++ .../com/rest/api/repo/UserJpaRepoTest.java | 46 +++++++ 11 files changed, 315 insertions(+), 29 deletions(-) delete mode 100644 src/test/java/com/rest/api/SpringRestApiApplicationTests.java create mode 100644 src/test/java/com/rest/api/controller/HelloControllerTest.java create mode 100644 src/test/java/com/rest/api/controller/v1/SignControllerTest.java create mode 100644 src/test/java/com/rest/api/controller/v1/UserControllerTest.java create mode 100644 src/test/java/com/rest/api/repo/UserJpaRepoTest.java diff --git a/build.gradle b/build.gradle index ccfd7b6..6fcc524 100644 --- a/build.gradle +++ b/build.gradle @@ -33,5 +33,6 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java index bf8923d..ca31785 100644 --- a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java +++ b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java @@ -32,8 +32,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .and() .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크 .antMatchers("/*/signin", "/*/signup").permitAll() // 가입 및 인증 주소는 누구나 접근가능 - .antMatchers(HttpMethod.GET, "helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 - .antMatchers("/*/users").hasRole("ADMIN") + .antMatchers(HttpMethod.GET, "/helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능 .and() .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler()) diff --git a/src/main/java/com/rest/api/controller/v1/SignController.java b/src/main/java/com/rest/api/controller/v1/SignController.java index 54b6cf6..82e2528 100644 --- a/src/main/java/com/rest/api/controller/v1/SignController.java +++ b/src/main/java/com/rest/api/controller/v1/SignController.java @@ -12,10 +12,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Collections; @@ -31,7 +28,7 @@ public class SignController { private final PasswordEncoder passwordEncoder; @ApiOperation(value = "로그인", notes = "이메일 회원 로그인을 한다.") - @GetMapping(value = "/signin") + @PostMapping(value = "/signin") public SingleResult signin(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, @ApiParam(value = "비밀번호", required = true) @RequestParam String password) { @@ -43,8 +40,8 @@ public class SignController { } @ApiOperation(value = "가입", notes = "회원가입을 한다.") - @GetMapping(value = "/signup") - public CommonResult signin(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, + @PostMapping(value = "/signup") + public CommonResult signup(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, @ApiParam(value = "비밀번호", required = true) @RequestParam String password, @ApiParam(value = "이름", required = true) @RequestParam String name) { diff --git a/src/main/java/com/rest/api/controller/v1/UserController.java b/src/main/java/com/rest/api/controller/v1/UserController.java index 6e91ad3..c5c4541 100644 --- a/src/main/java/com/rest/api/controller/v1/UserController.java +++ b/src/main/java/com/rest/api/controller/v1/UserController.java @@ -33,11 +33,11 @@ public class UserController { } @ApiImplicitParams({ - @ApiImplicitParam(name = "X-AUTH-TOKEN", value = "로그인 성공 후 access_token", required = false, dataType = "String", paramType = "header") + @ApiImplicitParam(name = "X-AUTH-TOKEN", value = "로그인 성공 후 access_token", required = true, dataType = "String", paramType = "header") }) @ApiOperation(value = "회원 단건 조회", notes = "회원번호(msrl)로 회원을 조회한다") @GetMapping(value = "/user") - public SingleResult findUserById(@ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) { + public SingleResult findUser() { // SecurityContext에서 인증받은 회원의 정보를 얻어온다. Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String id = authentication.getName(); diff --git a/src/main/java/com/rest/api/entity/User.java b/src/main/java/com/rest/api/entity/User.java index e4c0f4c..83689e9 100644 --- a/src/main/java/com/rest/api/entity/User.java +++ b/src/main/java/com/rest/api/entity/User.java @@ -25,7 +25,7 @@ public class User implements UserDetails { @Id // pk @GeneratedValue(strategy = GenerationType.IDENTITY) private long msrl; - @Column(nullable = false, unique = true, length = 30) + @Column(nullable = false, unique = true, length = 50) private String uid; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @Column(nullable = false, length = 100) diff --git a/src/main/java/com/rest/api/service/ResponseService.java b/src/main/java/com/rest/api/service/ResponseService.java index fdc196b..a86f7cf 100644 --- a/src/main/java/com/rest/api/service/ResponseService.java +++ b/src/main/java/com/rest/api/service/ResponseService.java @@ -12,7 +12,7 @@ public class ResponseService { // enum으로 api 요청 결과에 대한 code, message를 정의합니다. public enum CommonResponse { - SUCCESS(0, "성공하였습니디."); + SUCCESS(0, "성공하였습니다."); int code; String msg; diff --git a/src/test/java/com/rest/api/SpringRestApiApplicationTests.java b/src/test/java/com/rest/api/SpringRestApiApplicationTests.java deleted file mode 100644 index 607846f..0000000 --- a/src/test/java/com/rest/api/SpringRestApiApplicationTests.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.rest.api; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class SpringRestApiApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/src/test/java/com/rest/api/controller/HelloControllerTest.java b/src/test/java/com/rest/api/controller/HelloControllerTest.java new file mode 100644 index 0000000..e03c475 --- /dev/null +++ b/src/test/java/com/rest/api/controller/HelloControllerTest.java @@ -0,0 +1,52 @@ +package com.rest.api.controller; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class HelloControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void helloworldString() throws Exception { + mockMvc.perform(get("/helloworld/string")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType("text/plain;charset=UTF-8")) + .andExpect(content().string("helloworld")); + } + + @Test + public void helloworldJson() throws Exception { + mockMvc.perform(get("/helloworld/json")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json;charset=utf-8")) + .andExpect(jsonPath("$.message").value("helloworld")); + } + + @Test + public void helloworldPage() throws Exception { + mockMvc.perform(get("/helloworld/page")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType("text/html;charset=UTF-8")) + .andExpect(view().name("helloworld")) + .andExpect(content().string("helloworld")); + } +} \ No newline at end of file diff --git a/src/test/java/com/rest/api/controller/v1/SignControllerTest.java b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java new file mode 100644 index 0000000..094ebf3 --- /dev/null +++ b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java @@ -0,0 +1,89 @@ +package com.rest.api.controller.v1; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class SignControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void signin() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "1234"); + mockMvc.perform(post("/v1/signin").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.msg").exists()) + .andExpect(jsonPath("$.data").exists()); + } + + @Test + public void signinFail() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "12345"); + mockMvc.perform(post("/v1/signin").params(params)) + .andDo(print()) + .andExpect(status().is5xxServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value(-1001)) + .andExpect(jsonPath("$.msg").exists()); + } + + @Test + public void signup() throws Exception { + long epochTime = LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond(); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy_" + epochTime + "@naver.com"); + params.add("password", "12345"); + params.add("name", "happydaddy_" + epochTime); + mockMvc.perform(post("/v1/signup").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.msg").exists()); + } + + @Test + public void signupFail() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "12345"); + params.add("name", "happydaddy"); + mockMvc.perform(post("/v1/signup").params(params)) + .andDo(print()) + .andExpect(status().is5xxServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value(-9999)); + } +} \ No newline at end of file diff --git a/src/test/java/com/rest/api/controller/v1/UserControllerTest.java b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java new file mode 100644 index 0000000..f812038 --- /dev/null +++ b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java @@ -0,0 +1,118 @@ +package com.rest.api.controller.v1; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.json.JacksonJsonParser; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + private String token; + + @Before + public void setUp() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("id", "happydaddy@naver.com"); + params.add("password", "1234"); + ResultActions result = mockMvc.perform(post("/v1/signin").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.msg").exists()) + .andExpect(jsonPath("$.data").exists()); + + String resultString = result.andReturn().getResponse().getContentAsString(); + JacksonJsonParser jsonParser = new JacksonJsonParser(); + token = jsonParser.parseMap(resultString).get("data").toString(); + } + + @Test + public void invalidToken() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/users") + .header("X-AUTH-TOKEN", "XXXXXXXXXX")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/exception/entrypoint")); + } + + @Test + @WithMockUser(username = "mockUser", roles = {"ADMIN"}) // 가상의 Mock 유저 대입 + public void accessdenied() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/users")) + //.header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/exception/accessdenied")); + } + + @Test + public void findAllUser() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/users") + .header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.list").exists()); + } + + @Test + public void findUser() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .get("/v1/user") + .header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data").exists()); + } + + @Test + public void modify() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("msrl", "1"); + params.add("name", "행복전도사"); + mockMvc.perform(MockMvcRequestBuilders + .put("/v1/user") + .header("X-AUTH-TOKEN", token) + .params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + public void delete() throws Exception { + mockMvc.perform(MockMvcRequestBuilders + .delete("/v1/user/2") + .header("X-AUTH-TOKEN", token)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} \ No newline at end of file diff --git a/src/test/java/com/rest/api/repo/UserJpaRepoTest.java b/src/test/java/com/rest/api/repo/UserJpaRepoTest.java new file mode 100644 index 0000000..f94ed4c --- /dev/null +++ b/src/test/java/com/rest/api/repo/UserJpaRepoTest.java @@ -0,0 +1,46 @@ +package com.rest.api.repo; + +import com.rest.api.entity.User; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Collections; +import java.util.Optional; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringRunner.class) +@DataJpaTest +public class UserJpaRepoTest { + + @Autowired + private UserJpaRepo userJpaRepo; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Test + public void whenFindByUid_thenReturnUser() { + String uid = "angrydaddy@gmail.com"; + String name = "angrydaddy"; + // given + userJpaRepo.save(User.builder() + .uid(uid) + .password(passwordEncoder.encode("1234")) + .name(name) + .roles(Collections.singletonList("ROLE_USER")) + .build()); + // when + Optional user = userJpaRepo.findByUid(uid); + // then + assertTrue(user.isPresent()); + assertThat(user.get().getName(), is(name)); + } + +} \ No newline at end of file From 16ae0132d25dae7772cb4f4084ede3516c7f3899 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Wed, 17 Apr 2019 19:26:52 +0900 Subject: [PATCH 05/18] modify security matcher path --- .../com/rest/api/config/security/SecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java index bf8923d..923854e 100644 --- a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java +++ b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java @@ -32,7 +32,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .and() .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크 .antMatchers("/*/signin", "/*/signup").permitAll() // 가입 및 인증 주소는 누구나 접근가능 - .antMatchers(HttpMethod.GET, "helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 + .antMatchers(HttpMethod.GET, "/helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 .antMatchers("/*/users").hasRole("ADMIN") .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능 .and() From d2a6c9bc81dc16c508d115f5ce299f3e7eed2ccb Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Thu, 18 Apr 2019 15:20:56 +0900 Subject: [PATCH 06/18] =?UTF-8?q?SpringBoot2=EB=A1=9C=20Rest=20api=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0(9)=20=E2=80=93=20Unit=20Test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rest/api/controller/v1/UserControllerTest.java | 9 +++++---- src/test/java/com/rest/api/repo/UserJpaRepoTest.java | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/rest/api/controller/v1/UserControllerTest.java b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java index f812038..1f54444 100644 --- a/src/test/java/com/rest/api/controller/v1/UserControllerTest.java +++ b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java @@ -10,7 +10,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; @@ -36,15 +36,16 @@ public class UserControllerTest { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("id", "happydaddy@naver.com"); params.add("password", "1234"); - ResultActions result = mockMvc.perform(post("/v1/signin").params(params)) + MvcResult result = mockMvc.perform(post("/v1/signin").params(params)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.msg").exists()) - .andExpect(jsonPath("$.data").exists()); + .andExpect(jsonPath("$.data").exists()) + .andReturn(); - String resultString = result.andReturn().getResponse().getContentAsString(); + String resultString = result.getResponse().getContentAsString(); JacksonJsonParser jsonParser = new JacksonJsonParser(); token = jsonParser.parseMap(resultString).get("data").toString(); } diff --git a/src/test/java/com/rest/api/repo/UserJpaRepoTest.java b/src/test/java/com/rest/api/repo/UserJpaRepoTest.java index f94ed4c..0d7a1a6 100644 --- a/src/test/java/com/rest/api/repo/UserJpaRepoTest.java +++ b/src/test/java/com/rest/api/repo/UserJpaRepoTest.java @@ -12,8 +12,7 @@ import java.util.Collections; import java.util.Optional; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; @RunWith(SpringRunner.class) @DataJpaTest @@ -39,8 +38,9 @@ public class UserJpaRepoTest { // when Optional user = userJpaRepo.findByUid(uid); // then - assertTrue(user.isPresent()); - assertThat(user.get().getName(), is(name)); + assertNotNull(user);// user객체가 null이 아닌지 체크 + assertTrue(user.isPresent()); // user객체가 존재여부 true/false 체크 + assertEquals(user.get().getName(), name); // user객체의 name과 name변수 값이 같은지 체크 + assertThat(user.get().getName(), is(name)); // user객체의 name과 name변수 값이 같은지 체크 } - } \ No newline at end of file From 62c14bb3bc958496d89f78217a9fe6d573411a4d Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Thu, 18 Apr 2019 15:23:57 +0900 Subject: [PATCH 07/18] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index badbe85..d08d331 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,9 @@ - Document - https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/ - Git - - https://github.com/codej99/SpringRestApi/tree/feature/security \ No newline at end of file + - https://github.com/codej99/SpringRestApi/tree/feature/security +- SpringBoot2로 Rest api 만들기(9) – Unit Test + - Document + - https://daddyprogrammer.org/post/938/springboot2-restapi-unit-test/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/junit-test \ No newline at end of file From 9fcd390ceefaf42348d2aec897730c46371793b0 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Thu, 18 Apr 2019 19:41:37 +0900 Subject: [PATCH 08/18] =?UTF-8?q?SpringBoot2=EB=A1=9C=20Rest=20api=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0(9)=20=E2=80=93=20Social=20Login=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99(kakao)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../rest/api/SpringRestApiApplication.java | 6 ++ .../security/SecurityConfiguration.java | 2 +- .../controller/common/SocialController.java | 79 +++++++++++++++++++ .../rest/api/model/social/RetKakaoAuth.java | 14 ++++ src/main/resources/application.yml | 12 ++- src/main/resources/templates/social/login.ftl | 6 ++ .../templates/social/redirectKakao.ftl | 5 ++ 8 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/rest/api/controller/common/SocialController.java create mode 100644 src/main/java/com/rest/api/model/social/RetKakaoAuth.java create mode 100644 src/main/resources/templates/social/login.ftl create mode 100644 src/main/resources/templates/social/redirectKakao.ftl diff --git a/build.gradle b/build.gradle index 6fcc524..797f558 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'io.springfox:springfox-swagger2:2.6.1' implementation 'io.springfox:springfox-swagger-ui:2.6.1' implementation 'net.rakugakibox.util:yaml-resource-bundle:1.1' + implementation 'com.google.code.gson:gson' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'mysql:mysql-connector-java' diff --git a/src/main/java/com/rest/api/SpringRestApiApplication.java b/src/main/java/com/rest/api/SpringRestApiApplication.java index 5718fc4..64ad4d3 100644 --- a/src/main/java/com/rest/api/SpringRestApiApplication.java +++ b/src/main/java/com/rest/api/SpringRestApiApplication.java @@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.client.RestTemplate; @SpringBootApplication public class SpringRestApiApplication { @@ -16,4 +17,9 @@ public class SpringRestApiApplication { public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } + + @Bean + public RestTemplate getRestTemplate() { + return new RestTemplate(); + } } diff --git a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java index ca31785..a0f0357 100644 --- a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java +++ b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java @@ -31,7 +31,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증할것이므로 세션필요없으므로 생성안함. .and() .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크 - .antMatchers("/*/signin", "/*/signup").permitAll() // 가입 및 인증 주소는 누구나 접근가능 + .antMatchers("/*/signin", "/*/signup", "/social/**").permitAll() // 가입 및 인증 주소는 누구나 접근가능 .antMatchers(HttpMethod.GET, "/helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능 .and() diff --git a/src/main/java/com/rest/api/controller/common/SocialController.java b/src/main/java/com/rest/api/controller/common/SocialController.java new file mode 100644 index 0000000..4919864 --- /dev/null +++ b/src/main/java/com/rest/api/controller/common/SocialController.java @@ -0,0 +1,79 @@ +package com.rest.api.controller.common; + +import com.google.gson.Gson; +import com.rest.api.model.social.RetKakaoAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.http.*; +import org.springframework.stereotype.Controller; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.ModelAndView; + +@RequiredArgsConstructor +@Controller +@RequestMapping("/social/login") +public class SocialController { + + private final Environment env; + + private final RestTemplate restTemplate; + + private final Gson gson; + + @Value("${spring.url.base}") + private String baseUrl; + + @Value("${spring.social.kakao.client_id}") + private String kakaoClientId; + + @Value("${spring.social.kakao.redirect}") + private String kakaoRedirect; + + /** + * 카카오 로그인 페이지 + */ + @GetMapping + public ModelAndView socialLogin(ModelAndView mav) { + + StringBuilder loginUrl = new StringBuilder() + .append(env.getProperty("spring.social.kakao.url.login")) + .append("?client_id=").append(kakaoClientId) + .append("&response_type=code") + .append("&redirect_uri=").append(baseUrl).append(kakaoRedirect); + + mav.addObject("loginUrl", loginUrl); + mav.setViewName("social/login"); + return mav; + } + + /** + * 카카오 인증 완료 후 리다이렉트 화면 + */ + @GetMapping(value = "/kakao") + public ModelAndView redirectKakao(ModelAndView mav, @RequestParam String code) { + // Set header : Content-type: application/x-www-form-urlencoded + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // Set parameter + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", kakaoClientId); + params.add("redirect_uri", baseUrl + kakaoRedirect); + params.add("code", code); + // Set http entity + HttpEntity> request = new HttpEntity<>(params, headers); + ResponseEntity response = restTemplate.postForEntity(env.getProperty("spring.social.kakao.url.token"), request, String.class); + if (response.getStatusCode() == HttpStatus.OK) { + RetKakaoAuth authInfo = gson.fromJson(response.getBody(), RetKakaoAuth.class); + mav.addObject("authInfo", authInfo); + } + mav.setViewName("social/redirectKakao"); + return mav; + } +} diff --git a/src/main/java/com/rest/api/model/social/RetKakaoAuth.java b/src/main/java/com/rest/api/model/social/RetKakaoAuth.java new file mode 100644 index 0000000..5dbf0dc --- /dev/null +++ b/src/main/java/com/rest/api/model/social/RetKakaoAuth.java @@ -0,0 +1,14 @@ +package com.rest.api.model.social; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RetKakaoAuth { + private String access_token; + private String token_type; + private String refresh_token; + private long expires_in; + private String scope; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dce0ce2..f7ff651 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,4 +14,14 @@ spring: basename: i18n/exception encoding: UTF-8 jwt: - secret: govlepel@$& \ No newline at end of file + secret: govlepel@$& + social: + kakao: + client_id: XXXXXXXXXXXXXXXXXXXXXXXX # 앱생성시 받은 REST API 키 + redirect: /social/login/kakao + url: + login: https://kauth.kakao.com/oauth/authorize + token: https://kauth.kakao.com/oauth/token + profile: https://kapi.kakao.com/v2/user/me + url: + base: http://localhost:8080 diff --git a/src/main/resources/templates/social/login.ftl b/src/main/resources/templates/social/login.ftl new file mode 100644 index 0000000..439042f --- /dev/null +++ b/src/main/resources/templates/social/login.ftl @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/src/main/resources/templates/social/redirectKakao.ftl b/src/main/resources/templates/social/redirectKakao.ftl new file mode 100644 index 0000000..d5b67f1 --- /dev/null +++ b/src/main/resources/templates/social/redirectKakao.ftl @@ -0,0 +1,5 @@ +access_token : ${authInfo.access_token}
+token_type : ${authInfo.token_type}
+refresh_token : ${authInfo.refresh_token}
+expires_in : ${authInfo.expires_in}
+scope : ${authInfo.scope}
\ No newline at end of file From c2f1ea67e672b95fda1e727753deb4f6119e89e7 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Fri, 19 Apr 2019 02:04:23 +0900 Subject: [PATCH 09/18] =?UTF-8?q?SpringBoot2=EB=A1=9C=20Rest=20api=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0(10)=20=E2=80=93=20Social=20Login=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99(kakao)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rest/api/advice/ExceptionAdvice.java | 16 +++++-- .../exception/CCommunicationException.java | 15 ++++++ .../advice/exception/CUserExistException.java | 15 ++++++ .../security/SecurityConfiguration.java | 2 +- .../api/controller/v1/SignController.java | 47 ++++++++++++++++++- src/main/java/com/rest/api/entity/User.java | 9 ++-- .../rest/api/model/social/KakaoProfile.java | 19 ++++++++ .../java/com/rest/api/repo/UserJpaRepo.java | 2 + .../rest/api/service/user/UserService.java | 35 ++++++++++++++ src/main/resources/application.yml | 2 +- src/main/resources/i18n/exception_en.yml | 8 +++- src/main/resources/i18n/exception_ko.yml | 8 +++- 12 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/rest/api/advice/exception/CCommunicationException.java create mode 100644 src/main/java/com/rest/api/advice/exception/CUserExistException.java create mode 100644 src/main/java/com/rest/api/model/social/KakaoProfile.java create mode 100644 src/main/java/com/rest/api/service/user/UserService.java diff --git a/src/main/java/com/rest/api/advice/ExceptionAdvice.java b/src/main/java/com/rest/api/advice/ExceptionAdvice.java index 7ed7d40..8ff576d 100644 --- a/src/main/java/com/rest/api/advice/ExceptionAdvice.java +++ b/src/main/java/com/rest/api/advice/ExceptionAdvice.java @@ -1,8 +1,6 @@ package com.rest.api.advice; -import com.rest.api.advice.exception.CAuthenticationEntryPointException; -import com.rest.api.advice.exception.CEmailSigninFailedException; -import com.rest.api.advice.exception.CUserNotFoundException; +import com.rest.api.advice.exception.*; import com.rest.api.model.response.CommonResult; import com.rest.api.service.ResponseService; import lombok.RequiredArgsConstructor; @@ -55,6 +53,18 @@ public class ExceptionAdvice { return responseService.getFailResult(Integer.valueOf(getMessage("accessDenied.code")), getMessage("accessDenied.msg")); } + @ExceptionHandler(CCommunicationException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public CommonResult communicationException(HttpServletRequest request, CCommunicationException e) { + return responseService.getFailResult(Integer.valueOf(getMessage("communicationError.code")), getMessage("communicationError.msg")); + } + + @ExceptionHandler(CUserExistException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public CommonResult communicationException(HttpServletRequest request, CUserExistException e) { + return responseService.getFailResult(Integer.valueOf(getMessage("existingUser.code")), getMessage("existingUser.msg")); + } + // code정보에 해당하는 메시지를 조회합니다. private String getMessage(String code) { return getMessage(code, null); diff --git a/src/main/java/com/rest/api/advice/exception/CCommunicationException.java b/src/main/java/com/rest/api/advice/exception/CCommunicationException.java new file mode 100644 index 0000000..ef60d0c --- /dev/null +++ b/src/main/java/com/rest/api/advice/exception/CCommunicationException.java @@ -0,0 +1,15 @@ +package com.rest.api.advice.exception; + +public class CCommunicationException extends RuntimeException { + public CCommunicationException(String msg, Throwable t) { + super(msg, t); + } + + public CCommunicationException(String msg) { + super(msg); + } + + public CCommunicationException() { + super(); + } +} diff --git a/src/main/java/com/rest/api/advice/exception/CUserExistException.java b/src/main/java/com/rest/api/advice/exception/CUserExistException.java new file mode 100644 index 0000000..ff97170 --- /dev/null +++ b/src/main/java/com/rest/api/advice/exception/CUserExistException.java @@ -0,0 +1,15 @@ +package com.rest.api.advice.exception; + +public class CUserExistException extends RuntimeException { + public CUserExistException(String msg, Throwable t) { + super(msg, t); + } + + public CUserExistException(String msg) { + super(msg); + } + + public CUserExistException() { + super(); + } +} diff --git a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java index a0f0357..a7d1e19 100644 --- a/src/main/java/com/rest/api/config/security/SecurityConfiguration.java +++ b/src/main/java/com/rest/api/config/security/SecurityConfiguration.java @@ -31,7 +31,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증할것이므로 세션필요없으므로 생성안함. .and() .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크 - .antMatchers("/*/signin", "/*/signup", "/social/**").permitAll() // 가입 및 인증 주소는 누구나 접근가능 + .antMatchers("/*/signin", "/*/signin/**", "/*/signup", "/*/signup/**", "/social/**").permitAll() // 가입 및 인증 주소는 누구나 접근가능 .antMatchers(HttpMethod.GET, "/helloworld/**").permitAll() // hellowworld로 시작하는 GET요청 리소스는 누구나 접근가능 .anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능 .and() diff --git a/src/main/java/com/rest/api/controller/v1/SignController.java b/src/main/java/com/rest/api/controller/v1/SignController.java index 82e2528..a26724d 100644 --- a/src/main/java/com/rest/api/controller/v1/SignController.java +++ b/src/main/java/com/rest/api/controller/v1/SignController.java @@ -1,20 +1,28 @@ package com.rest.api.controller.v1; +import com.google.gson.Gson; import com.rest.api.advice.exception.CEmailSigninFailedException; -import com.rest.api.entity.User; +import com.rest.api.advice.exception.CUserExistException; +import com.rest.api.advice.exception.CUserNotFoundException; import com.rest.api.config.security.JwtTokenProvider; +import com.rest.api.entity.User; import com.rest.api.model.response.CommonResult; import com.rest.api.model.response.SingleResult; +import com.rest.api.model.social.KakaoProfile; import com.rest.api.repo.UserJpaRepo; import com.rest.api.service.ResponseService; +import com.rest.api.service.user.UserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; import java.util.Collections; +import java.util.Optional; @Api(tags = {"1. Sign"}) @RequiredArgsConstructor @@ -26,6 +34,10 @@ public class SignController { private final JwtTokenProvider jwtTokenProvider; private final ResponseService responseService; private final PasswordEncoder passwordEncoder; + private final RestTemplate restTemplate; + private final Environment env; + private final Gson gson; + private final UserService userService; @ApiOperation(value = "로그인", notes = "이메일 회원 로그인을 한다.") @PostMapping(value = "/signin") @@ -39,6 +51,17 @@ public class SignController { return responseService.getSingleResult(jwtTokenProvider.createToken(String.valueOf(user.getMsrl()), user.getRoles())); } + @ApiOperation(value = "소셜 로그인", notes = "소셜 회원 로그인을 한다.") + @PostMapping(value = "/signin/{provider}") + public SingleResult signinByProvider( + @ApiParam(value = "서비스 제공자 provider", required = true, defaultValue = "kakao") @PathVariable String provider, + @ApiParam(value = "소셜 access_token", required = true) @RequestParam String accessToken) { + + KakaoProfile profile = userService.getKakaoProfile(accessToken); + User user = userJpaRepo.findByUidAndProvider(String.valueOf(profile.getId()), provider).orElseThrow(CUserNotFoundException::new); + return responseService.getSingleResult(jwtTokenProvider.createToken(String.valueOf(user.getMsrl()), user.getRoles())); + } + @ApiOperation(value = "가입", notes = "회원가입을 한다.") @PostMapping(value = "/signup") public CommonResult signup(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id, @@ -53,4 +76,26 @@ public class SignController { .build()); return responseService.getSuccessResult(); } + + @ApiOperation(value = "소셜 계정 가입", notes = "소셜 계정 회원가입을 한다.") + @PostMapping(value = "/signup/{provider}") + public CommonResult signupProvider(@ApiParam(value = "서비스 제공자 provider", required = true, defaultValue = "kakao") @PathVariable String provider, + @ApiParam(value = "소셜 access_token", required = true) @RequestParam String accessToken, + @ApiParam(value = "이름", required = true) @RequestParam String name) { + + KakaoProfile profile = userService.getKakaoProfile(accessToken); + Optional user = userJpaRepo.findByUidAndProvider(String.valueOf(profile.getId()), provider); + if(user.isPresent()) + throw new CUserExistException(); + + User inUser = User.builder() + .uid(String.valueOf(profile.getId())) + .provider(provider) + .name(name) + .roles(Collections.singletonList("ROLE_USER")) + .build(); + + userJpaRepo.save(inUser); + return responseService.getSuccessResult(); + } } diff --git a/src/main/java/com/rest/api/entity/User.java b/src/main/java/com/rest/api/entity/User.java index 83689e9..5156ad8 100644 --- a/src/main/java/com/rest/api/entity/User.java +++ b/src/main/java/com/rest/api/entity/User.java @@ -1,10 +1,7 @@ package com.rest.api.entity; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -28,10 +25,12 @@ public class User implements UserDetails { @Column(nullable = false, unique = true, length = 50) private String uid; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) - @Column(nullable = false, length = 100) + @Column(length = 100) private String password; @Column(nullable = false, length = 100) private String name; + @Column(length = 100) + private String provider; @ElementCollection(fetch = FetchType.EAGER) @Builder.Default diff --git a/src/main/java/com/rest/api/model/social/KakaoProfile.java b/src/main/java/com/rest/api/model/social/KakaoProfile.java new file mode 100644 index 0000000..7fa45b1 --- /dev/null +++ b/src/main/java/com/rest/api/model/social/KakaoProfile.java @@ -0,0 +1,19 @@ +package com.rest.api.model.social; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class KakaoProfile { + private Long id; + private Properties properties; + + @Getter + @Setter + private static class Properties { + private String nickname; + private String thumbnail_image; + private String profile_image; + } +} diff --git a/src/main/java/com/rest/api/repo/UserJpaRepo.java b/src/main/java/com/rest/api/repo/UserJpaRepo.java index 3a267ec..f555f94 100644 --- a/src/main/java/com/rest/api/repo/UserJpaRepo.java +++ b/src/main/java/com/rest/api/repo/UserJpaRepo.java @@ -7,4 +7,6 @@ import java.util.Optional; public interface UserJpaRepo extends JpaRepository { Optional findByUid(String email); + + Optional findByUidAndProvider(String uid, String provider); } diff --git a/src/main/java/com/rest/api/service/user/UserService.java b/src/main/java/com/rest/api/service/user/UserService.java new file mode 100644 index 0000000..d431ea9 --- /dev/null +++ b/src/main/java/com/rest/api/service/user/UserService.java @@ -0,0 +1,35 @@ +package com.rest.api.service.user; + +import com.google.gson.Gson; +import com.rest.api.advice.exception.CCommunicationException; +import com.rest.api.model.social.KakaoProfile; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@RequiredArgsConstructor +@Service +public class UserService { + + private final RestTemplate restTemplate; + private final Environment env; + private final Gson gson; + + public KakaoProfile getKakaoProfile(String accessToken) { + // Set header : Content-type: application/x-www-form-urlencoded + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", "Bearer " + accessToken); + + // Set http entity + HttpEntity> request = new HttpEntity<>(null, headers); + ResponseEntity response = restTemplate.postForEntity(env.getProperty("spring.social.kakao.url.profile"), request, String.class); + if (response.getStatusCode() == HttpStatus.OK) + return gson.fromJson(response.getBody(), KakaoProfile.class); + else + throw new CCommunicationException(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f7ff651..84d4c43 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,7 @@ spring: secret: govlepel@$& social: kakao: - client_id: XXXXXXXXXXXXXXXXXXXXXXXX # 앱생성시 받은 REST API 키 + client_id: XXXXXXXXXXXXXXXXXXXXXXXXXX # 앱생성시 받은 REST API 키 redirect: /social/login/kakao url: login: https://kauth.kakao.com/oauth/authorize diff --git a/src/main/resources/i18n/exception_en.yml b/src/main/resources/i18n/exception_en.yml index 47db4aa..ff111c2 100644 --- a/src/main/resources/i18n/exception_en.yml +++ b/src/main/resources/i18n/exception_en.yml @@ -12,4 +12,10 @@ entryPointException: msg: "You do not have permission to access this resource." accessDenied: code: "-1003" - msg: "A resource that can not be accessed with the privileges it has." \ No newline at end of file + msg: "A resource that can not be accessed with the privileges it has." +communicationError: + code: "-1004" + msg: "An error occurred during communication." +existingUser: + code: "-1005" + msg: "You are an existing member." \ No newline at end of file diff --git a/src/main/resources/i18n/exception_ko.yml b/src/main/resources/i18n/exception_ko.yml index 7213648..e09b6dd 100644 --- a/src/main/resources/i18n/exception_ko.yml +++ b/src/main/resources/i18n/exception_ko.yml @@ -12,4 +12,10 @@ entryPointException: msg: "해당 리소스에 접근하기 위한 권한이 없습니다." accessDenied: code: "-1003" - msg: "보유한 권한으로 접근할수 없는 리소스 입니다." \ No newline at end of file + msg: "보유한 권한으로 접근할수 없는 리소스 입니다." +communicationError: + code: "-1004" + msg: "통신 중 오류가 발생하였습니다." +existingUser: + code: "-1005" + msg: "이미 가입한 회원입니다. 로그인을 해주십시오." \ No newline at end of file From 79dea54b731f279f2b2b716558cbdcf24d24ba76 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Fri, 19 Apr 2019 10:46:47 +0900 Subject: [PATCH 10/18] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d08d331..951fa87 100644 --- a/README.md +++ b/README.md @@ -67,4 +67,9 @@ - Document - https://daddyprogrammer.org/post/938/springboot2-restapi-unit-test/ - Git - - https://github.com/codej99/SpringRestApi/tree/feature/junit-test \ No newline at end of file + - https://github.com/codej99/SpringRestApi/tree/feature/junit-test +- SpringBoot2로 Rest api 만들기(10) – Social Login kakao + - Document + - https://daddyprogrammer.org/post/1012/springboot2-rest-api-social-login-kakao/ + - Git + - https://github.com/codej99/SpringRestApi/tree/feature/social-kakao \ No newline at end of file From 3acd2b66e0693fd83be0fbcaeef264119e64f734 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Fri, 19 Apr 2019 11:00:25 +0900 Subject: [PATCH 11/18] =?UTF-8?q?SpringBoot2=EB=A1=9C=20Rest=20api=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0(10)=20=E2=80=93=20Social=20Login=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99(kakao)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserService name change : KakaoService - Social signin, signup Test --- .../controller/common/SocialController.java | 25 +------ .../api/controller/v1/SignController.java | 20 ++---- .../rest/api/model/social/KakaoProfile.java | 21 +++--- .../rest/api/service/social/KakaoService.java | 70 +++++++++++++++++++ .../rest/api/service/user/UserService.java | 35 ---------- src/main/resources/templates/social/login.ftl | 2 +- .../api/controller/v1/SignControllerTest.java | 40 +++++++++-- .../api/service/social/KakaoServiceTest.java | 29 ++++++++ 8 files changed, 158 insertions(+), 84 deletions(-) create mode 100644 src/main/java/com/rest/api/service/social/KakaoService.java delete mode 100644 src/main/java/com/rest/api/service/user/UserService.java create mode 100644 src/test/java/com/rest/api/service/social/KakaoServiceTest.java diff --git a/src/main/java/com/rest/api/controller/common/SocialController.java b/src/main/java/com/rest/api/controller/common/SocialController.java index 4919864..2a400ea 100644 --- a/src/main/java/com/rest/api/controller/common/SocialController.java +++ b/src/main/java/com/rest/api/controller/common/SocialController.java @@ -1,14 +1,11 @@ package com.rest.api.controller.common; import com.google.gson.Gson; -import com.rest.api.model.social.RetKakaoAuth; +import com.rest.api.service.social.KakaoService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; -import org.springframework.http.*; import org.springframework.stereotype.Controller; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -21,10 +18,9 @@ import org.springframework.web.servlet.ModelAndView; public class SocialController { private final Environment env; - private final RestTemplate restTemplate; - private final Gson gson; + private final KakaoService kakaoService; @Value("${spring.url.base}") private String baseUrl; @@ -57,22 +53,7 @@ public class SocialController { */ @GetMapping(value = "/kakao") public ModelAndView redirectKakao(ModelAndView mav, @RequestParam String code) { - // Set header : Content-type: application/x-www-form-urlencoded - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - // Set parameter - MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "authorization_code"); - params.add("client_id", kakaoClientId); - params.add("redirect_uri", baseUrl + kakaoRedirect); - params.add("code", code); - // Set http entity - HttpEntity> request = new HttpEntity<>(params, headers); - ResponseEntity response = restTemplate.postForEntity(env.getProperty("spring.social.kakao.url.token"), request, String.class); - if (response.getStatusCode() == HttpStatus.OK) { - RetKakaoAuth authInfo = gson.fromJson(response.getBody(), RetKakaoAuth.class); - mav.addObject("authInfo", authInfo); - } + mav.addObject("authInfo", kakaoService.getKakaoTokenInfo(code)); mav.setViewName("social/redirectKakao"); return mav; } diff --git a/src/main/java/com/rest/api/controller/v1/SignController.java b/src/main/java/com/rest/api/controller/v1/SignController.java index a26724d..b77949c 100644 --- a/src/main/java/com/rest/api/controller/v1/SignController.java +++ b/src/main/java/com/rest/api/controller/v1/SignController.java @@ -1,6 +1,5 @@ package com.rest.api.controller.v1; -import com.google.gson.Gson; import com.rest.api.advice.exception.CEmailSigninFailedException; import com.rest.api.advice.exception.CUserExistException; import com.rest.api.advice.exception.CUserNotFoundException; @@ -11,15 +10,13 @@ import com.rest.api.model.response.SingleResult; import com.rest.api.model.social.KakaoProfile; import com.rest.api.repo.UserJpaRepo; import com.rest.api.service.ResponseService; -import com.rest.api.service.user.UserService; +import com.rest.api.service.social.KakaoService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; -import org.springframework.core.env.Environment; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.Optional; @@ -34,10 +31,7 @@ public class SignController { private final JwtTokenProvider jwtTokenProvider; private final ResponseService responseService; private final PasswordEncoder passwordEncoder; - private final RestTemplate restTemplate; - private final Environment env; - private final Gson gson; - private final UserService userService; + private final KakaoService kakaoService; @ApiOperation(value = "로그인", notes = "이메일 회원 로그인을 한다.") @PostMapping(value = "/signin") @@ -57,7 +51,7 @@ public class SignController { @ApiParam(value = "서비스 제공자 provider", required = true, defaultValue = "kakao") @PathVariable String provider, @ApiParam(value = "소셜 access_token", required = true) @RequestParam String accessToken) { - KakaoProfile profile = userService.getKakaoProfile(accessToken); + KakaoProfile profile = kakaoService.getKakaoProfile(accessToken); User user = userJpaRepo.findByUidAndProvider(String.valueOf(profile.getId()), provider).orElseThrow(CUserNotFoundException::new); return responseService.getSingleResult(jwtTokenProvider.createToken(String.valueOf(user.getMsrl()), user.getRoles())); } @@ -80,12 +74,12 @@ public class SignController { @ApiOperation(value = "소셜 계정 가입", notes = "소셜 계정 회원가입을 한다.") @PostMapping(value = "/signup/{provider}") public CommonResult signupProvider(@ApiParam(value = "서비스 제공자 provider", required = true, defaultValue = "kakao") @PathVariable String provider, - @ApiParam(value = "소셜 access_token", required = true) @RequestParam String accessToken, - @ApiParam(value = "이름", required = true) @RequestParam String name) { + @ApiParam(value = "소셜 access_token", required = true) @RequestParam String accessToken, + @ApiParam(value = "이름", required = true) @RequestParam String name) { - KakaoProfile profile = userService.getKakaoProfile(accessToken); + KakaoProfile profile = kakaoService.getKakaoProfile(accessToken); Optional user = userJpaRepo.findByUidAndProvider(String.valueOf(profile.getId()), provider); - if(user.isPresent()) + if (user.isPresent()) throw new CUserExistException(); User inUser = User.builder() diff --git a/src/main/java/com/rest/api/model/social/KakaoProfile.java b/src/main/java/com/rest/api/model/social/KakaoProfile.java index 7fa45b1..91594f0 100644 --- a/src/main/java/com/rest/api/model/social/KakaoProfile.java +++ b/src/main/java/com/rest/api/model/social/KakaoProfile.java @@ -2,18 +2,21 @@ package com.rest.api.model.social; import lombok.Getter; import lombok.Setter; +import lombok.ToString; @Getter @Setter +@ToString public class KakaoProfile { - private Long id; - private Properties properties; + private Long id; + private Properties properties; - @Getter - @Setter - private static class Properties { - private String nickname; - private String thumbnail_image; - private String profile_image; - } + @Getter + @Setter + @ToString + private static class Properties { + private String nickname; + private String thumbnail_image; + private String profile_image; + } } diff --git a/src/main/java/com/rest/api/service/social/KakaoService.java b/src/main/java/com/rest/api/service/social/KakaoService.java new file mode 100644 index 0000000..0290b87 --- /dev/null +++ b/src/main/java/com/rest/api/service/social/KakaoService.java @@ -0,0 +1,70 @@ +package com.rest.api.service.social; + +import com.google.gson.Gson; +import com.rest.api.advice.exception.CCommunicationException; +import com.rest.api.model.social.KakaoProfile; +import com.rest.api.model.social.RetKakaoAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@RequiredArgsConstructor +@Service +public class KakaoService { + + private final RestTemplate restTemplate; + private final Environment env; + private final Gson gson; + + @Value("${spring.url.base}") + private String baseUrl; + + @Value("${spring.social.kakao.client_id}") + private String kakaoClientId; + + @Value("${spring.social.kakao.redirect}") + private String kakaoRedirect; + + public KakaoProfile getKakaoProfile(String accessToken) { + // Set header : Content-type: application/x-www-form-urlencoded + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", "Bearer " + accessToken); + + // Set http entity + HttpEntity> request = new HttpEntity<>(null, headers); + try { + // Request profile + ResponseEntity response = restTemplate.postForEntity(env.getProperty("spring.social.kakao.url.profile"), request, String.class); + if (response.getStatusCode() == HttpStatus.OK) + return gson.fromJson(response.getBody(), KakaoProfile.class); + } catch (Exception e) { + throw new CCommunicationException(); + } + throw new CCommunicationException(); + } + + public RetKakaoAuth getKakaoTokenInfo(String code) { + // Set header : Content-type: application/x-www-form-urlencoded + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // Set parameter + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", kakaoClientId); + params.add("redirect_uri", baseUrl + kakaoRedirect); + params.add("code", code); + // Set http entity + HttpEntity> request = new HttpEntity<>(params, headers); + ResponseEntity response = restTemplate.postForEntity(env.getProperty("spring.social.kakao.url.token"), request, String.class); + if (response.getStatusCode() == HttpStatus.OK) { + return gson.fromJson(response.getBody(), RetKakaoAuth.class); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/rest/api/service/user/UserService.java b/src/main/java/com/rest/api/service/user/UserService.java deleted file mode 100644 index d431ea9..0000000 --- a/src/main/java/com/rest/api/service/user/UserService.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.rest.api.service.user; - -import com.google.gson.Gson; -import com.rest.api.advice.exception.CCommunicationException; -import com.rest.api.model.social.KakaoProfile; -import lombok.RequiredArgsConstructor; -import org.springframework.core.env.Environment; -import org.springframework.http.*; -import org.springframework.stereotype.Service; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestTemplate; - -@RequiredArgsConstructor -@Service -public class UserService { - - private final RestTemplate restTemplate; - private final Environment env; - private final Gson gson; - - public KakaoProfile getKakaoProfile(String accessToken) { - // Set header : Content-type: application/x-www-form-urlencoded - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - headers.set("Authorization", "Bearer " + accessToken); - - // Set http entity - HttpEntity> request = new HttpEntity<>(null, headers); - ResponseEntity response = restTemplate.postForEntity(env.getProperty("spring.social.kakao.url.profile"), request, String.class); - if (response.getStatusCode() == HttpStatus.OK) - return gson.fromJson(response.getBody(), KakaoProfile.class); - else - throw new CCommunicationException(); - } -} \ No newline at end of file diff --git a/src/main/resources/templates/social/login.ftl b/src/main/resources/templates/social/login.ftl index 439042f..bb2f64a 100644 --- a/src/main/resources/templates/social/login.ftl +++ b/src/main/resources/templates/social/login.ftl @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/src/test/java/com/rest/api/controller/v1/SignControllerTest.java b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java index 094ebf3..c21b831 100644 --- a/src/test/java/com/rest/api/controller/v1/SignControllerTest.java +++ b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java @@ -1,19 +1,16 @@ package com.rest.api.controller.v1; -import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.context.WebApplicationContext; import java.time.LocalDateTime; import java.time.ZoneId; @@ -86,4 +83,39 @@ public class SignControllerTest { .andExpect(jsonPath("$.success").value(false)) .andExpect(jsonPath("$.code").value(-9999)); } + + @Test + public void signInProviderFail() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("accessToken", "XXXXXXXX"); + mockMvc.perform(post("/v1/signin/kakao").params(params)) + .andDo(print()) + .andExpect(status().is5xxServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value(-1004)); + } + + @Test + public void signUpProvider() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("accessToken", "HizF3ir9522bMW3shkO0x0T9zBdXFCW1WsF56Qo9dVsAAAFqMwTqHw"); + params.add("name", "kakaoKing!"); + mockMvc.perform(post("/v1/signup/kakao").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)); + } + + @Test + public void signInProvider() throws Exception { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("accessToken", "HizF3ir9522bMW3shkO0x0T9zBdXFCW1WsF56Qo9dVsAAAFqMwTqHw"); + mockMvc.perform(post("/v1/signin/kakao").params(params)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data").exists()); + } } \ No newline at end of file diff --git a/src/test/java/com/rest/api/service/social/KakaoServiceTest.java b/src/test/java/com/rest/api/service/social/KakaoServiceTest.java new file mode 100644 index 0000000..bd2f1da --- /dev/null +++ b/src/test/java/com/rest/api/service/social/KakaoServiceTest.java @@ -0,0 +1,29 @@ +package com.rest.api.service.social; + +import com.rest.api.model.social.KakaoProfile; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class KakaoServiceTest { + + @Autowired + private KakaoService kakaoService; + + @Test + public void whenGetKakaoProfile_thenReturnProfile() { + + String accessToken = "xjsMzpQtIr4w13FIQvL3R7BW7X4yvm1KmzXCTwopyWAAAAFqMxEcwA"; + // given + KakaoProfile profile = kakaoService.getKakaoProfile(accessToken); + // then + assertNotNull(profile); + assertEquals(profile.getId(), Long.valueOf(1066788171)); + } +} \ No newline at end of file From 0226f99edce309dc360a6faa68833ff8d4504257 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Mon, 22 Apr 2019 14:48:28 +0900 Subject: [PATCH 12/18] bugfix - @Value @Value("spring.jwt.secret") -> @Value("${spring.jwt.secret}") --- .../java/com/rest/api/config/security/JwtTokenProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/rest/api/config/security/JwtTokenProvider.java b/src/main/java/com/rest/api/config/security/JwtTokenProvider.java index 7c32aa1..a283f71 100644 --- a/src/main/java/com/rest/api/config/security/JwtTokenProvider.java +++ b/src/main/java/com/rest/api/config/security/JwtTokenProvider.java @@ -22,7 +22,7 @@ import java.util.List; @Component public class JwtTokenProvider { // JWT 토큰을 생성 및 검증 모듈 - @Value("spring.jwt.secret") + @Value("${spring.jwt.secret}") private String secretKey; private long tokenValidMilisecond = 1000L * 60 * 60; // 1시간만 토큰 유효 From 3b75ea18c0afede7d355499a74410f9fdc85d32b Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Tue, 30 Apr 2019 16:22:41 +0900 Subject: [PATCH 13/18] seperate environment profile --- README.md | 26 +++++++++++++++++++++++- src/main/resources/application-alpha.yml | 16 +++++++++++++++ src/main/resources/application-local.yml | 18 ++++++++++++++++ src/main/resources/application.yml | 19 ++++------------- 4 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 src/main/resources/application-alpha.yml create mode 100644 src/main/resources/application-local.yml diff --git a/README.md b/README.md index 951fa87..3eec55b 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,32 @@ - Run -> SpringBootApiApplication - Swagger - http://localhost:8080/swagger-ui.html + +### 3. DDL +create table user ( + msrl bigint not null auto_increment, + name varchar(100) not null, + password varchar(100), + provider varchar(100), + uid varchar(50) not null, + primary key (msrl) + ) engine=InnoDB; + +create table user_roles ( + user_msrl bigint not null, + roles varchar(255) + ) engine=InnoDB; + + +alter table user +add constraint UK_a7hlm8sj8kmijx6ucp7wfyt31 unique (uid); -### 3. 목차 +alter table user_roles + add constraint FKel3d4qj41g0sy1mtp4sh055g7 + foreign key (user_msrl) + references user (msrl); + +### 4. 목차 - SpringBoot2로 Rest api 만들기(1) – Intellij Community에서 프로젝트생성 - Document - https://daddyprogrammer.org/post/19/spring-boot1-start-intellij/ diff --git a/src/main/resources/application-alpha.yml b/src/main/resources/application-alpha.yml new file mode 100644 index 0000000..d2c93d4 --- /dev/null +++ b/src/main/resources/application-alpha.yml @@ -0,0 +1,16 @@ +spring: + profiles: alpha + datasource: + url: jdbc:mysql://127.0.0.1:33060/daddyprogrammer?useUnicode=true&autoReconnect=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false + driver-class-name: com.mysql.cj.jdbc.Driver + username: happydaddy + password: daddy!@#1004 + jpa: + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + properties.hibernate: + hbm2ddl.auto: none + format_sql: true + showSql: true + generate-ddl: false + url: + base: http://alpha.daddyprogrammer.org diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..50f941e --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,18 @@ +server: + port: 8080 + +spring: + profiles: local + datasource: + url: jdbc:h2:tcp://localhost/~/test + driver-class-name: org.h2.Driver + username: sa + jpa: + database-platform: org.hibernate.dialect.H2Dialect + properties.hibernate: + hbm2ddl.auto: update + format_sql: true + showSql: true + generate-ddl: true + url: + base: http://localhost:8080 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 84d4c43..c0e050f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,20 +1,9 @@ -server: - port: 8080 - spring: - datasource: - url: jdbc:h2:tcp://localhost/~/test - driver-class-name: org.h2.Driver - username: sa - jpa: - database-platform: org.hibernate.dialect.H2Dialect - properties.hibernate.hbm2ddl.auto: update - showSql: true + profiles: + active: local # 디폴트 환경 messages: basename: i18n/exception encoding: UTF-8 - jwt: - secret: govlepel@$& social: kakao: client_id: XXXXXXXXXXXXXXXXXXXXXXXXXX # 앱생성시 받은 REST API 키 @@ -23,5 +12,5 @@ spring: login: https://kauth.kakao.com/oauth/authorize token: https://kauth.kakao.com/oauth/token profile: https://kapi.kakao.com/v2/user/me - url: - base: http://localhost:8080 + jwt: + secret: govlepel@$& From cdc10997fcc92da66c2f622c601a6a27cf02f3ac Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Wed, 1 May 2019 00:56:12 +0900 Subject: [PATCH 14/18] Add logging preferences --- .../java/com/rest/api/controller/HelloController.java | 4 ++++ src/main/resources/application-alpha.yml | 10 +++++++++- src/main/resources/application-local.yml | 6 ++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/rest/api/controller/HelloController.java b/src/main/java/com/rest/api/controller/HelloController.java index f8241a0..0cc567c 100644 --- a/src/main/java/com/rest/api/controller/HelloController.java +++ b/src/main/java/com/rest/api/controller/HelloController.java @@ -2,10 +2,12 @@ package com.rest.api.controller; import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; +@Slf4j @Controller public class HelloController { @@ -20,6 +22,8 @@ public class HelloController { @GetMapping(value = "/helloworld/string") @ResponseBody public String helloworldString() { + log.debug("Helloworld"); + log.info("Helloworld"); return HELLO; } diff --git a/src/main/resources/application-alpha.yml b/src/main/resources/application-alpha.yml index d2c93d4..16ee0df 100644 --- a/src/main/resources/application-alpha.yml +++ b/src/main/resources/application-alpha.yml @@ -1,3 +1,11 @@ +logging: + level: + root: warn + com.rest.api: info + path: /var/log + file: + max-history: 7 + spring: profiles: alpha datasource: @@ -13,4 +21,4 @@ spring: showSql: true generate-ddl: false url: - base: http://alpha.daddyprogrammer.org + base: http://alpha.daddyprogrammer.org \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 50f941e..3b3316c 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,5 +1,7 @@ -server: - port: 8080 +logging: + level: + root: warn + com.rest.api: debug spring: profiles: local From 5b11d0e1b31c6c0ee23619ab3a443ef8639e3c42 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Wed, 1 May 2019 01:21:00 +0900 Subject: [PATCH 15/18] Add logging preferences --- src/main/resources/application-alpha.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-alpha.yml b/src/main/resources/application-alpha.yml index 16ee0df..b854c06 100644 --- a/src/main/resources/application-alpha.yml +++ b/src/main/resources/application-alpha.yml @@ -12,7 +12,7 @@ spring: url: jdbc:mysql://127.0.0.1:33060/daddyprogrammer?useUnicode=true&autoReconnect=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver username: happydaddy - password: daddy!@#1004 + password: daddy1004 jpa: database-platform: org.hibernate.dialect.MySQL5InnoDBDialect properties.hibernate: From 2826699804348b9a3364b8cb94a305a099f16c1a Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Thu, 2 May 2019 10:27:48 +0900 Subject: [PATCH 16/18] seperate environment profile - modify log file location --- src/main/resources/application-alpha.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-alpha.yml b/src/main/resources/application-alpha.yml index 16ee0df..6e13820 100644 --- a/src/main/resources/application-alpha.yml +++ b/src/main/resources/application-alpha.yml @@ -2,7 +2,7 @@ logging: level: root: warn com.rest.api: info - path: /var/log + path: /home/ec2-user/api/log file: max-history: 7 From 46cacec2753522dc99a6d03deb26598087bbbcb5 Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Thu, 2 May 2019 10:30:33 +0900 Subject: [PATCH 17/18] seperate environment profile - modify url base --- src/main/resources/application-alpha.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-alpha.yml b/src/main/resources/application-alpha.yml index 6e13820..927dab3 100644 --- a/src/main/resources/application-alpha.yml +++ b/src/main/resources/application-alpha.yml @@ -21,4 +21,4 @@ spring: showSql: true generate-ddl: false url: - base: http://alpha.daddyprogrammer.org \ No newline at end of file + base: http://dev-api.daddyprogrammer.org \ No newline at end of file From e390958e9a8182d89141b690fb469e7f2b78f43b Mon Sep 17 00:00:00 2001 From: kimyonghwa Date: Thu, 2 May 2019 22:41:22 +0900 Subject: [PATCH 18/18] Modify Unit Test --- .../api/controller/v1/SignControllerTest.java | 24 +++++++++++++++---- .../api/controller/v1/UserControllerTest.java | 23 +++++++++++++++++- .../api/service/social/KakaoServiceTest.java | 3 ++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/rest/api/controller/v1/SignControllerTest.java b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java index c21b831..b28322d 100644 --- a/src/test/java/com/rest/api/controller/v1/SignControllerTest.java +++ b/src/test/java/com/rest/api/controller/v1/SignControllerTest.java @@ -1,11 +1,15 @@ package com.rest.api.controller.v1; +import com.rest.api.entity.User; +import com.rest.api.repo.UserJpaRepo; +import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; 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.security.crypto.password.PasswordEncoder; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; @@ -14,6 +18,7 @@ import org.springframework.util.MultiValueMap; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.Collections; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -29,6 +34,17 @@ public class SignControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private UserJpaRepo userJpaRepo; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Before + public void setUp() throws Exception { + userJpaRepo.save(User.builder().uid("happydaddy@naver.com").name("happydaddy").password(passwordEncoder.encode("1234")).roles(Collections.singletonList("ROLE_USER")).build()); + } + @Test public void signin() throws Exception { MultiValueMap params = new LinkedMultiValueMap<>(); @@ -95,8 +111,8 @@ public class SignControllerTest { .andExpect(jsonPath("$.code").value(-1004)); } - @Test - public void signUpProvider() throws Exception { + @Test @Ignore + public void signUpSocial() throws Exception { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("accessToken", "HizF3ir9522bMW3shkO0x0T9zBdXFCW1WsF56Qo9dVsAAAFqMwTqHw"); params.add("name", "kakaoKing!"); @@ -107,8 +123,8 @@ public class SignControllerTest { .andExpect(jsonPath("$.code").value(0)); } - @Test - public void signInProvider() throws Exception { + @Test @Ignore + public void signInSocial() throws Exception { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("accessToken", "HizF3ir9522bMW3shkO0x0T9zBdXFCW1WsF56Qo9dVsAAAFqMwTqHw"); mockMvc.perform(post("/v1/signin/kakao").params(params)) diff --git a/src/test/java/com/rest/api/controller/v1/UserControllerTest.java b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java index 1f54444..a4d410a 100644 --- a/src/test/java/com/rest/api/controller/v1/UserControllerTest.java +++ b/src/test/java/com/rest/api/controller/v1/UserControllerTest.java @@ -1,5 +1,8 @@ package com.rest.api.controller.v1; +import com.rest.api.entity.User; +import com.rest.api.repo.UserJpaRepo; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -7,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.json.JacksonJsonParser; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; @@ -16,6 +20,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import java.util.Collections; +import java.util.Optional; + +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -29,10 +37,17 @@ public class UserControllerTest { @Autowired private MockMvc mockMvc; + @Autowired + private UserJpaRepo userJpaRepo; + + @Autowired + private PasswordEncoder passwordEncoder; + private String token; @Before public void setUp() throws Exception { + userJpaRepo.save(User.builder().uid("happydaddy@naver.com").name("happydaddy").password(passwordEncoder.encode("1234")).roles(Collections.singletonList("ROLE_USER")).build()); MultiValueMap params = new LinkedMultiValueMap<>(); params.add("id", "happydaddy@naver.com"); params.add("password", "1234"); @@ -50,6 +65,10 @@ public class UserControllerTest { token = jsonParser.parseMap(resultString).get("data").toString(); } + @After + public void tearDown() throws Exception { + } + @Test public void invalidToken() throws Exception { mockMvc.perform(MockMvcRequestBuilders @@ -109,8 +128,10 @@ public class UserControllerTest { @Test public void delete() throws Exception { + Optional user = userJpaRepo.findByUid("happydaddy@naver.com"); + assertTrue(user.isPresent()); mockMvc.perform(MockMvcRequestBuilders - .delete("/v1/user/2") + .delete("/v1/user/" + user.get().getMsrl()) .header("X-AUTH-TOKEN", token)) .andDo(print()) .andExpect(status().isOk()) diff --git a/src/test/java/com/rest/api/service/social/KakaoServiceTest.java b/src/test/java/com/rest/api/service/social/KakaoServiceTest.java index bd2f1da..a06288b 100644 --- a/src/test/java/com/rest/api/service/social/KakaoServiceTest.java +++ b/src/test/java/com/rest/api/service/social/KakaoServiceTest.java @@ -1,6 +1,7 @@ package com.rest.api.service.social; import com.rest.api.model.social.KakaoProfile; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -16,7 +17,7 @@ public class KakaoServiceTest { @Autowired private KakaoService kakaoService; - @Test + @Test @Ignore public void whenGetKakaoProfile_thenReturnProfile() { String accessToken = "xjsMzpQtIr4w13FIQvL3R7BW7X4yvm1KmzXCTwopyWAAAAFqMxEcwA";