프로젝트

[트러블 슈팅] 스프링 시큐리티에서의 로그인 정보 dto 바인딩 오류

매일_공부 2025. 1. 20. 14:11
반응형

개인 프로젝트를 진행하는 도중 스프링 시큐리티에서 문제가 발생했다.

 

기존 까지는 로그인 과정을 직접 개발하면서 세션에 멤버 정보를 저장하면서 개발하였지만,

 

자잘한 문제와 소셜 로그인 로그인 로그아웃에서의 연결에서도 모호한 관계가 되어서 사용 안정성과 디버깅의 편리를 

위해서 스프링 시큐리티를 사용하기로 하였다.

 

package GoodPang.goodPang.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity//시큐리티 설정을 활성화한다. 우리가 작성한 설정이 기본 설정보다 우선 적용되게 한다.
@Configuration //설정 정보 명시 에노테이션
public class SecurityConfig {

//필터 체인을 정의하는 메소드
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //authorizeHttpRequest는 HTTP 요청에 대한 접근 제어를 설정
        http.authorizeHttpRequests(requests -> requests
                //permitAll에 있는 곳은 항상 접근을 허용
                .requestMatchers("/","/home/", "/login","/signup/**","/members/signup", "/css/**","/js/**","/fragments/**","https://getbootstrap.com/docs/**").permitAll()
                //hasRole은 해당 역할을 가진 사용자만 접근이 가능
                .requestMatchers("/admin/**").hasRole("ADMIN")
                //authenticated은 인증된 사용자만 접근 가능
                .anyRequest().authenticated()
        )
        // 폼 기반 로그인 설정
        .formLogin((form) -> form
                .loginPage("/login") //커스텀 로그인 페이지를 /login 경로로 설정
                .defaultSuccessUrl("/loginHome", true) //성공하면 home으로 리다이렉트
                .permitAll() //모든 요청에 대해서 접근 허용
        )
        //로그아웃 처리
        .logout(logout -> logout
                .logoutUrl("/logout") //로그아웃 페이지를 /logout 경로로 설정
                .logoutSuccessUrl("/login?logout") //로그아웃 성공시 /login?logout으로 리다이렉트
                .permitAll()
        );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        //Bcrypt..는 스프링 시큐리티가 제공하는 클래스로, 비밀번호를 암호화하는데 사용한다.
        //입력을 해시화 하고 솔트를 부여한다.
        return new BCryptPasswordEncoder();
    }


}

 


@Controller
public class HomeController {
    @GetMapping("/login")
    public String login(Model model) {
        return "members/login";
    }

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

    }

 

 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="canonical" href="https://getbootstrap.com/docs/5.3/examples/sign-in/">
  <link th:href="@{/css/bootstrap.min.css}" href="../css/bootstrap.min.css" rel="stylesheet">
  <link href="/css/sign-in.css" rel="stylesheet">
</head>
<body class="d-flex align-items-center py-4 bg-body-tertiary">
<main class="form-signin w-100 m-auto">
  <form th:action="@{/login}" method="post">

    <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
    <!-- 로그인 실패 시 에러 메시지 표시 -->
    <div th:if="${param.error}">
      <p class="field-error">Invalid login credentials</p>
    </div>
    <div class="form-floating">
      <input type="text" class="form-control" id="floatingInput" name="username" placeholder="ID">
      <label for="floatingInput">Login ID</label>
    </div>

    <div class="form-floating">
      <input type="password" class="form-control" id="password" name="password" placeholder="Password">
      <label for="password">Password</label>
    </div>

    <button class="btn btn-primary w-100 py-2 mb-3" type="submit">Sign in</button>
    <div class="d-grid gap-2">
      <button type="button" th:onclick="|location.href='@{/signup}'|" class="btn btn-warning btn-lg">Sign-up</button>
      <div class="container text-center mt-3">
        <a th:href="${location}">
          <img src="//k.kakaocdn.net/14/dn/btqCn0WEmI3/nijroPfbpCa4at5EIsjyf0/o.jpg" width="242" />
        </a>
      </div>
    </div>
  </form>
</main>

<script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>

 

 

 

위는 수정 후 컨트롤러이다. 위의 config를 통해서 기본 로그인 페이지는 /login 이므로, 로그인 하지 않은 회원이 접근하면 

getMapping(/login) 을 실행하게 되고 컨트롤러가 작동해서 "members/login'화면을 보여주게 된다.

 

 

기존 컨트롤러에는 model에 dto 객체를 담아서, login 화면에 전달하였다. 그 후, 화면에서 dto 객체에서 로그인 정보를 

담아 post 요청을 보냈다.

 

하지만, 모델에 정보는 잘 담기지만, 로그인 과정에서 로그인 정보가 바인딩이 되지 않는 오류가 발생하였다.

 

 

 

 

 

package GoodPang.goodPang.security;

import GoodPang.goodPang.domain.member.Member;
import GoodPang.goodPang.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomUserDetailService implements UserDetailsService {
    private final MemberRepository memberRepository;

    //DB에서 이메일로 조회하는 거니까 Transaction 필요하지 않다. (DB에 변경을 주지 않음)
    //email은 name 이자 로그인 id 이다.
    @Override
    public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
        log.info("loadUserByUsername", loginId); //여기서 바인딩이 안됨
        Member member = memberRepository.findByLoginId(loginId).orElseThrow(() ->
                new UsernameNotFoundException("해당 이메일을 가진 유저가 존재하지 않습니다. : " + loginId
                ));

        return User.withUsername(member.getLoginId())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();

    }
}

 

 

기본 설정으로 /login 경로로 post 요청을 보내면, 스프링에서 요청을 가로체서 커스텀 서비스를 작동하게 된다.

 

이 과정에서 String loginId로 로그인 정보를 바인딩해야 하지만, dto 객체에 담았기 때문에 정보를 바인딩 하지 

못했던 것이였다.

 

따라서 위의 코드처럼 수정하여 문제를 해결하게 되었다.

 

 

 

반응형