[스프링 시큐리티]_02_시큐리티 설정 커스터마이징
스프링 시큐리티 2부: 시큐리티 설정 커스터마이징
- 웹 시큐리티 설정
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/hello").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}
- UserDetailsServie 구현
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-authentication-userdetailsservice
- PasswordEncoder 설정 및 사용
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#core-services-password-encoding
※ 프로젝트 : springbootsecurity2 - 의존성:web
(실습 준비)
/hello
,/my
에 대한 요청을 처리하는 컨트롤러 추가-
index.html, hello.html, my.html 페이지 추가
- Thymleaf로 뷰 렌더링
- security 의존성 추가
목표
- /hello : 모든 사용자가 접근 가능
- /my : 로그인한 사용자만 접근 가능
실습
1. 특정 URL만 인증을 받도록 웹 시큐리티 설정을 커스터마이징
WebSecurityConfigurerAdapter을 상속받은 웹 시큐리티 설정 생성
※ SecurityAutoConfiguration 자동설정을 쓰지않고 다음과 같이 직접 빈을 등록해서 거의 동일한 기능을 사용할 수 있는 우리만의 웹 시큐리티 설정을 사용할 수 있다.
(이렇게 직접 등록할 경우 스프링부트가 제공하는 SecurityAutoConfiguration은 사용되지 않는다.)
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
http.authorizeRequests()
.antMatchers("/", "/hello").permitAll() // '/'와 '/hello'는 모든 사용자에게 접근 허용
.anyRequest().authenticated() // 그 외의 나머지 모든 요청은 인증이 필요
.and()
.formLogin() // 폼 로그인 사용
.and()
.httpBasic(); // Basic 인증도 사용
}
⇒ 애플리케이션 실행 : /
와 /hello
는 바로 접근 가능하고, /my
는 애플리케이션이 제공해주는 유저 정보로 로그인 후 접근 가능
2. 스프링 시큐리티가 생성해주는 유저정보를 사용하지 않고, 직접 유저를 생성해서 사용
(준비) DB와 유저 정보를 처리하는 서비스 구현
- Account : 유저 정보 생성
- AccountRepository : 데이터 관리
- SpringBoot Data JPA, H2 사용
- AccountService
- 새로운 Account를 DB에 저장하는 메서드
- 유저 데이터 추가
UserDetailsServie 구현
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-authentication-userdetailsservice
보통 유저 정보를 관리하는 서비스 계층에 UserDetailsServie 인터페이스를 구현하도록 한다.
또는, 서비스와는 별개로 UserDetailsServie 인터페이스를 구현한 다른 별도의 클래스를 만들어도 상관없다.
⇒ 중요한 것은 UserDetailsServie 타입의 빈이 등록되어야 한다.
그래야, 스프링 부트가 랜덤으로 유저를 생성하지 않고, 우리가 만든 유저 정보로 로그인 처리가 된다.
- 로그인 처리 시 loadUserByUsername()가 호출된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
-
로그인 페이지에서 입력받은 username이 매개변수로 들어온다. 넘겨받은 username으로 DB의 실제 유저정보를 확인
-
DB 유저정보 내의 패스워드와 입력받은 패스워드를 확인해서 같으면 로그인 처리, 다르면 에러를 발생시킨다.
-
반환값은
UserDetails
인터페이스 구현체를 리턴해야 한다. (우리가 가진 유저 정보를 가지고 변환해서 리턴)
※ UserDetails 는 (본인의 애플리케이션) 서비스마다 제각각으로 구현되어있는 유저정보. (현재 Account와 같은) 그런 유저 정보들의 인터페이스이다. 핵심적인 유저 정보를 담고있는 인터페이스
이 인터페이스 기본 구현체를 스프링 시큐리티가 User
라는 이름으로 제공해준다.
- 3번째 인자로 authorities를 넘겨준다. (→ 메서드 자동 완성 기능 사용)
- ~한 권한을 가진 유저라는 것을 셋팅해준다.
@Service
public class AccountService implements UserDetailsService { // UserDetailsServie 인터페이스를 구현
@Autowired
private AccountRepository accountRepository;
public Account createAccount(String username, String password) {
Account account = new Account();
account.setUsername(username);
account.setPassword(password);
return accountRepository.save(account);
}
//로그인 처리 시 loadUserByUsername()가 호출된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//로그인 페이지에서 입력받은 username이 매개변수로 들어온다. 넘겨받은 username으로 DB의 실제 유저정보를 확인
//DB 유저정보 내의 패스워드와 입력받은 패스워드를 확인해서 같으면 로그인 처리, 다르면 에러를 발생시킨다.
Optional<Account> byUsername = accountRepository.findByUsername(username);
Account account = byUsername.orElseThrow(() -> new UsernameNotFoundException(username));
//반환값은 `UserDetails` 인터페이스 구현체를 리턴해야 한다. (우리가 가진 유저 정보를 가지고 변환해서 리턴)
return new User(account.getUsername(), account.getPassword(), authorities());
}
private Collection<? extends GrantedAuthority> authorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")); //~한 권한을 가진 유저라는 것을 셋팅
}
}
⇒ 스프링 시큐리티가 반환된 UserDetails
를 가지고 로그인 시 입력한 사용자 정보가 유효한지 인증과정 진행
로그인 시도하면?
⇒ 아직 PasswordEncoder 설정이 되어있지 않아서 로그인은 실패한다.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
3. PasswordEncoder 설정 및 사용
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#core-services-password-encoding
여러가지 패스워드 인코딩방식이 있다.
현재는 패스워드를 평문으로 저장하고있는데, 이렇게 중요정보를 평문으로 저장하는 것은 아주 심각한 보안 이슈에 걸리므로 반드시 인코딩을 해야한다.
※ 문서를 참고하면 여러가지 인코딩을 지원한다.
Example 21. DelegatingPasswordEncoder Encoded Passwords Example
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
스프링 버전이 올라감에 따라 {noop}
이 붙어있는 패스워드여야 noop인코더(아무것도 인코딩하지 않는 인코더)로 디코딩을 시도한다.
- 안좋은 예로 인코딩을 우회하는 방법 (절대 사용하지 말 것 ★)
다음과 같이 PasswordEncoder
를 noop인코더를 쓰도록 설정(빈으로 등록)하는 것이다.
그러면 스프링 시큐리티는 더이상 기본 패스워드 인코더가 아닌 NoOpPasswordEncoder
를 사용하게 된다.
// SecurityConfig
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
- 스프링 시큐리티가 권장하는 패스워드 인코더
Example 18. Create Default DelegatingPasswordEncoder
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
(1) 패스워드 인코더 설정
※ 스프링 시큐리티에 PasswordEncoder를 빈으로 등록 → 애플리케이션 컨텍스트 전반에 걸쳐서 모두 사용할 수 있는 빈
// SecurityConfig
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
(2) 유저 정보를 저장하기 전에도 이 인코더를 사용해서 패스워드를 인코딩한 값으로 저장하도록 해야함
@Autowired
private PasswordEncoder passwordEncoder;
public Account createAccount(String username, String password) {
Account account = new Account();
account.setUsername(username);
account.setPassword(passwordEncoder.encode(password)); // 인코딩한 값으로 저장
return accountRepository.save(account);
}
⇒ 로그
※ 절대로 로그로 찍으면 안 됨.
solar password: {bcrypt}$2a$10$jajC6Swp8hKuziWrDxUvYuOOrhvMfS1GRsmrG3uF05AMeO8D0hThS
⇒ Id : solar, PW : pass 로 로그인 성공