본문 바로가기

Spring/spring security

스프링 시큐리티 DB를 이용한 로그인 구현 2 ( ajax 로그인 ) - 삽질중인 개발자

반응형

- spring security ajax login -

 

이 포스팅에서는 jQuery Ajax를 이용한 커스텀 로그인 페이지 구현을 할 것이다.

 

스프링 시큐리티에 기본 로그인 페이지는 로그인 실패시 페이지를 새로고침하고 쿼리스트링(localhost:8090/login?error)을 주는 방식으로 구현이 되어 있는데 이 방식으로 구현을 하면 페이지를 새로 띄워야 할 뿐만 아니라 입력했던 정보에 대해서 전부 사라지게 된다. 

 

위에나오는 불편함을 해소하기 위해 요즘 나오는 대부분의 사이트를 로그인시 ajax를 이용해서 login을 한다.

 

 

 

1. LoginController.java 생성

스프링 시큐리티의 기본 로그인 url은 /login 이다.

이걸 커스텀하기위해 /login 을 재정의한다.

아래의 코드에서는 로그인 완료 후 원했던 페이지로 이동하기 위한 이전 페이지 정보를 저장한다.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {
	@GetMapping("/login")
	public String Login(HttpServletRequest request, HttpServletResponse response) {
		RequestCache requestCache = new HttpSessionRequestCache();
		SavedRequest savedRequest = requestCache.getRequest(request, response); 
		
		try {
			//여러가지 이유로 이전페이지 정보가 없는 경우가 있음.
			//https://stackoverflow.com/questions/6880659/in-what-cases-will-http-referer-be-empty
			request.getSession().setAttribute("prevPage", savedRequest.getRedirectUrl());
		} catch(NullPointerException e) {
			request.getSession().setAttribute("prevPage", "/");
		}
		return "login";
	}
}

 

 

2. login.jsp 생성

정말 간단하게 ID 입력 input 과 PW 입력 input이 존재한다.

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script type="text/javascript" src="/js/login.js"></script>
</head>
<body>
	<div>
		<div>
			<div>
				<div>
					<div>
						<input type="text" id="member-id" autofocus="autofocus" placeholder="ID">
					</div>
					<div >
						<input type="password" id="member-password" placeholder="Password">
					</div>
					<div>
						<input type="hidden" id="token" data-token-name="${_csrf.headerName}" placeholder="Password" value="${_csrf.token}">
					</div>
					<div >
						<div >
							<div >
								<button id="login-button">로그인</button>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>
</body>

</html>

 

3. login.js 생성

로그인을 ajax를 통해서 구현할 것이다.

원래 시큐리티의 기본 ID , PASSWORD 파라미터는 username , password 이다.

하지만 username과 password은 헷갈려서 6번 과정에서 우리 DB에 맞게 memberId, memberPassword 로 설정을 한다.

 

여기서 중요한건 스프링 시큐리티는 기본적으로 CSRF 공격을 막도록 설정되어 있어서 beforeSend를 통하여 crsf token 값을 세팅하여 넘겨줘야 한다.

 

	$(function(){
		$("#login-button").click(function(){
			login();
		})		
	})
	
	/**
	* 로그인 
	*/
	function login(){
		$.ajax({
			url:"/login",
			type :  "POST",
			dataType : "json",
			data : {
				memberId : $("#member-id").val(),
				memberPassword : $("#member-password").val()
			},
			beforeSend : function(xhr)
			{
				//이거 안하면 403 error
				//데이터를 전송하기 전에 헤더에 csrf값을 설정한다
				var $token = $("#token");
				xhr.setRequestHeader($token.data("token-name"), $token.val());
			},
			success : function(response){
				if(response.code == "200"){
					// 정상 처리 된 경우
					window.location = response.item.url;	//이전페이지로 돌아가기
				} else {
					alert(response.message);
				}
			},
			error : function(a,b,c){
				console.log(a,b,c);
			}
			
		})
		
	}

 

4. json 응답을 위한 ResponseDataDTO.java 생성

 

이 응답 객체를 사용하면

{

code : string,

status : string,

message : string,

item : object

}

형태로 json을 응답해준다.

 

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class ResponseDataDTO {
	
	private String code;
	private String status;
	private String message;
	private Object item;
	
}
public class ResponseDataCode {
	public static final String SUCCESS = "200";
	public static final String ERROR = "999";
}
public class ResponseDataStatus {
	public static final String SUCCESS = "200";
	public static final String ERROR = "999";
}

 

 

 

5. 로그인 성공, 실패를 위한 핸들러 생성

만약 로그인이 성공 혹은 실패 후 필요한 비지니스 로직은 다음 핸들러에서 작성한다.

 

CustomAuthenticationSuccessHandler.java 

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.demo.common.config.ResponseDataCode;
import com.project.demo.common.config.ResponseDataStatus;
import com.project.demo.common.dto.ResponseDataDTO;

/**
 * 로그인 성공시 핸들러
 *
 */
@Component
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

	/**
	 * 로그인이 성공하고나서 로직
	 */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {
    	
    	ObjectMapper mapper = new ObjectMapper();	//JSON 변경용
    	
    	ResponseDataDTO responseDataDTO = new ResponseDataDTO();
    	responseDataDTO.setCode(ResponseDataCode.SUCCESS);
    	responseDataDTO.setStatus(ResponseDataStatus.SUCCESS);
    	
    	String prevPage = request.getSession().getAttribute("prevPage").toString();	//이전 페이지 가져오기
    	 
    	Map<String, String> items = new HashMap<String,String>();	
    	items.put("url", prevPage);	// 이전 페이지 저장
    	responseDataDTO.setItem(items);
    	
    	response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().print(mapper.writeValueAsString(responseDataDTO));
        response.getWriter().flush();
    }
}

CustomAuthenticationFailureHandler.java

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.demo.common.config.ResponseDataCode;
import com.project.demo.common.config.ResponseDataStatus;
import com.project.demo.common.dto.ResponseDataDTO;

/**
 * 로그인 실패시 로직
 *
 */
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler{
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		
		ObjectMapper mapper = new ObjectMapper();	//JSON 변경용
    	
    	ResponseDataDTO responseDataDTO = new ResponseDataDTO();
    	responseDataDTO.setCode(ResponseDataCode.ERROR);
    	responseDataDTO.setStatus(ResponseDataStatus.ERROR);
    	responseDataDTO.setMessage("아이디 혹은 비밀번호가 일치하지 않습니다.");

    	
    	response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().print(mapper.writeValueAsString(responseDataDTO));
        response.getWriter().flush();
	
	}
}

 

6. 이전 포스팅에서 2번 단계에서 작성한 SecurityConfig.java 에 아래 코드를 추가

	@Autowired
	private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

	@Autowired
	private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
	


	/**
	* 시큐리티에 관련된 설정
	*/
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.antMatchers("/member/**").hasAuthority("MEMBER") // /member/ 로 시작하는 URL은 MEMBER라는 권한을 가진 사용자만 접근 가능
                													//	만약 권한이 없으면 LoginPage로 이동한다.
                                                                    // 스프링 시큐리티에서는 ROLE_ 접두어로 붙여주기에 ROLE_MEMBER 를 가진 사람이다.
			.and()
			.formLogin()	//로그인 폼 사용
				.loginPage("/login")	//커스텀 로그인 페이지
				.successHandler(customAuthenticationSuccessHandler)
				.failureHandler(customAuthenticationFailureHandler)
				.usernameParameter("memberId")	//스프링 시큐리티에서는 username을 기본 아이디 매핑 값으로 사용하는데 이거 쓰면 변경
				.passwordParameter("memberPassword")	//이건 password를 변경
				.permitAll();
    }

 

 

7. MemberController.java 생성

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.project.demo.member.repository.MemberRepository;

@Controller
@RequestMapping("/member")
public class MemberController {

	@Autowired
	MemberRepository memberRepository;
	
	@GetMapping("/")
	private String test() {
		
		return "member/member";
	}
}

 

 

8. 로그인 테스트

 

localhost:8090/ 에서  localhost:8090/member/ 를 주소에 치면 아래와 같은 화면이 나온다.

 

로그인 페이지에서 admin / test 를 입력하면 alert 이 뜨며 admin / admin 을 입력하면 자동으로 localhost:8090/member/로 이동한다.

 

 

여기까지 하면 Ajax를 이용한 커스텀 로그인 페이지까지 완성이 된다.

 


스프링 시큐리티 커스텀 로그인 - DB 기반 로그인 시켜보기 - 

 

Spring security 커스텀 로그인 1 - 개발자 삽질 일기

스프링 시큐리티 스프링 기반의 어플리케이션의 보안(인증과 권한)을 담당하는 프레임워크이다. 스프링 시큐리티를 사용하면 로그인, 로그아웃 그 외의 각종 인증 관련 기능을 간편하게 구현할 수 있다. 개발 환경..

programmer93.tistory.com

 

로그아웃 구현하기

 

 

spring security 커스텀 로그인 3 ( 로그아웃 하기 ) - 개발자 삽질 일기

- 로그아웃 시키기 - 이 포스팅에서는 로그아웃을 구현한다. 1. 로그아웃 버튼 만들기 스프링 시큐리티의 기본 로그아웃 url은 POST 메소드의 /logout 이다.

programmer93.tistory.com

 

 

반응형