06. 사용자 및 경로 관리
- -
이제 본격적으로 Spring Security를 사용해보자.
사용자 관리와 UserDetailService
PasswordEncoder
Spring Security의 PasswordEncoder는 비밀번호가 안전하게 저장되도록 단방향 변환을 수행하는데 사용된다.
Spring Security의 기본 PasswordEncoder로 4.X 까지는 NoOpPasswordEncoder(비밀번호를 암호화 하지 않아도 괜찮음)가 사용되다가 5.X 부터는 BCryptPasswordEncoder(BCrypt 해시 함수로 해싱)가 사용된다.
하지만 기존의 애플리케이션들이 한번에 다 바뀔 수 없기 때문에 중간에 DelegatingPasswordEncoder를 도입하게 되었다. 그리고 가장 쉽게 처리하는 방식은 PasswordEncoderFactories를 활용하는 방법이다. 이 방법은 BCrypt 방식의 해싱을 사용한다.
@Bean
public static PasswordEncoder passwordEncoder() {
//return new BCryptPasswordEncoder();
//return new DelegatingPasswordEncoder("bcrypt", Map.of("bcrypt",new BCryptPasswordEncoder()));
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
메모리 기반의 인증 정보 관리
사용자 인증 처리를 위해서는 AuthenticationManager가 동작하며 이에 대한 구현체는 ProviderManager이다. ProviderManager는 여러개의 AuthenticationProvider들을 관리하는데 그 중 DaoAuthenticationProvider는 UserDetailsService를 사용한다.
UserDetailsService는 in-memory, jdbc 기반으로 인증을 처리할 수 있다. 일단 간단히 in-memory 기반으로 다음과 같은 3명의 사용자를 만들어보자.
@Bean
public UserDetailsService userDetailsService(PasswordEncoder pe) {
UserDetails user = User.builder().username("user").password(pe.encode("1234"))
.roles("USER").build();
UserDetails staff = User.builder().username("staff").password(pe.encode("123"))
.roles("STAFF").build();
UserDetails admin = User.builder().username("doding").password(pe.encode("1234"))
.roles("ADMIN").build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(user, staff);
manager.createUser(admin);
return manager;
}
사용자가 잘 생성되었는지 확인해보자.
@Import(SecurityConfig.class)
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
public class UserDetailsTest {
@Autowired
UserDetailsService userDetailsService;
@Autowired
PasswordEncoder passwordEncoder;
@Test
void 사용자생성_테스트() {
UserDetails user = userDetailsService.loadUserByUsername("doding");
Assertions.assertNotNull(user);
Assertions.assertTrue(passwordEncoder.matches("1234", user.getPassword()));
Assertions.assertTrue(user.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")));
}
}
SecurityFilterChain 빈과 URL 별 접근 제어
SecurityFilterChain
SecurityFilterChain은 HttpSecurity 타입의 빈을 주입 받아서 사용한다. HttpSecurity#authorizeHttpRequests는 경로 별로 인증 필요 여부 및 필요한 권한 등에 대한 설정이 가능하다. 이 경로가 secured resource가 된다. HttpSecurity#authorizeHttpRequests 에는 Customizer가 필요한데 일반적으로 lambda 식으로 작성한다. lambda 내부에서는 AuthorizationManagerRequestMatcherRegistry를 이용해 보호할 경로와 인증/인가 관련 내용을 매핑해서 등록한다. 내용을 작성할 때는 builder pattern을 이용한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/secured/user").authenticated() // user는 인증만 필요
.requestMatchers("/secured/staff").hasRole("STAFF") // STAFF role 필요
.requestMatchers("/secured/admin").hasRole("ADMIN") // ADMIN role 필요
.anyRequest().permitAll()); // 이외의 경로는 그냥 통과
http.formLogin(Customizer.withDefaults()); // 기본 form login 활용
http.logout(Customizer.withDefaults()); // 기본 logout 활용
return http.build();
}
URL의 패턴을 지정할 때는 일반적으로 Ant 표현식을 따르며 필요하다면 RequestMatcher 타입으로 정규표현식을 사용할 수도 있다.
- ?: 1개의 문자 매칭
- *: 단일 경로 세그먼트 내의 0개 이상의 문자 매칭
- **: 0개 이상의 경로 세그먼트 매칭
- {pattern1, pattern2}: 여러 패턴 중 하나
- [abc]: a, b, c 중 하나의 문자
.requestMatchers("/secured/**").authenticated() // /secured로 시작하는 모든 경로
.requestMatchers("/admin/*").hasRole("ADMIN") // /admin 바로 아래 경로만
.requestMatchers("/api/v1/**/*.json").hasRole("API_USER") // /api/v1 아래의 .json으로 끝나는 모든 경로
.requestMatchers("/public/??/*.html").permitAll() // /public/aa/x.html 같이 두 글자로 된 디렉토리
.requestMatchers("/images/*.{jpg,png,gif}").permitAll() // 이미지 확장자 패턴
.requestMatchers("/user/profile/*/edit").hasRole("USER") // /user/profile/아무값/edit
.requestMatchers("/docs/**/readme.txt").permitAll() // /docs 아래 모든 깊이의 readme.txt
.requestMatchers("/api/{version}/users/{userId}").authenticated() // 경로 변수 사용
.requestMatchers("/static/[abc]/*.js").permitAll() // a, b, c로 시작하는 디렉토리의 js 파일
다음은 role 설정에 사용되는 주요 메서드들이다.
메서드 명 | 내용 |
hasRole(role_name) | 사용자의 role이 role_name과 일치하는지 확인. ex) hasRole("admin") |
hasAnyRole(role_name, ...) | 사용자의 role이 항목 중에 일치하는 것이 있는지 확인. ex) hasAnyRole("admin", "staff") |
permitAll() | 모든 사용자의 접근을 허가한다. |
denyAll() | 모든 사용자의 접근을 금지한다. |
authenticated() | role과 상관 없이 인증 된 경우라면 허용한다. |
경로 설정 주의 사항
URL 단위의 접근 제어 설정 시 주의 사항은 순서대록 설정을 적용해 보면서 true가 반환되면 중지한다는 점이다. 예를 들어 다음의 설정을 살펴보자.
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/secured/**").authenticated() // secured/** 는 인증만 받으면 OK
.requestMatchers("/secured/admin").hasRole("ADMIN") // 인증만 받았다면 무사 통과
.requestMatchers("/secured/staff").hasRole("STAFF") // 인증만 받았다면 무사 통과
.anyRequest().permitAll());
위의 경우 /secured/**는 단지 인증만 받으면 사용할 수 있게 설정되었다. 따라서 /secured/admin, /secured/manager에 대한 요청도 이 부분에서 허용되어버린다. 따라서 admin, manager에서 특별한 권한 체크가 발생하지 않는다. 따라서 접근 제어의 순서는 세부적인 사항을 먼저 처리하고 뭉뚱구려서 처리하는 것은 마지막에 수행해야 한다.
또한 anyRequest() 이후에 다른 경로 설정은 불가하다.
동작 확인
staff/1234로 로그인하면 어떻게 동작할 지 생각해보고 테스트 해보자. staff는 계층적 role에 의해 USER와 STAFF role을 가지고 있는 상태다. 따라서 index, user, staff는 문제 없이 사용할 수 있고 admin은 403 forbidden을 발생시킨다.
단위테스트
인증 없는 상태에서 여러 엔드포인트 접근 테스트
@Test
void 인증없는_상태에서_여러_엔드포인트_접근_테스트() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isOk());
// AuthenticationEntryPoint에 의해 인증이 필요한 페이지는 /login 페이지로 리다이렉트 되어야 한다.
mockMvc.perform(MockMvcRequestBuilders.get("/secured/user"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.redirectedUrl("http://localhost/login"));
}
STAFF 권한자의 다양한 엔드포인트 접근 테스트
가상의 권한으로 로그인한 사용자를 설정하기 위해 @WithMockUser 애너테이션을 사용한다.
@Test
@WithMockUser(roles = "STAFF")
void STAFF_권한이_있는사용자가_권한별_페이지에_접근하는경우_확인() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/secured/staff"))
.andExpect(MockMvcResultMatchers.status().isOk());
mockMvc.perform(MockMvcRequestBuilders.get("/secured/admin"))
.andExpect(MockMvcResultMatchers.status().isForbidden());
}
'Spring security > 01.Security' 카테고리의 다른 글
08. JDBC를 이용한 인증 (0) | 2022.11.22 |
---|---|
07. 로그인/로그아웃 커스터마이징 (0) | 2022.11.18 |
05. 프로젝트 구성과 초기 동작 (0) | 2020.08.20 |
04. Servlet Authorization Architecture (0) | 2020.07.16 |
03. Servlet Authentication Architecture (0) | 2020.07.15 |
소중한 공감 감사합니다