인증과 권한 부여는 웹 애플리케이션 보안에서 중요한 개념이다.
이 둘 간의 차이를 명확하게 이해하는 것이 중요하다.
정의: 사용자를 식별하고, 해당 사용자가 자신의 신원을 확인하는 프로세스이다.
목적: 웹 애플리케이션에 접근하려는 유저를 식별하고자 한다.
발생 시점: 인증은 권한 부여(=인가) 전에 수행된다.
일반적으로 사용자의 정보가 필요하다. 이메일, 전화번호, 자격증명이나 OTP 등 세부 정보가 있어야 인증이 가능하다.
정의: 인증된 사용자에게 특정 리소스에 대한 액세스 권한(혹은 역할)을 부여하는 프로세스이다.
목적: 인증된 사용자가 특정 기능에 접근할 수 있는지를 결정한다.
발생 시점: 권한 부여(=인가)는 인증 후에 실행된다.
권한 부여는 사용자 자격 증명에 대해 걱정할 필요가 없다.
그의 권한이나 역할에만 신경을 쓴다.
이러한 권한 및 역할을 기준으로 그의 접근 레벨을 결정한다.
인증: 사용자의 신원을 확인하고 인증되었는지를 파악한다.
권한 부여: 인증된 사용자에게 특정 리소스에 대한 액세스 권한을 부여하거나 거부한다.
인증 없이 권한 부여를 진행하는 시나리오는 절대 존재하지 않는다.
항상 엔드 유저에게 시스템에 로그인할 것을 물어보고, 그 후에 권한과 접근 범위 및 역할에 대해 고려한다.
인증 실패: 에러 코드 401(Unauthorized)이 발생한다.
권한 부여 실패: 에러 코드 403(Forbidden)이 발생한다.
403은 인증은 성공적이나 이 유저는 접근 권한이 없다는 뜻이다.
인증: 여권과 티켓 정보를 제출하여 신원을 확인하는 과정이다.
권한 부여: 티켓에 명시된 목적지로만 여행할 수 있는 권한이 부여된다.
Spring Security에서 권한과 역할은 중요한 요소이며 구분되는 개념이다.
권한(Authoritys)과 역할(Roles)은 Spring Security 내에서 어떻게 저장되는지 이해해야 한다.
권한: 사용자에게 부여되는 특정한 권한을 나타냄.
역할: 사용자가 수행하는 행동이나 역할을 나타냄.
권한 및 역할은 GrantedAuthority 인터페이스
를 통해 저장된다.
SimpleGrantedAuthority 클래스
는 GrantedAuthority 인터페이스
의 구현체
이며, 권한 정보를 문자열 형태로 저장한다.
사용자에게 권한을 부여할 때는 SimpleGrantedAuthority 클래스
를 사용하여 생성자에 권한을 문자열 형태로 생성하고 저장한다.
Spring Security는 사용자의 권한을 확인할 때 getAuthority 메서드
를 활용한다.
이제 궁금증이 생길 수 있다.
현재는 사용자 상세 서비스와
AuthenticationProvider
를 활용하여 직접 로직을 작성함으로서 많은 인증 작업을 수행하고 있다.이 권한들이 사용자 상세 정보와 인증 인터페이스 내에서 어디에 저장되어 있는 걸까?
UserDetail 인터페이스
의 구현 클래스인User 클래스
아래에는 데이터베이스에서 로딩한 특정 사용자 권한을 가져오기 위해서 Spring Security에 의해 호출되는getAuthorites() 메소드
가 있다.마찬가지로
AuthenticationProvider
시나리오에서도UsernamePasswordAuthenticationToken
객체를 생성할 때 여전히 인터페이스 내부에 사용 가능한getAuthorites() 메소드
를 호출할 것이다.
<AuthenticationProvider
시나리오>
혹은
< UserDetail 인터페이스
시나리오 >
데이터베이스에는 각 고객이 하나의 역할만 가질 수 있는 구조이다.
고객 테이블의 레코드를 확인하면 각 고객이 특정 역할을 가지고 있다.
Spring Security는 각 유저에게 여러 권한 또는 역할을 부여할 수 있는 유연성을 제공한다.
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의 권한들을 입력하고자 한다.
계정보기, 카드보기, 대출 보기, 잔액 보기 등의 권한을 가질 것이다.
@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 관계임을 나타낸다.
@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으로 데이터를 보낼 때는 해당 필드를 무시하지만, 데이터를 받을 때는 필드를 설정할 수 있다.
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 프레임워크 내에서는 다음과 같은 메소드를 사용하여 권한 부여를 실행할 수 있다.
hasAuthority(): 특정 권한을 가진 사용자만 특정 엔드포인트에 접근할 수 있도록 설정한다.
hasAnyAuthority(): 여러 권한 중 하나라도 가진 사용자만 접근할 수 있도록 설정한다.
access(): SpEL을 사용하여 복잡한 권한 부여 규칙을 설정할 수 있다. OR, AND와 같은 논리 연산자를 사용하려는 다소 복잡한 요구사항이 있을 수 있다.
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")
: 반면 /myBalance
는 hasAnyAuthority()
메소드를 사용하여 VIEWACCOUNT
또는 VIEWBALANCE
권한을 가진 엔드 유저를 허용한다.
추가로 requestMatchers()
의 authenticated()
는 이미 앞에서 "/myAccount","/myBalance","/myLoans","/myCards"
에 대한 권한을 설정해주었기 때문에 지웠다.
여기서는 정규 표현식 패턴을 사용하지 않기 때문에 access() 메소드는 사용되지 않았다.
변경 사항을 저장하고 빌드한 후, 웹 애플리케이션을 시작한다.
이제 UI 애플리케이션에서 Happy 계정으로 로그인하여 모든 API에 접근할 수 있는지 확인한다.
이후에는 부정적인 시나리오를 테스트하여 권한 변경이 올바르게 작동하는지 확인한다.
예를 들어, VIEWCARDS 권한
을 VIEWCARDDETAILS
로 변경하여 엔드 유저에게 권한이 없는 API에 접근하면 403 에러
를 기대한다.
다시 이전 상태로 되돌려 놓고, 다시 로그인하여 변경사항이 제대로 적용되는지 확인한다.
이번엔 직접 REST API를 호출하여 변경사항을 확인했다.
권한은 사용자가 가질 수 있는 개별 특권이나 웹 애플리케이션 내에서 수행할 수 있는 개별 작업을 나타낸다.
VIEWACCOUNT, VIEWCARDS, VIEWLOANS와 같은 권한의 이름을 가지며, 각 권한은 계정, 카드 또는 대출 세부 정보 살펴보기 등과 같은 특정한 액션을 나타낸다.
권한은 개별 권한이나 작업을 표현하므로 액세스를 세밀하게 제어하는 데 사용될 수 있다.
역할은 여러 권한의 그룹을 나타낸다.
대부분의 기업 웹 애플리케이션에서는 다양한 작업을 지원하기 위해 수천 개의 작업이 있을 수 있다.
이러한 상황에서는 권한을 다루는 것이 매우 복잡
할 수 있으므로 권한을 역할로 묶을 수 있다.
역할은 일반적으로 권한이나 작업의 그룹을 나타내며, 역할을 사용하여 액세스를 제한한다.
역할은 일반적으로 권한을 그룹화하고, 이러한 역할을 사용하여 권한 부여 한다.
Spring Security에서는 역할을 나타내는 이름에 항상 ROLE_ 접두사를 사용한다.
예를 들어, ROLE_USER와 ROLE_ADMIN은 역할을 나타내며, 이러한 역할을 사용하여 권한 부여를 관리한다.
역할(Role)을 가지고 권한 부여 하는 것은 접근을 듬성 듬성 제한하고 있음을 의미한다.
세밀한 관리는 "VIEWACCOUNT", "VIEWCARDS"와 같은 REST API 작업 수준의 아주 작은 세부 정보로 이동하는 것이 좋다. 보통 권한은 아주 상세하게까지는 이동하지 않을 것이다.
엔드유저가 가진 모든 존재하는 권한을 삭제하고 그 안에 ROLE_ADMIN, ROLE_USER와 같은 몇 가지 역할을 생성하자.
hasRole(): 특정 역할을 인자로 받아 해당 역할을 가진 사용자만 특정 API에 액세스할 수 있다.
hasAnyRole(): 여러 역할 중 하나만 가지고 있어도 특정 API에 액세스할 수 있도록 한다.
access(): SpEL(스프링 표현 언어)를 사용하여 권한 부여 규칙을 정의합니다. 역할과 조건을 결합하여 권한을 부여할 수 있다.
이때, 역할을 지정할 때는 반드시 'ROLE_' 접두사를 사용해야 한다.
그러나 hasRole(), hasAnyRole(), 혹은 access() 메소드를 사용할 때에는 접두사를 명시적으로 언급할 필요가 없다.
Spring Security 프레임워크가 내부적으로 자동으로 접두사를 추가한다.
Spring Security에서는 requestMatchers 메소드를 통해 보안 요구사항을 정의한 후에 이 메소드를 호출할 수 있다.
이 메소드를 호출할 때는 역할 정보를 전달해야 한다.
즉, 사용자가 어떤 역할을 가지고 있는지를 명시해야 한다.
hasAuthority
와 hasAnyAuthority
메소드를 주석 처리한다.
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();
}
}
hasAuthority
를 hasRole
로, hasAnyAuthority
를 hasAnyRole
로 수정한다.
그리고 매개변수로 오는 것은 권한의 이름을 역할 이름으로 변경하여 USER
, USER
& ADMIN
로 바꿔주었다.
ROLE_USER
와 ROLE_ADMIN
이라는 역할을 데이터베이스에 생성했었다.
DB에 저장된 ROLE_USER
라는 풀네임을 적지 않고, 접두사 ROLE_
를 제거하여 적어주었다.
왜냐하면 hasRole
또는 hasAnyRole
과 같은 메소드 중 하나를 확인하면 다음과 같이 정의되어 있기 때문이다.
따라서 ROLE_
접두사는 불필요하다.
변경 사항을 저장하고 빌드 후 웹 애플리케이션을 시작한다.
이제 UI 애플리케이션에서 Happy 계정으로 로그인하여 모든 API에 접근할 수 있는지 확인한다.
"Happy"에게 MANAGER 역할
을 부여하여 부정적인 시나리오를 시도해보자.
이에 따라 "Happy"의 접근이 거부
된다.
테스트를 완료하면 본 상태로 되돌리자.