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가 그대로 노출된 것을 볼 수 있다.
위와 같은 문제로, 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); } } |
'개발 > 스프링 시큐리티' 카테고리의 다른 글
스프링 시큐리티 인 액션 - 인증 구현 (0) | 2023.09.04 |
---|---|
스프링 시큐리티 인 액션 - 암호 처리 (0) | 2023.09.01 |
스프링 시큐리티 인 액션 - 사용자 관리 (0) | 2023.08.30 |
스프링 시큐리티 인증 아키텍처 (0) | 2023.08.17 |
스프링 시큐리티 인 액션 - 오늘날의 보안 (0) | 2023.08.15 |