관리자 회원가입을 위해 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());
}
-------
참조