2024. 3. 8. 01:53ㆍspring/SECURITY
과제
대다수 웹 애플리케이션에는 특별히 보안에 신경 써야 할 민감한 URL이 있다. 이런 URL에 미인가 외부 유저가 제약 없이 접근할 수 있도록 보안하라
해결책
스프링 시큐리티는 HTTP 요청에 서블릿 필터를 적용해 보안을 처리한다.
AbstractSecurityWebApplicationInitializer라는 베이스 클래스를 상속하면 편리하게 필터들 등록하고 구성 내용이 자동 감지되게 할 수 있다.
WebSecurityConfigurerAdapter라는 구성 어댑터에 준비된 다양한 configure() 메서드를 이용하면 웹 애플리케이션 보안을 쉽게 구성할 수 있다. 간단하고 일반적인 보안 요건은 구성 파일을 건드리지 않아도 아래 기본 보안 설정을 바로 적용할 수 있다.
*폼 기반 로그인 서비스 : 유저가 애플리케이션에 로그인하는 기본 폼 페이지를 제공한다.
*HTTP 기본 인증 : 요청 헤더에 표시된 HTTP 기본 인증 크레덴셜을 처리한다.
*로그아웃 서비스 : 유저를 로그아웃시키는 핸들러를 기본 제공한다.
*익명 로그인 : 익명 유저도 주체를 할당하고 권한을 부여해서 마치 일반 유저처럼 처리한다.
*서블릿 API 연계 : HttpServletRequest.isUserInRole(), HttpServletRequest.getUserPrincipal() 같은 표준 API를 이용해 웹 애플리케이션에 위치한 보안 정보에 접근한다.
*CSRF : 사이트 간 요청 위조 방어용 토큰을 생성해 HttpSession에 넣는다.
*보안헤더 : 보안이 적용된 패키지에 대해서 캐시를 해제하는 식으로 XSS 방어, 전송 보안, X-Frame 보안 기능을 제공한다.
이러한 보안 서비스를 등록하면 특정 접근 권한을 요구하는 URL 패턴을 지정할 수 있다.
풀이
우선, 스프링 시큐리티가 사용하는 필터를 등록한다.
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
}
AbstractSecurityWebApplicationInitializer 생성자는 하나 이상의 구성 클래스를 인수로 받아 보안 기능을 가동한다.
스프링 시큐리티를 웹/서비스 레이어의 구성 클래스에 설정해도 되지만 SecurityConfig로 나눠 구성하는 편이 좋다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
public class WebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
System.out.println("chapter07");
// Root Config 설정 -s
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(RootConfig.class); //등록
rootContext.register(SecurityConfig.class);
ContextLoaderListener listener = new ContextLoaderListener(rootContext);
servletContext.addListener(listener);
// Root Context Config 설정 -e
// DispatcherServlet 설정 -s
// 1. DispatcherServlet WebApplicationContext 객체 생성
AnnotationConfigWebApplicationContext webContext
= new AnnotationConfigWebApplicationContext();
webContext.register(ServletConfig.class);
// 2. DispatcherServlet 객체 생성 및 추가
DispatcherServlet dispatcherServlet = new DispatcherServlet(webContext);
ServletRegistration.Dynamic servlet
= servletContext.addServlet("dispatcher", dispatcherServlet);
// 3. 서블릿 매핑 및 부가 설정
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
// DispatcherServlet 설정 -e
// Filter 설정 -s
FilterRegistration.Dynamic filter
= servletContext.addFilter("encodingFilter", CharacterEncodingFilter.class);
filter.setInitParameter("encoding", "UTF-8");
filter.addMappingForServletNames(null, false, "dispatcher");
// Filter 설정 -e
}
}
애플리케이션 배포 후 접속하면 스프링 시큐리티가 기본 제공하는 로그인 페이지가 나타난다.
URL 접근 보안하기
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.authorizeHttpRequests()
// .anyRequest().authenticated()
// .and()
// .formLogin().and()
// .httpBasic();
}
}
위 메서드는 기본적으로 anyRequest().authenticated() 해서 매번 요청이 들어올 때마다 인등을 받도록 강제한다.
또 HTTP 기본 인증 및 폼 기반 로그인 기능은 켜있는게 디폴트다.
- 보안규칙 작성
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/** 데이터 액세스로 권한을 인증하고 할 경우**/
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
/** 데이터 액세스로 권한을 인증하지 않고 하드코딩해서 시큐리티만 확인 할 경우**/
auth.inMemoryAuthentication()
.withUser("userId").password("userPwd").authorities("USER")
.and()
.withUser("adminId").password("adminPwd").authorities("ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.antMatchers("/todos*").hasAuthority("USER")
.antMatchers(HttpMethod.DELETE, "/todos*").hasAuthority("ADMIN")
.and()
.formLogin()
.and()
.csrf().disable();
}
}
configure (HttpSecurity http) 메서드를 오버라이드하면 더 정교한 인가 규칙을 적용할 수 있다.
URL 접근 보안은 authorizeRequests() 부터 시작되며 여러 가지 매처를 이용해 규칙을 정할 수 있다. 예제에서는 유저가 어떤 권한을 가져야 하는지 andMatcher로 매치 규칙을 지정한다.
(URL 패턴 끝의 와일드카드를 빼면 쿼리 매개변수가 있는 URL은 걸리지 않는다.)
이로써 /todos로 시작하는 URL은 USER 권한 유저만 접근할 수 있고 HTTP 메서드가 DELETE인 요청은 ADMIN 권한 유저만 실행할 수 있게 보안 설정을 했다.
인증 서비스는 configure(AuthenticationManagerBuilderAuth auth) 메서드를 오버라이드해서 구성한다.
/** 데이터 액세스로 권한을 인증하고 할 경우(s)**/
************************************************************************************
UserDetailService.java
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.spring.study.chapter07.domain.user.entity.User user = userRepository.findByName(username);
return User.builder()
.username(username)
.password(user.getPassword())
.authorities(
user.getRoleTypes().stream()
.map(RoleType::getName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toUnmodifiableSet())
)
.build();
}
}
************************************************************************************
User.java
@SequenceGenerator(
name = "USER_SEQ_GENERATOR",
sequenceName = "USER_SEQ"
)
@ToString
@NoArgsConstructor
@Entity
@Table(name = "USER2")
@Getter @Setter
public class User {
@Id @GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "USER_SEQ_GENERATOR"
)
private Long id;
@Column(unique = true)
private String userName;
private String password;
@Convert(converter = UserRoleConverter.class)
private Set<RoleType> roleTypes;
@Builder
public User(Long id, String userName, String password, Set<RoleType> roleTypes) {
this.id = id;
this.userName = userName;
this.password = password;
this.roleTypes = roleTypes;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return Objects.equals(getId(), user.getId()) &&
Objects.equals(getUserName(), user.getUserName()) &&
Objects.equals(getPassword(), user.getPassword()) &&
getRoleTypes() == user.getRoleTypes();
}
@Override
public int hashCode() {
return Objects.hash(getId(), getUserName(), getPassword(), getRoleTypes());
}
}
************************************************************************************
UserRoleConverter.java
@Slf4j
public class UserRoleConverter implements AttributeConverter<Set<RoleType>, String> {
@Override
public String convertToDatabaseColumn(Set<RoleType> roleTypes) {
if (roleTypes.isEmpty()) return null;
return roleTypes.stream()
.map(RoleType::getName)
.collect(Collectors.joining(","));
}
@Override
public Set<RoleType> convertToEntityAttribute(String dbData) {
if (dbData == null) return Collections.emptySet();
try {
return RoleType.fromCode(dbData);
} catch (IllegalArgumentException e) {
log.error("failure to convert cause unexpected code[{}]", dbData, e);
}
return Collections.emptySet();
}
}
************************************************************************************
/** 데이터 액세스로 권한을 인증하고 할 경우(e)**/
CSRF 공격 방어
- csrf란?
https://www.imperva.com/learn/application-security/csrf-cross-site-request-forgery/
CSRF 방어 기능이 작동하는 상태에서 스프링 시큐리티는 CsrfTokenRepository 인터페이스의 구현체를 이용해 토큰 값을 생성/보관하는 CsrfFilter를 보안 필터 목록에 추가한다.
기본 구현체인
HttpSessionCsrfTokenRepository 클래스는 그 이름 그대로 생성한 토큰을 HttpSession에 저장하고 CookiesrfTokenRepository 클래스는 쿠키에 토큰 정보를 보관한다.
아래 예제는 기본 구현체인 HttpSessionCsrfTokenRepository를 명시적으로 구성한 예제 코드이다.
@Override
protected void configure(HttpSecurity http) throws Exception {
HttpSessionCsrfTokenRepository repo = new HttpSessionCsrfTokenRepository();
repo.setSessionAttributeName("csrf_token");
repo.setParameterName("csrf_token");
http.csrf().csrfTokenRepository(repo)
}
CSRF 방어 기능이 켜진 상태에서 애플리케이션에 로그인 후 할 일을 완료 또는 삭제하려고 시도하면 CSRF 토큰이 없기 때문에 실패한다. 콘텐트를 수정하는 요청을 전송할 때 CSRF 토큰을 서버에 재전송하면 된다.
HttpSessionCsrfTokenRepository는 (구성하지 않고 기본적으로) _csrf라는 세션값으로 토큰을 보관한다.
폼에서는 _csrf의 parameterName, token 속성을 이용해 input 태그를 렌더링할 수 있다.
할 일을 완료하고 삭제하는 각 폼에 아래 한 줄을 각각 넣는다.
<input type="hidden" name="${csrf.parameterName}" value="${csrf.token}" />
다시 폼을 전송해보면 토큰과 함께 전송되고 정상 처리 된다.
스프링 MVC 폼 태그를 사용하면 CSRF 토큰은 자동으로 추가된다. 이 부분은 스프링이 CsrfRequestDataValueProcessor 클래스를 등록하고 이 클래스가 토큰을 알아서 폼에 넣어주기 때문이다.