DongDD's IT

[Spring] Spring Security - 인증, 인가, CSRF, Test 본문

프로그래밍/Spring

[Spring] Spring Security - 인증, 인가, CSRF, Test

DongDD 2019. 4. 20. 19:21

[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


1

- Client의 요청을 FilterChainProxy에서 받음

- HttpFirewall에 방화벽 기능 호출

- Security Filter에게 요청을 넘기고 Seucirty Filter 정상 종료 시, Resource에 접근


1. FilterChainProxy

- 프레임워크 진입점 역할을 하는 서블릿 필터 클래스

- 전체 흐름 제어, 보안 기능 추가


2. HttpFirewall

- HttpServletRequest/Response에 방화벽 기능 ㅊ추가


3. SecurityFilterChain

- FilterChainProxy가 받은 요청에 적용할 보안 필터 목록 관리


4. Security Filter

- 보안 기능을 제공하는 Servlet Filter 클래스



인증 처리


인증 처리


2

- 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 설정 시, 인증을 요청하는 페이지를 보여줌

- 결과

3


인증


- 권한을 얻은 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 등록

4

- 로그인 후 접근

5



인가 처리


인가 처리


- 사용자가 접근할 수 있는 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)

7

- Authorization에서 username, password 설정

- 결과(인증O)

8

- 결과(인증X)

9


접근 정책


- 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이면 접근 허용

- 결과(성공)

10

- 결과(실패)

11


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

- 결과(성공)

12

- 결과(실패)

13



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)

14

->  Token없이도 GET 요청은 성공 


- 결과(Cookie)

15

->  server에 요청 시, Response Cookie에 XSRF-TOKEN 발급


- 결과(Token없이 POST)

16

-> Server에서 받은 cookie가 request에 포함되어있지만 Server에서는 header(X-XSRF-TOKEN)로 검증


- 결과(POST)

17

-> 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이 적용되는지 확인

- 결과(성공)

18

- 결과(실패)

19


인가 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만 지정해도 테스트 통과

- 결과(성공)

20

- 결과(실패)

21


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을 사용하는 것처럼 테스트

- 결과(성공)

22

- 결과(실패)

23


Comments