Spring Boot 3 & Spring Framework 6 - Section 9 :

이정수·2024년 8월 5일

Udemy학습-Spring & React

목록 보기
13/20

Spring Security 활용

Access Control Check :
。Client가 Application의 특정 Resource에 접근할 수 있는지를 확인하는 보안절차.
▶ 보안정책을 준수하고 권한이 없는 Client로부터의 Access를 차단하는 중요한 역할을 수행.

Access Control Check의 주요개념

  • 인증 ( Authentication ) :
    。사용자의 신원을 증명하는 과정 ( ex. ID/PW 로그인 , JWT 등 )
    ▶ 로그인 정보를 받아 인증된 Authentication을 구축.

  • 인가 ( Authorization ) :
    。Authenticated된 사용자가 Server의 특정 Resource에 접근가능한지 확인하는 과정.

Spring Security 학습 내용
로그인 양식 하드코딩
MVC , Spring MVC
Spring Security - 1 : 로그인 양식 사용 설정
Spring Security - 2 : Filter Chain 관련 설정
Spring Security - 3
Spring Securty - 기초 및 원리

  • Spring Security를 활용한 로그인 양식 작성하기.
    。Spring Security는 기본적으로 log-in, log-out의 전용 URL을 제공.
    ▶ 따로 log-in , log-out의 url 및 JSP page를 구현할 필요는 없다.
    하드코딩 : 로그인 양식
    • 기존 하드코딩된 로그인 기능은 Spring Security가 담당 SpringSecurity - DB로그인기능설정
      login.jsp, AuthenticationService.java 등 하드코딩 로그인 기능 관련 파일 삭제.
      。필요한 source : welcome.jsp , 해당 jsp를 Mapping 하는 Controller ( gotoLoginPage.java )
     // gotoLoginPage.java
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.ModelMap;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.SessionAttributes;
    @Controller
    @SessionAttributes("name")
    public class gotoLoginPage {
        @RequestMapping(value="/", method=RequestMethod.GET)
        public String gotoLoginPageGET(ModelMap model) {
            model.addAttribute("name","wjdtn747");
            return "welcome";
        }
    }

    welcome.jspForm Backing Object에 의한 Data Binding에 의해 Spring Bean이 필요하므로 임시로 Model 객체을 생성해서 바인딩 되도록함.



  • Spring Security dependency 추가하기
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
   </dependency>

。 dependency를 추가 후 spring application을 재실행 시 console의 log창에 개발용 자격증명의 PW가 생성되어 표시.

Spring Security Framework의 dependency를 추가하는 경우 Spring application의 Controller가 mapping하는 모든 url로 연결 시 화면과 같은 로그인 창이 도출.

  • Controller Method에 의해 Mapping된 모든 url은 Spring Security에 의해 보호되고있으므로, 자격증명에 log-in을 해야 사용이 가능함.

    Spring Security의 개발용 자격증명

    • Spring Security는 해당 Framework의 Dependency만 정의하더라도 Controller에 의해 Mapping된 URL 접속 시 Spring Security의 기본인증폼을 전송하며, Auto-Configuration에 의해 다음 개발용 자격증명을 자동 생성.
      Console에 표시된 PW는 Application이 재실행될때마다 초기화
      。ID : user
      。PW : console에 표시된 PW

    • 개발용 자격증명의 Username과 Password 변경방법
      application.properties에 다음 구문을 추가.
    spring.security.user.name=user123
    spring.security.user.password=pw456

    。해당 구문 추가 후 로그인 시 변경된 개발용 자격증명에 로그인이 가능.
    ▶ 해당 application.properties에서 정의된 개발용자격증명의 경우 In-Memory DB에 저장되므로, TEST 용도로만 활용된다.



  • 사용자자격증명 ( User Credential ) 저장소
    • In-Memory DB :
      。개발용 Local DB로 사용이 용이한 RDMBS.
      Memory에 저장되는 방식으로 Application 재가동 시 초기화되는 특징을 지니므로 Production 환경에 적용하기에는 매우 부적합한 특징을 지님.
      Local 또는 Test 환경에서 주로 사용됨.

    • DB :
      JDBC 또는 JPA를 통해 DB로 자격증명을 Access 하도록 설정.

    • LDAP ( Lightweight Directory Access Protocol )
      Directory Service에 접근하기위한 Protocol.
      ▶ 사용자, 그룹, 네트워크장비 등의 정보를 중앙에서 관리할 수 있도록 도와주는 프로토콜.

      Directory Service : 조직 내 사용자, 부서, 프린터, 서버 등의 정보를 저장하고 검색하는 서비스

      LDAP 특징

      • 중앙 집중식 사용자 관리
        。사용자 계정 및 권한을 한 곳에서 관리

      • Tree 구조 ( 계층적 데이터모델 ) :
        。조직 내 계정, 그룹 등을 체계적으로 저장.

      • 경량 프로토콜 :
        TCP/IP 기반에서 빠르고 효율적으로 동작.

      • 인증 및 권한 부여.
        。로그인 인증, 접근 제어 등에 활용됨.


  • 사용자가 임의로 지정한 ID와 PW로 계정을 생성하여 Spring Security 접근하기.
    。 ID, PW 등의 정보를 UserDetails instance에 저장 및 해당 instance를 저장한 InMemoryUserDetailsManager instance를 생성하여 Spring Bean instance로서 설정하여 로그인 계정 생성.
    ▶ 임의의 사용자 지정 ID와 PW를 저장 시 LDAP 혹은 DB를 활용해야하지만, 현재 과정에서는 InMemoryUserDetailsManager을 사용.
    • User.Builder()UserDetails의 instance 생성
      InMemoryUserDetailsManager 객체 생성
      @Configuration Class를 생성 후 InMemoryUserDetailsManager instance를 생성하여 Spirng Bean으로 return하는 @Bean Method를 생성
      InMemoryUserDetailsManager()의 생성자는 가변매개변수를 받을 수 있으므로, 여러개의 UserDetails instance를 넣을수 있음.
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    @Configuration // Spring Bean을 생성 후 return 하는 역할.
    public class SpringSecurityConfiguration {
        @Bean
        public InMemoryUserDetailsManager createUserDetailsManager(){
            UserDetails userDetails =
                    User.builder()
                    .passwordEncoder(input -> passwordEncoderB().encode(input))
                    .username("wjdtn")
                    .password("123456")
                    .roles("USER","ADMIN") // 특정 작업을 수행하기 위한 역할 설정
                    .build();
            return new InMemoryUserDetailsManager(userDetails);
        }
        @Bean
        public PasswordEncoder passwordEncoderB(){
            return new BCryptPasswordEncoder();
        }
    }

    UserDetails instance 생성 시 .passwordEncoder(람다식)을 통해 passwordEncoderB()에 의해 BCrypt 알고리즘으로 비밀번호가 인코딩되는 람다식을 정의하여 PasswordEncoder 정의.

    User Class를 통해 UserDetails instance를 생성 후 InMemoryUserDetailsManager의 생성자매개변수로 전달된 InMemoryUserDetailsManager instance를 생성하여 Spring Bean instance로 return.

    。 이후 Spring Application을 실행하여 해당 생성된 username과 password을 입력 시 정상적으로 로그인됨.

    • 새로운 사용자 계정을 추가하기
      Extract Method 기능을 수행하여 UserDetails의 instance를 생성하는 method를 하나 따로 구축 한 후 각각의 ID,PW를 담은 UserDetails 객체를 생성하여 new InMemoryUserDetailsManager()의 생성자 가변매개변수에 추가하기.
      @Bean
        public InMemoryUserDetailsManager createUserDetailsManager(){
            return new InMemoryUserDetailsManager(
                    getUserDetails("wjdtn1","123456"),
                    getUserDetails("wjdtn2","123456"));
        }
        private UserDetails getUserDetails(String username ,String password) {
            UserDetails userDetails =
                    User.builder()
                    .passwordEncoder(input -> passwordEncoder().encode(input))
                    .username(username)
                    .password(password)
                    .roles("USER","ADMIN") // 특정 작업을 수행하기 위한 역할 설정
                    .build();
            return userDetails;
        }
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    // 생성한 자격증명의 role 지정 용도의 Enum
    enum roles { ADMIN,USER }
    @Configuration
    public class SpringSecurityConfig {
        @Bean
        public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
            // User Class의 User.roles() 설정용 Role은 Enum으로 설정하는게 좋다.
            roles role1 = roles.USER;
            roles role2 = roles.ADMIN;
            return new InMemoryUserDetailsManager(
                getUserDetails("wjdtn1","1234", role1),
                    getUserDetails("wjdtn2","4567", role2)
            );
        }
        private UserDetails getUserDetails(String username, String password,roles role){
            return User.builder().passwordEncoder((input)->
                passwordEncoderB().encode(input)
            ).username(username).password(password).roles(role.toString()).build();
        }
        @Bean
        public PasswordEncoder passwordEncoderB() {
            return new BCryptPasswordEncoder();
        }
    }

    User객체.roles(역할) 설정 시 Enum을 활용하여 Role을 지정하는게 좋다.
    Enum객체.toString()으로 문자열로 변경하여 지정.



  • Controller method의 Model에 하드코딩된 문자열ID가 아닌 InMemoryUserDetailsManager의 ID로 Refactoring 하여 표현하기.
    Spring SecuritySecurityContextHolder 활용.
    • 실행중인 thread의 SecurityContextAuthentication instance에서 username를 가져오는 Method 생성
      Process : System에서 실행중인 프로그램을 의미
      thread : 하나의 Process 내부에서 독립적으로 실행되는 작은 실행 단위.
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
      private String getLoggedinUsername(){
            Authentication authentication = 
                    SecurityContextHolder.getContext().getAuthentication();
            // 실행중인 thread의 SecurityContext의 Authentication을 return.
            return authentication.getName(); // 사용자 ID Return. 
     }
    • Controller methodmodel.put()에 하드코딩으로 기입한 문자열ID를 실행중인 thread의 Authentication instance에서 추출한 ID로 대체하기
             // gotoLoginPage.java
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.ModelMap;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.SessionAttributes;
    @Controller
    @SessionAttributes("name")
    public class gotoLoginPage {
        @RequestMapping(value="/", method=RequestMethod.GET)
        public String gotoLoginPageGET(ModelMap model) {
            model.addAttribute("name",getLoggedinUsername());
            return "welcome";
        }
        private String getLoggedinUsername(){
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            return auth.getName();
        }
    }

    localhost:8080/으로 접속 시 getLoggedinUsername() method로 실행중인 thread의 Authentication instance에서 추출한 ID값을 @SessionAttribute에 의해 Model에 "name"이라는 변수명으로 Session에 저장하여 다른 Controller Class의 Model에 전달되어 사용이 가능.

    。기존 하드코딩된 ID로 Model에 작성 시 다른 url로(localhost:8080/list-todo) 최초 접속 시에는 Session에 해당 변수가 Model로부터 생성되지않아 전달되지 않으므로, 다른 Controller에서의 Model은 해당 변수를 사용이 불가능하다.
    ▶ 다른 URL을 Mapping하는 Controller ClassController Method에 기존 Model의 하드코딩 부분을 Extract Method( Method로 추출하는 기능 : getLoggedinUsername() )를 생성하여 Spring Security를 이용하여 Authentication에서 추출한 ID로 대체하여 사용.
    ▶ 이때, Extract Method getLoggedinUsername()는 해당 Controller methodModel instance를 매개변수로 받아야한다.

         // 수정 전
     @RequestMapping("list-todo")
        public String ListAllTodos(ModelMap model){
            List<Todo> todos = todoService.findByUsername("wjdtn747");
            model.put("todos",todos);
            return "listTodos";
        }
        // 수정 후
      @RequestMapping("list-todo")
        public String ListAllTodos(ModelMap map){
            String userName = getLoggedInUserName(map);
            List<Todo> todos = todoService.findByUsername(userName);
            map.addAttribute("todos",todos);
            return "listTodos";
        }
               // Extract Method에 Controller method의 Model instance를 매개변수로 받아옴.
        private static String getLoggedInUserName(ModelMap map) {
            Authentication authentication =
                    SecurityContextHolder.getContext().getAuthentication();
            return authentication.getName();
        }

    "wjdtn1" 이라는 username을 가진 Spring Bean( = Todo instance )들이 Static List에서 정상적으로 조회됨.

    • Authentication :
      。사용자의 인증 정보를 지시하는 Interface.
      ▶ 접근하는 해당 사용자의 정보와 권한을 담음.

      Authentication 객체는 인증 시 id와 password를 포함하여 인증 검증을 위해 전달되며 인증이 성공할 경우 해당 시스템에 대한 액세스 권한을 부여받고 SecurityContext에 저장 및 전역적으로 참조가 된다.

      Authentication객체.getName() :
      사용자 id를 return.

      Authentication interface 구조

      • principal : 사용자 ID
      • credentials : 사용자 PW
      • authorities : 사용자가 보유한 권한 목록
      • details : 인증에 관한 부가 정보
      • Authenticated : boolean type으로서 인증 여부


    • SecurityContext :
      。인증된 Authentication instance의 저장소
      。해당 Context는 ThreadLocal에 저장 및 동일 Thread일 경우 전역적으로 참조됨.

      Thread Local :
      。Java에서 지원하는 Thread Safe한 기술로서 멀티스레드 환경에서 각각의 Thread에게 별도 저장공간을 할당하여 필요한 정보를 저장할 수 있도록 설정.

    • SecurityContextHolder :
      SecurityContext의 세부정보를 저장 및 추출할 수 있는 기능을 제공.

      SecurityContextHolder.getContext().getAuthentication() :
      。실행중인 threadSecurityContextAuthentication instance를 return.
      • SpringContextHolder를 통해 SecurityContext에 접근
      • SecurityContext를 통해 Authentication에 접근.
profile
공부기록 블로그

0개의 댓글