Basic 토큰 인증
BasicAuthenticationFilter
- 기본적으로 로그인 페이지를 사용할 수 없는 상황에서 사용한다. (form login사용 불가능 한경우)
- SPA 페이지(react, angular, vue.. )
- SPA의 경우 server에서 login form을 만들어서 사용하는 것이 아닌 Client Browser에서 javascript를 통해 login form을 만들어 사용한다.
- 브라우저 기반의 모바일 앱(브라우저 기반의 앱, ex: inoic)
- SPA 페이지(react, angular, vue.. )
설정 방법
1 2 3 4 5 6 7
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic(); } }
- Authorization 헤더에 ID, PW를 BASE64 encoding해서 보내면 filter가 dispatch servlet가기 전에 request를 가로채서 ID, PW를 가지고 인증을 수행하고, 인증이 되면 Token을 security context에 넣어주는 작업을 한다.
- BASE64 encoding은 누구나 decoding할 수 있기 때문에 보안상 좋지 않음
- Authorization 헤더에 ID, PW를 BASE64 encoding해서 보내면 filter가 dispatch servlet가기 전에 request를 가로채서 ID, PW를 가지고 인증을 수행하고, 인증이 되면 Token을 security context에 넣어주는 작업을 한다.
- http에서는 username:password가 header에 포함되어 가기 때문에 보안에 매우 취약하다. 그래서 반드시 https프로토콜에서 사용할 것을 권장한다.
- 최초 로그인시에만 인증을 처리하고 이후에는 session에 의존한다.
- ERROR시 401(UnAuthorized)를 보낸다.
로그인 하는 방식
- Login Page에서 id,pw를 입력하면 해당 id,pw로 basic token을 만들어서 서버로 보낸 뒤 서버에서 인증이 되었다면 Authentication Principal에 해당하는 User 객체를 반환해준다.
Basic 토큰 인증 실습
인증 사용 X
config
1 2 3 4 5 6 7
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().permitAll() .and() .httpBasic(); } }
controller
1 2 3 4 5 6 7 8
@RestController public class HomeController { @GetMapping("/test") public String test(){ return "hello"; } }
test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class BasicAuthenticationTest { @LocalServerPort int port; RestTemplate client = new RestTemplate(); private String testUrl() { return "http://localhost:" + port + "/test"; } @DisplayName("1. 인증 실패") @Test void test_1(){ String response = client.getForObject(greetingUrl(), String.class); System.out.println("-----------------------------"); System.out.println(response); System.out.println("-----------------------------"); } }
- client port를 random으로 사용해 restTemplate를 통해 API요청
result
- permitAll로 설정했기 때문에 정상적으로 hello를 출력한다.
인증 사용 O (인증 실패 -> 401 Error)
config
1 2 3 4 5 6 7 8 9
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .httpBasic(); } }
- permitAll을 authenticated로 변경
test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class BasicAuthenticationTest { @LocalServerPort int port; RestTemplate client = new RestTemplate(); private String testUrl() { return "http://localhost:" + port + "/test"; } @DisplayName("1. 인증 실패") @Test void test_1(){ HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, ()-> { client.getForObject(greetingUrl(), String.class); }); assertEquals(401, exception.getRawStatusCode()); } }
인증 사용 O (인증 성공)
config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser( User.withDefaultPasswordEncoder() .username("user1") .password("1111") .roles("USER") .build() ); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and() .httpBasic(); } }
- 인증에 사용할 User생성
Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@DisplayName("2. 인증 성공") @Test void test_2(){ HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString( "user1:1111".getBytes() )); HttpEntity entity = new HttpEntity(null, headers); ResponseEntity<String> resp = client.exchange(greetingUrl(), HttpMethod.GET, entity, String.class); assertEquals("hello", resp.getBody()); System.out.println("-------------------------"); System.out.println(resp.getBody()); System.out.println("-------------------------"); }
- HttpHeader에 AUTHORIZATION으로 Base64 encoding한 user id, pw를 넣고 HttpEntity로 API요청
- HttpEntity entity = new HttpEntity(null, headers);
- Body는 없으므로 null
result
TestRestTemplate
Test
1 2 3 4 5 6 7
@DisplayName("3. 인증 성공2") @Test void test_3(){ TestRestTemplate testClient = new TestRestTemplate("user1", "1111"); String resp = testClient.getForObject(greetingUrl(), String.class); assertEquals("hello", resp); }
- TestRestTemplate에서는 기본적으로 Basic토큰을 지원한다.
- ID, PW를 넣어서 RestTemplate를 실행하면 Basic Header를 넣어서 요청을 보낸다.
POST에서 Basic 인증
controller
1 2 3 4 5 6 7 8 9 10 11 12 13
@RestController public class HomeController { @GetMapping("/test") public String hello(){ return "hello"; } @PostMapping("/test") public String hello(@RequestBody String name){ return "hello " + name; } }
Test
1 2 3 4 5 6 7
@DisplayName("4. POST 인증") @Test void test_4(){ TestRestTemplate testClient = new TestRestTemplate("user1", "1111"); ResponseEntity<String> resp = testClient.postForEntity(testUrl(), "kms", String.class); assertEquals("hello kms", resp); }
result
- 위의 Test는 실패한다. 데이터가 전송되지 않는데 이유는 POST는 CSRF filter가 작동하기 때문이다.
config에서 csrf를 disable시키면 Test는 정상적으로 통과한다.
1 2 3 4 5 6
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().authorizeRequests().anyRequest().authenticated() .and() .httpBasic(); }
- 위와 같이 Rest Controller의 service를 SPA페이지에 대해서 제공하려고 하면 csrf를 disable시켜야 하는 상황이 발생한다. 하지만 해당 서버가 동시에 Web Page도 서비스하고 있어서 csrf를 enable시켜야 한다면 서로 다른 Login정책이 서버에 공존해야한다.
- 이런 문제를 해결하기 위해서는 web page 용, 모바일이나 SPA용 2개의 filter를 구성한 뒤 Basic 인증 filter를 order를 1로 web의 formLogin filter를 order2로 설정하면된다. 이때 Basic인증용 filter에서는 csrf를 disable시켜야한다.