Home Basic 토큰 인증
Post
Cancel

Basic 토큰 인증

Basic 토큰 인증

BasicAuthenticationFilter

  • 기본적으로 로그인 페이지를 사용할 수 없는 상황에서 사용한다. (form login사용 불가능 한경우)
    • SPA 페이지(react, angular, vue.. )
      • SPA의 경우 server에서 login form을 만들어서 사용하는 것이 아닌 Client Browser에서 javascript를 통해 login form을 만들어 사용한다.
    • 브라우저 기반의 모바일 앱(브라우저 기반의 앱, ex: inoic)
  • 설정 방법

    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할 수 있기 때문에 보안상 좋지 않음
  • 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를 출력한다.

    result

인증 사용 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

    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시켜야한다.
This post is licensed under CC BY 4.0 by the author.