[스프링 시큐리티]_02_시큐리티 설정 커스터마이징

3 minute read

스프링 시큐리티 2부: 시큐리티 설정 커스터마이징

  1. 웹 시큐리티 설정
@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();
   }
}
  1. UserDetailsServie 구현

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-authentication-userdetailsservice

  1. 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;
}
  1. 로그인 페이지에서 입력받은 username이 매개변수로 들어온다. 넘겨받은 username으로 DB의 실제 유저정보를 확인

  2. DB 유저정보 내의 패스워드와 입력받은 패스워드를 확인해서 같으면 로그인 처리, 다르면 에러를 발생시킨다.

  3. 반환값은 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인코더(아무것도 인코딩하지 않는 인코더)로 디코딩을 시도한다.

  1. 안좋은 예로 인코딩을 우회하는 방법 (절대 사용하지 말 것 ★)

다음과 같이 PasswordEncoder를 noop인코더를 쓰도록 설정(빈으로 등록)하는 것이다.

그러면 스프링 시큐리티는 더이상 기본 패스워드 인코더가 아닌 NoOpPasswordEncoder를 사용하게 된다.

// SecurityConfig 
@Bean
public PasswordEncoder passwordEncoder() {
  return NoOpPasswordEncoder.getInstance();
}
  1. 스프링 시큐리티가 권장하는 패스워드 인코더

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 로 로그인 성공