HTTP Basic

기본 구성해보기

openssl req -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365

프로젝트 생성

3가지 Starter 추가

 

컨트롤러 추가

생성 간단한 RestController 만든다.

 

 

동작확인

아무 것도 안한 상태에서 실행하면,

스프링 시큐리티가 기본 계정을 생성해준다.

ID : user  고정

PW : UUID , 실행 마다 변경된다.

 

여기서 API 툴로 API Tester를 사용했다.

위와 같이 401 코드를 반환한다.

 

Add authorization 눌러 아래와 같이 입력한다.

pw 스프링 실행 출력된 값이다.

 

이제 성공한다.

 

Basic 뒤에 값을 복사해서 아래 사이트에 붙여넣고 Decode 해보면,

ID, PW 그대로 노출된 것을 있다.

 

https://www.base64decode.org/

위와 같은 문제로, HTTP Basic 실제 현업에서 사용할 일이 없다.

 

흐름은 UsernamePasswordAuthenticationFilter 추가적으로 첨가한 그림이다.

그림을 보면, 모든 관계가 Interface 통해 이뤄지고 있다.

, 어떤 Filter 인증을 하던 간에 위와 같다.

 

AuthenticationProvider 의존하는 인터페이스는 다음과 같은 책임을 가지고 있다.

UserDetailsSerivce 주어진 username(id) 저장소에 존재하는지 확인

PasswordEncoder 주어진 자격증명(password) 자장소에 저장된 값과 같은지

 

 

기본구성 재정의

UserDetailsService, PasswordEncoder

 

@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
        
        //책임_저장소에서 해당 유저가 존재하는지 확인 및 유저 권한 조회
        @Bean
        UserDetailsService userDetailsService() {
                var userDetailsService = new InMemoryUserDetailsManager();        
                userDetailsService.createUser(
                                User.withUsername("scott")
                                        .password("tiger")
                                        .passwordEncoder(pw -> passwordEncoder().encode(pw))
                                        .authorities(List.of())
                                        .build()
                                );
                
                return userDetailsService;
        }
        //책임_Password를 해싱, 입력된 Password를 해싱된 password와 비교하여 일치하는지 확인
        @Bean
        PasswordEncoder passwordEncoder() {
                return new BCryptPasswordEncoder();
        }
        
        @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
                http
                        .authorizeHttpRequests(authorize -> authorize
                                .anyRequest().authenticated() //모든 경로는 인증돼야 함
                        );
                http.httpBasic(Customizer.withDefaults());
                
                return http.build();
        }
}

변경된 구성 동작 확인

 

주의사항

UserDetailsService 재정의할 반드시 PasswordEncoder 재정의해야한다.

PasswordEncoder password 검증하려고, 하는데 없기 때문이다.

정확히는 아무 PasswordEncoder 등록되지 않을 등록되는 더미 UnmappedIdPasswordEncoder 예외를 던진다.

 

커스텀 AuthenticationProvider 해보기

 

 



@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{
        Logger logger = LoggerFactory.getLogger(getClass());
        
        @Override
        public Authentication authenticate(Authentication authentication)
                        throws AuthenticationException {
                
                String username = authentication.getName();
                String password = authentication.getCredentials().toString();
                
                logger.info("username = " + username + ", password = " + password);
                
                if(!"scott".equals(username) || ! "tiger".equals(password)) {
                        throw new UsernameNotFoundException("인증 실패");
                }
                
                return new UsernamePasswordAuthenticationToken(username, password, List.of());
        }


        @Override
        public boolean supports(Class<?> authentication) {
                return UsernamePasswordAuthenticationToken.class
                                .isAssignableFrom(authentication);
        }
}
//SecurityConfig


        @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
                http
                        .authorizeHttpRequests(authorize -> authorize
                                .anyRequest().authenticated() //모든 경로는 인증돼야 함
                        );
                //인증 활성화
                http.httpBasic(Customizer.withDefaults());
                
                //커스텀 인증 제공자 등록
                http.authenticationProvider(new CustomAuthenticationProvider());
                
                return http.build();
        }

동작 확인

 

커스텀 구성에서 PasswordEncoder UserDetailsService 사용하지 않았다.

실제로는 이렇게 구성하면 안된다.

프레임워크에 설계된 아키텍처 그대로를 지켜야 한다.



@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{
        private final Logger logger = LoggerFactory.getLogger(getClass());
        @Autowired
        private PasswordEncoder encoder;
        @Autowired
        private UserDetailsService userDetailsService;


        @Override
        public Authentication authenticate(Authentication authentication)
                        throws AuthenticationException {
                
                String username = authentication.getName();
                String password = authentication.getCredentials().toString();
                
                logger.info("username = " + username + ", password = " + password);
                
//                simpleAuthentication(username, password);
                
                UserDetails user = userDetailsService.loadUserByUsername(username);
                if(user == null || !encoder.matches(password, user.getPassword())) {
                        throw new UsernameNotFoundException("인증 실패");
                }
                return new UsernamePasswordAuthenticationToken(username, password, List.of());
        }


        private void simpleAuthentication(String username, String password) {
                if(!"scott".equals(username) || ! "tiger".equals(password)) {
                        throw new UsernameNotFoundException("인증 실패");
                }
        }


        @Override
        public boolean supports(Class<?> authentication) {
                return UsernamePasswordAuthenticationToken.class
                                .isAssignableFrom(authentication);
        }
}

테스트 코드



@SpringBootTest
@AutoConfigureMockMvc
class SpringSecurity01ApplicationTests {
        
        @Autowired
        private MockMvc mvc;
        
        @Autowired
        private PasswordEncoder encoder;
        
        @Test
        void HTTP_BASIC_인증실패() throws Exception {
                mvc.perform(get("/resource"))
                        .andExpect(status().isUnauthorized()); // 401
        }
        @Test
        void HTTP_BASIC_인증성공() throws Exception {
                mvc.perform(get("/resource")
                                        .with(httpBasic("scott", "tiger")))
                                .andExpect(content().string("resource"))
                                .andExpect(status().isOk());
        }
        @Test
        @WithMockUser(username = "root", password = "root")
        void HTTP_BASIC_테스트계정_인증성공() throws Exception {
                mvc.perform(get("/resource")
                                .with(httpBasic("root", "root")))
                .andExpect(content().string("resource"))
                .andExpect(status().isOk());
        }
        
        @Test
        void PasswordEncoder_테스트() {
                String password = "12345";
                assertThat(encoder.matches(password, encoder.encode(password))).isEqualTo(Boolean.TRUE);
        }
}

 

spring-security-01-httpbasic.zip
0.07MB

 

+ Recent posts