[EC-Spring] 4주차-Spring Security

umtuk·2022년 4월 30일
0

EC-Spring

목록 보기
4/6

Authenticationprincipalidentity를 증명하는 과정
principal는 유저, 기기, 시스템 등, 보통 유저(사용자)를 의미
principal는 자신을 인증해달라고 신원 증명 정보, credential를 제시
principal가 유저일 경우 credential은 대개 패스워드

Authorization은 인증을 마친 유저에게 authority를 부여하여 대상 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정
Authorization는 반드시 Authentication 과정 이후 수행, authorityrole 형태로 부여하는 게 일반적

Access control은 애플리케이션 리소스에 접근하는 행위를 제어하는 일
어떤 유저가 어떤 리소스에 접근하도록 허락할지를 결정하는 행위, Access controll decision
리소스의 접근 속성과 유저에게 부여된 권한, 다른 속성을 견주어 결정

Authentication

Authentication: 특정 리소스에 액세스하려는 사용자의 신원을 확인하는 방법
사용자를 인증하는 일반적인 방법은 usernamepassword를 입력하도록 요구하는 것

Password Storage

Spring Security의 PasswordEncoder 인터페이스는 password를 안전하게 저장하기 위해 변환해주는 기능을 가짐

password를 날 것 그대로 저장하면 보안에 좋지 않음
hash과정을 덧붙임

Authentication을 시도하면 기존의 저장되어있는 hashed password와 그들이 적은 passwordhash한 값을 비교
hash는 단방향이므로 기존의 hashed passwordpassword를 유추하기가 어려움

Rainbow Tables라 불리는 테이블로 password를 매번 작성할 때마다 그에 따른 hash 값을 테이블에 저장
hashed password가 동일한 모든 사용자에 대해서 같은 passwordAuthentication이 가능

salt를 덧붙여 각 사용자별로 password가 동일하더라도 hashed password가 서로 다르게 생성

또한 Authentication 과정에서 일부러 resource(i.e. CPU, memory, etc) 사용량을 극대화시켜 (예를 들면 password 검증에 1초가 걸리게) 시도 횟수를 현저하게 줄여 보안을 유지

DelegatingPasswordEncoder

기존의 PasswordEncoder는 다음과 같은 문제가 있다

  • 기존의, 예전에 사용되오던 password encodings들이 많고, 그것들이 쉽게 병합되지 않음
  • 가장 실용적인 password storagepassword를 다시 변경하는 것
  • 프레임워크에 관점에서 Spring Security는 주요 변경 사항을 자주 만들수 없음

Instead Spring Security introduces DelegatingPasswordEncoder which solves all of the problems by:
대신 DelegatingPasswordEncoder를 사용하면 해당 문제를 해결 가능하다

  • Ensuring that passwords are encoded using the current password storage recommendations
  • Allowing for validating passwords in modern and legacy formats
  • Allowing for upgrading the encoding in the future
PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

Protection Against Exploits

빈번하게 발생하는 Exploits으로부터의 Protection을 제공

Cross Site Request Forgery (CSRF)

CSRF 공격에 대해 Spring Security는 다음과 같은 두 가지 기능을 제공

  • The Synchronizer Token Pattern
  • Specifying the SameSite Attribute on your session cookie

Security HTTP Response Headers

Security HTTP Response Headers

  • Default Security Headers
  • Cache Control
  • Content Type Options
  • HTTP Strict Transport Security (HSTS)
  • HTTP Public Key Pinning (HPKP)
  • X-Frame-Options
  • X-XSS-Protection
  • Content Security Policy (CSP)
  • Referrer Policy
  • Feature Policy
  • Permissions Policy
  • Clear Site Data
  • Custom Headers

Architecture

filter

Spring SecurityServletServlet Filter에 기반을 둠

Filter는 다음과 같은 순서로 진행

  • ChannelProcessingFilter: Ensures a web request is delivered over the required channel.
  • WebAsyncManagerIntegrationFilter: Provides integration between the SecurityContext and Spring Web's WebAsyncManager
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

04_filterchain
04_securityfilterchain

  • SecurityContextHolder : Spring Security가 인증을 마친 유저의 세부 정보를 저장하는 곳
  • SecurityContext
  • Authentication
  • GrantedAuthority : Authentication에서 principal에게 부여되는 authority
  • AuthenticationManager : Spring SecurityFiltersAuthentication을 수행하는 방법을 정의하는 API
  • ProviderManager : AuthenticationManager를 수행하는 가장 흔한 방식
  • AuthenticationProvider : ProviderManager에서 특정 유형의 Authentication을 수행하는 데 사용
  • AuthenticationEntryPoint :
  • AbstractAuthenticationProcessingFilter : 인증에 사용되는 기본 필터

04_securitycontextholder
04_abstractauthenticationprocessingfilter
04_usernamepasswordauthenticationfilter

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
	http
		// ...
		.authorizeHttpRequests(authorize -> authorize                           // 1
			.mvcMatchers("/resources/**", "/signup", "/about").permitAll()        // 2 
			.mvcMatchers("/admin/**").hasRole("ADMIN")                            // 3 
			.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")  // 4
			.anyRequest().denyAll()                                               // 5
		);

	return http.build();
}

04_authorizationfilter

예제

https://github.com/umtuk/ec-spring/tree/master/springsecurity

build.gradle

plugins {
    id 'org.springframework.boot' version '2.6.7'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'org.ec'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

Default

IndexController.java

package org.ec.springsecurity;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping
public class IndexController {

    @GetMapping
    public String index() {
        return "index";
    }
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
    <h1>index</h1>
</body>
</html>

콘솔에 password

Using generated security password: c0c0f62d-0339-47ec-b84f-0c225269510c

04_default_signin

Spring Security Starter를 추가한 것만으로도 다음과 같은 보안 구성이 제공

  • 모든 HTTP 경로는 인증되어야 함
  • 어떤 특정 역할이나 권한이 없음
  • 로그인 페이지가 따로 없음
  • 스프링 시큐리티의 HTTP 기본 인증을 사용해 인증
  • 사용자는 하나만 있으며, 이름은 user

사용자 인증 커스터마이징

AuthorController.java

package org.ec.springsecurity;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/author")
public class AuthorController {

    @GetMapping
    public String auth() {
        return "author";
    }
}

SecurityConfig.java

package org.ec.springsecurity.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public SecurityConfig(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
        ;

        http
                .authorizeRequests()
                    .antMatchers("/author").access("hasRole('ROLE_USER')")
                    .antMatchers("/", "/**").access("permitAll")
                .and()
                    .httpBasic();
                ;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(encoder())
        ;
    }
}

http.csrf()를 편하게 활성화/비활성화 가능
여기서는 POST를 정상적으로 수행하기 위해 비활성화
/author 페이지에 들어가기 위해서는 ROLE_USER 권한을 가지고 있어야 함

userDetailsService를 활용하기 위해 아래 코드를 추가

User.java

package org.ec.springsecurity.user.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Arrays;
import java.util.Collection;

@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String username;
    private String password;

    public User() {}

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserDetailsimplementsusername, password, authority를 반환하는 메서드를 오버라이드
아래의 네 가지 boolean을 리턴하는 메서드로 해당 User의 상태가 어떤지를 반환

UserRepository.java

package org.ec.springsecurity.user.repository;

import org.ec.springsecurity.user.entity.User;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {
    User findByUsername(String username);
}

UserRepositoryUserDetailsService.java

package org.ec.springsecurity.user.service;

import org.ec.springsecurity.user.entity.User;
import org.ec.springsecurity.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    @Autowired
    public  UserRepositoryUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user != null)
            return user;

        throw new UsernameNotFoundException("User '" + username + "' not found");
    }
}

해당 ServiceUserDetailsServiceimplementsloadUserByUsername라는 메서드를 오버라이드
UserRepository를 활용하여 User 객체를 load

User의 회원 가입 및 조회를 위해 다음과 같은 코드를 작성

RegistrationForm.java

package org.ec.springsecurity.user.dto;

import lombok.Data;
import org.ec.springsecurity.user.entity.User;
import org.springframework.security.crypto.password.PasswordEncoder;

@Data
public class RegistrationForm {

    private String username;
    private String password;

    public User toUser(PasswordEncoder passwordEncoder) {
        return new User(username, passwordEncoder.encode(password));
    }
}

UserController.java

package org.ec.springsecurity.user.controller;

import org.ec.springsecurity.user.dto.RegistrationForm;
import org.ec.springsecurity.user.entity.User;
import org.ec.springsecurity.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/register")
public class UserController {

    private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;

    @Autowired
    public UserController(
            UserRepository userRepository,
            PasswordEncoder passwordEncoder
    ) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @GetMapping
    public Iterable<User> getAll() {
        return userRepository.findAll();
    }

    @PostMapping
    public ResponseEntity<User> singUp(@RequestBody RegistrationForm registrationForm) {
        User user = userRepository.save(registrationForm.toUser(passwordEncoder));

        return ResponseEntity.ok(user);
    }
}

/register에는 authorizeRequests가 적용되어 있지 않아 모든 사용자가 조회할 수 있어 실제로는 좋은 코드가 아님
UserpasswordEncoder를 적용한 모습을 쉽게 관찰하기 위함

결과

index.html에 진입 (보안이 적용되어 있지 않음)
04_indexhtml

author.html에 진입 (보안이 적용 ROLE_USER 권한을 가진 사용자만 접근 가능)
04_author접근시로그인절차

/register에 GET 요청으로 현재 사용자 조회
04_registerget요청
실행 결과 : 비어 있음
04_registerget요청결과

사용자 등록을 위해/register POST 요청
04_registerpost요청
/register POST 요청 결과
04_registerpost요청결과

password가 암호화 후 User 객체로 변환 및 DB에 저장됨을 알 수 있음

이전에 작성한 User 객체로 인증
04_이전에작성한User객체로인증
암호화 된 password를 입력하는 게 아닌 그대로의 password를 입력

author.html 진입
04_author진입

참고

스프링 5 레시피
스프링 인 액션
https://docs.spring.io/spring-security/reference/index.html

profile
https://github.com/umtuk

0개의 댓글