본문 바로가기

Server Programming/Spring Boot Backend Programming

[Spring 부트 - 운동 클럽 프로젝트] 1. 스프링 시큐리티 연동 (1) 기본 설정

728x90
반응형

진행 순서

  • 스프링 시큐리티를 연동하기 위한 의존성 추가
  • 스프링 시큐리티 연동을 위한 타임리프 확장 플러그인
  • +) 시큐리티의 자세한 로그 파악을 위해 낮은 로그 설정 
  • 스프링 시큐리티 동작 이해를 위한 핵심 개념의 이해
    1. SecurityConfig를 통한 프로젝트의 시큐리티 설정 관리
    2. 필터와 인증/인가 관리 객체를 이용한 스프링 시큐리티 동작
    3. 스프링 시큐리티의 인증
      1. 필터 체인 구조와 인증 매니저
      2. UserNamePasswordAuthenticationToken 토큰을 이용해 인증매니저가 AuthenticationProvider를 통해 인증
      3. AuthenticationProvider가 인증 확인 후, UserDetailService를 통해 실제 인증을 위한 데이터 전달
    4. 스프링 시큐리티의 인가
      1. Authentication의 Roles 정보를 통해 Access-control
      2. Access-control를 통해 원하는 목적지에 접근 제한을 걸고, 이에 맞는 인증을 처리
  • 인증과 인가를 위한 스프링 시큐리티 커스터마이징 설정
    1. InMemoryUserDetailsManager 객체 이용한 인증 매니저 설정
    2. SecurityFilterChain 객체 이용한 자원의 접근권한 설정

 


의존성 추가

  • Spring Boot DevTools
  • Lombok
  • Spring Data JPA
  • Oracle Driver
  • Spring Web
  • Thymeleaf
  • Spring Security
  • OAuth2 Client

 

추가 Thymeleaf 확장 플러그인

  <!-- 타임리프 날짜시간의존성-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
<!-- 타임리프 시큐리티의존성-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

 

추가 로그 설정

logging.level.org.springframework.security.web=trace
logging.level.com.club=debug
#시큐리티 관련 부분의 로그 레벨을 낮게 설정해 자세한 로그를 확인할 수 있도록 설정


스프링 시큐리티 사용시 임시 비밀번호가 생성된다.

-> 기본으로 생성되는 user 계정의 패스워드

 

Using generated security password: 25e3e6cc-dc8c-4326-a735-d90*****58b7

This generated password is for development use only. Your security configuration must be updated before running your application in production.

 

더보기

브라우저에서 강제 로그아웃을 원할 땐,

브라우저 내의 개발자도구에서 쿠키 중 톰캣이 사용하는 JSESSIONID를 삭제하면 로그아웃 처리가 된다.

 


시큐리티 설정 클래스

  • 스프링 부트의 자동 설정 기능으로 연동처리가 수행된다.
  • 스프링 시큐리티를 사용한다면, 해당 프로젝트에 맞는 설정을 추가해주는 것이 바람직하다.
    • 프로젝트 내에 config 패키지를 추가해, SecurityConfig 클래스를 작성
    • 이전의 WebSecurityConfigurerAdaptor의 deprecated로 빈등록을 통해 설정을 관리한다.
    • 시큐리티 관련 기능의 쉬운 설정을 위해 WebSecurityConfigurerAdaptor 클래스를 상속으로 처리
    • override를 통해 여러 설정을 조정한다.

SecurityConfig

package com.club.boot5.config;

import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@Log4j2
public class SecurityConfig {

}

 

권한에 따른 접근 단계를 구분

  • 로그인을 하지 않은 사용자도 접근할 수 있는 ‘/sample/all
  • 로그인한 사용자만이 접근할 수 있는 ‘/sample/member
  • 관리자(admin) 권한이 있는 사용자만이 접근할 수 있는 ‘/sample/admin

각자 접근 가능한 페이지 작성

all.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <h1>For ALL</h1>
</body>
</html>

member.html

<!DOCTYPE html>
<html xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>For Member ....................</h1>

<div sec:authorize="hasRole('USER')">Has USER ROLE</div>
<div sec:authorize="hasRole('MANAGER')">Has MANAGER ROLE</div>
<div sec:authorize="hasRole('ADMIN')">Has ADMIN ROLE</div>

<div sec:authorize="isAuthenticated()">
  Only Authenticated user can see this Text
</div>

Authenticated username:
<div sec:authentication="name"></div>
Authenticated user roles:
<div sec:authentication="principal.authorities">

</div>

</div>

</body>
</html>

 

admin.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>For Admin</h1>
</body>
</html>

스프링 시큐리티의 동작 원리 이해

:'/sample/all' 경로 호출시 서버 로그 중심 확인

 

 

'/sample/all' 호출시 여러 개의 필터가 동작

2022-10-19 12:30:37.046 DEBUG 25408 --- [nio-8080-exec-3] o.s.s.w.s.HttpSessionRequestCache        : Saved request http://localhost:8080/sample/all to session
2022-10-19 12:30:37.047 DEBUG 25408 --- [nio-8080-exec-3] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.HeaderContentNegotiationStrategy@108eb536, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
2022-10-19 12:30:37.048 DEBUG 25408 --- [nio-8080-exec-3] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@ec2f3a5
2022-10-19 12:30:37.048 DEBUG 25408 --- [nio-8080-exec-3] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/login
2022-10-19 12:30:37.048 TRACE 25408 --- [nio-8080-exec-3] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]
2022-10-19 12:30:37.048 DEBUG 25408 --- [nio-8080-exec-3] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.049 DEBUG 25408 --- [nio-8080-exec-3] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.049 DEBUG 25408 --- [nio-8080-exec-3] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-10-19 12:30:37.057 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@2c2a0092, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@391bcb2a, org.springframework.security.web.context.SecurityContextPersistenceFilter@3769ae23, org.springframework.security.web.header.HeaderWriterFilter@51201289, org.springframework.security.web.csrf.CsrfFilter@3c82494a, org.springframework.security.web.authentication.logout.LogoutFilter@f84b383, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@77a8a715, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@12c59e15, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@5e2184ec, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@c46ebf0, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7141d341, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@fcad42c, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@11d26668, org.springframework.security.web.session.SessionManagementFilter@12e0cc6c, org.springframework.security.web.access.ExceptionTranslationFilter@41004dfe, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@42a13c94]] (1/1)
2022-10-19 12:30:37.057 DEBUG 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Securing GET /login
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking SecurityContextPersistenceFilter (3/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Did not find SecurityContext in HttpSession DC3B3206E2410D7707E2B8FE9BCFDA10 using the SPRING_SECURITY_CONTEXT session attribute
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Created SecurityContextImpl [Null authentication]
2022-10-19 12:30:37.058 DEBUG 25408 --- [nio-8080-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking CsrfFilter (5/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.csrf.CsrfFilter         : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking LogoutFilter (6/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.s.w.a.logout.LogoutFilter            : Did not match request to Ant [pattern='/logout', POST]
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking UsernamePasswordAuthenticationFilter (7/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] w.a.UsernamePasswordAuthenticationFilter : Did not match request to Ant [pattern='/login', POST]
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Invoking DefaultLoginPageGeneratingFilter (8/16)
2022-10-19 12:30:37.059 TRACE 25408 --- [nio-8080-exec-4] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]
2022-10-19 12:30:37.059 DEBUG 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.060 DEBUG 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.060 DEBUG 25408 --- [nio-8080-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

스프링 시큐리티의 필터 동작

 

: 스프링 시큐리티 동작시 여러 개의 객체가 서로 데이터를 주고 받으면서 이루어진다.

-> 필터와 AuthenticationManager 등의 객체 이용

 

핵심 동작은 Authentication Manager (인증 매니저)를 통해 이루어지는데

Authentication Provider가 인증 매니저가 어떻게 동작하는지 결정하고, 실제 인증은 UserDetailsService에 의해 이루어진다.

 

 

스프링 시큐리티의 인증과 인가

: 사용자가 은행에 있는 금고의 내용을 열어보는 과정과 유사

  1. 사용자는 은행에 가서 신분증으로 자신을 증명한다. -인증
  2. 은행에서 신분증을 통해 사용자의 신분을 확인한다.
  3. 은행에서 신분증을 통해 해당 금고의 접근 권한 확인 -인가
  4. 접근권한이 있을 경우 금고를 열어준다.

필터와 필터 체이닝

: 서블릿이나 JSP에서 사용하는 필터와 유사하지만, 스프링의 빈과 연동할 수 있는 스프링 시큐리티의 필터

-> 스프링 시큐리티와 다르게 일반적인 필터는 스프링의 빈을 사용하지 못해 별도 클래스를 상속하는 형태로 이루어짐

 

스프링 시큐리티 내부에서 여러 개의 필터가 필터 체인 구조로 요청을 처리한다.

-> 개발 시에 필터를 확장하고 설정시, 다양한 형태의 로그인 처리가 가능하다.

 

 

 

인증을 위한 AuthenticationManager가 가진 인증 처리 메서드

: 파라미터도 Authentication, 리턴 타입도 Authentication

-> 로그인 과정에서 실제 사용자 검증 행위가 이루어지는 곳

 

UserNamePasswordAuthenticationToken

: 실제 동작에서 전달되는 파라미터로 인증 관련된 정보를 토큰이라는 객체로 만들어서 전달

  1. request를 통해 사용자 정보를 받아 UserNamePasswordAuthenticationToken 객체 생성
  2. AuthenticationManager의 authenticate()에 파라미터로 전달
  3. 인증매니저는 다양한 방식의 인증처리 방법을 제공한다.
    1. DB 이용
    2. 메모리상 정보 이용
  4. 인증매니저는 해당 인증을 AuthenticationProvider로 처리한다.
  5. AuthenticationProvider가 토큰 타입 처리를 확인 후, authenticate() 수행
  6. AuthenticationProvider가 UserDetailService를 이용해 실제 인증 위한 데이터를 가져온다.
    • JPA로 리포지토리 제작시 UserDetailsService를 활용해 사용자 인증 정보를 처리한다.


인가의 권한과 접근 제한

: 인증 단계 이후 사용자의 권한이 적절한가? 확인 후 인가 처리

 

  1. 인증 처리 단계 후, 필터에서 호출하는 인증매니저의 authenticate() 메서드가 리턴하는 Authentication 인증정보의 Roles 속성의 권한 정보를 통해 접근 제한 처리를 수행한다.
    : Access-control
  2. 원하는 목적지에 접근 제한을 걸고, 스프링 시큐리티를 통해 인증을 처리한다.

인증과 인가 진행 순서

  1. 사용자가 원하는 URL 접근
  2. 요청한 URL에 알맞는 권한을 가진 사용자인지 인증 절차 수행 [로그인]
  3. 사용자가 계정정보를 통해 정보 전달
  4. 인증매니저가 전달된 정보를 통해 적절한 AuthenticationProvider를 찾아 인증
  5. AuthenticationProvider가 UserDetailsService를 구현한 객체로 처리하는데,
    올바른 사용자라고 인증되면 사용자의 정보를 Authentication 타입으로 전달하는 것으로 인증 처리
  6. 인증 처리가 완료되면 전달받은 객체를 이용해 해당 URL에 인증된 정보에 접근 권한을 가지고 있는지 확인하는 인가 처리

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

: SecurityConfig를 통해 적절한 접근 권한 관리

-> 빈 등록을 통해 동작을 제어한다.

 

사용할 커스터마이징

  1. PasswordEncoder
    1. 패스워드 암호화 수행
    2. bcrypt 해시 함수를 이용한 암호화를 수행하는
      BCryptPasswordEncoder를 구현해서 사용

 

 

 

PasswordEncoder로 BCryptPasswordEncoder를 구현해서 비밀번호 암호화를 수행

  • BCryptPasswordEncoder
    1. bcrypt 해시 함수를 이용한 암호화
    2. 암호화된 패스워드는 복호화 불가능 매번 암호화된 값이 다르다
    3. 값은 다르지만 길이는 똑같다.
    4. 원본 내용을 볼 수 없고, 특정 암호화된 문자열의 결과값인지만 확인 가능
package com.club.boot5.config;

import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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 테스트

: 암호화를 문자열로 확인 불가능하므로, 어떤 값들을 사용할 수 있는지 확인하는 테스트

package com.club.boot5.security;

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);
        
        boolean matchResult = passwordEncoder.matches(password, enPw);

        System.out.println("matchResult : "+matchResult);
    }
}

 

결과

enPw : $2a$10$wO8xgibV2LVtJScXhtFLtOXkM4jppH9/gjPIsVjsHWFkQ6ukIzw4.
matchResult : true
  1. matchResult를 통해 암호화한 비밀번호가 실제 비밀번호와 같은지 확인
  2. 매번 암호화된 비밀번호 결과가 다르기 때문에 확인하는 matches메서드를 통해 확인 처리가 필요하다.
  3. 암호화된 패스워드를 이용해 로그인 과정에서 사용한다.

 


AuthenticationManager 설정

: 빈을 통해 InMemoryUserDetailsManager를 반환하는  userDetailsService() 메서드를 등록시켜서 직접 인증 매니저를 설정한다.

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

        log.info("userDetailsService............................");
        log.info(user);

        return new InMemoryUserDetailsManager(user);
    }

-> 최소한의 코드로 로그인을 확인하는 InMemoryUserDetailsManager로 사용자를 생성해서 테스트를 진행한다.

생성한 사용자가 가지는 정보

  • username
  • password
  • roles

시큐리티 필터 체인을 반환하는 필터 체인 메서드를 이용해 필터를 설정

    //시큐리티 필터체인을 반환형으로
    //HttpSecurity 객체인 http의 authorizeHttpRequests 메서드로 인가 절차를 수행하는데,
    //앤트 스타일 패턴으로 원하는 자원을 선택 -> AuthenticationManagerBuilder 객체인 auth를 이용해 선택
    //antMatchers() : 원하는 자원을 선택할 수 있다. -> 접근 제한 페이지 관리
    //hasRole() : 권한에 따라 접근 제한을 선택할 수 있다.
    //permitAll() : 권한에 관계없이 모든 접근을 허용한다.
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((auth) -> {
            auth.antMatchers("/sample/all").permitAll();
            auth.antMatchers("/sample/member").hasRole("USER");
        });

        //인가/인증 절차에서 문제 발생시 권한 획득을 유도하는 페이지 리턴
        http.formLogin();
        
        return http.build();
    }

 

로그인 폼을 이용한다면, 별도의 디자인 적용을 위한 추가 설정

  1. loginPage() - 별도의 로그인 페이지 이용하기 위한 메서드
  2. loginProcessUrl()
  3. defaultSuccessUrl()
  4. failureUrl()

 

인증된 사용자를 의미하는 ROLE_USER 상수

  • 권한을 설정할 때 USER 권한을 요구하도록 지정하는데, 인증된 사용자를 의미한다
  • 따라서 로그인 성공시 사용자는 ROLE_USER 권한을 갖도록 지정된다.

 

728x90
반응형