[SpringBoot] 스프링 시큐리티 DB에서 사용자 정보 가져와서 로그인, 권한 적용 해보기

    Build Tool : Gradle

    Spring Booot Version : 2.7.6

    Spring Security Version : 5.7

    DB Version : MySQL 5.7

    DB 연동 : MyBatis

     

    Gradle 빌드툴 사용하였고 SpringBoot 2.7.6으로 스프링 시큐리티5 버전의 세팅이다. DB연동은 MyBatis이다.

     

    1. 빌드 추가

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    testImplementation 'org.springframework.security:spring-security-test'

    SpringBoot2.7 버전은 스프링시큐리티 5버전을 사용하여서 thymeleaf 시큐리티 의존성을 넣을 때 5버전으로 해야 한다.

    의존성을 주입하면 기본적으로 login 폼이 주어지며 아이디는 user, 패스워드는 콘솔창에 UUID로 나온다. 이대로는 서비스가 불가능 하니 스프링 시큐리티 적용은 기본적으로 주어지는 시큐리티 환경 속에서 커스텀하여 사용하는 것이 관건이다. 

     

    2. UserDetails 상속

    @Getter
    @Builder
    public class Member implements UserDetails {
        private Long memberId;
        private String email;
        private String password;
        private String name;
        private String telNo;
        private int totalPaymentPrice;
        private int point;
        private MemberRole role;
        private MemberGrade grade;
        private LocalDateTime createdAt;
        private LocalDateTime modifiedAt;
        private LocalDateTime deletedAt;
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
           return List.of(new SimpleGrantedAuthority("ROLE_" + String.valueOf(role)));
        }
    
        @Override
        public String getUsername() {
           return email;
        }
    
        @Override
        public boolean isAccountNonExpired() {
           //true -> 계정 만료되지 않았음
           return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
           //true -> 계정 잠금되지 않음
           return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
           //true -> 패스워드 만료 되지 않음
           return true;
        }
    
        @Override
        public boolean isEnabled() {
           //ture -> 계정 사용 가능
           return true;
        }
    }
    

     

    public enum MemberRole {
        ADMIN, SELLER, USER
    }

     

    Member클래스에 UserDetails를 상속받아 인증 객체로 사용한다.

    MemberRole Enum사용으로 USER, SELLER, ADMIN 권한 설정을 DB에서 가져와서 적용하도록 하였고 앞에 "ROLE_" 을 붙인 이유는 권한으로 사용될 데이터라는 것을 스프링 시큐리티에게 알려주기 위해서 이다.

     

    ※ Member 객체가 Domain Object 여서 인증 객체로 같이 사용되는 것은 설계상 좋지가 않다. 그래서 UserDetails를 별도로 구현하는 것이 좋다.

     

    3. Repository 만들기

    @Mapper
    public interface MemberRepository {
        Optional<Member> findByEmail(String email);
    }

     

    <select id="findByEmail" resultType="org.store.clothstar.member.domain.Member">
        select * from member where email = #{email}
    </select>

     

    로그인 할 때 입력한 아이디로 DB에서 사용자 정보를 가져올 수 있게 Repository를 구현한다.

     

    4. UserDetailsService 구현

    @Service
    @RequiredArgsConstructor
    public class MemberDetailsService implements UserDetailsService {
        private final MemberRepository memberRepository;
    
        @Override
        public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
           return memberRepository.findByEmail(email)
              .orElseThrow(() -> new UsernameNotFoundException(email + " not found"));
        }
    }

     

    UserDetailsService를 구현하여 로그인을 진행할 때 사용자 정보를 가져온다. 즉 loadUserByUsername()는 로그인하면 실행되는 함수이다.

    loadUserByUsername(String email)의 파라미터의 이름은 login Form에서 입력한 input 태그의 name 이름과 동일해야 한다.

    그래서 기본 login Form을 사용하면 email을 username으로 변경해야 한다. 상세 내용은 아래 URL 참조

    https://deftkang.tistory.com/278

     

    5. 시큐리티 설정

    @Configuration
    public class SecurityConfiguration {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
           return new BCryptPasswordEncoder();
        }
    
        @Bean
        public WebSecurityCustomizer configure() {
           return (web -> web.ignoring()
              .requestMatchers(PathRequest.toStaticResources().atCommonLocations()));
        }
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
           http.csrf().disable()
              .cors().disable()
              .authorizeRequests()
              .antMatchers("/", "/login", "/signup", "/v1/members/**").permitAll()
              .antMatchers("/user").authenticated()
              .antMatchers("/admin").hasRole("ADMIN")
              .antMatchers("/seller").hasRole("SELLER")
              .anyRequest().authenticated()
              .and()
              .formLogin()
              .loginPage("/login")
              .defaultSuccessUrl("/")
              .usernameParameter("email");
    
           return http.build();
        }

     

    PasswordEncoder 빈을 만들어서 DB에 패스워드를 저장할 때 반드시 암호화를 해야 한다. 안 그러면 PasswordEncoder가 없다고 에러 난다.

    참고 : Spring Security 5부터는 PasswordEncoder가 필수적으로 등록되어야 한다. 비밀번호 암호화 방식 중 BCryptPasswordEncoder 쓰는 것이 일반적이다.

     

    WebSecurityCustomizer 빈을 만들어서 정적 파일들은 인증을 무시하도록 설정하였다. 저 설정을 안 하면 html에서 src로 javascript, css 같은 정적파일을 import 하려고 할 때 에러가 발생한다.

    SecurityFilterChain 빈을 만들어서 HTTP 요청에 대한 보안을 설정한다. 기본적으로 csrf, cors를 적용해야 하는데 기본적인 인증과 권한만을 설정하기 위해서 disable 하였다.

    authorizeRequests()로 어떤 antMatchers()로 설정한 URI로 요청이 왔을 때 permitAll()로 누구나 접근하게 할 건지, authenticated()로 인증된 사용자만 접근 가능하게 할 건지, hasRole()로 특정 권한이 있는 사람만 접근 가능하게 할 건지 설정한다. 

     

    참고 : hasRole()은 접두사 "ROLE_" 이 붙는다.  권한 확인으로 사용되는 메서드로 hasAuthority()가 있는데 이걸 사용하면 앞에 접두사를 직접적으로 붙여줘야 한다. 따라서 ADMIN권한을 확인 할ㄷ 때 hasAuthority("ROLE_ADMIN")  또는 hasRole("ADMIN") 을 사용할 수 있다.

     

    anyRequest().authenticated()는 antMatchers로 설정한 URL 말고 어떠한 요청이 있을 때마다 인증을 확인한다는 것이다. anyRequest().permitAll()로 하면 누구나 접근가능 하게 한다. 

    formLogin()은 스프링 시큐리티가 Login Form을 인증 절차를 사용해서 사용자에 대한 인증을 진행한다는 것이고 loginPage()로 직접 만든 login 페이지를 사용 defaultSuccessUrl()로 로그인 성공했을 때 이동되는 URL 설정이다.

    usernameParameter()는 커스텀한 login 페이지에서 아이디를 입력할 input 태그의 name을 변경할 때 사용된다.

     

    6. 테스트

    위 적용까지 끝나면 스프링 시큐리티 설정은 끝났다. 테스트해보기 위해 아래 role을 SELLER로 적용한 멤버를 회원가입하여 만들었다. 

     

    위 멤버는 권한이 SELLER여서 로그인하면 설정대로 /user, /seller 화면은 들어갈 수 있고, /admin 화면을 들어가려고 하면 403 에러가 발생해야 한다.

     

     

    댓글

    Designed by JB FACTORY