Skip to content

다중 로그인 페이지와 authentication manager 2개를 빈으로 등록하여 사용하는 방법

Carol edited this page Aug 28, 2024 · 1 revision

일반 사용자와 식당 사장님은 서로 다른 페이지에서 로그인을 하고 있습니다. 또 각각 다른 엔티티로 구현되어 있습니다.

일반 사용자 → /login MemberEntity

식당 사장님 → /owner/login OwnerEntity

일반 사용자의 경우 OAuth, 일반 로그인 둘 다 구현하려고 하며,

식당 사장님의 경우 일반 로그인으로만 구현되어 있습니다.

일반 사용자가 일반 로그인을 시도하면 MemberJwtAuthenticationFilter가 동작하며,

식당 사장님이 일반 로그인을 시도하면 OwnerJwtAuthenticationFilter 가 동작합니다.

두 필터 모두 UsernamePasswordAuthenticationFilter 를 상속받습니다.

MemberJwtAuthenticationFilter 의 경우 /login 에서 로그인 시도가 있을 때 내부의 메서드가 실행됩니다. OwnerJwtAuthenticationFilter 의 경우에도 default로 /login 에서 실행되기 때문에 /owner/login에서 로그인 시도가 있을 때 필터가 실행되도록 변경하고자 합니다.

setFilterProcessesUrl("/owner/login"); 를 통해 필터가 동작할 url을 설정했습니다.

다만, 식당 사장님이 로그인 시도를 했을 때 필터가 동작하지 않고 아래와 같은 에러 메시지가 출력됩니다.

Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported]

하나의 security config 파일 내에 동일한 UsernamePasswordAuthenticationFilter 을 상속받은 두 필터가 있어서 동작하지 않은 것일까 생각했고, 공통 security config 파일을 만들고, 일반 사용자와 식당 사장님 config 파일을 아래와 같이 분리했습니다.

  • 공통 Security Config 코드

    package org.example.catch_line.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    
    import java.util.Arrays;
    import java.util.List;
    
    @Configuration
    @Order(0)
    public class SecurityConfig {
    
        @Bean
        public BCryptPasswordEncoder bCryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public CorsConfigurationSource corsConfigurationSource() {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080", "http://localhost:8081")); // 모든 IP의 응답을 허용
            configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 모든 종류의 요청을 허용
            configuration.setAllowedHeaders(List.of("*")); // 모든 Header에 응답을 허용
            configuration.setAllowCredentials(true); // 내 서버가 응답을 할 때 json을 자바스크립트에서 처리할 수 있도록 할지 설정
            configuration.addExposedHeader("Authorization");
            configuration.addExposedHeader("Set-Cookie");
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }
    
    }
  • 일반 사용자 Security Config 코드

    package org.example.catch_line.config;
    
    @EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록된다.
    @EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
    // secured 어노테이션 사용 가능(특정 메서드에 간단하게 걸고 싶을 때 사용) "ROLE_ADMIN"
    // @PreAuthorize 어노테이션 사용 가능 "hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')" 함수가 실행되기 전에 권한을 검사
    @Configuration
    @RequiredArgsConstructor
    @Order(1)
    public class MemberSecurityConfig {
    
        private final MemberDefaultLoginService memberDefaultLoginService;
        private final OAuth2LoginService oauth2LoginService;
        private final OAuth2SuccessHandler oAuth2SuccessHandler;
        private final JwtTokenUtil jwtTokenUtil;
    
        private final SecurityConfig securityConfig;
    
        @Bean("authenticationManager")
        @Primary
        public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
            AuthenticationManagerBuilder authenticationManagerBuilder =
                    http.getSharedObject(AuthenticationManagerBuilder.class);
            authenticationManagerBuilder.userDetailsService(memberDefaultLoginService)
                    .passwordEncoder(securityConfig.bCryptPasswordEncoder());
            return authenticationManagerBuilder.build();
        }
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http, @Qualifier("authenticationManager") AuthenticationManager authenticationManager) throws Exception {
    
            // form Login disable -> 해당 필터 사용할 수 있도록 추가, AuthenticationManager 넣어줘야 한다.
    
            // /login 및 /owner/login 경로에 대해 JWT 필터 적용
            MemberJwtAuthenticationFilter memberJwtAuthenticationFilter = new MemberJwtAuthenticationFilter(authenticationManager, jwtTokenUtil);
            MemberJwtAuthorizationFilter memberJwtAuthorizationFilter = new MemberJwtAuthorizationFilter(authenticationManager, jwtTokenUtil, memberDefaultLoginService, oauth2LoginService);
    
            http
                    .cors(cors -> cors.configurationSource(securityConfig.corsConfigurationSource()))  // CORS 설정을 최신 방식으로 변경
                    .csrf(AbstractHttpConfigurer::disable)
                    .formLogin(AbstractHttpConfigurer::disable)  // 폼 로그인 비활성화
                    .httpBasic(AbstractHttpConfigurer::disable)  // HTTP Basic 인증 비활성화
                    .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()))  // frameOptions 설정
                    .authorizeHttpRequests(requests ->requests
                            .requestMatchers("/static/**", "/images/**", "/signup", "/login", "/restaurants/**", "/").permitAll()
                            .requestMatchers("/members/**", "/history/**").hasRole("USER")
                            .anyRequest().permitAll() // 그 외의 요청은 권한 없이 접속 가능
                    )
    
                    .addFilter(memberJwtAuthenticationFilter)
                    .addFilter(memberJwtAuthorizationFilter)
                    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 비활성화
                    .logout(logout -> logout
                            .logoutUrl("/logout")  // 로그아웃 URL 설정
                            .logoutSuccessUrl("/")
                            .deleteCookies("JWT_TOKEN")// 로그아웃 후 이동할 URL 설정
                            .permitAll()
                    )
                    // Oauth 로그인
                    .oauth2Login(login -> login
                            .loginPage("/login/oauth")
                            .defaultSuccessUrl("/loginSuccess")
                            .successHandler(oAuth2SuccessHandler) // OAuth2 성공 핸들러 설정
                            .userInfoEndpoint()
                            .userService(oauth2LoginService) // OAuth 사용자 로그인 처리
                    )
                    .userDetailsService(memberDefaultLoginService); // 일반 사용자 로그인 처리
    
            return http.build();
        }
        // CORS config
    
    }
  • 식당 사장님 Security Config 코드

    package org.example.catch_line.config;
    
    @RequiredArgsConstructor
    @EnableWebSecurity
    @EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
    @Configuration
    @Order(2)
    public class OwnerSecurityConfig {
    
        private final OwnerLoginService ownerLoginService;
        private final JwtTokenUtil jwtTokenUtil;
        private final SecurityConfig securityConfig;
    
        @Bean("ownerAuthenticationManager")
        public AuthenticationManager ownerAuthenticationManager(HttpSecurity http) throws Exception {
            AuthenticationManagerBuilder authenticationManagerBuilder =
                    http.getSharedObject(AuthenticationManagerBuilder.class);
            authenticationManagerBuilder.userDetailsService(ownerLoginService)
                    .passwordEncoder(securityConfig.bCryptPasswordEncoder());
            return authenticationManagerBuilder.build();
        }
    
        @Bean
        public SecurityFilterChain ownerSecurityFilterChain(HttpSecurity http, @Qualifier("ownerAuthenticationManager") AuthenticationManager authenticationManager) throws Exception {
    
            // Owner JWT 필터 설정
            OwnerJwtAuthenticationFilter ownerJwtAuthenticationFilter = new OwnerJwtAuthenticationFilter(authenticationManager, jwtTokenUtil);
            ownerJwtAuthenticationFilter.setFilterProcessesUrl("/owner/login");
    
            OwnerJwtAuthorizationFilter ownerJwtAuthorizationFilter = new OwnerJwtAuthorizationFilter(authenticationManager, jwtTokenUtil, ownerLoginService);
    
            http
                    .cors(cors -> cors.configurationSource(securityConfig.corsConfigurationSource()))
                    .csrf(AbstractHttpConfigurer::disable)
                    .formLogin(AbstractHttpConfigurer::disable)
                    .httpBasic(AbstractHttpConfigurer::disable)
                    .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()))
                    .authorizeHttpRequests(requests ->requests
                            .requestMatchers( "/owner", "/owner/login", "/owner/signup", "/static/**", "/images/**").permitAll()
                            .requestMatchers("/owner/restaurants/**").hasRole("OWNER")
                            .anyRequest().permitAll()
                    )
                    .addFilter(ownerJwtAuthenticationFilter)
                    .addFilter(ownerJwtAuthorizationFilter)
                    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .userDetailsService(ownerLoginService);
    
            return http.build();
        }
    }

다만, 여전히 /owner/login으로 접속해서 로그인 폼을 제출했을 때 해당하는 필터가 동작하지 않고 있습니다.

궁금한 점은 아래와 같습니다.

  1. 로그인 페이지가 다를 때 Security Config 파일을 나눠서 각각 처리하는 것이 올바른 방법일까요?
  2. 하나의 필터를 여러 클래스가 상속받아 사용해도 정상적으로 동작할까요?
  3. 지금 AuthenticationFilter 뿐만 아니라 AuthorizationFilter, UserDetailsService 도 일반 사용자와 식당 사장님별로 각각 구현되어 있는데 이를 나누어서 처리하는 것이 맞는 방법일까요?

https://hbyun.tistory.com/178

  • 사장님 인증 필터 코드

    package org.example.catch_line.filter;
    
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.Cookie;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.example.catch_line.config.auth.OwnerUserDetails;
    import org.example.catch_line.exception.login.LoginException;
    import org.example.catch_line.user.token.JwtTokenUtil;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    
    // 스프링 시큐리티의 필터
    // /login 요청해서 username, password post로 전송하면
    // 해당 필터가 동작
    // security config에서 formLogin disable해서 동작 안함.
    
    @Slf4j
    public class OwnerJwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
        private final AuthenticationManager authenticationManager;
        private final JwtTokenUtil jwtTokenUtil;
    
        public OwnerJwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil) {
            this.authenticationManager = authenticationManager;
            this.jwtTokenUtil = jwtTokenUtil;
            setFilterProcessesUrl("/owner/login");
        }
    
        // /login 요청을 하면 로그인 시도를 위해서 실행되는 함수
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws LoginException {
            log.info("로그인 시도: JwtAuthenticationFilter");
    
            try {
    // BufferedReader를 통해 입력받은 데이터를 문자열로 결합
                BufferedReader br = request.getReader();
                StringBuilder sb = new StringBuilder();
                String input;
                while((input = br.readLine()) != null) {
                    sb.append(input);
                }
    
                // 전체 입력된 데이터
                String requestBody = sb.toString();
                log.info("Raw input: " + requestBody);
    
                // URL 인코딩된 데이터를 파싱하기 위해 split 사용
                String[] params = requestBody.split("&");
                String username = null;
                String password = null;
    
                for (String param : params) {
                    String[] keyValue = param.split("=");
                    if (keyValue.length == 2) {
                        String key = keyValue[0];
                        String value = java.net.URLDecoder.decode(keyValue[1], "UTF-8");
                        if ("username".equals(key)) {
                            username = value;
                        } else if ("password".equals(key)) {
                            password = value;
                        }
                    }
                }
    
                log.info("Decoded username = " + username);
                log.info("Decoded password = " + password);
    
                UsernamePasswordAuthenticationToken authenticationToken =
                        new UsernamePasswordAuthenticationToken(username, password);
    
                // PrincipalDetailsService의 loadUserByUsername() 함수가 실행됨
                // 정상이면 authentication이 리턴됨.
                // DB에 있는 username과 password가 일치한다.
                Authentication authentication =
                        authenticationManager.authenticate(authenticationToken);
    
                OwnerUserDetails ownerUserDetails = (OwnerUserDetails) authentication.getPrincipal();
    
                log.info("OwnerUserDetails Member ID = " + ownerUserDetails.getOwner().getOwnerId());
                log.info("OwnerUserDetails Member Password = " + ownerUserDetails.getOwner().getPassword().getEncodedPassword());
    
                // authentication 객체가 session 영역에 저장됨. -> 로그인이 되었다는 뜻.
                // return 하는 이유는 권한 관리를 security가 대신 해주기 때문에!
                // 굳이 JWT 토큰을 사용하면서 세션을 만들 이유가 없음. 단지 권한 처리 때문에 session에 넣어준다.
    
                return authentication;
    
            } catch (IOException e) {
                throw new LoginException("로그인 실패");
            }
    
            // 1. username, password 받아서
            // 2. 정상인지 로그인 시도를 해본다.
             /*
             * authenticationManager로 로그인 시도를 하면 PrincipalDetailsService가 호출이 됨
             * loadUserByUserName()이 자동으로 실행된다.
             */
            // 3. PrincipalDetails를 세션에 담고 (세션에 담지 않을 경우 권한 관리가 되지 않는다. 권한 관리 할 필요가 없다면 세션에 담을 필요 없다.)
            // 4. JWT 토큰을 만들어서 응답해주면 된다.
        }
    
        // attemptAuthentication 실행 후 인증이 정상적으로 되었다면 successfulAuthentication 함수가 실행된다.
        // JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해주면 된다.
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            log.info("successfulAuthentication 실행, 인증 완료 !!!");
    
            OwnerUserDetails ownerUserDetails = (OwnerUserDetails) authResult.getPrincipal();
            String jwtToken = jwtTokenUtil.generateToken(ownerUserDetails.getUsername());
    
            response.addHeader("Authorization", "Bearer " + jwtToken);
    
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write("{\"token\": \"" + jwtToken + "\"}");
    
            // jwt 토큰을 쿠키에 저장
            Cookie jwtCookie = new Cookie("JWT_TOKEN", jwtToken);
            jwtCookie.setHttpOnly(true);  // XSS 공격 방지
            jwtCookie.setSecure(true);    // HTTPS에서만 사용
            jwtCookie.setPath("/");       // 애플리케이션의 모든 경로에서 사용 가능
            jwtCookie.setMaxAge(600); // 쿠키의 유효기간을 1시간으로 설정 (필요에 따라 조정 가능)
    
            response.addCookie(jwtCookie);
    
    //        response.sendRedirect("http://localhost:8080/restaurants" + "?token=Bearer "+ jwtToken);
    
            super.successfulAuthentication(request, response, chain, authResult);
        }
    
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
            // 로그인 실패 시 처리할 로직
            log.error("unsuccessfulAuthentication 실행, 인증 실패: " + failed.getMessage());
    
            // 에러 메시지를 로그인 페이지로 전달
            response.sendRedirect("/owner/login?error=true&exception=" + java.net.URLEncoder.encode(failed.getMessage(), "UTF-8"));
        }
    }
  • 사장님 인가 필터 코드

    package org.example.catch_line.filter;
    
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.Cookie;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.example.catch_line.config.auth.*;
    import org.example.catch_line.user.token.JwtTokenUtil;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
    
    import java.io.IOException;
    import java.util.Arrays;
    import java.util.List;
    
    public class OwnerJwtAuthorizationFilter extends BasicAuthenticationFilter {
    
        private final JwtTokenUtil jwtTokenUtil;
        private final OwnerLoginService ownerLoginService;
    
        private static final List<String> WHITELIST_URLS = Arrays.asList(
                "/login",
                "/signup",
                "/public/**",
                "/static/**",
                "/images/**",
                "/restaurants",
                "/"
        );
    
        public OwnerJwtAuthorizationFilter(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, OwnerLoginService ownerLoginService) {
            super(authenticationManager);
            this.jwtTokenUtil = jwtTokenUtil;
            this.ownerLoginService = ownerLoginService;
        }
    
        public OwnerJwtAuthorizationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, JwtTokenUtil jwtTokenUtil, OwnerLoginService ownerLoginService) {
            super(authenticationManager, authenticationEntryPoint);
            this.jwtTokenUtil = jwtTokenUtil;
            this.ownerLoginService = ownerLoginService;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
            String requestURI = request.getRequestURI();
    
            if (isWhitelisted(requestURI)) {
                chain.doFilter(request, response);
                return;
            }
    
            System.out.println("인증이나 권한이 필요한 주소 요청이 됨.");
    
            String jwtToken = null;
    
            if (request.getCookies() != null) {
                for (Cookie cookie : request.getCookies()) {
                    if ("JWT_TOKEN".equals(cookie.getName())) {
                        jwtToken = cookie.getValue();
                        break;
                    }
                }
            }
    
            if (jwtToken != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                String username = jwtTokenUtil.getUsernameFromToken(jwtToken);
    
                if (username != null && jwtTokenUtil.validateToken(jwtToken, username)) {
                    OwnerUserDetails ownerUserDetails = (OwnerUserDetails) ownerLoginService.loadUserByUsername(username);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(ownerUserDetails, null, ownerUserDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
            chain.doFilter(request, response);
        }
    
        private boolean isWhitelisted(String requestURI) {
            return WHITELIST_URLS.stream().anyMatch(url -> requestURI.startsWith(url));
        }
    }
  • 사장님 로그인 서비스 코드

    package org.example.catch_line.config.auth;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.example.catch_line.exception.login.LoginException;
    import org.example.catch_line.user.owner.model.entity.OwnerEntity;
    import org.example.catch_line.user.owner.repository.OwnerRepository;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.stereotype.Service;
    
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class OwnerLoginService implements UserDetailsService {
    
        private final OwnerRepository ownerRepository;
        private OwnerEntity owner;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws LoginException {
    
            log.info("사장님 로그인 서비스 시작");
            OwnerEntity owner = ownerRepository.findByLoginId(username).orElseThrow(()-> new LoginException("존재하지 않는 사장님"));
            return new OwnerUserDetails(owner);
    
        }
    }
  • 사장님 UserDetails

    package org.example.catch_line.config.auth;
    
    import lombok.Data;
    import lombok.Getter;
    import org.example.catch_line.common.constant.Role;
    import org.example.catch_line.user.member.model.entity.MemberEntity;
    import org.example.catch_line.user.owner.model.entity.OwnerEntity;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.ArrayList;
    import java.util.Collection;
    
    @Data
    public class OwnerUserDetails implements UserDetails {
    
        private OwnerEntity owner;
    
        public OwnerUserDetails(OwnerEntity owner) {
            this.owner = owner;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(Role.USER.getAuthority()));
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return owner.getPassword().getEncodedPassword();
        }
    
        @Override
        public String getUsername() {
            return owner.getLoginId();
        }
    }

Member와 Owner를 한 엔티티로 합치는 것이 좋다. authentication manager, filter, userDetailsService 모두 하나를 사용하고 Spring Security에서 hasRole을 통해 role base로 구분하는 것이 좋을 것 같다.

지금은 식당 사장님이 관리자와 같은 역할을 수행한다. 그래서 이보다는, 고객의 등급을 나누는 편이 더 맞다.

만약 지금처럼 따로 구현하려면 userDetailsService가 2개이니 authentication manager 또한 2개 구현해야 한다.

Member와 Owner를 구분한 이유는 연관관계 때문이었다.

Member가 사라졌을 때 Review가 사라져야 하는게 맞다면 연관관계를 지어라. 아니라면, 연관관계를 끊고 ID 기반으로 통신을 할 수 있다.

Restaurant도 마찬가지이다. Restaurant이 owner Id를 가지도록 해야 한다. Query Dsl로 조인해서 가져올 수 있다.

요즘 연관관계는 생명 주기 단계로 끊는 추세이다. 그렇지 않으면 점점 복잡해진다.

Authentication Manager Bean에 2개 등록하는 방법

  1. Spring이 Bean에 등록할 때 이름을 따로 지정해주지 않으면 메서드명으로 등록된다.
  2. 두 AuthenticationManager의 이름을 다르게 지정한다.
  3. 각자 사용하는 UserDetailsService가 다르니, 이를 각각 설정해준다.
@Bean
// Primary 지정해버리면 다른 config 파일이더라도 무조건 이것만 사용된다. -> Bean에 이름 지정 필요
@Primary
public AuthenticationManager memberAuthenticationManager(HttpSecurity http) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(memberDefaultLoginService);
        provider.setPasswordEncoder(bCryptPasswordEncoder());
        return new ProviderManager(provider);
}

@Bean
public AuthenticationManager ownerAuthenticationManager(HttpSecurity http) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(ownerLoginService);
        provider.setPasswordEncoder(bCryptPasswordEncoder());
        return new ProviderManager(provider);
}
  1. @Qualifier를 통해 사용할 AuthenticationManager의 이름을 지정해준다.

  2. FilterChain에 AuthenticationManager authenticationManager만 파라미터로 넣어주면 @Primary로 지정한 AuthenticationManager만 들어간다. 다른 Security Config 파일을 사용해도 마찬가지다. 그래서 반드시 이름으로 둘을 구분하고, 하나의 config에서 둘 다 사용할 경우 둘 다 넣어줘야 한다.

  3. AuthenticationFilter의 경우 아래와 같이 authenticationManager의 authenticate을 통해 UserDetailsService를 호출하여 로그인 요청을 처리한다.

    *Authentication* authentication = authenticationManager.authenticate(authenticationToken);
  4. AuthorizationFilter의 경우 BasicAuthenticationFilter를 상속받는다. 이 필터가 내부에 AuthenticationManager를 갖고 있기 때문에 필요하다. → 🌟**OncePerRequestFilter로 변경 가능할지?**

  5. 위와 같은 이유로 두 개의 필터에서 모두 AuthenticationManager를 필요로 한다.

  6. Authentication Manager를 잘 맞게 넣어준다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
				@Qualifier("memberAuthenticationManager") AuthenticationManager memberAuthenticationManager,
        @Qualifier("ownerAuthenticationManager") AuthenticationManager ownerAuthenticationManager) throws Exception {

// form Login disable -> 해당 필터 사용할 수 있도록 추가, AuthenticationManager 넣어줘야 한다.

		MemberJwtAuthenticationFilter memberJwtAuthenticationFilter = new MemberJwtAuthenticationFilter(memberAuthenticationManager);
		MemberJwtAuthorizationFilter memberJwtAuthorizationFilter = new MemberJwtAuthorizationFilter(memberAuthenticationManager);

		OwnerJwtAuthenticationFilter ownerJwtAuthenticationFilter = new OwnerJwtAuthenticationFilter(ownerAuthenticationManager);
		OwnerJwtAuthorizationFilter ownerJwtAuthorizationFilter = new OwnerJwtAuthorizationFilter(ownerAuthenticationManager);
	 /*
	 * 생략 
	 */
}