이 글은 크레이그 윌즈의 "스프링 인 액션(5판)"을 읽고 간략히 정리한 글이다.
스프링 시큐리티 활성화
pom.xml에 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
첫번째는 스프링 부트 보안 의존성이고, 두번째는 보안 테스트 의존성이다.
그러면 위와 같이 기본 인증 대화상자가 나타난다. (어느 페이지를 접속하던 /login으로 리다이렉트)
(user / password는 무작위로 자동 생성)
보안 스타터를 프로젝트 빌드 파일에 추가만 했을 때 제공되는 기본 보안 구성
- 모든 HTTP 요정 경로는 인증authentication되어야 한다.
- 어떤 특정 역할이나 권한이 없다.
- 로그인 페이지가 따로 없다.
- 스프링 시큐리티의 HTTP 기본 인증을 사용해서 인증된다.
- 사용자는 하나만 있으며, 이름은 user다. 비밀번호는 암호화해 준다.
하지만 실제 애플리케이션에서 보안을 구축하려면 최소한 다음과 같은 기능이 필요하다.
- 스프링 시큐리티 인증 대화상자 대신 애플리케이션에서 제공하는 로그인 페이지로 인증한다.
- 다수의 사용자를 제공하며, 사용자로 등록할 수 있는 페이지가 있어야 한다.
- 서로 다른 HTTP 요청마다 서로 다른 보안 규칙을 적용한다. 홈페이지와 사용자 등록 페이지는 인증이 필요하지 않다.
스프링 시큐리티 구성
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers("/", "/**").access("permitAll")
.and().httpBasic();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user1")
.password("{noop}password1")
.authorities("ROLE_USER")
.and()
.withUser("user2")
.password("{noop}password2")
.authorities("ROLE_USER");
}
}
- SecurityConfig는 보안 구성 클래스인 WebSecurityConfigurerAdapter의 서브 클래스다.
- configure(AuthenticationManagerBuilder) 메서드는 사용자 인증정보를 구성하는 메서드다. 사용자 스토어를 이 메서드에서 구성한다.
- /design, /orders에서는 인증을 요구한다. ROLE_USER 권한을 가지고 있어야 한다.
- /, /**에서는 인증을 요구하지 않는다.
- 인메모리에 임의의 유저 정보를 저장한다.
여러명의 사용자 정보를 유지,관리하기 위해서는 사용자 스토어를 구성해야 한다.
스프링 시큐리티에서는 여러가지 사용자 스토어 구성방법을 제공한다.
- 인메모리 In-Memory 사용자 스토어
- JDBC 기반 사용자 스토어
- LDAP 기반 사용자 스토어
- 커스텀 사용자 명세 서비스
인메모리 사용자 스토어
auth.inMemoryAuthentication()
.withUser("user1")
.password("{noop}password1")
.authorities("ROLE_USER")
.and()
.withUser("user2")
.password("{noop}password2")
.authorities("ROLE_USER");
- inMemoryAuthentication() 메서드로 보안 구성 자체에 사용자 정보를 직접 지정할 수 있다.
- withUser()로 사용자를 구성할 수 있다. (username) / 비밀번호와 권한은 각각 password(), authorities()로 전달한다.
- 스프링5부터는 반드시 비밀번호를 암호화해야한다. password()를 호출해서 암호화하지 않으면 접근거부(403) 또는 500 에러가 발생한다.
- 인메모리 사용자 스토어는 테스트 목적이나 간단한 애플리케이션에는 편리하나, 사용자 정보의 추가와 변경이 쉽지 않다. 사용자를 추가, 삭제, 변경하려면 보안 구성 코드를 변경한 후 애플리케이션을 다시 빌드하고 배포, 설치해야 한다.
JDBC 기반의 사용자 스토어
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, enabled from users " +
"where username=?")
.authoritiesByUsernameQuery(
"select username, authority from authorities " +
"where username=?")
.passwordEncoder(new NoEncodingPasswordEncoder());
- jdbcAuthentication()을 통해 JDBC 기반 인증을 설정할 수 있으며, 이때 dataSource를 설정해야 한다.
- usersByUsernameQuery()와 authoritiesByUsernameQuery()를 통해 스프링 시큐리티에서 사용하는 기본 사용자 쿼리를 대체할 수 있다.
- 해당 쿼리에서 사용하는 테이블의 이름은 스프링 시큐리티의 기본 데이터베이스 테이블과 달라도 된다. 하지만 테이블이 갖는 열의 데이터 타입과 길이는 일치해야 한다. (username, password, enabled)
- passwordEncoder()는 스프링 시큐리티의 인터페이스를 구현하는 어떤 객체도 인자로 받을 수 있다. 모듈에 포함되어 있는 구현 클래스는 다음과 같다.
- BCrptPasswordEncoder : bcrypt 해싱 암호화
- NoOpPasswordEncoder : 암호화하지 않음
- Pbkdf2PasswordEncoder : PBKDF2를 암호화
- SCryptPasswordEncoder : scrypt를 해싱 암호화
- StandardPasswordEncoder : SHA-256을 해싱 암호화
LDAP 기반의 사용자 스토어
LDAP이란? (Lightweight Directory Access Protocol)
네트워크 상에서 조직이나 개인정보 혹은 파일이나 데이터를 찾아보는 것을 가능하게 만든 소프트웨어 프로토콜
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
.passwordCompare()
.passwordEncoder(new BCryptPasswordEncoder())
.passwordAttribute("userPasscode");
- ldapAuthentication()으로 LDAP 인증 설정
- userSearchFilter(), groupSearchFilter()는 기본 쿼리의 필터 제공, userSearchBase(), groupSearchBase()는 탐색에 대한 기준점을 제공한다.
- passwordCompare()는 비밀번호를 비교하는 방법으로 LDAP을 인증할 때 사용
- 비밀번호가 다른 속성에 있을 때 passwordAttribute()를 사용해 이를 지정할 수 있다.
사용자 인증 커스터마이징
코드가 많아서 따로 적지는 않고 간단히 방법만 기술
- 스프링 데이터 리퍼지터리를 사용한다.
- 사용자 정보를 저장하는 도메인 클래스를 만들고 리퍼지터리 인터페이스를 생성한다. (JPA)
- 스프링 시큐리티의 사용자 명세 서비스, UserDetailsService를 구현한다. (구현 클래스, 사용자 커스텀 명세 서비스 생성)
- SecurityConfig 파일에 UserDetailsService를 자동 주입한다.그리고 userDetailService() 메서드를 호출한다.
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
- 남은 것은 사용자 등록 페이지와 컨트롤러를 만드는 것.
웹 요청 보안 처리
요구사항 : 홈페이지, 로그인, 회원가입 페이지는 사용자 인증 없이 접근이 가능해야 하고, 타코 디자인과 주문 전 페이지는 사용자 인증이 필요하다.
http.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers("/", "/**").access("permitAll")
커스텀 로그인 페이지 경로를 지정한다.
.and()
.formLogin()
.loginPage("/login")
로그아웃시 이동할 URL을 지정한다.
.and()
.logout()
.logoutSuccessUrl("/")
CSRF 공격 방어
.and()
.csrf();
최종
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers("/", "/**").access("permitAll")
.and()
.formLogin()
.loginPage("/login")
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.csrf();
}
사용자 인지
Order Entity와 User Entity를 연관시키기 위해 Order 쪽에 User 추가 (@ManyToOne)
사용자가 누구인지 결정하는 방법
- Principal 객체를 컨트롤러 메서드에 주입한다.
- Authentication 객체를 컨트롤러 메서드에 주입한다.
- SecurityContextHolder를 사용해서 보안 컨텍스트를 읽는다.
- @AuthenticationPrincipal 애노테이션을 메서드에 지정한다.
코드 예시 (@AuthenticationPrincipal)
@GetMapping("/current")
public String orderForm(@AuthenticationPrincipal User user, @ModelAttribute Order order) {
...
}
- @AuthenticationPrincipal의 장점은 타입 변화이 필요 없고 Authentication과 동일하게 특정 코드만 갖는다는 것.
실제로 프로젝트를 생성하고 스프링 시큐리티를 적용해서 회원기능을 구현하려고 하면 잘 안 된다. 책이 2020년 5월에 출판되서 코드가 낡았.. Spring Security 5.7.0-M2 버전에서 WebSecurityConfigurerAdapter가 Deprecated 되었기 때문이다. (이 말인 즉슨 5.4버전에선 잘 된다는 소리다.)
그에 대한 대처 방법은 다음과 같다.
이전 코드
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
return new ShopmeUserDetailsService();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login").permitAll()
.antMatchers("/users/**", "/settings/**").hasAuthority("Admin")
.hasAnyAuthority("Admin", "Editor", "Salesperson")
.hasAnyAuthority("Admin", "Editor", "Salesperson", "Shipper")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.usernameParameter("email")
.permitAll()
.and()
.rememberMe().key("AbcdEfghIjklmNopQrsTuvXyz_0123456789")
.and()
.logout().permitAll();
http.headers().frameOptions().sameOrigin();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**");
}
}
수정한 코드
@Configuration
public class SecurityConfiguration {
@Bean
public UserDetailsService userDetailsService() {
return new ShopmeUserDetailsService();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login").permitAll()
.antMatchers("/users/**", "/settings/**").hasAuthority("Admin")
.hasAnyAuthority("Admin", "Editor", "Salesperson")
.hasAnyAuthority("Admin", "Editor", "Salesperson", "Shipper")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.usernameParameter("email")
.permitAll()
.and()
.rememberMe().key("AbcdEfghIjklmNopQrsTuvXyz_0123456789")
.and()
.logout().permitAll();
http.headers().frameOptions().sameOrigin();
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**");
}
}
자세한 것은 다음 링크 참고.
https://www.codejava.net/frameworks/spring-boot/fix-websecurityconfigureradapter-deprecated
Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated
DetailsWritten by Nam Ha Minh Last Updated on 01 June 2022 | Print Email In this short article, I’d like to share how to get rid of the warning saying that “The type WebSecurityConfigurerAdapter is deprecated” in Spring-based application w
www.codejava.net
'공부 > Spring' 카테고리의 다른 글
[스프링 인 액션] Chapter 5 구성 속성 사용하기 (0) | 2022.07.03 |
---|---|
[스프링 인 액션] Chapter 3 데이터로 작업하기 (0) | 2022.05.08 |
[스프링 인 액션] Chapter 2 웹 어플리케이션 개발하기 (0) | 2022.04.18 |
[스프링 인 액션] Chapter 1 스프링 시작하기 (0) | 2022.04.08 |
[스프링 프레임워크 Core] 스프링 IoC 컨테이너와 빈 (0) | 2020.06.06 |