부트 개념과 활용-30.Starter-Security(스프링 시큐리티)
스프링 시큐리티 1부: Starter-Security
스프링 시큐리티
- 웹 시큐리티
- 메소드 시큐리티
- 다양한 인증 방법 지원
- LDAP, 폼 인증, Basic 인증, OAuth, …
스프링 부트 시큐리티 자동 설정
- SecurityAutoConfiguration
- UserDetailsServiceAutoConfiguration
- spring-boot-starter-security
- 스프링 시큐리티 5.* 의존성 추가
- 모든 요청에 인증이 필요함.
- 기본 사용자 생성
- Username: user
- Password: 애플리케이션을 실행할 때 마다 랜덤 값 생성 (콘솔에 출력 됨.)
- spring.security.user.name
- spring.security.user.password
- 인증 관련 각종 이벤트 발생
- DefaultAuthenticationEventPublisher 빈 등록
- 다양한 인증 에러 핸들러 등록 가능
실습
의존성 추가(thymeleaf) (참고. 이거 안하면 / -> index.html 못찾음.. 구글링..)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
컨트롤러에서 다른 로직 없이 뷰로만 전달할 때 (controller로 구현해도 동일)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/my").setViewName("my");
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>index</h1>
<a href="/hello">hello</a>
<a href="/my">my</a>
</body>
</html>
test
@ExtendWith(SpringExtension.class)
@WebMvcTest(WebConfig.class)
class HomeControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void hello() throws Exception {
mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("hello"));
}
@Test
public void my() throws Exception {
mockMvc.perform(get("/my"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("my"));
}
}
현재 상태에서는 정상적으로 테스트가 되나.. 스프링 시큐리티 의존성을 추가하면 에러가 뜬다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
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 = []
Error message = Unauthorized
스프링 시큐리티를 추가하면 모든 요청이 다 스프링 시큐리티를 통해 인증이 필요로 하게 된다.
(폼 인증, Basic 인증 둘 다 적용이 됨)
현재는 베이직 인증으로 받는 것을 확인이 가능하다.
그런데 이 인증 방식은 보내주는 accept 헤더에 따라 달라진다.
이 요청이 원하는 응답의 형태가 현재는 아무것도 지정하지 않아서 폼 인증에 대한 응답으로 보내지 않고, Basic 인증의 형태로 보내준 것인데,
보통 대부분의 경우는 TEXT_HTML을 accept 헤더에 담아서 보낸다. 이런 경우에는 폼 인증으로 넘어가게 된다.
이에 대한 테스트를 확인하면
@Test
public void hello() throws Exception {
mockMvc.perform(get("/hello")
.accept(MediaType.TEXT_HTML))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("hello"));
}
TEXT_HTML 미디어 타입으로 보내면,
MockHttpServletResponse:
Status = 302
Error message = null
Headers = [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", Location:"http://localhost/login"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = http://localhost/login
Cookies = []
아까와 다른 응답을 확인할 수 있다. (401 -> 302)
이제 테스트가 아닌 메인 애플리케이션을 실행하면
Using generated security password: 1dafcde2-29b1-4032-bc1a-2f2a541ecc7e
콘솔창에 패스워드가 생성되는 것을 확인할 수 있고,
/ 에서 다른 링크로 가려고 한다면,
브라우저에서 내장하고 있는 기본 로그인 폼이 뜨는 것을 확인할 수 있다. (인증이 되지 않았기 때문.)
id : user이고 비밀번호는 매 실행시마다 생성되는 것을 확인할 수 있다.
스프링 시큐리티에서 기본적으로는
모든 요청에 대해 인증을 필요로 하고,
유저를 하나 생성하고 랜덤으로 패스워드를 생성한다. (콘솔 출력)
스프링 부트 시큐리티 테스트 (인증을 추가한 테스트)
@WithMockUser를 활용해 가능하다.
의존성 추가
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring-security.version}</version>
<scope>test</scope>
</dependency>
@ExtendWith(SpringExtension.class)
@WebMvcTest(WebConfig.class)
//@WithMockUser
class HomeControllerTest {
@Autowired
MockMvc mockMvc;
@Test
@WithMockUser // 개별 적용도 가능하고, 클래스에 적용도 가능
public void hello() throws Exception {
mockMvc.perform(get("/hello")
.accept(MediaType.TEXT_HTML))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("hello"));
}
@Test
public void my() throws Exception {
mockMvc.perform(get("/my"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("my"));
}
}