[Spring Security] HTTP Basic Auth

황승수 (Seungsu)·2023년 3월 25일
1

Spring Security

목록 보기
1/3

HTTP Basic Authentication

스프링 시큐리티 적용시 기본 인증 방식으로 지정되며, 인증 개념은 다음과 같다.

1. 자격 증명이 되지 않은 클라이언트 요청인 경우

서버는 401 Unauthorized 응답과 함께 WWW-Authentication 헤더를 기술해서 인증 방법을 전달

2. 증명된 클라이언트의 요청인 경우

정상적인 상태 코드와, 추가적인 인증 알고리즘에 대한 정보를 Authentication-info 헤더에 기술하여 응답

HTTP Basic 인증은 자격 증명의 기밀성이 보장되지 않는다. Base64 인코딩이 단지 전송의 편의를 위할 뿐 암호화나 해싱 방법이 아니므로 전송 중에 자격 증명을 가로채면 누구든 볼 수 있기 때문이다. 따라서 실제 개발 환경에서 사용할 일은 거의 없다.


HTTP Basic 인증을 이용한 엔드포인트 호출

스프링 시큐리티에서 사용자에 관한 세부 정보는 UserDetailsService 인터페이스를 구현하는 객체가 관리하며, 기본 구현에서 사용자 이름은 "user", 기본 암호는 UUID(Universally Unique Identifier) 형식으로 스프링 컨텍스트 로드 시점에 자동 생성된다.

HTTP Basic 접근 인증 방식은 클라이언트가 사용자 이름과 암호를 HTTP 헤더를 통해 보내기만 하면 된다. Authorization 헤더 값에 접두사 Basic을 붙이고 사용자 이름과 암호를 콜론으로 연결한 문자열을 Base64 인코딩하고 붙여 요청한다.


먼저 테스트를 위한 간단한 엔드포인트를 생성한다.

@RestController
public class SecurityController {
    @GetMapping("/")
    public String Test() {
        return "Ok!";
    }
}

그 다음 curl 방식으로 엔드포인트를 호출한다.

헤더에 붙일 Base64 인코딩 값을 얻기 위해 터미널에 다음 명령어를 입력한다.
아래 UUID는 시큐리티에서 자동 생성해준 값을 이용한다.

echo -n user:UUID | base64

요청 헤더에 인코딩된 문자열을 담아 엔드포인트 호출하면 정상적으로 응답을 받을 수 있다.

curl -H "Authorization: Basic {credentials}" http://localhost:8080
OK!

만약 postman을 이용한다면 Auth 탭의 Basic Auth를 선택하고 username과 password를 각각 user, UUID로 채워넣어 요청하거나 혹은 직접 header에 인코딩된 문자열을 위 방식으로 담아 보내면 정상적으로 서버로부터 응답을 받을 수 있다!


기본 구성 재정의

구성 방법

  • UserDetailsService와 PasswordEncoder를 각각 빈으로 등록
  • WebSecurityConfigurerAdapter의 configure 메서드를 오버라이딩

스프링 시큐리티의 기본 설정을 재정의하여 HTTP Basic 인증을 시도해보자.
인증에 이용되는 UserDetailsService와 PasswordEncoder를 재구성하여 스프링 시큐리티에서 생성해주는 기본 유저가 아닌 자체 관리 자격 증명을 인증에 이용할 수 있다.

config 패키지 아래 시큐리티 설정 클래스를 하나 생성한다.
자격 증명(사용자 이름, 암호)이 있는 사용자를 UserDetailsService에서 관리하도록 추가하고, UserDetailsService와 주어진 암호를 검증하는 PasswordEncoder 각각을 빈으로 등록한다.

@Configuration
public class ProjectConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();

        UserDetails user = User.withUsername("seungsu")
                .password("1111")
                .authorities("read")
                .build();

		// userDetailsService에서 관리하도록 사용자 추가
        userDetailsService.createUser(user);    

        return userDetailsService;
    }

    /* PasswordEncoder를 컨텍스트에 추가 */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 암호에 암호화나 해시를 적용하지 않고 일반 텍스트처럼 처리해주는 인스턴스
        return NoOpPasswordEncoder.getInstance();
    }
}

위 코드에서는 복잡성을 최소화시키고 작동 원리에 집중하기 위해 InmemoryUserDetailsManager 구현과 NoOpPasswordEncoder 인스턴스를 사용한다. 여기서 InmemoryUserDetailsManager는 UserDetailsService와 다름없이 사용되고, 설정을 적용한 뒤에는 더이상 시큐리티의 기본 UUID 패스워드는 생성되지 않는다.


다른 방식으로 위와 같은 구성을 설정할 수 있다.
WebSecurityConfigurerAdapter 클래스의 오버로드된 세가지 다른 configure 메서드 중 AuthenticationManagerBuilder 형식의 매개 변수로 UserDetailsService와 PasswordEncoder를 설정할 수 있다.

코드는 다음과 같다. 두 구현 방식을 혼합하여 사용하지 않도록 주의하자.

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 모든 요청에 대해 인증을 요구하도록 지정 (디폴트)
        http
                .httpBasic()
            .and()
                .authorizeRequests()
                .anyRequest()
                .authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 인메모리 사용자 구성 및 PasswordEncoder 추가
        auth
                .inMemoryAuthentication()
                .withUser("seungsu")
                .password("1111")
                .authorities("read")
            .and()
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
}

curl 호출시 -u 옵션을 통해 사용자 이름과 패스워드를 전달하여 엔드포인트를 호출한다

curl -u seungsu:1111 http://localhost:8080
OK!

인증공급자 재정의

AuthenticationProvider는 인증 논리를 구현하고 사용자 관리와 암호 관리를 각각 UserDetailsService와 PasswordEncoder에 위임한다. 따라서 인증공급자를 재정의하여 맞춤 구성 인증 논리를 구현할 수 있다.


커스텀 인증 공급자 전체 구현

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    /* 인증 논리를 추가하는 메서드 */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        // 원래는 UserDetailsService 및 PasswordEncoder를 호출해서 사용자 이름과 암호를 검증한다
        if (username.equals("seungsu") && password.equals("1111")) {
            return new UsernamePasswordAuthenticationToken(username, password, List.of());
        }

        throw new AuthenticationCredentialsNotFoundException("Error in Authentication");
    }

    /* Authentication 형식의 구현을 추가하는 메서드 */
    @Override
    public boolean supports(Class<?> authenticationType) {
        return UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authenticationType);
    }
}

커스텀 인증공급자 구현 등록

구성 클래스의 configure(AuthenticationManagerBuilder auth) 메서드에서 AuthenticationProvider를 등록할 수 있다.

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomAuthenticationProvider authenticationProvider;
	
    // ...
	
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }
}

이제 인증 논리에 정의된 유일하게 인식된 사용자를 이용하여 엔드포인트를 호출할 수 있다.

curl -u seungsu:1111 http://localhost:8080
OK!

요약

  • 스프링 시큐리티를 의존성 추가하면 스프링 부트가 약간의 기본 구성을 제공한다. 개발자는 기본 구성을 재정의하여 애플리케이션에 맞는 환경을 구성한다.
  • User 클래스로 사용자를 정의할 수 있다. 이름, 암호, 권한을 가지며 권한은 애플리케이션에서 사용자가 수행할 수 있는 작업을 지정한다.
  • 스프링 시큐리티는 UserDetailsService의 간단한 구현인
    InMemoryUserDetailsManager를 제공한다. 이를 이용하여 애플리케이션의 메모리에서 사용자를 관리할 수 있다.
  • NoOpPasswordEncoder는 PasswordEncoder 인터페이스 구현체로서 암호를 일반 텍스트로 처리한다. 개발 단계에서 사용하며 운영 단계에는 적합하지 않다.
  • AuthenticationProvider 인터페이스를 이용하여 애플리케이션 맞춤 인증 논리를 구현할 수 있다.

0개의 댓글