[Spring Boot] 12. Spring Security

  • 웹개발의 막보스 스프링 시큐리티는 매우 지능적이며 간단한 선언으로 작동하므로
    대량의 코드를 손쉽게 절약
  • 몇십 줄만으로 대형 서비스사와 비슷한 수준의 보안을 유지
  • ACEGI라는 이름으로 시작하고 10년째 서비스

보안

  • Authentication 후 Authorization
  • 인증 후 권한 부여, 비슷한 단어가 아니니 구분 확실히
  • 종류
    • Credential(자격)기반 인증
      • 웹의 대부분의 인증방식
      • 대개 사용자명과 비밀번호를 입력받고 저장된 정보와 일치하는지 확인
      • 스프링 시큐리티가 구현할 인증
    • Two facotor Authentication
      • 한번에 2가지 방식의 인증
      • 별거 아니지만 하나의 인증이 추가 되면 프로그램 변화할 부분이 많음
    • physical Authentication
      • 지문인식, 키삽입
      • 웹을 벗어낫지만 가장 효과적인 보안 수단

스프링 시큐리티

  • 지금꺄지 웹 개발자가 직접 구현했던 아이디/비번 입력,로그인, 사욘ㅇ자 인증후
  • 각각의 기능에 대한 Authorization에 대한 작업을 구현한 보안 프레임워크
  • 프로그램 외 리소스 접근 제어 가능

스프링 부트의 스프링 시큐리티

  • spring-boot-starter-security만 추가해주면 자동설정 적용
    • 모든 request에 대해 인증이 걸리게 된다
    • 또한 Basic Authentication, form Authentication 둘다 적용된다
    • 401 응답 헤더를 보면 WWW-Authenticate:”Basic realm=”Realm””를 확인 할 수있다
    • 브라우저가 이 응답을 받으면 Basic Authentication form을 띄우게 된다
    • Basic Authentication의 응답은 요청의 Accept 헤더에 따라 달라진다
      • 피들러나 Junit Test시 Accept헤더에 아무것도 없으므로 401 응답
        헤더를 보면 WWW-Authenticate:”Basic realm=”Realm””를 확인 할 수있다
      • 브라우저의 경우 accept가 html이 있으므로 302응답과 login에 대한url을 제공함
        으로써 리다이렉션해서 login창이 뜨도록 한다
      • login으로 갈때 콘솔에 Using generated security password: 0abed2f9-e76d-4282-9645-e3af074a84dd를 user의 패스워드로 사용할 수 있다
      • 이것을 입력해서 인증받으면 예제들의 요청에 접근이 가능하다
스프링 부트 자동설정-SecurityAutoConfiguration
  • SecurityAutoConfiguration
    • 가장 처음 보이는 것은 @Bean밑의 DefaultAuthenticationEventPublisher 등록
      • 비번틀림,계정없음등등의 대한 많은 이벤트를 발생하고 있음
      • 개발자는 그 이벤트에 대한 핸들러를 등록해서 여러가지 일을 할 수 있음
      • 스프링 부트가 아닌 일반 스프링 사용할때는 직접 이것을 등록해서 동일 효과
  • SpringBootWebSecurityConfiguration
    • WebSecurityConfigurerAdapter 상속받은 빈(empty) 클래스 설정
      • WebSecurityConfigurerAdapter
        • 스프링 부트가 아닌 스프링 시큐리티에서 제공하는 기본 설정 클래스
        • 스프링 사용자의 경우 이 설정 클래스를 기본적으로 상속받아서 만듬
      • 스프링 부트는 이 기본설정을 그대로 사용하겠다는 뜻이기도 하다
      • WebSecurityConfigurerAdapter에서 시큐리티를 반환하는 getHttp()를 보면
        기본 설정을 볼 수 있다
WebSecurityConfigurerAdapter.getHttp()끝부분, 기본 설정이 어떤건지 확인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
configure(http);
return http;
  • 설정이 없을떄 기본적으로 어떤 설정이 되어있는지 확인 가능하다
WebSecurityConfigurerAdapter의 configure()
1
2
3
4
5
6
7
8
9
10
11
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

http
.authorizeRequests() //
.anyRequest().authenticated() //모든 요청에 대해서 인증을 하겠다
.and()
.formLogin().and() //fomLoigin을 사용하겠다
.httpBasic(); //httpBasic도 사용하겠다
}
// @formatter:on
  • 핵심: 스프링 시큐리티가 제공하며 스프링 부트가 아무 일 하지 않아서 그대로 등록됨
  • UserDetailsServiceAutoConfiguration
    • 미설정시 login시에 자동으로 패스워드 지원했던 그 부분
    • 스프링 시큐리티의 기본적인 랜덤 인메모리 유저를 만들어서 제공
    • 다른 설정클래스가 없을때만 자동설정이 지원됨
    • 즉 개발하면서 UserDetailsService를 구현하면 이 자동설정은 반영되지 않음

테스트 통과하기위해서는?

  • 의존성 추가
    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>${spring-security.version}</version>
    <scope>test</scope>
    </dependency>
  • Test에 @WithMockUser 추가
    • 테스트 클래스 적용시 해당 클래스의 모든 테스트에 적용
    • 테스트 메소드 적용시 해당 테스트에만 적용
    • 가짜 유저 인증 정보를 추가해서 테스트를 돌려주게 되서 테스트가 성공하게 됨

커스터마이징

다음 3가지를 커스터마이징 할 것이다

  1. WebSecurityConfigurerAdapter
  2. UserDetailsService
  3. PasswordEncoder
  • WebSecurityConfigurerAdapter

    • 직접 구현하면 SpringBootWebSecurityConfiguration 자동설정이 작동안된다
      WebSecurityConfigurerAdapter 구현 직접
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      @Configuration
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
      @Override
      protected void configure(HttpSecurity http) throws Exception {

      //원래 값
      /*
      http
      .authorizeRequests()
      .anyRequest().authenticated()
      .and()
      .formLogin().and()
      .httpBasic();
      */
      http.authorizeRequests()
      .antMatchers("/","/hello").permitAll()
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .and()
      .httpBasic();


      }
      }
      -> anyRequest앞에 /, /hello에 대해서 제한을 풀었음
  • UserDetailsService

    • Account를 관리하는 Service 계층에서 이 인터페이스를 구현하도록 하면 된다
    • 서비스와 별개로 다른 클래스에서 이 인터페이스를 구현하게 하여도 상관은 없다
    • 즁요한것은 UserDetailsService 타입의 빈이 등록이 되어있어야 한다는 것이다
    • 그렇지 않으면 위에서 보았던 UserDetailsServiceAutoConfiguration 자동설정이
      적용되어버림
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class AccountService implements UserDetailsService {

@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);

}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

Account account =
accountRepository.findByUsername(username) //Optional<Account>
.orElseThrow(() -> new UsernameNotFoundException(username));
return new User(account.getUsername(),account.getPassword(), authorities());
}

private Collection<? extends GrantedAuthority> authorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
}

  • 설명
    • Account를 관리하는 서비스 계층
    • AccountRepository는 JpaRepository<Account,Long>를 상속받은 인터페이스
    • UserDetailsService 인터페이스 구현을 위해 loadUserByUsername 구현
    • loadUserByUsername()
      • 로그인할때 입력한 이름이 username 아규먼트로 들어온다
      • findByUsername로 유저 정보를 찾아온다
      • 리턴값인 UserDetails
        • 서비스마다 제각각으로 구현되어있는 유저 정보를 공통적으로 처리하기 위한
          인터페이스
        • 스프링이 구현체로 User 제공
    • 기본 계정은 ApplicationRunner 혹은 PostConstructure를 이용해서 생성
    • 테스트시 실패 -> 패스워드 인코더가 없습니다

PasswordEncoder

  • 패스워드 인코딩은 반드시 필요

  • 다양한 패스워드 인코딩 방식이 존재

  • 시큐리티 5.0 이전에는 기본 PasswordEncoder는 NoOpPasswordEncoder로 인코딩 없는
    패스워드가 사용되었다

  • 패스워드의 발전사

  • 그렇다고 바로 BCryptPasswordEncoder같은 것을 default로 하면 문제가 생긴다

    • 쉽게 마이그레이션이 힘든 많은 구 프로그램들이 예전 패스워드 방식 사용
    • 암호저장에 대한 모범사례가 계속 변경됨
    • 스프링 시큐리티가 자주 변경될 수 없는 커스텀 프레임워크를 사용
  • DelegatingPasswordEncoder

    • 현재 암호저장 권장사항을 이용해서 암호가 인코딩 가능한지 확인
    • 기본방식과 현대 방식의 암호들의 검증 가능
    • 향후 인코딩 업그레이드가 가능
    • 생성방법 : 쉬운 방법과 커스텀하게 만드는 방법이 있음
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      //쉬운 방법
      PasswordEncoder passwordEncoder =
      PasswordEncoderFactories.createDelegatingPasswordEncoder();

      //커스텀 방법
      String idForEncode = "bcrypt";
      Map encoders = new HashMap<>();
      encoders.put(idForEncode, new BCryptPasswordEncoder());
      encoders.put("noop", NoOpPasswordEncoder.getInstance());
      encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
      encoders.put("scrypt", new SCryptPasswordEncoder());
      encoders.put("sha256", new StandardPasswordEncoder());

      PasswordEncoder passwordEncoder =
      new DelegatingPasswordEncoder(idForEncode, encoders);

      패스워드 포맷 형태
      1
      2
      3
      4
      5
      {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
      {noop}password
      {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
      {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
      {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
  • 절대 하면 안되는 방법

    • WebSecurityConfig에 @Bean을 붙여 PasswordEncoder를 리턴값을 받는 메소드를
      생성후 NoOpPasswordEncoder.getInstance()설정
    • 현재 등록된 패스워드 인코더를 NoOpPasswordEncoder로 교체해버리는 방법
    • 쓰지말자

이제 PasswordEncoder가 빈으로 등록이 되었으니 AccountService에서 @Autowired로
받아서 사용가능하다
내부적으로 처리할때는 PasswordEncoder의 encode(문자열)를 사용하면 된다

정말 빙산의 일각이고 앞으로 더 공부해보자

Related POST

공유하기