8 Commits

Author SHA1 Message Date
kimyonghwa
53da4ab810 bugfix
- update ftl path
- delete duplicate security settings
2019-11-14 11:39:46 +09:00
kimyonghwa
d2a6c9bc81 SpringBoot2로 Rest api 만들기(9) – Unit Test 2019-04-18 15:20:56 +09:00
kimyonghwa
e4d5cf3a77 Spring Unit Test 2019-04-17 19:24:09 +09:00
codej99
ad6ab44345 Merge pull request #5 from codej99/feature/sonarqube
Modify sonarqube analysis
2019-04-17 11:00:35 +09:00
codej99
e163695ef6 Merge pull request #4 from codej99/feature/readme.md
Create README.md
2019-04-16 17:06:10 +09:00
kimyonghwa
b5157aa381 Create README.md 2019-04-16 17:05:35 +09:00
codej99
ee20205692 Merge pull request #3 from codej99/feature/readme.md
Create README.md
2019-04-16 17:04:55 +09:00
kimyonghwa
e663724567 Create README.md 2019-04-16 17:04:23 +09:00
13 changed files with 383 additions and 31 deletions

65
README.md Normal file
View File

@@ -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

View File

@@ -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'
}

View File

@@ -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/**", "/favicon.ico").permitAll() // 등록한 GET요청 리소스는 누구나 접근가능
.anyRequest().hasRole("USER") // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
.and()
.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())

View File

@@ -13,7 +13,7 @@ public class HelloController {
@Setter
@Getter
public static class Hello {
private static class Hello {
private String message;
}
@@ -33,6 +33,6 @@ public class HelloController {
@GetMapping(value = "/helloworld/page")
public String helloworld() {
return HELLO;
return "helloworld";
}
}

View File

@@ -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<String> 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) {

View File

@@ -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<User> findUserById(@ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) {
public SingleResult<User> findUser() {
// SecurityContext에서 인증받은 회원의 정보를 얻어온다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String id = authentication.getName();

View File

@@ -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)

View File

@@ -12,7 +12,7 @@ public class ResponseService {
// enum으로 api 요청 결과에 대한 code, message를 정의합니다.
public enum CommonResponse {
SUCCESS(0, "성공하였습니.");
SUCCESS(0, "성공하였습니.");
int code;
String msg;

View File

@@ -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() {
}
}

View File

@@ -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"));
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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));
}
}

View File

@@ -0,0 +1,119 @@
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.MvcResult;
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<String, String> params = new LinkedMultiValueMap<>();
params.add("id", "happydaddy@naver.com");
params.add("password", "1234");
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())
.andReturn();
String resultString = result.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<String, String> 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));
}
}

View File

@@ -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.*;
@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> user = userJpaRepo.findByUid(uid);
// then
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변수 값이 같은지 체크
}
}