ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Spring Security 설정, 예제, 용어와 흐름
    Back-End/Spring Boot 2023. 6. 23. 10:17

     

    📑 Spring Security 설정

     

    스프링 시큐리티를 구현하는 프로젝트를 생성한다.

     

     

    생성된 패스워드는 기본으로 사용해볼 수 있는 'user' 계정의 패스워드이다. 프로젝트가 정상적으로 실행된다면 브라우저를 열어 localhost:/8080/login의 경로로 접근했을 때  다음과 같은 화면이 나온다. 

     

     

     

     

     

    스프링 부트가 아닌 경우 web.xml 설정을 변경해서 시큐리티 설정이 가능한데, 번거롭고 복잡하다. 하지만 스프링 시큐리티는 자동 설정 기능이 있어 별도의 설정 없이도 연동 차리는 된다.

     

    하지만 이용하는 모든 프로젝트는 프로젝트에 맞는 설정을 추가하는 것이 일반적이므로 이를 위한 SecurityConfig 클래스를 추가해준다.

     

     

     

     

    시큐리티와 관련된 설정이 정상적으로 동작하는지 확인하기 위해 간단한 컨트롤러를 구성한다. 

     

     

    <SampleController.java>

    package com.wish.securityPractice.controller;
    
    import lombok.Getter;
    import lombok.extern.log4j.Log4j2;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @Log4j2
    @RequestMapping("/sample")
    public class SampleController {
    
        //로그인을 하지 않은 사용자도 접근할 수 있다.
        @GetMapping("/all")
        public void exAll(){
            log.info("exAll........");
        }
        //로그인한 사용자만이 접근할 수 있다.
        @GetMapping("/member")
        public void exMember(){
            log.info("exMember........");
        }
        //관리자(admin) 권한이 있는 사용자만이 접근할 수 있다.
        @GetMapping("/admin")
        public void exAdmin(){
            log.info("exAdmin........");
        }
    }

     

     

    view 아래는 saple\경로에 메서드 이름과 같은 페이지를 아래와 같이 작성해준다. 

     

     

     

     

    📑 스프링 시큐리티 용어와 흐름

     

    프로젝트를 실행하고 '/sample/all'과 같은 경로를 호출하면 시큐리티로 인해 로그인 화면이 보이는 것을 확인할 수 있다. 서버의 로그를 살펴보면 내부적으로 여러 개의 필터가 동작하는 것을 확인할 수 있다. 

     

    스프링 시큐리티 간단한 흐름

     

    핵심 역할은 Authentication Manager(인증 매니저)를 통해 이루어진다. Authentication  Provide는 인증 매니저가 어떻게 동작해야 하는지를 결정하고 최종적으로 실제 인증은 UserDetailsService에서 이루어진다. 

     

    스프링 시큐리티를 관통하는 가장 핵심 개념은 인증(Authentication)과 인가(Autherization)이다. 쉽게 이해하기 위해 간단한 예를 들어보면 은행에 금고가 하나 있고, 사용자가 금고의 내용을 열어본다고 가정해보자.

     

    1. 사용자는 은행에 가서 자신이 어떤 사람인지 자신의 신분증으로 자신을 증명한다.
    2. 은행에서는 사용자의 신분을 확인한다.
    3. 은행에선 사용자가 금고가 열어 볼 수 있는 사람인지 판단한다.
    4. 만일 적절한 권리나 권한이 있는 사용자의 경우 금고를 열어준다.

     

    위의 과정 중 1은 인증(Authentication)에 해당하는 작업으로 자신을 '증명'하는 것이다. 3에서는 사용자를 '인가'하는 일종의 허가를 해주는 과정이다. 

     

     

     

    📌 필터와 필터 체이닝 

     

    스프링 시큐리티에서 필터는 서블릿이나 JSP에서 사용하는 필터와 같은 개념이다. 다만, 스프링 시큐리티에서는 스프링의 빈과 연동할 수 있는 구조로 설계되어 있다. 일반적인 필터는 스프링의 빈을 사용할 수 없기 때문에 별도의 클래스를 상속받는 형태가 많다. 

     

    스프링 시큐리티의 내부에는 여러 개의 필터가 Filter Chain이라는 구조로 Request를 처리하게 된다. 앞에서 실행 시 로그를 보면 15개 정도의 필터가 동작하는 것을 볼 수 있는 것과 같다. 개발 시에 필터를 확장하고 설정하면 스프링 시큐리티를 이용해 다양한 형태의 로그인 처리가 가능하게 된다. 

     

     

     

    📌 인증을 위한 AuthenticationManager

     

    필터의 핵심적인 동작은 AuthenticationManager를 통해 인증이라는 타입의 객체로 작업을 하게된다. 흥미롭게AuthenticationManager가 가진 인증 처리 메서드는 파라미터도 Authentication 타입으로 받고 리턴 타입 역시 Authentication 이다. 

     

    인증(Authentication)을 쉽게 이해하려면 '주민등록증'과 비슷하다고 생각하면 된다. '인증'이라는 용어는 '스스로 증명하다'라는 의미이다. 예를 들어 로그인하는 과정에서는 사용자의 아이디/패스워드로 자신이 어떤 사람인지를 전달한다. 전달된 아이디/패스워드로 실제 사용자에 대해 검증하는 행위는 Authentication Manager(인증 매니저)를 통해 이뤄진다. 

     

    실제 동작에서 전달되는 파라미터는 UsernamePasswordAuthentication Token과 같이 토큰이라는 이름으로 전달된다. 이 사실이 의미하는 바는 스프링 시큐리티 필터의 주요 역할이 인증 관련된 정보를 토큰이라는 객체로 만들어서 전달하는 의미이다. 

     

     

    📌 인가(Autherization)와 권한/접근 제한 

     

    인증처리 단계가 끝나면 다음으로 동작하는 부분은 '사용자의 권한이 적절한가?'에 대한 처리이다. 인가(Autherization)는 '승인'의 의미이다. 인증이 사용자가 스스로 자신을 증명하는 것이라면 인가는 '허가'의 의미이다.

     

    필터에서 호출하는 Authentication Manager에는 authenticate()라는 메서드가 있는데 이 메서드의 리턴값은 Authentication이라는 '인증'정보이다. 이 인증 정보 내에는 Roles라는 '권한'에 대한 정보가 있다. 이 정보로 사용자가 원하는 작업을 할 수 있는지 '허가'하게 되는데, 이러한 행위를 Access-Cotnrol(접근 제한)이라고 한다. 일반적인 경우라면 설정으로 원하는 목적지에 접근 제한을 걸고, 스프링 시큐리티에 이에 맞는 인증을 처리한다. 

     


     

    위에서 설명할 내용을 실제 결과에 이어 생각해보자.

     

     

    ✔ 1. 사용자는 원하는 url을 입력한다. 
    -> localhost:8082/sample/all

     

     

      2. 스프링 시큐리티에서는 인증/인가가 필요하다고 판단하고(필터에서 판단) 사용자가 인증하도록 로그인 화면을 보여준다. 

     

     

    만약 id와 password가 일치하지 않는 경우 아래와 같은 페이지가 뜬다. 

     

     

     

    ✔ 3. 정보가 전달되면 AuthenticationManager가 적절한 AuthenticationProvider를 찾아서 인증을 시도한다. 

     

    AuthenticationProvider의 실제 동작은 UserDetailsService를 구현한 객체로 처리한다. 만약 올바른 사용자라고 인증되면 사용자의 정보를 Authentication 타입으로 전달한다. 전달된 객체로 사용자가 적절한 권한이 있는지 확인하는 '인가(Autherization)' 과정을 거치게 된다. 이때 문제가 없음 처음에 요청했던 페이지 화면을 볼 수 있다. 

     

     

     

     

     

    📑 스프링 시큐리티 커스터마이징 

     

    위에 실행 결과를 보면 별도의 설정 없이 기본적으로 스프링 시큐리티는 동작하지만 개발 시에는 적절하게 인증 방식이나 접근 제한을 지정할 수 있어야 된다. 

     

    앞에 만들어둔 SecurityConfig 클래스를 이용해 override로 이러한 동작을 제어할 수 있다. 

     

     

     

    📌 passwordEncoder

     

    가장 먼저 설정이 필요한 것은 passwordEncoder라는 객체이다. passwordEncoder는 말 그대로 패스워드를 인코딩하는 것인데 주목적은 패스워드를 암호화하는 것이다. 스프링부트 2.0부터는 인증을 위해 반드시 passwordEncoder를 지정해야한다. 

     

    passwordEncoder는 인터페이스로 설계되어 있으므로 실제 설정에서는 이를 구현하거나 구현된 클래스들을 이용해야한다. 스프링 시큐리티는 여러 종류의 passwordEncoder를 제공하고 있는데 그중에서 가장 많이 사용하는 것은 BCryptpasswordEncoder라는 클래스이다. 

     

    BCryptpasswordEncodersms 'bcrypt'라는 해시 함수르 이용해 패스워드를 암호화하는 목적으로 설계된 클래스이다. BCryptpasswordEncoder로 암호화된 패스워드는 다시 원래대로 복호화가 불가능하고 매번 암호화된 값도 다르게 된다. 대신 특정한 문자열이 암호화된 결과인지만을 확인할 수 있기 때문에 원본 내용을 볼 수 없으므로 최근에 많이 사용되고 있다. 

     

    <SecurityConfig.java>

    package com.wish.securityPractice.config;
    
    import lombok.extern.log4j.Log4j2;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    @Log4j2
    public class SecurityConfig {
    
        @Bean
        PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }

     

     

    BCryptpasswordEncoder를 이용하는 암호화는 문자열로 알아보기 어렵기 때문에 테스트 코드로 미리 어떤 값들을 사용할 수 있는지 확인해 두는 것이 좋다. 

     

    test 폴더 내에 security 패키지를 생성하고, PasswordTests라는 테스트 클래스를 작성한다. 

     

     

    <PasswordTests.java>

     

    package com.wish.securityPractice;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @SpringBootTest
    public class PasswordTests {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Test
        public void testEncode(){
    
            String password = "1111";
    
            String enPw = passwordEncoder.encode(password);
            System.out.println("enPw : " + enPw);
            //matches로 암호화한 결과가 '1111'에 맞는지 확인하는 것이다.
            boolean matchResult = passwordEncoder.matches(password, enPw);
            System.out.println("matchResult: " + matchResult);
    
        }
    }

     

     

    testEncode()는 내부적으로 '1111'이라는 문자열을 암호화하고, 해당 문자열을 암호화한 결과가 '1111'이 맞는지(match)확인하는 것이다. testEncode()를 실행하면 매번 다른 결과가 만들어지는 것을 확인할 수 있다. 

     

     

    📌 AuthenticationManager  설정

     

    암호화된 패스워드를 이용하기 위해 해당 암호를 사용하는 사용자가 필요하다. 우선적으로 코드를 통해 사용자 계정을 지정하고 이를 InMemoryUserDetailsManager 타입의 객체를 생성하도록 한다. 

     

     

    <SecurityConfig.java>

     

    @Bean
    public InMemoryUserDetailsManager userDetailsManager(){
        UserDetails user = User.builder()
                .username("user1")
                .password(passwordEncoder().encode("1111"))
                .roles("USER")
                .build();
    
        log.info("userDetailsService...............");
        log.info(user);
    
        return new InMemoryUserDetailsManager(user);
    }

     

    추가된 userDetailsService()는 InMemoryUserDetailsManager 타입의 객체를 생성하는데 InMemoryUserDetailsManager 는 말 그대로 메모리상에 있는 데이터를 이용하는 인증 매니저(AuthenticationManager)를 생성한다. 단순 테스트 용으로 적합하다. 

     

    User.builder()build()를 이용해 작성하고 'user1/1111' 사용자르 생성하고 권한에는 'USER'라는 권한을 지정한다. 

     

    처음 'localhost:8082/sample/all'를 호출했을 때 로그인창으로 넘어가고 user1/1111이라는 계정으로 로그인하면 sample/all페이지로 접속이 가능해진다. 

     

     

     

     

     

    📌 인가(Authorization)가 필요한 리소스 설정

     

     

    스프링 시큐리티를 이용해 특정한 리소스(자원-웹의 경우 특정한 URL)에 접근 제한을 하는 방식은 크게 2가지가 있다

     

    ✔ 설정을 통해 패턴을 지정
    ✔ 어노테이션을 이용해 적용하는 방법

     

    어노테이션을 이용하는 방식이 더 간단하지만 우선 SecurityConfig 클래스로 설정해본다. SecurityConfig 클래스에는 SecurityFilterChain을 반환하는 메서드를 아래와 같이 구성해 접근 제한을 처리할 수 있다. 

     

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeHttpRequests((auth) -> {
            auth.antMatchers("/sample/all").permitAll();
            auth.antMatchers("/sample/member").hasRole("USER");
        });
    
        //인가/인증 문제시 로그인 화면
        //별도의 디자인을 적용하려면 loginPage()를 많이 이용한다.
        http.formLogin();
    
        return http.build();
    }

     

     

    추가된 filterChain(HttpSecurity http)에는 http.authorizeHttpRequest()로 인증이 필요한 자원들을 설정할 수 있고, antMatchers()는 '**/*'와 같은 앤트 스타일의 패턴으로 원하는 자원을 선택할 수 있다. permitAll()은 말 그대로 모든 사용자에게 허락한다는 의미이므로 로그인하지 않은 사용자도 익명의 사용자로 간주되어서 접근이 가능하게 된다. 

     

    HttpSecurity의 객체 대부분의 경우 연속적으로 '.'를 이용해 처리하는 빌더(builder) 방식의 구성이 가능하다. 이를 통해 추가적 자원에 대한 설정을 위와 같이 사용할 수 있다. '/sample/member'라는 경로에는 'USER'라는 권하이 있는 사용자만이 사용할 수 있도록 구성하였다. 

     

    또한, http.formLogin();이라는 기능은 인가/인증 절차에서 문제가 발생했을 때 로그인 페이지를 보여주도록 지정할 수 있고, 화면으로 로그인 방식을 지원한다는 의미로 사용된다. formLogin()을 이용하는 경우 별도의 디자인을 적용하기 위해 추가적인 설정이 필요하다. loginPage()나 loginProcessUrl() ,,, 등을 이용해 필요한 설정을 지정할 수 있다. 

     

     

     

     

     

     

     

     

     

     

     


    [참고]

    코드로 배우는 스프링 부트 웹 프로젝트 서적

    반응형

    댓글

Designed by Tistory.