• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    미해결

ajax 구현 부분 작동이 안되서 질문드립니다.

24.01.18 23:58 작성 조회수 221

0

ajax가 아예 진행이 안되서 질문드립니다.

 

springSecurity6, 스프링부트 3.2.1 사용중입니다.

 

코드는 아래와 같은데 이게 어디서 어디가 틀렸는지를 도저히 모르겠습니다.

ajax 전까지 form 방식은 정상적으로 작동하고 있으며,

ajax 요청 보낼 시

POST http://localhost:8080/api/login

org.apache.http.client.ClientProtocolException

 

이런 에러가 발생합니다. ajax.http 파일은 강의 문서를 다시 다운받아 했으며

postman으로 요청시 이유는 모르겠지만 get 요청으로 처리되고

위의 요청시 아래와 같은 로그 발생합니다

 

10000자 제한떄문에 댓글로 변경

 

curl 요청시 아래와 같습니다

 

10000자 제한떄문에 댓글로 변경

 

아래 코드에서 csrf disable을 하였음에도 계속 동일한 상태이고

강의 git 코드를 여러 브랜치에서 계속 참고해서 막 섞여있어서 어디서부터 고쳐야될지 전혀 모르겠습니다.

 

거의 6시간 넘게 헤매고 있는데 전혀 모르겠습니다.

 

혹시 확인 가능하시면 변경해야될 부분 부탁드립니다. 감사합니다.

 

@Configuration
@Slf4j
@Order(0)
public class AjaxSecurityConfig {

    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }

    @Qualifier("ajaxAuthenticationProvider")
    @Autowired
    private AuthenticationProvider authenticationProvider; // 변경된 부분

    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) throws  Exception {

        log.info("여기옴2");
        http
                .authorizeHttpRequests(auth->
                        auth
                                .requestMatchers("/api/login").permitAll()
                                .anyRequest().authenticated()
                );

        http.exceptionHandling(exceptionHandling ->
                exceptionHandling
                        .authenticationEntryPoint(new AjaxLoginAuthenticationEntryPoint())
                        .accessDeniedHandler(ajaxAccessDeniedHandler())
        );


        http.addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);

        http.csrf(csrf -> csrf.disable());



        return http.build();
    }

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    @Bean
    public AuthenticationSuccessHandler ajaxAuthenticationSuccessHandler(){
        return new AjaxAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler ajaxAuthenticationFailureHandler(){
        return new AjaxAuthenticationFailureHandler();
    }

    public AccessDeniedHandler ajaxAccessDeniedHandler() {
        return new AjaxAccessDeniedHandler();
    }
    @Bean
    public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter();
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        filter.setAuthenticationSuccessHandler(ajaxAuthenticationSuccessHandler());
        filter.setAuthenticationFailureHandler(ajaxAuthenticationFailureHandler());
        return filter;
    }


//    @Bean
//    public AjaxLoginProcessingFilter ajaxLoginProcessingFilter(){
//        AjaxLoginProcessingFilter ajaxLoginProcessingFilter =  new AjaxLoginProcessingFilter();
//        ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean());
//        return ajaxLoginProcessingFilter;
//    }
}

 

@Slf4j
@Component
public class AjaxAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 검증에 대한 구현 부분
     *
     * 여기서 전달받는 authentication 객체는 AuthenticationManager에서 전달받는 것
     * @param authentication the authentication request object.
     * @return
     * @throws AuthenticationException
     */
    @Transactional
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String username = authentication.getName();
        String password = (String) authentication.getCredentials();

        log.info("ajax Authentication"+ authentication);
        log.info("ajax username : "+username);
        log.info("ajax password : "+password);

        /**
         * db에 있는 사용자를 가져오는 부분
         */
        AccountContext accountContext = (AccountContext)userDetailsService.loadUserByUsername(username);
        if (!passwordEncoder.matches(password, accountContext.getPassword())){
            throw new BadCredentialsException("ajax invalid password!");
        }

        /**
         * 섹션 4-8에서 secret key 추가해서 검증하는 부분
         */
//        FormWebAuthenticationDetails formWebAuthenticationDetails = (FormWebAuthenticationDetails) authentication.getDetails();
//        String secretKey = formWebAuthenticationDetails.getSecretKey();
//
//        if (secretKey == null || !"secret123".equals(secretKey)){
//            throw new InsufficientAuthenticationException("secret key invalid");
//        }

        AjaxAuthenticationToken authenticationToken = new AjaxAuthenticationToken(accountContext.getAccount(),null,accountContext.getAuthorities());

//        log.info(""+accountContext.getAccount());
//        log.info(""+authenticationToken);
        return authenticationToken;
    }

    /**
     * parameter 로 전달되는 authentication 타입과 여기서 검증하려는 대상의 타입이 일치하면 검증하도록 하는거
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(AjaxAuthenticationToken.class);
    }
}

 

public class AjaxAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    private Object credentials;

    /**
     * 인증받기전에 사용자가 입력한 아이디와 비밀번호 담는 생성자
     *
     */
    public AjaxAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * 여기는 인증 완료후 아이디와 비번, 권한정보를 담는 생성자
     */
    public AjaxAuthenticationToken(Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }


    public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
        return new UsernamePasswordAuthenticationToken(principal, credentials);
    }


    public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
                                                                    Collection<? extends GrantedAuthority> authorities) {
        return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }


}

 

@Slf4j
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private ObjectMapper objectMapper = new ObjectMapper();

    private static final String XML_HTTP_REQUEST = "XMLHttpRequest";
    private static final String X_REQUESTED_WITH = "X-Requested-With";


    public AjaxLoginProcessingFilter() {
        /**
         * 여기서 정한 경로의 요청만 받음
         * 여기서 path 가 일치해야만 수행함
         */
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

        log.info("isAjax processing");
        if (isAjax(request)){
            throw new IllegalStateException("Authentication is not supported");
        }

        AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);

        /**
         * accountDto 비어있는지 확인하는 부분인데 StringUtils.isEmpty 가 deprecated 되어서 ObjectUtils.isEmpty 사용
         */
        if (ObjectUtils.isEmpty(accountDto.getUsername()) || ObjectUtils.isEmpty(accountDto.getPassword())){
            throw new AuthenticationServiceException("Username op Password is empty");
        }

        AjaxAuthenticationToken token = new AjaxAuthenticationToken(accountDto.getUsername(), accountDto.getPassword());

        return this.getAuthenticationManager().authenticate(token);
    }

    /**
     * 클라이언트와 약속 정해서
     * 그게 참이면 ajax 요청이 맞다고 판단함
     * @param request
     * @return
     */
    private boolean isAjax(HttpServletRequest request) {
        return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
    }

}

 

 

 

답변 4

·

답변을 작성해보세요.

0

소스를 실행해 보니 몇가지 수정해야 할 부분이 있습니다.

다만 최신 버전에 대한 상세한 설명은 범위를 벗어나므로 별도로 설명 드리지 못하는 점 양해 바랍니다

 

첫번째,

AjaxSecurityConfig 에서

http.securityMatcher("/api/**") .authorizeHttpRequests(auth-> auth .requestMatchers("/api/login").permitAll() .anyRequest().authenticated() );

로 수정해 주시기 바랍니다.

최신 버전에서는 SecurityConfig 와 AjaxSecurityConfig 로 나누어서 설정하는 것이 아닌 하나의 설정 클래스에서 SecurityFilterChain 빈 설정을 여러개 하는 것으로 변경되었습니다.

그래서 Order 설정이 제대로 적용되지 않습니다.

 

두번째,

AjaxLoginProcessingFilter 에서

super(new AntPathRequestMatcher("/ogin", "POST")); 을

super(new AntPathRequestMatcher("/api/login", "POST")); 로 변경해 주시기 바랍니다.

 

세번째,

SecurityConfig 에서

@Bean

public WebSecurityCustomizer webSecurityCustomizer() {

return(web) -> web.ignoring().

requestMatchers(new AntPathRequestMatcher("/h2-console/**"),new AntPathRequestMatcher("/api/**"));

}

의 주석을 해제하고 수정해 주시기 바랍니다.

이유는 /api/** Url 로 보내는 요청에 대해서는 SecurityConfig 에서는 무시하고 AjaxSecurityConfig 에서 처리하기 위함입니다.

SecurityConfig 가 모든 요청에 대해 먼저 수신을 하고 있는데 이것이 Order 로 강제가 안되고 있는 상황이고 AjaxSecurityConfig 처럼 /api/** 와 같이 특정한 Url 의 요청만 받도록 설정이 안되어 있기 때문입니다.

일단 이렇게 설정하고 실행하면 정상적으로 동작하는 것을 확인할 수 있습니다.

 

그러나 위의 변경건은 올바른 해결책은 아닙니다.

최신버전에 맞는 문법과 규칙에 의한 코드가 작성되어야 합니다.

0

전체 소스를 공유해 주시면 제가 테스트 해 보도록 하겠습니다.

그리고 본강의의 소스는 스프링 시큐리티 최신버전과 호환성이 맞지 않기 때문에 오류가 나는 원인이 시큐리티의 큰 뼈대 혹은 구조적으로 문제가 발생할 경우 해결점을 찾아가기가 쉽지 않을 것으로 판단됩니다

이와 관련된 다른 질문에서도 답변해 드렸지만 본 강의를 듣는 가장 좋은 방법은 본 강의의 버전대로 학습하시면서 스프링 시큐리티의 전반적인 흐름과 원리 그리고 각 기능을 먼저 충분히 습득하시고 나서 최신 버전으로 마이그레이션 하는 것을 권장하고 있습니다.

물론 이전 버전과 최신 버전의 차이가 클 경우 마이그레이션 하는 것도 어려울 수 있지만 저 개인적인 경험으로는 그게 더 빠르게 최신버전을 소화할 수 있는 방향 인 것 같습니다.

일단 위의 문제는 전체 소스를 내려 받아 실행해 봐야 알 수 있을 것 같습니다.

https://drive.google.com/file/d/1yM-cjroq9j5SmkIu7vXyIMw9EkYpIgHe/view?usp=sharing
첨부파일은 어딘지 몰라서 이렇게 올리면 되나요?

현재 ajaxSecurityConfig 로 접근? 요청 자체가 안가는거 같습니다.

0

2024-01-18T23:51:53.108+09:00 DEBUG 61228 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : GET "/login", parameters={}

2024-01-18T23:51:53.108+09:00 DEBUG 61228 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to io.security.corespringsecurity.controller.login.loginController#login(String, String, Model)

2024-01-18T23:51:53.108+09:00 INFO 61228 --- [nio-8080-exec-3] i.s.c.controller.login.loginController : 요청

2024-01-18T23:51:53.108+09:00 DEBUG 61228 --- [nio-8080-exec-3] o.s.w.s.v.ContentNegotiatingViewResolver : Selected '*/*' given [*/*]

2024-01-18T23:51:53.113+09:00 DEBUG 61228 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Completed 200 OK

0

PS C:\Users\T.K> curl -i -X POST -H "Content-Type: application/json" -H "X-Requested-With: XMLHttpRequest" -d '{"username": "user", "password": "1111"}' http://localhost:8080/api/login

HTTP/1.1 302

Vary: Origin

Vary: Access-Control-Request-Method

Vary: Access-Control-Request-Headers

Set-Cookie: JSESSIONID=011CC19FE72CB6FF3729B2763B395527; Path=/; HttpOnly

X-Content-Type-Options: nosniff

X-XSS-Protection: 0

Cache-Control: no-cache, no-store, max-age=0, must-revalidate

Pragma: no-cache

Expires: 0

X-Frame-Options: DENY

Location: http://localhost:8080/denied?exception=Could not verify the provided CSRF token because no token was found to compare.

Content-Length: 0

Date: Thu, 18 Jan 2024 14:53:12 GMT