[Spring] Spring-Security 로그인 구현

yunSeok·2023년 11월 14일
0

사이드 프로젝트

목록 보기
1/14

📌 Aajx를 이용해 로그인을 구현합니다!!


기존 방식

login.jsp

form 태그에 토큰을 hidden으로 같이 넘기는 방식을 사용하고있습니다.

<form id="loginForm">
   <div class="mb-3">
      <label for="idInput" class="form-label">아이디</label>
      <input type="text" class="form-control" name="id" id="idInput" placeholder="아이디를 입력해주세요.">
      <c:remove var="idInput"/>
   </div>

   <div class="mb-3">
      <label for="pwInput" class="form-label">비밀번호</label>
      <input type="password" class="form-control" name="pw" id="pwInput" placeholder="비밀번호를 입력해주세요.">
   </div>
            
   <div class="d-flex justify-content-center">
     <button type="submit" id="loginButton" class="btn btn-primary" style="width: 350px;" disabled>로그인</button>
     <input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
   </div>
</form>

Ajax

$("#loginForm").submit(function(event) {
        	event.preventDefault();
        	
            var formData = $("#loginForm").serialize();
                 
	    $.ajax({
	    	type: "POST",
	        url: "<c:url value='/user/login'/>",
	        data: formData,
	        dataType: "json",
	        success: function (response) {
	            if (response === "ok") {
	            	alert("로그인 성공!");
	            	location.reload();
	            } else {
	                alert("아이디와 비밀번호를 확인해주세요.");
	            }
	        },
	          error: function(error) {
	            alert(error.responseText);
	            console.log("Error:", error);
	            console.log("Form Data:", formData);
            }
	     });
	  });

로그인 요청

현재 계속 오류가 발생하는데.......

WARN : org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]

기존 form 태그에 action 속성을 사용했을땐 오류없이 잘 되는데 ajax를 사용해서 그런지 같은 오류의 반복이다.........

로그를 봐보면

INFO : jdbc.resultsettable - 
|----------|----------|
|id        |auth      |
|----------|----------|
|yunseok   |ROLE_USER |
|----------|----------|

INFO : jdbc.resultsettable - 
|---------|--------------------------------------------|-----|
|id       |pw                                          |name |
|---------|--------------------------------------------|-----|
|yunseok  |$x0a7wlF97uzAa6nKYlD.oUEosmWS9siddAzPBLjvqXm|이름  |
|---------|--------------------------------------------|-----|

테이블에서 아이디, 비밀번호와 설정한 권한의 이름까지 잘 가져와지긴하는데..

console을 봐도 Form Data에 토큰도 잘 넘어가는걸 볼 수 있다.

구글링을 해보면 보통 오류발생 원인이

  1. 토튼을 전달하지 않아서
  2. GET, POST 매핑의 불일치
  3. AJAX 데이터 및 형식
  4. controller에서 @RequestParam 변수명 다름

정도로 찾을 수 있었는데

  1. 토큰도 전달되어지고있고,
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
  1. 컨트롤러에서 메소드로 POST를 사용중이고,
@PostMapping(value = "/login")
  1. ajax에서 form 데이터의 정보들을 보내주고,
var formData = $("#loginForm").serialize();
            .
            .
  data: formData,
  dataType: "json",      
  1. ajax에서 Form Data로 보내지는 값을 컨트롤러에서 처리할때
    @RequestParam 으로 데이터 이름을 일치시켜주었다.
   @PostMapping(value = "/login")
   @ResponseBody
   public ResponseEntity<String> loginPOST(
		   @RequestParam("id") String id
		   , @RequestParam("pw") String pw
		   , HttpSession session) throws Exception {
                .
                .
                .
   }

그래도 같은 오류가 계속 발생.....


해결과정들.....

ajax의 data로 전달되어지는 과정에서 json 형식에 문제가 있다고 생각되어서 아이디와 패스워드의 데이터를 전달하는 방식을 변경해야 되겠다고 생각했다.
왜냐..? 토큰도 전달했고 시큐리티 설정도 알맞게 했다고 생각했기 때문...
제발 돼라ㅠㅠㅠ

Ajax 수정

  1. 일단 먼저 토큰을 전달하는 방식을 변경했습니다.
    기존
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">

input 태그로 전달되는 방식에서


var csrfHeaderName = "${_csrf.headerName}";
var csrfTokenValue = "${_csrf.token}";

$(document).ajaxSend(function(e, xhr){
	xhr.setRequestHeader(csrfHeaderName, csrfTokenValue);
});

ajax요청시 전달되어지도록 변경

이 방식을 사용하면
Ajax 요청 시 CSRF 토큰이 전달되어서 beforeSend 를 설정할 필요가 없다!

  1. 전달 값을 JSON 문자열형식으로 변경했습니다.
var formData = JSON.stringify({
     id: $("#idInput").val(),
     pw: $("#pwInput").val()
 });

$.ajax({
	    	type: "POST",
	        url: "<c:url value='/user/login'/>",
	        data: JSON.stringify(formData),
            Content-Type: "application/json",
	        dataType: "json",
	        success: function (response) {
	            if (response === "ok") {
	            	alert("로그인 성공!");
	            } 
	        },
	          error: function(error) {
	            alert(error.responseText);
	            console.log("Error:", error);
	            console.log("Form Data:", formData);
            }
	     });
	  });

변경 후
다시 로그인 요청!!!.... 은 에러..가 발생했네요

콘솔창에는 responseText: "ok"로 응답이 잘되었다고 나오고
하지만 statusText: "parsererror"라는 오류가 나오는걸 확인했습니다.

컨트롤러

JSON 형식의 데이터를 응답 받고 있어서
기존 @RequestParam으로 전달 받았던 값들을 @RequestBody로 한번에 받는 형식으로 변경하였다.

@PostMapping(value = "/login")
@ResponseBody
public ResponseEntity<String> loginPOST(
		   @RequestBody Userinfo userinfo
		   , HttpSession session) throws Exception {
	   
            System.out.println("아이디 : " +userinfo.getId() + ", 비밀번호 : " +userinfo.getPw());
            
            try {
            	Userinfo userinfoAuth = userinfoservice.getUserinfoLogin(userinfo.getId());
                System.out.println(userinfoAuth);

		          if (userinfoAuth != null) {
		        	  
		             // 로그인 시
		              if (pwEncoder.matches(userinfo.getPw(), userinfoAuth.getPw())) {
		            	  
		            	  System.out.println("로그인 성공");
		            	  userinfoAuth.setPw("");
		                  session.setAttribute("userinfo", userinfoAuth);
		                  userinfoservice.updateUserLogindate(userinfoAuth.getId());
		                  
		                  return ResponseEntity.ok("ok");
		                  
		              } else {
		            	  System.out.println("비빌번호 틀림");
		                  return ResponseEntity.badRequest().body("비밀번호가 일치하지 않습니다.");
		              }
		              
		          // 계정이 존재하지 않을때
		          } else {
		        	  System.out.println("아이디 비번 모두 틀림");
		             return ResponseEntity.badRequest().body("아이디와 비밀번호를 확인해주세요.");
		          }
		          
			} catch (Exception e) {
				System.out.println("로그인 실패");
		        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("서버 오류가 발생했습니다.");
			}
      
   }

중간에 메시지를 출력하도록 해서 어느부분이 실행되었는지 확인해봤는데

INFO : jdbc.audit - 1. PreparedStatement.close() returned 
INFO : jdbc.connection - 1. Connection closed
INFO : jdbc.audit - 1. Connection.close() returned 
로그인 성공
INFO : jdbc.connection - 2. Connection opened
INFO : jdbc.audit - 2. Connection.new Connection returned 
INFO : jdbc.audit - 2. Connection.getAutoCommit() returned true
INFO : jdbc.audit - 2. PreparedStatement.new PreparedStatement returned

요청한 데이터를 컨트롤러에서는 잘 응답해주고 있었다.
하지만 statusText: "parsererror" 오류가 발생하고 있어서 오류내용을 찾아보았다.

ajax호출 시 dataType을 "application/json"으로 하고 있는데,
컨트롤러에서 응답하는 데이터는 text 이기 때문에 발생하는 오류였다...

ajax에서 dataType을 수정해주었다!
dataType: "json" -> dataType: "text"

드디어 성공...

Request method 'POST' not supported 라는 메소드 에러 메시지만 계속 나와서 (메소드 자체의 문제는 아니었지만..)
혼자서 이것저것 많이 바꿔가면서 고생했지만 ajax에서 넘어가는 데이터의 형식으로도 이런 에러가 발생할 수 있다는걸 알 수 있었던게 가장 큰 수확이 아닐까 싶다😅


컨트롤러에서 인증 사용자 처리

마지막!!!!!

📌 사실은 끝이 아니다!!

로그인 처리는 잘 되지만 스프링 시큐리티를 적용했기 때문에 인증 사용자로 등록하는 과정이 필요하다.
로그인만 처리되고 아무 권한을 얻을 수 없기 때문에 사실상 비로그인 상태와 다를바없는 사태가 일어나고만다...

인증된 사용자 정보를 저장하기 위해서
이전글에서 설정한 UserDetails를 이용해서 생성한 CustomUserDetails 클래스를 이용해야한다.

컨트롤러로 넘어온 로그인 정보들을 CustomUserDetails 클래스를 이용해 인증 처리를 하겠습니다!

Userinfo userinfoAuth = userinfoservice.getUserinfoLogin(userinfo.getId());
            	
CustomUserDetails customUserDetails=new  CustomUserDetails(userinfoAuth);
        		
Authentication authentication=new UsernamePasswordAuthenticationToken (customUserDetails, null, customUserDetails.getAuthorities());
        		
SecurityContextHolder.getContext().setAuthentication(authentication);

✅ UsernamePasswordAuthenticationToken

  • Spring Security에서 인증된 사용자로 등록하고, 인증에 성공한 사용자를 인증된 사용자로 처리해줍니다.

✅ Authentication

  • 인증된 사용자 정보를 담고 있는 객체입니다.
  • 다른컨트롤러에서 authentication.getPrincipal() 을 사용해서 사용자 정보를 조회할 수 있습니다.

✅ SecurityContextHolder

  • 인증된 사용자의 정보를 저장하는 객체입니다.

🥲 드디어 며칠의 고통...끝에 스프링 시큐리티로 로그인 구현을 완료했습니다..
아래는 로그인 전체 컨트롤러 코드입니다!

   @PostMapping(value = "/login")
   @ResponseBody
   public ResponseEntity<String> loginPOST(
		   @RequestBody Userinfo userinfo
		   , HttpSession session) throws Exception {

            try {
            	Userinfo userinfoAuth = userinfoservice.getUserinfoLogin(userinfo.getId());
            	
            	CustomUserDetails customUserDetails=new CustomUserDetails(userinfoAuth);
        		
        		Authentication authentication=new UsernamePasswordAuthenticationToken
        				(customUserDetails, null, customUserDetails.getAuthorities());
        		
        		SecurityContextHolder.getContext().setAuthentication(authentication);
            	
		          if (userinfoAuth != null) {
		              // 로그인 시
		              if (pwEncoder.matches(userinfo.getPw(), userinfoAuth.getPw())) {
		            	  
		            	  userinfoAuth.setPw("");
		                  session.setAttribute("userinfo", userinfoAuth);
		                  session.setMaxInactiveInterval(60 * 60); 
		                  userinfoservice.updateUserLogindate(userinfoAuth.getId());
		                  
		                  return ResponseEntity.ok("ok");
		                  
		              } else {
		                  return ResponseEntity.badRequest().body("비밀번호가 일치하지 않습니다.");
		              }
		              
		          // 계정이 존재하지 않을때
		          } else {
		             return ResponseEntity.badRequest().body("아이디와 비밀번호를 확인해주세요.");
		          }
		          
			} catch (Exception e) {
		        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("서버 오류가 발생했습니다.");
			}
      
   }

jsp 코드!

<form id="loginForm">
   <div class="mb-3">
     <label for="idInput" class="form-label">아이디</label>
     <input type="text" class="form-control" name="id" id="idInput" placeholder="아이디를 입력해주세요.">
      <c:remove var="idInput"/>
   </div>
   <div class="mb-3">
     <label for="pwInput" class="form-label">비밀번호</label>
     <input type="password" class="form-control" name="pw" id="pwInput" placeholder="비밀번호를 입력해주세요.">
   </div>
            
   <div class="d-flex justify-content-center">
     <button type="submit" id="loginButton" class="btn btn-primary" style="width: 350px;" disabled>로그인</button>
   </div>
</form>

자바스크립트로 따로 토큰을 보내고있지 않으면 꼭 작성해주세요!!

<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">

Mapper에서 로그인한 계정의 권한 확인

<resultMap type="Userinfo" id="securityUserinfoResultMap">
	<id column="id" property="id"/>
	<result column="pw" property="pw"/>
	<result column="name" property="name"/>
	<result column="nickname" property="nickname"/>
	<result column="address" property="address"/>
	<result column="regdate" property="regdate"/>
	<result column="status" property="status"/>
	<result column="enabled" property="enabled"/>
	<collection property="securityAuthList" select="selectUserinfoAuthById" column="id"/>
</resultMap> 
  
<select id="selectUserinfoAuthById" resultType="com.project.dto.UserinfoAuth">
	SELECT 
		id
		, auth 
	FROM userinfo_auth 
	WHERE id=#{id}
</select>
 
<select id="selectUserinfoLogin" resultMap="securityUserinfoResultMap">
	SELECT 
		user.id
		, user.pw
		, user.name
		, user.nickname
		, user.address
		, user.email
		, user.regdate
		, user.status  
		, user.enabled
		, auth.auth
	FROM userinfo user
   	LEFT JOIN userinfo_auth auth
   	ON user.id=auth.id
   	WHERE user.id=#{id}
   	AND user.status=1
</select>

마지막으로!!
로그인시 로그인한 정보를 userinfo_auth 테이블에 저장된 해당 아이디의 권한을 같이 출력해주어 로그인 계정에 권한을 부여해주게 됩니다.

0개의 댓글