[Spring] Spring Security - 인증, 인가, CSRF, Test
[Spring] Spring Security
Spring Security
- 어플리케이션에 보안 기능을 구현할 때 사용되는 프레임워크
- servlet container에 배포하는 어플리케이션에 활용
기능
1. 인증/인가
- 인증 : 사용자의 정당성 확인
- 인가 : 사용자의 리소스 접근에 대한 제어
2. 세션 관리
- Session을 이용한 공격에 대한 보호와 Session 라이프 사이클 제어
3. CSRF 방지
- CSRF(Cross-Site Request Forgery)로부터 보호
-> 이외에도 여러가지 기능이 많음
설정
1. Dependency 추가
1 2 3 4 5 6 7 8 9 10 | <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.1.5.RELEASE</version> </dependency> | cs |
2. Config에 Bean 설정
1 2 3 4 5 | @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { } | cs |
- @EnableWebSecurity로 활성화
3. Servlet Filter 설정
1) ServletFilter 설정
1 2 3 | @Configuration public class SecurityFilterConfig extends AbstractSecurityWebApplicationInitializer { } | cs |
- AbstractSecurityWebApplicationInitializer를 상속받는 class 생성
2) Filter 등록
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @Configuration public class DispatcherConfig extends AbstractAnnotationConfigDispatcherServletInitializer { protected Class<?>[] getRootConfigClasses() { return new Class[0]; } protected Class<?>[] getServletConfigClasses() { return new Class<?>[] {WebMvcConfig.class}; } protected String[] getServletMappings() { return new String[]{"/"}; } @Override protected Filter[] getServletFilters() { return new Filter[] { new DelegatingFilterProxy("springSecurityFilterChain") }; } } | cs |
- dispatcher servlet config 파일에 Filterchain 추가
Spring Security Architecture
모듈
- spring-security-core : 인증/인가 구현을 위한 컴포넌트
- spring-security-web : 웹 어플리케이션 보안 기능을 위한 컴포넌트
- spring-security-config : 컴포넌트 설정을 위한 컴포넌트
- spring-security-taglibs : 인증/인가를 사용하기위한 JSP 태그 라이브러리
Architecture
- Client의 요청을 FilterChainProxy에서 받음
- HttpFirewall에 방화벽 기능 호출
- Security Filter에게 요청을 넘기고 Seucirty Filter 정상 종료 시, Resource에 접근
1. FilterChainProxy
- 프레임워크 진입점 역할을 하는 서블릿 필터 클래스
- 전체 흐름 제어, 보안 기능 추가
2. HttpFirewall
- HttpServletRequest/Response에 방화벽 기능 ㅊ추가
3. SecurityFilterChain
- FilterChainProxy가 받은 요청에 적용할 보안 필터 목록 관리
4. Security Filter
- 보안 기능을 제공하는 Servlet Filter 클래스
인증 처리
인증 처리
- Spring Security가 제공하는 인증 메커니즘
1. AuthenticationFilter
- 인증 처리 방식
- 클라이언트의 요청 파라미터에서 인증정보(계정, 패스워드)를 받고 Authentication Manager의 method 호출
2. AuthenticationManager(implementation : ProviderManager)
- 인증 처리를 수행하기 위한 인터페이스
- 기본 구현 : AuthenticationProvider에게 인증 처리를 위임하고 인증 결과를 받는 구조
3. AuthenticationProvider(implementation : DaoAuthenticationProvider)
- 인증 처리 기능을 구현하기 위한 인터페이스
인증 페이지
1 2 3 4 | @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { } | cs |
- Spring Security에서는 위와 같이 @EnableWebSecurity 설정 시, 인증을 요청하는 페이지를 보여줌
- 결과
인증
- 권한을 얻은 user만 접근할 수 있도록 설정
1. config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password(encoder().encode("admin")).roles("ADMIN") .and() .withUser("user").password(encoder().encode("user")).roles("USER"); } @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } } | cs |
- "admin" 계정과 "user" 계정 생성
- Login 페이지에서 위의 계정으로 로그인 시, 모든 controller를 이용할 수 있음
- 비 로그인 시, 모든 요청이 "/login"으로 리다이렉트
- 로그인 시, 쿠키에 JSESSIONID 등록
- 로그인 후 접근
인가 처리
인가 처리
- 사용자가 접근할 수 있는 resource 제어
- resource에 대한 접근 정책 정의, resource 접근 시 접근 정책 확인 후 허용 여부 결정
1. ExceptionTranslationFilter
- 인가 과정에서 발생한 예외 처리
- 클라이언트에 응답을 하기 위한 servlet filter
- 인증되지 않은 사용자 접근 시, 로그인 페이지로 리다이렉트
2. FilterSecurityInterceptor
- HTTP 요청에 대한 인가 처리를 적용하기 위한 servlet filter
- AccessDecisionManager의 method를 호출하여 인가 처리
3. AccessDecisionManager(implementation : AffirmativeBased)
- AccessDecisionVoter의 method를 호출하여 접근 가능 여부 투표
- 하나의 AccessDecisionVoter가 부여로 투표하면 접근 권한 부여
4. AccessDecisionVoter(implementation : WebExpressionVoter)
- 접근하려는 resource에 접근 정책을 참조하여 접근 권한 부여 여부 투표
REST 인증/인가
- REST에서 인증을 사용
1. config 설정
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin").password(encoder().encode("admin")).roles("ADMIN") .and() .withUser("user").password(encoder().encode("user")).roles("USER"); } @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .httpBasic() .and() .authorizeRequests() .antMatchers("/db/**").hasRole("ADMIN"); } @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } } | cs |
1) configure(AuthenticationManagerBuilder auth)
- AuthenticationManagerBuilder를 사용해 어떠한 계정의 어떠한 role을 갖는지 설정
- admin -> "ADMIN"
- user -> "USER"
2) configure(HttpSecurity httpSecurity)
- csrf().disable() : basic auth를 사용하기 위해 csrf 보호 기능 disable
- httpBasic() : basic auth 사용
- authorizeRequests() : 모든 request를 인증
- antMatchers().hasRole() : 특정 URI를 특정 role을 갖는 계정만 접근할 수 있음
3) encoder
- 평문 password를 해시화하여 저장하기 위해 사용
- BCryptPasswordEncoder : Bcrypt 알고리즘으로 해시
- StandardPasswordEncoder : SHA-256 알고리즘으로 해시
- NoOpPasswordEncoder : 해시화 하지 않음
2. Test용 controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @RestController @RequestMapping("/db") public class DatabaseController { @Autowired private TRepository tRepository; @RequestMapping(value = "/create", method = RequestMethod.POST) public void create(@RequestBody TestModel testModel) { tRepository.create(testModel); } @RequestMapping(value = "/{id}", method = RequestMethod.GET) public TestModel get(@PathVariable String id) { TestModel test = tRepository.getOne(id); return test; } } | cs |
- DB에 저장하는 create 핸들러와 select하는 get 핸들러
1) 인증 정보 설정(curl)
- curl 사용 시, -u 옵션과 id:password로 전송
- ex) curl localhost:8080/db/tester -u admin:admin
2) 인증 정보 설정(Postman)
- Authorization에서 username, password 설정
- 결과(인증O)
- 결과(인증X)
접근 정책
- resource에 접근하기 위한 정책 설정
1. config
1 2 3 4 5 6 7 8 9 10 11 12 13 | @EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .antMatchers("/db").hasRole("ADMIN") .antMatchers("/db/{id}").access("isAuthenticated() or (#id == 'tester')") .antMatchers("/sample").hasAnyRole("ADMIN","USER") .antMatchers("/resources").permitAll() .anyRequest().authenticated(); } } | cs |
1) 표현식
- hasRole : 특정 Role에 해당하면 성공
- hasAnyRole : 특정 Role들 중 하나에 해당하면 성공
- isAnonymous : 로그인하지 않은 사용자면 성공
- isAuthenticated : 이미 인증된 사용자면 성공
- permitAll : 항상 성공
- denyAll : 항상 실패
- access : 경로 변수나 여러개의 접근 정책 설정
2) Resource 지정
- antMatchers : ant 형식으로 지정한 경로 패턴의 resource에 적용
- regexMatchers : 정규표현식으로 지정한 경로 패턴의 resource에 적용
- requestMatchers : RequestMatcher 인터페이스 구현과 일치하는 resource에 적용
- anyRequest : 기타 resource에 적용
Method 인가
- AOP를 이용해 method에 대해 인가 처리를 할 수 있음
1. config
1 2 3 4 5 | @EnableWebSecurity @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { } | cs |
- config파일에 @EnableGlobalMethodSecurity(prePostEnabled = true) 추가
2. method 접근 정책 지정
1) @PreAuthorize
- method 실행 전 접근 정책을 지정하고 접근 정책 통과 시, method 실행
1 2 3 4 5 6 7 8 9 | @RestController @RequestMapping(value = "/sample") public class SampleController { @RequestMapping(value = "/{id}", method = RequestMethod.GET) @PreAuthorize("hasRole('ADMIN') or (#id == 'tester')") public String auth(@PathVariable String id) { return id; } } | cs |
- @Preauthorize 지정
- "ADMIN" role인 계정이나 path 경로의 id가 tester이면 접근 허용
- 결과(성공)
- 결과(실패)
2) @PostAuthorize
- method 실행 후 적용할 접근 정책 지정
- method가 실행된 후, 지정한 접근 정책 통과 시, response 전달
1 2 3 4 5 6 7 8 9 | @RestController @RequestMapping(value = "/sample") public class SampleController { @RequestMapping(value = "/{id}", method = RequestMethod.GET) @PostAuthorize("(returnObject.equals('admin'))") public String auth(@PathVariable String id) { return id; } } | cs |
- returnObject가 'admin'이면 response
- 결과(성공)
- 결과(실패)
CSRF 방지
CSRF(Cross-Site Request Forgery)
- server가 신뢰하는 client에게 특정 action을 취하는 script를 실행하게하여 신뢰된 client가 특정 server에 공격 request를 전송하게함
Spring Security CSRF
- Session 단위로 무작위 토큰(CSRF Token)을 발급
- 요청 파라미터에 token을 포함시켜 전송하여 정상 요청인지, 공격자로부터 의도된 요청인지 판단하는 기능이 있음
1. CSRF 보호 기능 활성화
- Spring Security에서는 @EnableWebSecurity 지정 시, 자동으로 CSRF 보호 기능이 활성화
- 비활성화가 필요할 경우, config 파일에 추가
1 2 3 4 5 6 7 8 9 | @EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable(); } } | cs |
- HttpSecurity에 있는 csrf().disable() method를 사용하여 disable
2. CSRF Token
- CSRF 보호는 데이터 변조를 할 수 있는 POST, PUT 등의 method에 적용
1) Config
- CSRF Token을 사용하기 위해 Config 파일에 추가
1 2 3 4 5 6 7 8 9 10 | @EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } } | cs |
- csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())를 사용하여 csrf token 사용 지정
2) Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 | @RestController @RequestMapping(value = "/csrf") public class CsrfController { @RequestMapping(value = "/create", method = RequestMethod.POST) public TestModel create(@RequestBody TestModel testModel) { return testModel; } @RequestMapping(value = "/", method = RequestMethod.GET) public String get() { return "token"; } } | cs |
- GET/POST test를 위한 Controller
3) 결과
- 결과(GET)
-> Token없이도 GET 요청은 성공
- 결과(Cookie)
-> server에 요청 시, Response Cookie에 XSRF-TOKEN 발급
- 결과(Token없이 POST)
-> Server에서 받은 cookie가 request에 포함되어있지만 Server에서는 header(X-XSRF-TOKEN)로 검증
- 결과(POST)
-> request header에 "X-XSRF-TOKEN"을 포함하여 전송
보안 헤더
보안 헤더 설정
- Spring Security에서는 @EnableWebSecurity 지정 시, 자동으로 보안 헤더를 활성화
- 비활성화가 필요할 경우, config 파일에 추가
1 2 3 4 5 6 7 8 9 | @EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .headers().disable(); } } | cs |
- HttpSecurity의 headers().disable()을 사용하여 비활성화
종류
- Cache-Control : Content의 캐시 방법을 지시
- X-Frame-Options : <frame> tag에서 content 표시를 인가할지에 대한 여부를 지시
- X-Content-Type-Options : Content의 종류를 결정하는 방법을 지시
- X-XSS-Protection : 브라우저의 XSS 필터를 사용해 악성 스크립트를 검출할 방법을 지시
- Strict-Transport-Security : HTTPS 접근 후 HTTP로 접근하려할 때, HTTPS로 접근하도록 지시
헤더 선택
- 모든 보안 헤더를 사용하는 대신, 원하는 헤더만 사용하고 싶을 때 헤더를 지정할 수 있음
1. config 파일에 추가
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .headers() .defaultsDisabled() .cacheControl() .and() .xssProtection(); } } | cs |
- defaultsDisabled() : 기본 헤더 설정을 모두 disable
- 사용하고 싶은 헤더만 추가(Cache-Control, X-XSS-Protection)
- 사용하고 싶지 않은 헤더만 비활성화하고 싶은 경우, cacheControl().disable() 과 같이 비활성화할 수 있음
Spring Security Test
- MocvMvc를 사용하여 Spring Security의 인증/인가를 테스트
Spring Security Test 설정
1. Dependency 추가
1 2 3 4 5 | <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>5.1.5.RELEASE</version> </dependency> | cs |
- spring-security-test 디펜던시 추가
2. Test Class 설정
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @RunWith(SpringRunner.class) @ContextConfiguration(classes = {WebMvcConfig.class, WebSecurityConfig.class}) @WebAppConfiguration public class TestDatabaseController { @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(springSecurity()).build(); } } | cs |
- @ContextConfiguration을 사용하여 Security 설정 파일(WebSecurityConfig) 참조
- MockMvc에 sercurity servlet filter 추가
인증 Test
- Spring Security에서 제공하는 인증 처리를 테스트
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @RunWith(SpringRunner.class) @ContextConfiguration(classes = {WebMvcConfig.class, WebSecurityConfig.class}) @WebAppConfiguration public class TestSampleController { @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(springSecurity()).build(); } @Test public void getModel() throws Exception { mockMvc.perform(get("/login").with(httpBasic("admin","admin"))) .andExpect(authenticated().withRoles("ADMIN")); } } | cs |
- Config에 설정해놓았던 admin/admin이 들어오면 "ADMIN"이라는 role이 적용되는지 확인
- 결과(성공)
- 결과(실패)
인가 Test
- 인증된 사용자의 Resource에 대한 인가 처리를 테스트
1. 인증 정보 설정
1) @WithMockUser
- Annotation에 username/password/role 등의 정보를 등록하여 인증 정보로 사용
- @WithMockUser(username = "admin", password = "admin", roles = "ADMIN")
2) @WithUserDetails
- Annotation에 지정된 username에 대응하는 UserDetails를 DI Container에 등록된 UsesrDetailsService에서 가져옴
- @WithUserDetails("admin")
2. Test
1 2 3 4 5 6 7 | @Test @WithMockUser(username = "admin", password = "admin", roles = "ADMIN") public void getGet() throws Exception { mockMvc.perform(get("/sample/")) .andExpect(status().isOk()) .andExpect(content().string(objectMapper.writeValueAsString("sample"))); } | cs |
- @WithMockUser를 사용하여 username/password/role 지정
- Controller에 @PreAuthorize로 role만 지정해놓았기 때문에 role만 지정해도 테스트 통과
- 결과(성공)
- 결과(실패)
Csrf Test
- Csrf 기능 활성화된 상태로 post 요청을 테스트할 때 유효한 CSRF Token 값을 넣어 테스트해야함
- csrf()를 사용하여 유효한 token처럼 테스트할 수 있음
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @RunWith(SpringRunner.class) @ContextConfiguration(classes = {WebMvcConfig.class, WebSecurityConfig.class}) @WebAppConfiguration public class TestCsrfController { @Autowired private WebApplicationContext webApplicationContext; @Autowired private ObjectMapper objectMapper; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(springSecurity()).build(); } @Test public void create() throws Exception { TestModel testModel = new TestModel(); testModel.setId("tester"); testModel.setName("tester"); mockMvc.perform(post("/csrf/create") .content(objectMapper.writeValueAsString(testModel)) .contentType(MediaType.APPLICATION_JSON) .with(csrf())) .andExpect(status().isOk()) .andExpect(content() .string(objectMapper.writeValueAsString(testModel))); } } | cs |
- post요청으로 TestModel을 보내주고 해당 TestModel을 받는 controller를 테스트
- with(csrf())로 유효한 csrf token을 사용하는 것처럼 테스트
- 결과(성공)
- 결과(실패)