-
Notifications
You must be signed in to change notification settings - Fork 3
다중 로그인 페이지와 authentication manager 2개를 빈으로 등록하여 사용하는 방법
일반 사용자와 식당 사장님은 서로 다른 페이지에서 로그인을 하고 있습니다. 또 각각 다른 엔티티로 구현되어 있습니다.
일반 사용자 → /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
으로 접속해서 로그인 폼을 제출했을 때 해당하는 필터가 동작하지 않고 있습니다.
궁금한 점은 아래와 같습니다.
- 로그인 페이지가 다를 때
Security Config
파일을 나눠서 각각 처리하는 것이 올바른 방법일까요? - 하나의 필터를 여러 클래스가 상속받아 사용해도 정상적으로 동작할까요?
- 지금
AuthenticationFilter
뿐만 아니라AuthorizationFilter
,UserDetailsService
도 일반 사용자와 식당 사장님별로 각각 구현되어 있는데 이를 나누어서 처리하는 것이 맞는 방법일까요?
-
사장님 인증 필터 코드
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로 조인해서 가져올 수 있다.
요즘 연관관계는 생명 주기 단계로 끊는 추세이다. 그렇지 않으면 점점 복잡해진다.
- Spring이 Bean에 등록할 때 이름을 따로 지정해주지 않으면 메서드명으로 등록된다.
- 두 AuthenticationManager의 이름을 다르게 지정한다.
- 각자 사용하는 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);
}
-
@Qualifier
를 통해 사용할 AuthenticationManager의 이름을 지정해준다. -
FilterChain에
AuthenticationManager authenticationManager
만 파라미터로 넣어주면@Primary
로 지정한 AuthenticationManager만 들어간다. 다른 Security Config 파일을 사용해도 마찬가지다. 그래서 반드시 이름으로 둘을 구분하고, 하나의 config에서 둘 다 사용할 경우 둘 다 넣어줘야 한다. -
AuthenticationFilter의 경우 아래와 같이 authenticationManager의 authenticate을 통해 UserDetailsService를 호출하여 로그인 요청을 처리한다.
*Authentication* authentication = authenticationManager.authenticate(authenticationToken);
-
AuthorizationFilter의 경우 BasicAuthenticationFilter를 상속받는다. 이 필터가 내부에 AuthenticationManager를 갖고 있기 때문에 필요하다. → 🌟**
OncePerRequestFilter로 변경 가능할지?
** -
위와 같은 이유로 두 개의 필터에서 모두 AuthenticationManager를 필요로 한다.
-
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);
/*
* 생략
*/
}