정수원님의 강의 스프링 시큐리티 완전 정복 [6.x 개정판] 보면서 공부한 내용입니다.
    @Bean
    @Order(1)
    public SecurityFilterChain restSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/api/login")
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/css/**","/images/**","/js/**","/favicon.*","/*/icon-*").permitAll()
                        .anyRequest().permitAll()
                )
              .csrf(AbstractHttpConfigurer::disable)
        ;
        return http.build(); // securityFilterChain 빈 생성
    }

http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)http.addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)http.addFilter(new CustomFilter());http.addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // 비동기식으로 올 때만 인증 필터 작용하도록 설정
        if(HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)){
            throw new IllegalArgumentException("POST나 비동기 방식이 아닙니다.");
        }
        // 사용자가 입력한 정보를 가져와서 인증처리 진행히여 AccountDto에 담도록 설정
        AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
        if(!StringUtils.hasText(accountDto.getUsername()) || !StringUtils.hasText(accountDto.getPassword())){
            // username 또는 password에 값이 없으면 예외 발생
            throw new AuthenticationServiceException("아이디 또는 비밀번호가 없습니다.");
        }
        // 모든 조건 통과하면 인증 처리되도록 진행
        RestAuthenticationToken restAuthenticationToken = new RestAuthenticationToken(accountDto.getUsername(),accountDto.getPassword());
        return getAuthenticationManager().authenticate(restAuthenticationToken);
    }
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String loginId = authentication.getName();
        String password = (String) authentication.getCredentials();
        AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(loginId);
        if(!passwordEncoder.matches(password, accountContext.getPassword())){
            throw new BadCredentialsException("Invalid password");
        }
        return new RestAuthenticationToken(accountContext.getAuthorities(), accountContext.getAccountDto(), null);
    }
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        AccountDto accountDto = (AccountDto) authentication.getPrincipal();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        accountDto.setPassword(null);
        mapper.writeValue(response.getWriter(),accountDto);
        clearAuthenticationAttributes(request);
    }
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        // 401 코드 => 인증 실패
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        if(exception instanceof BadCredentialsException){
            mapper.writeValue(response.getWriter(), "유효하지 않은 아이디 또는 비밀번호 입니다.");
        }
        mapper.writeValue(response.getWriter(),"인증 실패");
    }
    /**
     * 세션을 사용하도록 설정
     */
    private SecurityContextRepository getSecurityContextRepository(HttpSecurity http) {
        SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
        if(securityContextRepository == null){
            securityContextRepository = new DelegatingSecurityContextRepository(
                    new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository()
            );
        }
        return securityContextRepository;
    }
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 인증받지 못한 상태에서 왔기 때문에 오류코드를 응답해야됨
        // => 401 코드
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        // int SC_UNAUTHORIZED = 401; 로 정의되어 있음
        response.getWriter().write(mapper.writeValueAsString(HttpServletResponse.SC_UNAUTHORIZED));
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 인증 받은 상태에서 접근 거부 당했기 때문에 오류코드를 응답해야됨
        // => 403 코드
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        // int SC_FORBIDDEN = 403; 로 정의되어 있음
        response.getWriter().write(mapper.writeValueAsString(HttpServletResponse.SC_FORBIDDEN));
    }

. exceptionHandling(exption -> exption
   .authenticationEntryPoint(new RestAuthenticationEntryPoint())
   .accessDeniedHandler(new RestAccessDeniedHandler()))
    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response){
        Authentication authentication = SecurityContextHolder.getContextHolderStrategy().getContext().getAuthentication();
        if(authentication != null){
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }
        return "logout";
    }
function login() {
            const csrfHeader = $('meta[name="_csrf_header"]').attr('content');
            const csrfToken = $('meta[name="_csrf"]').attr('content')
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            fetch('/api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest',
                    [csrfHeader]: csrfToken
                },
                body: JSON.stringify({ username, password }),
            })
                .then(response => {
                    response.json().then(function (data) {
                        console.log(data);
                        window.location.replace('/api')
                    })
                })
                .catch(error => {
                    console.error('Error during login:', error);
                });
        }

.with(new RestApiDsl<>(), restDsl -> restDsl
  .restSuccessHandler(restSuccessHandler)
  .restFailureHandler(restFailureHandler)
  .loginPage("/api/login")
  .loginProcessingUrl("/api/login"))