[60일차] SpringSecurity, Filter, DelegatingPasswordEncoder

유태형·2022년 7월 23일
0

코드스테이츠

목록 보기
60/77

오늘의 목표

  1. Spring Security
  2. Filter
  3. DelegatingPasswordEncoder



내용

Spring Security

SpringSecurity는 스프링 기반 웹 애펄리케이션의 인증(Authentication)인가(Authorization) 기능을 가진 프레임워크 입니다.
스프링 기반 애플리케이션의 보안 표준 이며 현재 웹 애플리케이션에서 많이 사용중입니다.
확장성에 유리하며 추가와 변경을 다양하게 수행할 수 있습니다.

  • 주체(Principal)
    • 유저,기기,시스템등 웹 애플리케이션을 사용하는 대상을 의미할 수 있지만 보통 사용자를 의미합니다.
  • 인증(Authentication)
    • 특정 리소스에 접근하려고 하는 사용자가 누구인지 확인합니다.
    • 주체는 자신의 신원 증명 정보를 제시하여 신원을 증명하는 과정입니다.
  • 인가(Authentication)
    • 인증을 마친 주체에게 권한을 부여하여 특정 리소스에 접근할 수 있도록 허가하는 과저입니다.
    • 인증 과정을 마친 주체에게만 수행되어야 합니다.
  • 접근 통제(Access control)
    • 어떤 유저가 애플리케이션 리소스에 접근하도록 허락할지 제어하는 행위입니다.


SpringSecurity 사용 이유

  1. 모든 요청에 대해서 인증을 요구
  2. 사용자 이름 및 암호를 가지고 사용자가 인증할 수 있도록 허용
  3. 사용자가 로그아웃 할 수 있도록 허용
  4. CSRF(Cross-Site Request Forgery)공격 방지
  5. Session Fixsation 보호
  6. 보안 헤더 통합
  • HSTS 강화
  • X-Content-TypeOptions
  • 캐시 컨트롤
  • X-XSS-Protection XSS 보안
  • 클릭재킹 방지, X-Frame 옵션 통합

Spring Security를 사용하기 위해선 다른 하위 프레임 워크와 마찬가지로 build.gradle 외부 의존 라이브러리를 추가합니다.

dependencies {
	annotationProcessor 'org.projecctlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-security
    implementation 'org.springframework.boot:spring-boot-starter-mustache'
    ...
}

securitymustache를 추가하여 해당 라이브러리를 사용할 수 있게 되었습니다.
이외에도 h2, Spring Data JPA, web, lombok을 추가합니다.

h2jpa사용시 그래왔던 것 처럼 application.yml에 설정을 추가합니다.

spring:
	h2:
    	console:
        	enabled: true
            path: /h2
    datasource:
    	url: jdbc:h2:mem:test
    jpa:
    	hibernate:
        	ddl-auto: create
        show-sql: true


로그인 구현

스프링 시큐리티 라이브러리를 추가하고 웹 애플리케이션을 실행시키면

애프리케이션으로 로그인과 로그아웃이 가능합니다. Usernameuser, Password는 콘솔창의 비밀번호를 입력시 로그인 가능합니다.

@Controller
public class 컨트롤러{
	@GetMapping("/")
    public @ResponseBody String index() {
    	return "index";
    }
    
    @GetMapping("/join")
    public @ResponseBody String join(){
    	return "join";
    }
    
    ..
}

@ResponseBody는 핸들러메서드의 반환값을 단순히 resposneBody에 포함시킵니다.

@Configuration
@EnableWebSecurity
public class 시큐리티컨픽{
	@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    	http.csrf().disable();
        http.headers().frameOptions().disable();
        
        http.authorizeRequests()
        	.antMatchers("/경로1/**").authenticated()
            .antMatchers("/경로2/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
            .anyRequest().permitAll()
            .and()
            .formLogin()
            .loginPage("/login");
        
        return http.build();
    }
}
  • http.csrf().disable() : HTML의 form태그로만 요청이 가능합니다. 다른 경로의 요청은 거절하게 됩니다.

  • http.headers().frameOptions().disable() : h2를 연결할 때 필요합니다.

  • HttpSecurity.authorizeRequest() : 요청에 의한 보안검사를 시작합니다.

  • HttpSecurity.antMatchers("/경로1/**").authenticated() : 인증 한 사용자들은 경로1의 모든 하위 리소스에 접근 가능합니다.

  • HttpSecurity.antMatchers("/경로2/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')") : 메니저 권한과 어드민 권한을 가진 사용자는 경로2의 모든 하위 리소스에 접근이 가능합니다. 간혹 ROLE_ADMIN이 아닌 ADMIN만 사용 하는 경우도 존재하지만 스프링이 자동으로 ROLE_을 추가해줘서 상관 없습니다.

  • anyRequest() : .antMatchers()에서 정의한 요청외 나머지 요청들을 지정합니다.

  • permitAll() : 인증여부와 상관없이 모두 허가합니다.
    만약 anyRequest().permitAll()로 나머지 요청들에 대한 접근 허가가 없다면 기본적으로 나머지 요청들은 모두 거부됩니다.

  • .and().formLogin().loginPage("/login") : 권한이 없는 페이지에 접근하려 하면 자동으로 로그인페이지로 redirect 시켜 줍니다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer{
	public void configureViewResolvers(ViewResolverRegistry registry){
    	MustacheViewResolver resolver = new MustacheViewResolver();
        resolver.setCharset("UTF-8");
        resolver.setContentType("text/html; charset=UTF-8");
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        
        registry.viewResolver(resolver);
    }
}

먼저 ViewResolver의 역할을알아야 합니다. 컨트롤러의 핸들러메서드는 ResopnseEntity와같은 객체를 반환하거나 일반적인 String을 반환합니다. 별도의 지정이 없다면 String을 반환시 해당 String 문자열에 해당하는 View객체를 찾아 뷰를 열어줍니다. 만일 보안이나 다른 옵션들을 직접 지정하고 싶다면 Viewresolver를 개발자가 WebMvcConfigurer를 implements하고 ConfigureViewResolvers(ViewResolverRegistry registry)메서드를 오버라이딩 하여 정의합니다.

  • ViewResolverRegistry.setCharset("UTF-8") : utf-8로 인코딩을 수행합니다.
  • ViewResolverRegistry.setContentType("text/html; charset=UTF-8") : 내용물을 HTML 형식으로 읽어들일 것인가를 결정합니다.
  • ViewResolverRegistry.setPrefix() : 헨들러 메서드가 반환하는 문자열 앞에 붙이는 문자열 입니다.
  • ViewResolverRegistry.setSuffix() : 핸들러 메서드가 반환하는 문자열 뒤에 붙히는 문자열 입니다.

.setPrefix("classpath:/templates/") + 핸들러 메서드 반환 문자열("login") + setSuffix(".html")을 조합하여 스프링에서 뷰 객체를 찾게 됩니다.

classpath:/templates/login.html 과 같이 앞 뒤로 바로 붙어 해당 경로의 HTML문서를 반환합니다.(없으면 에러)

  • @Data : @Getter + @Setter + @RequiredArgsConstructor + @ToString + @EqualsAndHashCode 입니다. 엔티티 클래스 작성시 유용합니다.


회원 가입 보안 적용

회원 가입시 보안을 위하여 2차, 3차 피해를 막기 위해 서버에서 사용자의 패스워드를 저장할 시 해싱하여 만약 해커에 의해 DB가 탈취 당하더라도 원본 비밀번호를 알 수 없도록 해싱하여 비밀번호를 저장합니다.

스프링 시큐리티에서는 비밀번호를 고차원의 해시값으로 변환해주는 다양한 클래스들을 지원합니다.

public class 시큐리티컨픽{
	...
    
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
    	return new BCryptPasswordEncoder();
    }
}

@Configuration 존재하는 config클래스에 BCryptPasswordEncoder클래스의 빈 객체를 스프링 컨테이너에 추가합니다.

public class 컨트롤러{
	...
    
    private final BcryptPasswordEncoder bCryptPasswordEncoder;
    private final 레포지토리 레포지토리
    
    public 컨트롤러(BcryptPasswordEncoder bCryptPasswordEncoder,
    				레포지토리 레포지토리){
    	this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.레포지토리 = 레포지토리;
    }
    
    @PostMapping("/회원가입")
    public class 회원가입(엔티티 엔티티){
    	String 원본비밀번호 = 엔티티.getPassword();
        String 변환비밀번호 = bCryptPasswordEncoder.enocde(rawPassword);
        엔티티.setPassword(변환비밀번호);
        
        레포지토리.save(엔티티);
        
        return "redirect:/로그인";
    }
}

BCryptPasswordEncoder 빈 객체를 이용하여 엔티티의 원래 비밀번호를 해싱된 비밀번호로 변경하고 DB에 저장하였습니다.

  • bCryptPasswordEncoder.encode(비밀번호) : 비밀번호를 암호화 합니다.
  • return "redircet:/로그인" : 현재 회원가입 페이지에 POST HTTP메서드를 보내면 처리 후 로그인 페이지로 돌아갑니다.


로그인 보안 적용

UserDetails 인터페이스는 Spring Security에서 구현한 클래스를 사용자 정보로 인식하고 인증하는 작업을 합니다. UserDetails 인터페이스는 VO 역할을 합니다. 단순히 추상메서드들을 오버라이딩하여 필요한 정보를 넘겨 줄 수 있도록 합니다.

public class PrincipalDetails implements UserDetails{
	private 엔티티 엔티티;
    
    public PrincipalDetails(엔티티 엔티티){
    	this.엔티티 = 엔티티;
    }
    
    //오버라이딩
   	public Collection<? extends GrantedAuthority> getAuthorities(){
    	Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority(){
        	public String getAuthority(){
            	return 엔티티.getRole();
            }
        });
    }
    
    public String getPassword(){
    	return 엔티티.getPassword();
    }
    
    public String getUsername(){
    	return 엔티티.getUsername();
    }
    
    public boolean isAccountNotExpired(){
    	return true;
    }
    
    public boolean isAccountNonLocked(){
    	return true;
    }
    
    public boolean isCredentialsNonExpried(){
    	return true;
    }
    
    public boolean isEnabled(){
    	return true;
    }
}
메서드설명
getAuthorities()계정이 가진 권한 목록을 반환합니다.
getPassword()계정의 비밀번호를 반환합니다.
getUsername()계정의 이름을 반환합니다.
isAccountNonExpired()계정이 만료되지 않았는 지 반환합니다.(true: 만료x)
isAccountNonLocked()계정이 잠겨 있지 않았는 지 반환합니다.(true: 잠김x)
isCredentialNonExpired()비밀번호가 만료되지 않았는 지 반환합니다.(true: 만료x)
isEnabled()계정이 활성화 인지반환합니다.(true:활성화)
@Service
public class Principal서비스 implements UserDetailsService{
	@Autowired
    private 레포지토리 레포지토리;
    
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
    	엔티티 엔티티 = 레포지토리.findByUsername(username);
        if(엔티티 != null)
        	return new PrincipalDetails(엔티티);
            
        return null;
    }
}
public interface 레포지토리 implements JpaRepository<엔티티,ID타입>{
	public 엔티티 findByUsername(String username);
}

DB에서 유저 정보를 불러오는 비즈니스 로직을 가지는 서비스를 UserDetailsService 인터페이스를 구현한 서비스 입니다.

loadUserByUsername()메서드를 사용하여 DB로 부터 데이터를 읽어와 엔티티 객체를 만들고 엔티티 객체를 UserDetails를 구현한 클래스로 다시한번 덫 데어 리턴합니다.

굳이 UserDetails를 구현한 클래스, UserDetailsService를 구현한 클래스로 따로 구현하여 리턴하는 이유는 SpringSecurity보안을 적용하기 위함입니다.



권한 처리

시큐리티컨픽에 에너테이션을 적용하여 설정정보 클래스의 SecurityFilterChain filterChain(HttpSecurity http) 빈 객체에서 뿐만 아니라 컨트롤러에서도 접근권한을 처리할 수 있도록 할 수있는 에너테이션을 지원합니다.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class 시큐리티컨픽{
	...
}

여기서 @EnableGlobalMethodSecurity 에너테이션의 securedEnabled = true 애트리뷰트는 @Secured 에너테이션을 prePostEnabled = true 애트리뷰트는 @PreAuthroize, @PostAuthorize 애테너이션을 컨트롤러에서 사용 가능하게 설정합니다.

@Secured("ROLE_ADMIN")
@GetMapping("/리소스1")
public String 리소스1(){
	return "리소스1";
}
@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
@GetMapping("/리소스2")
public String 리소스2(){
	return "리소스2";
}
  • @Secured("ROLE_ADMIN") : .antMatchers("/리소스/**").access("hasRole('ROLE_ADMIN')")를 SecurityFilterChain 빈 클래스에 추가하는 것과 동일합니다.
  • @PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") : .antMatchers("/리소스2/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")를 SecurityFilterChain 빈 클래스에 추가하는 것과 동일합니다.

보통 @Secured 에너테이션은 1개의 권한을, @PreAuthorize는 1개 이상의 권한을 부여할 때 사용합니다.




Filter

Spring Security는 servlet Filter를 기반으로 서블릿을 지원합니다.
클라이언트가 요청을 하게 되면 Servlet Filter를 제일 먼저 거치고 인증인가에 대한 처리를 Filter에서 수행하고 난 후 DispatcherServlet에서 요청이 처리됩니다.

FilterChain은 사슬처럼 여러개의 Filter들이 연결되어 있고 서로 연결되어 동작합니다.

서블릿에는 하나의 단일 요청을 처리하지만, 필터는 체인을 형성하여 실제 요청을 순서대로 수행합니다.

순서는 2가지로 지정가능합니다. 첫번재로 @Order에너테이션이나 Ordered를 구현하는 것이고 다른 하나는 FilterRegistrationBean의 일부가 되어 순서를 가집니다.

FilterChain은 서블릿 컨테이너에서 작동합니다. 만일 빈 객체로 만들어 스프링 컨테이너에서 관리되게 만들 수도 있습니다.



Filter 인터페이스

public class FirstFilter implements Filter{
	public void init(FilterConfig filterConfig) throws ServletException{
    	Filter.super.init(filterConfig);
        System.out.println("FirstFilter 생성됨");
    }
    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    	System.out.println("========First 필터 시작========");
        chain.doFilter(request,response);
        System.out.println("========First 필터 종료========");
    }
    
    public void destroy(){
    	System.out.println("FirstFilter 사라짐");
        Filter.super.destroy();
    }
}
public class SecondFilter implements Filter{
	public void init(FilterConfig filterConfig) throws ServletException{
    	Filter.super.init(filterConfig);
        System.out.println("SecondFilter가 생성됨");
    }
    
   	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    	System.out.println("==========Second 필터 시작==========");
        chain.doFilter(request, response);
        System.out.println("==========Second 필터 종료==========");
    }
    
    public void destroy(){
    	System.out.println("SecondFilter가 사라집니다.")
        Filter.super.destroy();
    }
}

Filter인터페이스를 구현한 클래스는 필터는 3개의 메서드를 오버라이딩 하여 사용합니다.

  • void init(FilterConfig) : 서블릿 컨테이너에 필터가 적재되면서 실행됩니다.
  • void doFilter(ServletRequest, ServletResponse, FilterChain) : url로 요청이 오면 doFilter를 실행시킨 후 다음 필터를 실행합니다.
  • void destroy() : 서블릿 컨테이너에서 제거되면서 실행됩니다.
@Configuration
public class 설정정보클래스{
	@Bean
    public FilterRegistrationBean<FirstFilter> firstFilterRegister() {
    	FilterRegistrationBean<FirstFilter> registrationBean = new FilterRegistrationBean<>(new FirstFilter());
        registrationBean.setOrder(1);
        return registrationBean;
    }
    
    @Baen
	public FilterRegistrationBean<SecondFilter> secondFilterRegister(){
    	FilterRegistrationBean<SecondFilter> registrationBean = new FilterRegistrationBean<>(new SecondFilter());
        registrationBean.setOrder(2);
        return registrationBean;
    }
}

FilterRegistrationBean<필터> 빈을 만들어 FilterChain에 Filter를 추가할 수 있습니다. 이때 FilterRegistrationBean<필터>.setOrder(순서)를 지정하여 필터에서 몇번째로 실행 시킬 것인지 순서를 지정할 수도 있습니다.



DelegatingFilterProxy

스프링 시큐리티가 모든 애플리케이션 요청을 감싸게 해서 보안이 적용되게 하는 서블릿 필터입니다. 스프링 프레임워크의 스프링 컨테이너와 서블릿 컨테이너를 연계해 스프링 컨테이너의 빈 객체로 등록할 수 있게 합니다.

스프링 부트는 DelegatingFilterProxy라는 Filter구현체로 서블릿 컨테이너의 생명주기와 스프링의 스프링컨테이너를 연결합니다.

연결함으로써 서블릿 컨테이너 자체적 뿐만 아니라 모든 처리를 스프링 컨테이너의 Filter를 구현한 스프링 빈으로 위임해줍니다.



FilterChainProxy

스프링 시큐리티가 제공하는 특별한 Filter로 SecurityFilterChain을 통해 여러 Filter인스턴스로 위임할 수 있습니다.

이해가 많이 어려운 부분입니다. 서블릿 컨테이너에 존재하는 FilterChainProxyDelegatingFilterProxy가 스프링 컨테이너의 SecurityFilterChain 에 등록하여 빈 객체로 사용될 수 있도록 합니다.

어떻게 가능하게 하면 FilterChainProxyfilterChain이라는 변수에 SecurityFilterChain의 리스트를 가지고 있으면 리스트마다 다시 필터들의 리스트를 가집니다.




DelegatingPasswordEncoder

이름에서 알 수 있듯이 패스워드엔코더는 비밀번호를 그대로 데이터베이스에 저장하기 보다 한번 해싱을 거쳐 데이터베이스에 저장하기 위해 사용합니다.

PasswordEncoder들 중 DelegatingPasswordEncoder를 사용하는 이유는 3가지가 있습니다.

  1. 비밀번호를 현재 권장하는 저장 방식으로 인코딩함을 보장합니다.
  2. 비밀번호 검증은 최신 형식과 레거시 형식을 모두 지원합니다.
  3. 나중에 인코딩을 변경할 수 있습니다.
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoder.put("pbkdf2",new Pbkdf2PasswordEncoder());
encoder.put("scrypt", new SCryptPasswordEncoder());
encoder.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);

인스턴스를 반환 받을 수도 있고 아래와 같이 여러 인코딩 방식의 패스워드 인코더들중 선택하여 사용하는 객체를 생성할 수도 있습니다.

아래의 경우 BCryptPasswordEncoder, NoOpPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder, StandardPasswordEncoder등 여러 비밀번호 인코더들을 해시맵에 저장하여 선택 할 수 있도록 합니다.




후기

Spring은 범위가 매우 넓고 체득 하는데 역시 만만한 프레임워크가 아니구나 다시 한번 느끼고 있습니다. 특히나 보안 같은 경우는 엄격히 다루어져야 하니 더더욱 그런 것 같습니다. 이제 기초를 배우는데 열심히 따라가도록 하겠습니다.




GitHub

https://github.com/ds02168/CodeStates_Spring/tree/main/section4-week1-FRI

profile
오늘도 내일도 화이팅!

0개의 댓글