스프링시큐리티

[스프링시큐리티] 커스텀 Login 화면과 csrf

여웅사랑 2023. 7. 26. 09:34

스프링 시큐리티에서는 기본적으로 Login 화면을 제공한다.

 

이 로그인 화면을 커스텀 하기 위해서는

 

1. 스프링 시큐리티 Config 설정

-> Bean으로 등록했던 스프링 시큐리티 설정값에서 로그인 페이지와 처리 url을 표기해준다

 

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    
    //인증 방식
    http
            .formLogin() //폼 로그인 방식
            .loginPage("/login") // 사용자 정의 로그인 페이지
            .loginProcessingUrl("/loginProc") // 로그인 프로세스 url
            .defaultSuccessUrl("/", false) // 로그인 성공 후 원래 페이지로 리다이렉트
            .failureUrl("/login") //로그인 실패 시 이동할 경로
            .usernameParameter("userNickName") // 아이디 파라미터 설정
            .passwordParameter("password") //패스워드 파라미터 설정
            .permitAll();

 

2.  html  페이지 작성, 파라미터와 처리  url을 동일하게 맞춰준다.

<form action="/loginProc" class="form-signin" method="post">
    <div class="form-group">
        <input type="text" class="form-control form-control-user"
               id="userNickName"  name="userNickName"
               placeholder="userNickName">
    </div>
    <div class="form-group">
        <input type="password" class="form-control form-control-user"
               id="password" name ="password" placeholder="Password">
    </div>
    <div class="form-group">
        <div class="custom-control custom-checkbox small">
            <input type="checkbox" class="custom-control-input" id="customCheck">
            <label class="custom-control-label" for="customCheck">Remember
                Me</label>
        </div>
    </div>
    <button type="submit" class="btn btn-primary btn-user btn-block">
        Login
    </button>
    <hr>
    <a href="index.html" class="btn btn-google btn-user btn-block">
        <i class="fab fa-google fa-fw"></i> Login with Google
    </a>
    <a href="index.html" class="btn btn-facebook btn-user btn-block">
        <i class="fab fa-facebook-f fa-fw"></i> Login with Facebook
    </a>
</form>

 

3. 컨트롤러를 만들어준다.

-> Login Page이동을 위한 컨트롤러만 만들어주면 된다.

Login처리를 위한 컨트롤러는 별도로 작성하지 않아도 1에서 등록한 loginProcessinUrl과 2에서 작성한 action 태그 값만 같으면 스프링 시큐리티가 알아서 처리해준다.

 

@Controller
@Slf4j
public class LoginController {

    @GetMapping("/login")
    public String login(){
        return "member/login/login";
    }

4. 근데 이렇게 하면 로그인 처리가 정상적으로 작동하질 않는다.

 

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

 

라고 오류가 뜨는데 메소드 URL이나 전달 방식이 오류가 있는게 아니라

 

스프링 시큐리티에서 지원하는 CSRF설정에서 막혀서 그렇다.

 

5.  CSRF 는 웹 서비스 취약점 중 하나로 

 

"웹 서비스에 로그인한 사용자가 실제로 의도하지 않았음에도 공격자가 준비한 악성 웹 페이지로 인해 웹 서비스에 위조된 요청을 보내게 되는 것입니다"

 

 라고 한다.  스프링 시큐리티에서는 이 CSRF 공격을 막기 위해 자동으로 토큰을 발급하고 검증하는데, 위 요청에서는 CSRF 토큰을 함께 전달하지 못하기 때문에 요청이 막히게 된다.

 

6.  해결방법

 

(1) csrf 미사용( 비추 )

 -> 스프링 시큐리티 설정에서 csrf 설정을 disbale 해준다.  간단하지만 보안상 좋지 않다.

http.csrf().disable();

 

(2) 타임리프 사용

 -> 타임리프는 스프링 시큐리티와 함께 사용할 경우 <form>  태그를 사용할 때, 자동으로 csrf 파라미터가 폼에 추가되어 토큰이 전송된다. 그렇기 때문에 타임리프를 사용할 경우 csrf 설정으로 막혀있는지 조차 모를 수 있음.

 

(3) 메타데이터에서 직접 받아서 함께 전달

-> 메타데이터에서 직접 토큰 값을 받아 form태그에 추가하여 전달한다. jQuery를 사용하면 조금 더 간편

 

  <meta name="_csrf_parameter" content="${_csrf.parameterName}">
  <meta name="_csrf" content="${_csrf.token}">
function getMetaContent(name) {
  const meta = document.querySelector(`meta[name="${name}"]`);
  return meta ? meta.content : '';
}

const csrfParameterName = getMetaContent('_csrf_parameter');
const csrfToken = getMetaContent('_csrf');

const form = document.querySelector('.form-signin');

const csrfInputElement = document.createElement('input');
csrfInputElement.type = 'hidden';
csrfInputElement.name = csrfParameterName;
csrfInputElement.value = csrfToken;

form.appendChild(csrfInputElement);

-> 비동기통신을 활용한 방법

  <meta name="_csrf_parameter" content="${_csrf.parameterName}">
  <meta name="_csrf" content="${_csrf.token}">
$(document).ready(function() {
  var csrfParameterName = $('meta[name="_csrf_parameter"]').attr('content');
  var csrfToken = $('meta[name="_csrf"]').attr('content');
  
  // AJAX 호출 설정
  $.ajaxSetup({
    beforeSend: function(xhr) {
      xhr.setRequestHeader(csrfParameterName, csrfToken);
    }
  });

  // AJAX 호출
  $.ajax({
    url: '/example-endpoint',
    type: 'POST',
    data: {
      // 데이터
    },
    success: function(response) {
      // 성공 시 처리
    },
    error: function(xhr, status, error) {
      // 실패 시 처리
    }
  });
});