본문 바로가기
공부/Trouble Shooting

UserDetailService 2개 구현 시 발생하는 에러 처리

by son_i 2023. 10. 25.
728x90

관리자 회원가입을 위해 UserDetailSevice를 상속한 ManageService와 UserDetails를 상속한 Manager엔티티를 구현했다.

그리고 일반회원 회원가입을 위해 똑같이 MemberService와 Member를 구현했는데 엄청 긴 에러 중에  이런 것이 있었다.

factory method 'authenticationmanagerbean' threw exception

왜 이럴까 아니면 그냥 따로 서비스 파일을 만들지 않고 한 번에 몰아서 해야하나 고민을 하다가 

UserDetailService가 두 개 일 때 발생하는 에러에 대해서 찾아보았다. 그리고 해결책을 찾았다.

 

➡️원인

UserDetailService를 두 개 구현할 경우 securityConfig에도 각각의 서비스를 등록해줘야한다는 것 !

 

➡️해결

securityConfig 파일을 하나 더 만들어서 중복되는 configure 메소드와 authenticationManagerBean() 메소드를 없애주고 중복되는 어노테이션인 @EnableWebSecurity와 @EnableGlobalMethodSecurity(prePostEnabled = true)도 없애주었다.

 

제일 중요한 것은 @Order(Ordered.HIGHEST_PRECEDENCE) 이 어노테이션을 먼저 등록할 config 파일에 꼭 달아줘야한다 !

 

MemberSecurityConfig.java

package com.soni.reservation.config;

import com.soni.reservation.security.JwtAuthenticationFilter;
import com.soni.reservation.service.MemberService;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@RequiredArgsConstructor
public class MemberSecurityConfig extends WebSecurityConfigurerAdapter {
    private final MemberService memberService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberService);
    }
}

 

ManagerSecurityConfig.java

package com.soni.reservation.config;

import com.soni.reservation.security.JwtAuthenticationFilter;
import com.soni.reservation.service.ManageService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ManagerSecurityConfig extends WebSecurityConfigurerAdapter {
    private final ManageService manageService;
    private final JwtAuthenticationFilter authenticationFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/**/register", "/**/login").permitAll()
                .and()
                .addFilterBefore(this.authenticationFilter, UsernamePasswordAuthenticationFilter.class);
                //UsernamePasswordAuthenticationFilter 직전에 authenticationFilter가 걸리도록 함.
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**");
    }

    //spring boot 2.X이상부터 선언해줘야함.
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();

        //Authentication Manager
        //인증은 SpringSecurity의 AuthenticationManager를 통해 처리되는데
        //실질적으로 AuthenticationManager에 등록됨 AuthenticationProvider에 의해 처리가 됨.
        //인증이 성공하면 인증이 성공한 Authentication 객체를 생성하여 Security Context에 저장.
        //인증상태 유지 위해 세션에 보관, 인증 실패한 경우 AUthenticationException 발생.

        //AuthenticationManager를 implements 한 ProviderManager는 실제 인증과정에 대한 로직을 갖고있는
        //AuthenticationProvider를 List로 가지고 있으며 ProviderManager는 for문을 통해 모든 provider를
        //조회하면서 authenticate처리를 함.

        //ProviderManager에 우리가 만든 TokenProvider를 등록하는 방법은
        //WebSecurityConfigurerAdapter를 상속해 만든 SecurityConfig에서 가능. 근데 해주진 않은 듯?
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(manageService);
    }
}

---------------------------

이렇게 하고보니 JWT토큰을 발급까진 ok

근데 인제 인증할 때 GetAuthentication 메소드에서 manageService의 loadUserByUserName으로부터 인증정보를 가져오게됨. 여기서 관리자인지 일반회원인지 ROLE구분에 따라 어떤 서비스의 loadUserByUserName을 가져올지 판단해줘야함.

 

token provider에 role을 가져오는 getRole 메소드 생성

public String getRole(String token) {
        return (String) this.parseClaims(token).get(KEY_ROLE);
    }

 

인증하는 클래스인 GetAuthentication에서 해당 getRole 메소드를 호출해서 역할에 맞게 UserDetailService 인터페이스에 서비스를 주입해줌.

private UserDetailsService userDetailsService;
    private final ManageService manageService;
    private final MemberService memberService;
    private final TokenProvider tokenProvider;

    public Authentication getAuthentication(String jwt) {
        String role = tokenProvider.getRole(jwt);

        if (role.equals("ROLE_MANAGER")) {
            System.out.println("매니저 " + role);
            userDetailsService = manageService;
        } else {
            System.out.println(role);
            userDetailsService = memberService;
        }
        //jwt 토큰으로부터 인증정보를 가져옴
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(tokenProvider.getMail(jwt));

        //스프링에서 지원해주는 토큰 형식으로 바꿔줌.
        //리턴되는 토큰은 사용자 정보와 사용자 권한 정보를 가지게 됨.
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

 

-------

참조

https://crazy-horse.tistory.com/entry/UserDetailsService%EB%A5%BC-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B3%A0-%EC%9E%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

[스프링] UserDetailsService를 여러 개 구현하고 잘 사용하기

그동안의 프로젝트들은 UserDetails를 하나만 구현했었습니다. 최근 진행한 프로젝트에서 관리자와 작업자 간 도메인이 명확히 달랐고, 그에 따른 인증과 인가 과정이 필요했습니다. 이런 경우 보

crazy-horse.tistory.com