모르지 않다는 것은 아는것과 다르다.

Spring Security

Security Config

채마스 2022. 2. 28. 20:30

스프링 시큐리티 의존성 추가

  • 스프링 시큐리티 의존성만 추가해도 서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 이루어진다.
  • 별도의 설정이나 구현을 하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동되어 작동한다.
    • 모든 요청은 인증 되어야 자원에 접근이 가능하다.
    • 인증 방식은 폼 로그인 방식과 httpBasic 로그인 방식을 제공한다.
    • 기본 로그인 페이지를 제공한다.
    • 기본 계정을 한개 제공한다.





WebSecurityConfigurerAdapter

  • 스프링 시큐리티의 웹 보안 기능을 초기화 및 설정하는 역할을 한다.
  • HttpSecurity 를 생성한다. -> HttpSecurity 는 세부적인 보안 기능을 설정할 수 있는 API 를 제공한다.

  • HttpSecurity는 위와 같이 인증 API, 인가 API 를 설정할 수 있다.





인가 API

  • authenticated() : 인증된 사용자의 접근을 허용
  • fullyAuthenticated() : 인증된 사용자의 접근을 허용, rememberMe 인증 제외
  • permitAll() : 무조건 접근을 허용
  • denyAll() : 무조건 접근을 허용하지 않음
  • anonymous() : 익명사용자의 접근을 허용
  • rememberMe() : 기억하기를 통해 인증된 사용자의 접근을 허용
  • access(String) : 주어진 SpEL 표현식의 평가 결과가 true이면 접근을 허용
  • hasRole(String) : 사용자가 주어진 역할이 있다면 접근을 허용
  • hasAuthority(String) : 사용자가 주어진 권한이 있다면
  • hasAnyRole(String...) : 사용자가 주어진 권한이 있다면 접근을 허용
  • hasAnyAuthority(String...) : 사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용
  • hasIpAddress(String) : 주어진 IP로부터 요청이 왔다면 접근을 허용



Security Config

  • 사용자 정의 보안 설정 클래스이다.
  • 아무 설정을 하지 않아도 아래와 같이 기본적으로 세팅이 되어있다.

// WebSecurityConfigurerAdapter.java
protected void configure(HttpSecurity http) throws Exception {
    logger.debug("Using default configure......")

    http
        .authorizeRequests()
            .anyRequest().authenticated() // 모든 요청을 검증하겠다.
            .and()
        .formLogin().and()
        .httpBasic();
}
  • 하지만 보안 설정을 커스텀하기 위해서는 WebSecurityConfigurerAdapter 를 상속받아서 구현해야 한다. -> 정확히는 위의 configure 메소드를 Override 한다고 생각하면 된다.

 

필터 On/Off (Security Config)

  • Spring Security 의 특정 필터를 disable하여 동작하지 않게 한다.
  • 사용하지 않을 필터를 명시적으로 disable하는 것도 좋은 방법이다.
  • 코드예시
// basic authentication
http.httpBasic().disable(); // basic authentication filter 비활성화
// csrf
http.csrf();
// remember-me
http.rememberMe();

 

로그인 & 로그아웃 페이지 관련 기능 (Security Config)

  • 폼 로그인의 로그인 페이지를 지정하고 로그인에 성공했을때 이동하는 URL을 지정한다.
  • 로그아웃 URL을 지정하고 로그아웃에 성공했을때 이동하는 URL을 지정한다.
  • 코드예시
// login
http.formLogin()
        .loginPage("/login")
        .defaultSuccessUrl("/")
        .permitAll(); // 모두 허용
// logout
http.logout()
        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
        .logoutSuccessUrl("/");

 

Url Matchers 관련 기능 (Security Config)

  • antMatchers
    • http.authorizeRequests().antMatchers("/signup").permitAll()
    • “/signup” 요청을 모두에게 허용한다.
  • mvcMatchers
    • http.authorizeRequests().mvcMatchers("/signup").permitAll()
    • “/signup”, “/signup/“, “/signup.html” 와 같은 유사 signup 요청을 모두에게 허용한다.
  • regexMatchers
    • 정규표현식으로 매칭한다.
  • requestMatchers
    • antMatchers, mvcMatchers, regexMatchers는 결국에 requestMatchers로 이루어져있다.
    • 명확하게 요청 대상을 지정하는 경우에는 requestMatchers를 사용한다.

 

인가 관련 설정 (Security Config)

  • authorizeRequests()
    • http.authorizeRequests()
    • 인가를 설정한다.
  • permitAll()
    • http.authorizeRequests().antMatchers(“/home").permitAll()
    • “/home” 요청을 모두에게 허용한다.
  • hasRole()
    • http.authorizeRequests().antMatchers(HttpMethod.POST, "/notice").hasRole("ADMIN")
    • 권한을 검증한다.
  • authenticated()
    • http.authorizeRequests().anyRequest().authenticated()
    • 인증이 되었는지를 검증한다.
  • 코드예시
// authorization
http.authorizeRequests()
        // /와 /home은 모두에게 허용
        .antMatchers("/", "/home", "/signup").permitAll()
        // hello 페이지는 USER 롤을 가진 유저에게만 허용
        .antMatchers("/note").hasRole("USER")
        .antMatchers("/admin").hasRole("ADMIN")
        .antMatchers(HttpMethod.POST, "/notice").hasRole("ADMIN")
        .antMatchers(HttpMethod.DELETE, "/notice").hasRole("ADMIN")
        .anyRequest().authenticated();
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new LoggingFilter(), WebAsyncManagerIntegrationFilter.class);

        http.authorizeRequests()
                .mvcMatchers("/", "/info", "/account/**", "/signup").permitAll()
                .mvcMatchers("/admin").hasRole("ADMIN")
                .mvcMatchers("/user").hasRole("USER")
                .anyRequest().authenticated()
                .expressionHandler(expressionHandler());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("junho").password("{noop}123").roles("USER").and()
                .withUser("admin").password("{noop}!@#").roles("ADMIN");
    } 
}
  • WebSecurityConfigurerAdapter 를 상속받으면 Security 설정을 할 수 있다.
  • http.authorizeRequests() 는 요청을 어떻게 인가할지에 대한 설정이고, chaining 형태로 설정할 수 있다.
  • anyRequest().authenticated() 는 그 밖에 기타 등등은 인증만 되면 접근이 가능하다는 뜻이다.
  • 위와 같이 auth를 매개변수로 받는 configure를 오버라이딩 하면 인메모리 유저를 생성할 수 있다.





Ignoring (Security Config)

  • 모든 요청은 시큐리티 필터들을 거치면서 권한을 검사한다.
  • 하지만 검사하고 싶지 않은 리소스까지 전부 검사하면 자원낭비다. -> 예를들어 favicon.io
  • 아래와 같이 ignoring() 을 사용하면 위의 문제를 해결할 수 있다.
@Override
public void configure(WebSecurity web) throw Exception {
    web.ignoring().mvcMatchers("/favicon.io");
}
  • 만약 favicon 과 같은 정적리소스에 모두 적용하고 싶다면 아래와 같이 구현하면 된다.
@Override
public void configure(WebSecurity web) throw Exception {
    web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
  • 실제로 FilterChainFoxy 에 break point 를 걸어보면 시큐리티 필터를 하나도 거치지 않는 것을 확인할 수 있다.
  • 그렇다면 아래와 같이 설정한 것과는 어떤 차이가 있을까?
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
}
  • 결과는 똑같다. -> 하지만 과정이다르다.
  • 이 경우 필터 15개를 모두 거치게 된다. -> 다만 가장 마지막 필터인 FilterSecurityinterceptor에서 권한을 인정해줄 뿐이다.
  • 위의 경우도 필터를 거치므로 자원낭비가 발생한다. 그러므로 web.ignoring() 방식이 더 좋다고 할 수 있다.
  • 하지만 "/", "/info", "/account/**", "/signup" 와 같은 동적인 리소스를 관리하는 요청의 경우 위와 같이 처리하는 것이 바람직하다.





인메모리 사용자 추가

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser("user").password("{noop}1111").roles("USER");
    auth.inMemoryAuthentication().withUser("manager").password("{noop}1111").roles("MANAGER");
    auth.inMemoryAuthentication().withUser("admin").password("{noop}1111").roles("ADMIN");
}





AuthenticationManager 빈으로 등록

  • Test 코드를 짤때, @Autowired 로 AuthenticationManager 를 가져다가 쓸 일이 있을 것이다.
  • 하지만 AuthenticationManager 는 기본적으로 빈으로 노출이 되어있지 않기 때문에 아래와 같이 빈으로 등록시켜야 사용할 수 있다.
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throw Exception {
    return super.authenticationManagerBean();
}





PasswordEncoder 빈으로 등록

  • 비밀번호를 암호화 하기 위해서는 아래와 같이 PasswordEncoder 을 빈으로 등록해야한다.
@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
  • 아래와 같이 비밀번호를 암호화해서 저장해 줄 수 있다.
private final PasswordEncoder passwordEncoder;

//...

account.setPassword(passwordEncoder.encode(account.getPassword())); 




REFERENCES

  • 백기선님의 스프링 시큐리티
  • 정수원님의 스프링 시큐리티
  • 안성훈님의 스프링 시큐리티