[Spring Security] Authorization(권한부여/인가)의 개념 이해 및 구현

10000JI·2024년 2월 7일
0

Spring Boot

목록 보기
9/15

Authentication (인증) VS Authorization (권한부여/인가)

인증과 권한 부여는 웹 애플리케이션 보안에서 중요한 개념이다.

이 둘 간의 차이를 명확하게 이해하는 것이 중요하다.

인증(Authentication)

정의: 사용자를 식별하고, 해당 사용자가 자신의 신원을 확인하는 프로세스이다.

목적: 웹 애플리케이션에 접근하려는 유저를 식별하고자 한다.

발생 시점: 인증은 권한 부여(=인가) 전에 수행된다.

일반적으로 사용자의 정보가 필요하다. 이메일, 전화번호, 자격증명이나 OTP 등 세부 정보가 있어야 인증이 가능하다.

권한 부여(Authorization)

정의: 인증된 사용자에게 특정 리소스에 대한 액세스 권한(혹은 역할)을 부여하는 프로세스이다.

목적: 인증된 사용자가 특정 기능에 접근할 수 있는지를 결정한다.

발생 시점: 권한 부여(=인가)는 인증 후에 실행된다.

권한 부여는 사용자 자격 증명에 대해 걱정할 필요가 없다.
그의 권한이나 역할에만 신경을 쓴다.
이러한 권한 및 역할을 기준으로 그의 접근 레벨을 결정한다.

주요 차이점

인증: 사용자의 신원을 확인하고 인증되었는지를 파악한다.

권한 부여: 인증된 사용자에게 특정 리소스에 대한 액세스 권한을 부여하거나 거부한다.

인증 없이 권한 부여를 진행하는 시나리오는 절대 존재하지 않는다.

항상 엔드 유저에게 시스템에 로그인할 것을 물어보고, 그 후에 권한과 접근 범위 및 역할에 대해 고려한다.

실패 시의 에러 코드

  1. 인증 실패: 에러 코드 401(Unauthorized)이 발생한다.

  2. 권한 부여 실패: 에러 코드 403(Forbidden)이 발생한다.

    403은 인증은 성공적이나 이 유저는 접근 권한이 없다는 뜻이다.

예시: 여행 예약 과정

인증: 여권과 티켓 정보를 제출하여 신원을 확인하는 과정이다.

권한 부여: 티켓에 명시된 목적지로만 여행할 수 있는 권한이 부여된다.

Spring Security의 권한 및 역할 저장 위치

권한과 역할

Spring Security에서 권한과 역할은 중요한 요소이며 구분되는 개념이다.

권한(Authoritys)과 역할(Roles)은 Spring Security 내에서 어떻게 저장되는지 이해해야 한다.

권한과 역할의 차이

권한: 사용자에게 부여되는 특정한 권한을 나타냄.

역할: 사용자가 수행하는 행동이나 역할을 나타냄.

Spring Security에서의 저장 방식

권한 및 역할은 GrantedAuthority 인터페이스를 통해 저장된다.

SimpleGrantedAuthority 클래스GrantedAuthority 인터페이스구현체이며, 권한 정보를 문자열 형태로 저장한다.

권한 및 역할의 설정

사용자에게 권한을 부여할 때는 SimpleGrantedAuthority 클래스를 사용하여 생성자에 권한을 문자열 형태로 생성하고 저장한다.

Spring Security는 사용자의 권한을 확인할 때 getAuthority 메서드를 활용한다.

권한 및 역할 정보의 활용

이제 궁금증이 생길 수 있다.

현재는 사용자 상세 서비스와 AuthenticationProvider를 활용하여 직접 로직을 작성함으로서 많은 인증 작업을 수행하고 있다.

이 권한들이 사용자 상세 정보와 인증 인터페이스 내에서 어디에 저장되어 있는 걸까?

UserDetail 인터페이스의 구현 클래스인 User 클래스 아래에는 데이터베이스에서 로딩한 특정 사용자 권한을 가져오기 위해서 Spring Security에 의해 호출되는 getAuthorites() 메소드가 있다.

마찬가지로 AuthenticationProvider 시나리오에서도 UsernamePasswordAuthenticationToken 객체를 생성할 때 여전히 인터페이스 내부에 사용 가능한 getAuthorites() 메소드를 호출할 것이다.

<AuthenticationProvider 시나리오>

혹은

< UserDetail 인터페이스 시나리오 >

데이터베이스에 권한 및 역할 저장

기존 데이터베이스 구조

데이터베이스에는 각 고객이 하나의 역할만 가질 수 있는 구조이다.

고객 테이블의 레코드를 확인하면 각 고객이 특정 역할을 가지고 있다.

Spring Security의 유연성

Spring Security는 각 유저에게 여러 권한 또는 역할을 부여할 수 있는 유연성을 제공한다.

Authorities 테이블을 새로 생성하여 각 유저의 권한 종류를 정의할 수 있다.

새로운 Authorities 테이블

Authorities 테이블은 id와 customer_id 열로 구성된다.

customer_id는 고객 테이블과의 외래키 연결을 위한 열이다.

권한과 역할의 이름은 이름 열에 저장된다.

 create table authorities (
 	id int not null auto_increment,
 	customer_id int not null,
 	name varchar(50) not null,
 	primary key(id),
 	key customer_id (customer_id),
 	constraint `authorities_ibfk_1` foreign key (customer_id) references customer (customer_id)
 )
 
insert into authorities (customer_id, name) values (1, "VIEWACCOUNT");

insert into authorities (customer_id, name) values (1, "VIEWCARDS");

insert into authorities (customer_id, name) values (1, "VIEWLOANS");

insert into authorities (customer_id, name) values (1, "VIEWBALANCE");

Insert Script는 cstomer_id가 1인 customer의 권한들을 입력하고자 한다.

계정보기, 카드보기, 대출 보기, 잔액 보기 등의 권한을 가질 것이다.

데이터베이스 권한 및 역할 관리 코드 변경

Authority 엔터티 클래스 생성

@Entity
@Table(name = "authorities")
public class Authority {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    // 생성자, getter 및 setter 메서드
}

Authority 엔터티 클래스는 authorities 테이블과 매핑된다.

id 필드는 주요 키로, 자동 생성된다.

name 필드는 권한의 이름을 저장한다.

customer 필드Customer 엔터티와의 관계를 나타낸다.
@ManyToOne 어노테이션을 사용함으로써 Athority가 N, Customer가 1 관계임을 나타낸다.

Customer 엔터티 클래스 수정

@Entity
public class Customer {

    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
    private Set<Authority> authorities;

    // 생성자, getter 및 setter 메서드
}

authorities 필드는 고객의 권한 세트를 나타낸다.

@OneToMany를 사용하여 한 고객이 여러 권한을 가질 수 있음을 나타낸다.

mappedBy 속성은 Authority 엔터티의 customer 필드와 매핑된다.

fetch = FetchType.EAGER로 설정하여 고객 세부 정보를 로드하려고 할 때 권한 정보를 즉시 로드하도록 설정하였다.

@JsonIgnore를 쓴 필드는 UI 애플리케이션에 JSON Response로 보내지지 않는다는 뜻이다.
이 민감한 정보를 UI 애플리케이션에 공유하고 싶지 않기 때문이다.
권한 정보는 오직 백엔드 애플리케이션에서만 사용하고자 한다.

반면에 비밀번호에는 @JsonIgnore를 사용하지 않았다.
왜냐하면 UI 애플리케이션에서 백엔드로 비밀번호의 세부정보가 필요하기 때문이다. 예를 들어 로그인하거나 회원가입할 때 말이다.

이럴 때에는 @JsonProperty 어노테이션을 사용해야 한다.
그리고 그 액세스 값을 WRITE_ONLY로 정의할 수 있다. JsonProperty.Access.WRITE_ONLY로 설정하면서 JSON으로 데이터를 보낼 때는 해당 필드를 무시하지만, 데이터를 받을 때는 필드를 설정할 수 있다.

AuthenticationProvider 수정

package com.eazybytes.springsecsection2.config;

import com.eazybytes.springsecsection2.model.Authority;
import com.eazybytes.springsecsection2.model.Customer;
import com.eazybytes.springsecsection2.repository.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Component
@RequiredArgsConstructor
public class EazyBankUsernamePwdAuthenticationProvider implements AuthenticationProvider {

    private final CustomerRepository customerRepository;

    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //1. 데이터 베이스에서부터 UserDetails 사항을 불러와야 함
        //2. 비밀번호 비교

        //username 불러오기 (여기선 email)
        String username = authentication.getName();
        //password 불러오기
        String pwd = authentication.getCredentials().toString();
        //db에서 username(->email) 문자열을 가진 Customer있다면 customer로 반환
        List<Customer> customer = customerRepository.findByEmail(username);
        //customer가 존재한다면
        if (customer.size() > 0) {
            //비밀번호가 일치하다면
            if (passwordEncoder.matches(pwd, customer.get(0).getPwd())) {
                //UsernamePasswordAuthenticationToken 대상을 새롭게 생성
                return new UsernamePasswordAuthenticationToken(username, pwd, getGrantedAuthorities(customer.get(0).getAuthorities()));
            }
            //비밀번호가 일치하지 않다면
            else {
                throw new BadCredentialsException("Invalid password!");
            }
        }
        //customer가 존재하지 않다면
        else {
            throw new BadCredentialsException("No user registered with this details!");
        }
    }

    private List<GrantedAuthority> getGrantedAuthorities(Set<Authority> authorities) {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        for (Authority authority : authorities) {
            grantedAuthorities.add(new SimpleGrantedAuthority(authority.getName()));
        }
        return grantedAuthorities;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

getGrantedAuthorities 메서드를 생성하였다.

데이터베이스에서 권한이름을 불러와 SimpleGrantedAuthority 클래스 생성자 매개변수에 넣어 권한을 설정해 준 뒤 GrantedAuthority 리스트에 추가하고 이를 반환한다.

이전에 AuthenticationProvider의 메소드 authenticate를 오버라이드 하여 UsernamePasswordAuthenticationToken을 생성하여 반환해주었었다.

UsernamePasswordAuthenticationToken의 매개변수에 설정해놨던 권한을 삭제하고 getGrantedAuthorities 메서드를 호출하는 것으로 변경하였다.

디버그하여 확인해본 결과 customer의 정보를 잘 불러오는 것을 확인할 수 있다. 권한 아래 "VIEWCARDS"와 같이 네 개의 레코드도 잘 보인다.

Spring Security로 웹 애플리케이션 내 권한 설정

Spring Security의 권한 부여 메소드 소개

Spring Security 프레임워크 내에서는 다음과 같은 메소드를 사용하여 권한 부여를 실행할 수 있다.

  1. hasAuthority(): 특정 권한을 가진 사용자만 특정 엔드포인트에 접근할 수 있도록 설정한다.

  2. hasAnyAuthority(): 여러 권한 중 하나라도 가진 사용자만 접근할 수 있도록 설정한다.

  3. access(): SpEL을 사용하여 복잡한 권한 부여 규칙을 설정할 수 있다. OR, AND와 같은 논리 연산자를 사용하려는 다소 복잡한 요구사항이 있을 수 있다.

Matchers 메소드를 통한 권한 부여 설정

Spring Security에서는 Matchers 메소드를 사용하여 API 경로에 대한 권한 부여를 설정한다. 예를 들어,

  • hasAuthority() : 하나의 권한(특정 권한)을 가진 사용자만 접근할 수 있도록 설정한다.

  • hasAnyAuthority() : 여러 권한 중 하나를 가진 사용자만 접근할 수 있도록 설정한다.

  • .authenticated() : 로그인한 사용자라면 누구든 접근할 수 있도록 설정한다. API를 위해 어떠한 권한 부여도 실행X

  • permitAll() : 모든 사용자에게 접근을 허용한다.

권한 부여 관련 변경 필요

requestMatchers를 사용하여 보안 구성을 정의한 위치를 살펴보자.

package com.eazybytes.springsecsection2.config;

import com.eazybytes.springsecsection2.filter.CsrfCookieFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import javax.sql.DataSource;
import java.util.Collections;

@Configuration
public class ProjectSecurityConfig {

    //람다(Lambda) DSL 스타일 사용을 권장
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
        requestHandler.setCsrfRequestAttributeName("_csrf");

        http.securityContext((context) -> context.requireExplicitSave(false))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
                //CorsConfigurationSource 인터페이스를 구현하는 익명 클래스 생성하여 getCorsConfiguration() 메소드 재정의
                .cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
                    //`etCorsConfiguration() 메소드에서 CorsConfiguration 객체를 생성하고 필요한 설정들을 추가
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration config = new CorsConfiguration();
                        //허용할 출처(도메인)를 설정
                        config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
                        //허용할 HTTP 메소드를 설정
                        config.setAllowedMethods(Collections.singletonList("*"));
                        //인증 정보 허용 여부를 설정
                        config.setAllowCredentials(true);
                        //허용할 헤더를 설정
                        config.setAllowedHeaders(Collections.singletonList("*"));
                        //CORS 설정 캐시로 사용할 시간을 설정
                        config.setMaxAge(3600L);
                        return config;
                    }
                })).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/register","/contact")
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
                        .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
                .authorizeHttpRequests((requests)->requests
                        .requestMatchers("/myAccount","/myBalance","/myLoans","/myCards","/user").authenticated()
                        .requestMatchers("/notices","/contact","/register").permitAll())
                .formLogin(Customizer.withDefaults())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }


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

}

requestMatchers를 사용하여 권한 부여의 규칙을 정의했었다.

.requestMatchers("/myAccount","/myBalance","/myLoans","/myCards","/user").authenticated()에 해당하는 엔드포인트들은 로그인한 사용자라면 접근할 수 있다.

.requestMatchers("/notices","/contact","/register").permitAll())은 로그인 하지 않는 사용자도 (모든 사용자) 엔드포인트에 접근이 가능했다.

그러나 이제는 권한 부여를 강제하려고 합니다.

권한 부여 강제를 위한 변경사항

package com.eazybytes.springsecsection2.config;

import com.eazybytes.springsecsection2.filter.CsrfCookieFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import javax.sql.DataSource;
import java.util.Collections;

@Configuration
public class ProjectSecurityConfig {

    //람다(Lambda) DSL 스타일 사용을 권장
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
        requestHandler.setCsrfRequestAttributeName("_csrf");

        http.securityContext((context) -> context.requireExplicitSave(false))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
                //CorsConfigurationSource 인터페이스를 구현하는 익명 클래스 생성하여 getCorsConfiguration() 메소드 재정의
                .cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
                    //`etCorsConfiguration() 메소드에서 CorsConfiguration 객체를 생성하고 필요한 설정들을 추가
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration config = new CorsConfiguration();
                        //허용할 출처(도메인)를 설정
                        config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
                        //허용할 HTTP 메소드를 설정
                        config.setAllowedMethods(Collections.singletonList("*"));
                        //인증 정보 허용 여부를 설정
                        config.setAllowCredentials(true);
                        //허용할 헤더를 설정
                        config.setAllowedHeaders(Collections.singletonList("*"));
                        //CORS 설정 캐시로 사용할 시간을 설정
                        config.setMaxAge(3600L);
                        return config;
                    }
                })).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/register","/contact")
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
                        .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
                .authorizeHttpRequests((requests)->requests
                        .requestMatchers("/myAccount").hasAuthority("VIEWACCOUNT")
                        .requestMatchers("/myBalance").hasAnyAuthority("VIEWACCOUNT","VIEWBALANCE")
                        .requestMatchers("/myLoans").hasAuthority("VIEWLOANS")
                        .requestMatchers("/myCards").hasAuthority("VIEWCARDS")
                        .requestMatchers("/user").authenticated()
                        .requestMatchers("/notices","/contact","/register").permitAll())
                .formLogin(Customizer.withDefaults())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }


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

}
  • .requestMatchers("/myAccount").hasAuthority("VIEWACCOUNT"): /myAccount 엔드포인트 같은 경우는 권한 VIEWACCOUNT의 도움을 받아 권한 규칙을 강제하고자 함으로 hasAuthority() 메소드를 사용하고 있다.

  • .requestMatchers("/myBalance").hasAnyAuthority("VIEWACCOUNT","VIEWBALANCE"): 반면 /myBalancehasAnyAuthority() 메소드를 사용하여 VIEWACCOUNT 또는 VIEWBALANCE 권한을 가진 엔드 유저를 허용한다.

  • 추가로 requestMatchers()authenticated()는 이미 앞에서 "/myAccount","/myBalance","/myLoans","/myCards" 에 대한 권한을 설정해주었기 때문에 지웠다.

  • 여기서는 정규 표현식 패턴을 사용하지 않기 때문에 access() 메소드는 사용되지 않았다.

변경 사항의 적용

변경 사항을 저장하고 빌드한 후, 웹 애플리케이션을 시작한다.

이제 UI 애플리케이션에서 Happy 계정으로 로그인하여 모든 API에 접근할 수 있는지 확인한다.

부정적인 시나리오 테스트

이후에는 부정적인 시나리오를 테스트하여 권한 변경이 올바르게 작동하는지 확인한다.

예를 들어, VIEWCARDS 권한VIEWCARDDETAILS로 변경하여 엔드 유저에게 권한이 없는 API에 접근하면 403 에러를 기대한다.

변경사항 적용 확인

다시 이전 상태로 되돌려 놓고, 다시 로그인하여 변경사항이 제대로 적용되는지 확인한다.

이번엔 직접 REST API를 호출하여 변경사항을 확인했다.

Spring Security에서의 권한 vs 역할

권한(Authority)

권한은 사용자가 가질 수 있는 개별 특권이나 웹 애플리케이션 내에서 수행할 수 있는 개별 작업을 나타낸다.

VIEWACCOUNT, VIEWCARDS, VIEWLOANS와 같은 권한의 이름을 가지며, 각 권한은 계정, 카드 또는 대출 세부 정보 살펴보기 등과 같은 특정한 액션을 나타낸다.

권한은 개별 권한이나 작업을 표현하므로 액세스를 세밀하게 제어하는 데 사용될 수 있다.

역할(Role)

역할은 여러 권한의 그룹을 나타낸다.

대부분의 기업 웹 애플리케이션에서는 다양한 작업을 지원하기 위해 수천 개의 작업이 있을 수 있다.

이러한 상황에서는 권한을 다루는 것이 매우 복잡할 수 있으므로 권한을 역할로 묶을 수 있다.

역할은 일반적으로 권한이나 작업의 그룹을 나타내며, 역할을 사용하여 액세스를 제한한다.

역할과 권한의 관계

역할은 일반적으로 권한을 그룹화하고, 이러한 역할을 사용하여 권한 부여 한다.

Spring Security에서는 역할을 나타내는 이름에 항상 ROLE_ 접두사를 사용한다.

예를 들어, ROLE_USER와 ROLE_ADMIN은 역할을 나타내며, 이러한 역할을 사용하여 권한 부여를 관리한다.

역할(Role)을 가지고 권한 부여 하는 것은 접근을 듬성 듬성 제한하고 있음을 의미한다.

세밀한 관리는 "VIEWACCOUNT", "VIEWCARDS"와 같은 REST API 작업 수준의 아주 작은 세부 정보로 이동하는 것이 좋다. 보통 권한은 아주 상세하게까지는 이동하지 않을 것이다.

데이터베이스 '권한 부여' -> '역할' 테이블로 변경

엔드유저가 가진 모든 존재하는 권한을 삭제하고 그 안에 ROLE_ADMIN, ROLE_USER와 같은 몇 가지 역할을 생성하자.

Spring Security로 웹 애플리케이션 내 역할 권한 설정

역할 권한을 부여하기 위한 세 가지 메소드

  • hasRole(): 특정 역할을 인자로 받아 해당 역할을 가진 사용자만 특정 API에 액세스할 수 있다.

  • hasAnyRole(): 여러 역할 중 하나만 가지고 있어도 특정 API에 액세스할 수 있도록 한다.

  • access(): SpEL(스프링 표현 언어)를 사용하여 권한 부여 규칙을 정의합니다. 역할과 조건을 결합하여 권한을 부여할 수 있다.

이때, 역할을 지정할 때는 반드시 'ROLE_' 접두사를 사용해야 한다.

그러나 hasRole(), hasAnyRole(), 혹은 access() 메소드를 사용할 때에는 접두사를 명시적으로 언급할 필요가 없다.

Spring Security 프레임워크가 내부적으로 자동으로 접두사를 추가한다.

Matchers 메소드를 통한 역할 권한 부여 설정

Spring Security에서는 requestMatchers 메소드를 통해 보안 요구사항을 정의한 후에 이 메소드를 호출할 수 있다.

이 메소드를 호출할 때는 역할 정보를 전달해야 한다.

즉, 사용자가 어떤 역할을 가지고 있는지를 명시해야 한다.

주석 처리 및 코드 수정

hasAuthorityhasAnyAuthority 메소드를 주석 처리한다.

package com.eazybytes.springsecsection2.config;

import com.eazybytes.springsecsection2.filter.CsrfCookieFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import javax.sql.DataSource;
import java.util.Collections;

@Configuration
public class ProjectSecurityConfig {

    //람다(Lambda) DSL 스타일 사용을 권장
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
        requestHandler.setCsrfRequestAttributeName("_csrf");

        http.securityContext((context) -> context.requireExplicitSave(false))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
                //CorsConfigurationSource 인터페이스를 구현하는 익명 클래스 생성하여 getCorsConfiguration() 메소드 재정의
                .cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
                    //`etCorsConfiguration() 메소드에서 CorsConfiguration 객체를 생성하고 필요한 설정들을 추가
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration config = new CorsConfiguration();
                        //허용할 출처(도메인)를 설정
                        config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
                        //허용할 HTTP 메소드를 설정
                        config.setAllowedMethods(Collections.singletonList("*"));
                        //인증 정보 허용 여부를 설정
                        config.setAllowCredentials(true);
                        //허용할 헤더를 설정
                        config.setAllowedHeaders(Collections.singletonList("*"));
                        //CORS 설정 캐시로 사용할 시간을 설정
                        config.setMaxAge(3600L);
                        return config;
                    }
                })).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/register","/contact")
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
                        .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
                .authorizeHttpRequests((requests)->requests
                        /*.requestMatchers("/myAccount").hasAuthority("VIEWACCOUNT")
                        .requestMatchers("/myBalance").hasAnyAuthority("VIEWACCOUNT","VIEWBALANCE")
                        .requestMatchers("/myLoans").hasAuthority("VIEWLOANS")
                        .requestMatchers("/myCards").hasAuthority("VIEWCARDS")*/
                        .requestMatchers("/myAccount").hasRole("USER")
                        .requestMatchers("/myBalance").hasAnyRole("USER","ADMIN")
                        .requestMatchers("/myLoans").hasRole("USER")
                        .requestMatchers("/myCards").hasRole("USER")
                        .requestMatchers("/user").authenticated()
                        .requestMatchers("/notices","/contact","/register").permitAll())
                .formLogin(Customizer.withDefaults())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }


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

}

hasAuthorityhasRole로, hasAnyAuthorityhasAnyRole로 수정한다.

그리고 매개변수로 오는 것은 권한의 이름을 역할 이름으로 변경하여 USER, USER & ADMIN로 바꿔주었다.

ROLE_USERROLE_ADMIN이라는 역할을 데이터베이스에 생성했었다.

DB에 저장된 ROLE_USER라는 풀네임을 적지 않고, 접두사 ROLE_를 제거하여 적어주었다.

왜냐하면 hasRole 또는 hasAnyRole과 같은 메소드 중 하나를 확인하면 다음과 같이 정의되어 있기 때문이다.

따라서 ROLE_ 접두사는 불필요하다.

변경 사항 저장 및 빌드

변경 사항을 저장하고 빌드 후 웹 애플리케이션을 시작한다.

이제 UI 애플리케이션에서 Happy 계정으로 로그인하여 모든 API에 접근할 수 있는지 확인한다.

부정적인 시나리오 시도

"Happy"에게 MANAGER 역할을 부여하여 부정적인 시나리오를 시도해보자.

이에 따라 "Happy"의 접근이 거부된다.

테스트를 완료하면 본 상태로 되돌리자.

profile
Velog에 기록 중

0개의 댓글