Security(시큐리티)
스프링에서는 시큐리티를 제공함으로서 여러 방면에서 보안 기능을 지원합니다.
- 웹시큐리티
- 메소드 시큐리티
- 다양한인증방법지원
- LDAP, 폼 인증, Basic 인증, OAuth, … 등
참고자료 : 스프링공식문서
SpringBoot Security
스프링 부트 자체 시큐리티 자동 설정이 있습니다.
- SecurityAutoConfiguration : 시큐리티에 대한 설정 값들을 자동 실행해줍니다. 만약 사용하지 않고 커스텀을 원한다면 WebSecurityConfigurerAdapter를 상속받아 커스텀 할 수 있습니다.
- UserDetailsServiceAutoConfiguration : 자동으로 랜덤한 User(계정)을 생성해 줍니다.
실습을 통해서 시큐리티가 어떻게 작동하는지 학습해보겠습니다.
Thymeleaf 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
타임리프 의존성을 추가하여 View를 구성하겠습니다.
타임리프가 궁금하시다면 타임리프 포스팅을 참고해주시면 되겠습니다.
Controller 클래스
@Controller
public class HomeController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/my")
public String my() {
return "my";
}
}
단순한 View로 이동하는 컨트롤러를 구현합니다.
단순 View Controller를 커스텀으로 구현하기
별다른 Controller 설정없이 요청이 들어오면 View로 보내는 설정이 하고싶으면 아래와 같이 설정하는 방법도 있습니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/hello").setViewName("hello");
}
}
하지만 추가 설정 및 도중 구현 목적이 달라질 수도 있기 때문에 Controller에 구현하는 것이 좋겠습니다.
View
Controller에 선언한 경로로 이동하기 위해 View를 만듭니다.
경로를 src/resource/templates에 생성합니다.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>Welcome junjang's Blog!!!</h1>
<a href="/hello">hello!</a>
<a href="/my">My Page</a>
</body>
</html>
hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>Hello</h1>
</body>
</html>
my.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>My</h1>
</body>
</html>
완성된 View의 모습입니다.
이제 View를 생성했다면 Controller와 연동이 되었는지 테스트를 해야합니다.
Test 클래스
@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
public class HomeControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void hello() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(view().name("hello"))
}
@Test
public void my() throws Exception {
mockMvc.perform(get("/my"))
.andExpect(status().isOk())
.andExpect(view().name("my"))
.andDo(print());
}
}
슬라이스 테스트를 구현하고 각 경로에 따라 상태, view 이름이 일치하는지 테스트합니다.
테스트가 완료되었다면 애플리케이션을 구동시킵니다.
각 페이지별 이상 없이 출력되는 것을 확인하실 수 있습니다.
이제 애플리케이션까지 이상없이 구동되는 것을 확인 했으니 본격적으로 시큐리티를 적용해보겠습니다.
Security 적용하기
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
의존성을 추가하는 순간 내장되어있는 자동 설정이 구동합니다.
Test 클래스
의존성을 추가 후 다시 테스트를 구동하면 Unauthorized 401에러가 납니다.
Unauthorized 401에러
MockHttpServletResponse:
Status = 401
Error message = Unauthorized
Headers = {WWW-Authenticate=[Basic realm="Realm"],
X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block],
Cache-Control=[no-cache, no-store, max-age=0, must-revalidate],
Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
이유는 Basic Authentication의 인증 정보가 없기 때문인데 이를 해결하기 위해 Accept Header를 주어 Form Authentication으로 인증을 할 수 있도록 조치해야합니다.
@Test
public void hello() throws Exception {
mockMvc.perform(get("/hello")
.accept(MediaType.TEXT_HTML))
.andExpect(status().isOk())
.andExpect(view().name("hello"))
..andDo(print());
}
accept Header 설정에 임의로 MediaType.TEXT_HTML을 적용하면 form 인증을 합니다.
자동설정에 의한 기본 계정 생성
Application이 구동될 때마다 랜덤으로 유저를 생성하여 Form 인증시 사용할 수 있게 해줍니다.
- Username : user
- Password : 랜덤 출력(콘솔)
user와 콘솔에 출력된 비밀번호를 입력하면 페이지가 출력됩니다.
자동설정을 커스텀 하고 싶다면?
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
위 클래스 안에 직접 커스텀을 할 수 있습니다. 커스텀을 하는 방법은 다음에 더 자세히 알아보도록 하겠습니다.
여기까지 애플리케이션에 로그인 Form까지 완성되었을 것입니다. 하지만 정작 테스트에서는 계정을 컨트롤 할 수 없기 때문에 에러페이지가 계속 뜰텐데 이를 해결할 방법은 뭐가 있을까요?
Spring Security test 의존성 추가
참고자료 : 스프링공식문서
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring-security.version}</version>
<scope>test</scope>
</dependency>
시큐리티 테스트라는 의존성을 추가해줍니다.
@WithMockUser 사용하기
@WithMockUser는 가짜 유저 정보를 자체적으로 만들어주어 테스트를 진행합니다.
아래 예제처럼 메소드마다 적용할 수도 있지만 컨트롤러 전체에도 적용 가능합니다.
@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
public class HomeControllerTest {
@Autowired
MockMvc mockMvc;
@Test
@WithMockUser
public void hello() throws Exception {
mockMvc.perform(get("/hello")
.accept(MediaType.TEXT_HTML))
.andExpect(status().isOk())
.andExpect(view().name("hello"))
.andDo(print());
}
@Test
@WithMockUser
public void my() throws Exception {
mockMvc.perform(get("/my"))
.andExpect(status().isOk())
.andExpect(view().name("my"))
.andDo(print());
}
}
만약 유저 정보가 자동으로 생성되었다면 200, 유저 정보가 없다면 401 Unauthorized 에러 메시지를 출력할 것입니다.