본문 바로가기
Category/Project

[개인프로젝트] 2일차 - 시큐리티 권한 세팅하기, 영속성 컨텍스트

by developer__Y 2024. 2. 19.
- 개인프로젝트 2일차

 

SpringBoot 3.2.2

JPA

Spring security 6.2

MariaDB

 

위의 버전을 기반으로 진행하였다.

현재 스프링 시큐리티를 사용하여 로그인 및 사용자 권한별 url 접근을 막는 작업 진행중이다.

 

package com.project.findjob.config;

import com.project.findjob.service.UserDetailService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final UserDetailService userDetailService;
    @Bean
    public static BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf((csrf) -> csrf.disable())
                .authorizeHttpRequests((requests) -> requests
                        .requestMatchers("/", "/css/**","/image/**","/regist/**","/js/**").permitAll() // 다음 url은 인증없이 모든 사용자에게 허용
                        .anyRequest().authenticated() // 그 이외의 모든 요청은 인증 필요함
                )
//                허가되지않은 경우 -> /login으로 이동
                .formLogin((form) -> form
                        .loginPage("/login")
                        .permitAll()
                        .defaultSuccessUrl("/main_user")


                )
                .logout((logout) -> logout.permitAll()
                        .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                        .logoutSuccessUrl("/login"));

        return http.build();
    }

    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{
        return authenticationConfiguration.getAuthenticationManager();
    }

}

 

 

다음과 같은 시큐리티 설정을 통해서 login에 성공한 유저만 접근권한을 주었다.

 

 

회원가입

 

 

 

회원가입할때 2개의 사용자별로 나누어 선택하게한뒤,

regist() 컨트롤러 메소드를 통해 role id=1 또는 role id=2로 그에 맞는 권한을 부여하였다.

 

 

왼쪽 테이블은 권한 정보를 나타내는 role Table, 오른쪽은 유저별 가지고있는 권한을 나타내는 user_role 테이블이다

User 테이블과 role 테이블은 Many to Many이므로 JPA에서 ORM을 가능하게하기위해 중간 테이블인 user_role이 1대다 관계로 받아주기 위한 참조테이블이다.

 

 

그리고, 로그인 할때에 유저가 가진 권한 정보를 Entity에서 표현하기위해 user_role테이블에서 권한정보를 가져와

User 테이블에 ArrayList<Role> 타입의 roles 멤버를 추가해주었다.

 

문제1 : DB에서 유저 권한정보를 가져와 세팅하기

 

스프링 시큐리티를 통해 로그인을 하면,

DB에 있는 user_role 테이블의 유저별 권한을 가져와 user 엔티티의 roles에 해당 유저가 가진 권한을 세팅해주어야했는데, 시큐리티의 작동순서와 원리에 대한 이해가 부족하여 어느 시점에 권한을 세팅해주는지 알지못해 애를 먹었다.

 

구글링과 공식문서를 찾아보면서 알아낸 결과를 정리하면,

 

 

 시큐리티 설정을 통해 /login url로 POST 요청이 들어오면, 해당 요청을 Security가 가로채서 로그인 인증 작업을 수행한다.

그때, UserDetailsService를 구현한 커스텀 UserService에서 오버라이드한 loadUserByUsername에서

JPA의 findByUserid()를 통해 DB에서 해당 아이디값을 가지고 User를 꺼내온다.

 

나는 회원가입 시점에 해당 유저의 권한을 DB에 넣었으므로,

로그인을 하는시점에는 이미 DB에 해당 유저가 가진 권한이 이미 들어가있기때문에

loadUserByUsername()메소드를 통해 User 객체를 가져올때에 이미 user객체의 멤버인 ArrayList<Role> roles에 권한 정보가 세팅이 되는것이다.

 

 

그런데 나는 그런줄도 모르고 로그인을 한뒤 UserDetails를 상속받은 User 객체의 getAuthorities() 메소드에서  DB에서 해당 유저의 권한을 가져와 세팅해주려고했었다.

여기서 다시 DB 메소드를 통해 가져와 세팅을 해줘도 되긴하지만 이미 JPA가 해준걸 해준지도 모르고 계속 하려고했던것이다.

이 간단한걸 알지못해서 하루종일 시큐리티만 찾아보고있었다..

결국 getAutorities() 메소드는 이미 세팅이 완료된 User 객체의 roles 멤버를 까서 권한정보를 알려주는 용도로 사용하는걸로 해결하였다.

 

2. LazyInitializationException

 

loadUserByUsername() 메소드에서 사용자 정보를 가져올때에 LazyInitializationException이 발생하였다.

그런데 나는 DB의 user_role이 별개의 하나의 Entity가 아니라 User Entity 안의 멤버로 있고, 

@ManyToMany
@JoinTable(
        name = "user_role",
        joinColumns = @JoinColumn(name="user_id"),
        inverseJoinColumns = @JoinColumn(name="role_id")
)

 

를 통해서 user_role테이블에 접근하는 형태였다.

 

JPA Hibernate에서는 지연로딩(Lazy Loading)이라는 기능을 제공하는데, 한꺼번에 많은 양의 데이터를 로딩하지 않도록 하여 성능 향상을 위해 User와 roles에 연관된 엔티티 실제로 사용할 때까지 데이터베이스에서 로딩을 지연시킨다고한다. 

즉, User 테이블을 조회하고 영속성 컨텍스트가 열려있는 상태에서는 user_role에 접근할수있지만,

해당 세션이나 트랜젝션이 종료되면 Hibernate가 roles를 Lazy Loading하고있기때문에 roles에 접근할수 없는것이다.

 

해당 문제를 해결하기위해 기본적으로 Laze(지연)으로 설정된 Fetch 전략을 Eager(즉시)로 바꿔주면 해결할수있지만

사소한 문제를 해결하기위해 영속성 컨텍스트를 계속 열어두고 한꺼번에 DB에서 정보를 가져오는 작업을 하는것은 효율적이지 않고 성능을 떨어뜨린다고한다.따라서 간단하게 해당 영속성 컨텍스트를 유지시켜주기위해 @Transactional 어노테이션을 통해 트렌젝션 처리를 해주는것이 좋은 해결책이다.

 

 

트랜젝션 처리를 통해서 영속성컨텍스트 문제를 해결하고 정상적으로 사용자별 권한정보를 받아올수있었다.