로그인과 로그아웃 처리 (security-context 설정과 로그인 관련 Handler 사용법)

1. 접근제한 설정

. security-context.xml 에 설정
. security:intercept-url 네임스페이스 사용

<!-- 생략 -->
	<security:http>
		<security:intercept-url pattern="/sample/all" access="permitAll"/>
		<security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
		<security:intercept-url pattern="/sample/admin" access="hasRole('ROLE_ADMIN')"/>
	</security:http>
<!-- 생략 -->

# access 속성을 표현하는 두가지 방법

  1. 표현식 : 위와 같이 기본설정이 표현식이다.
  2. 권한명 의미하는 문자열
<security:http auto-config="true" use-expressions="false">
    <security:intercept-url pattern="/sample/member" access="ROLE_MEMBER"/>
    <security:intercept-url pattern="/sample/manager" access="ROLE_MANAGER"/>
    <security:intercept-url pattern="/sample/admin" access="ROLE_ADMIN"/>
</security:http>

2. 사용자 접근 에러 메시지 처리

권한이 없는 사용자가 페이지에 접근했을 때, 에러 페이지를 보여주는 페이지

테스트를 위해서 사용자 정보를 하드코딩 한 후, 알아보자.

. security-context.xml

<!-- 생략 -->
<security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/>
                <security:user name="admin" password="{noop}admin" authorities="ROLE_MEMBER, ROLE_ADMIN"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
<!-- 생략 -->

스프링5에서는 PasswordEncoder를 꼭 사용해야 하지만, 지금은 테스트를 위해서 PasswordEncoder를 사용하지 않고 사용자들 등록할 수 있도록 ‘password'앞에 '{noop}'을 붙여준다.

권한이 없는 사용자가 페이지 접근시 에러메시지를 설정하는 방법에는 두가지가 있다.

첫번째는, security-context.xml의 security:access-denied-handler 네임스페이스의 error-page 속성을 이용하는 방법이다.

. security-context.xml에 속성을 추가한다.

<security:http>
    <!-- 생략 -->
    <security:access-denied-handler error-page="/accessError"/>
</security:http>

. CommonController 에 /accessError 링크에 대한 처리 메소드를 작성한다.

package org.example.controller;

@Controller
@Log4j
public class CommonController {

    @GetMapping("/accessError")
    public void accessDenied(Authentication auth, Model model) {
        log.info("access Denied : " + auth);
        model.addAttribute("msg", "Access Denied!");
    }
}

. ‘/accessError' 의 뷰 ‘views/accessError.jsp' 를 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <h1>Access Denied Page</h1>
    <h2><c:out value="${SPRING_SECURITY_403_EXCEPTION.getMessage()}"/></h2>
    <h2><c:out value="${msg}"/></h2>
</body>
</html>

두번째는, security-context.xml 의 ref 속성을 이용한다.
접속거부를 담당하는 AccessDeniedHandler 인터페이스를 구현하는 구현체 CustomAccessDeniedHandler를 작성해 Bean 으로 등록 후,
security:access-denied-handler 네임스페이스의 ref 속성에 구현체의 id를 입력한다.

<bean id="customAccessDenied" class="org.example.security.CustomAccessDeniedHandler"></bean> 
<security:http>
    <!-- 생략 -->
    <security:access-denied-handler ref="customAccessDenied"/>  
</security:http>

빈으로 등록했던 구현체 CustomAccessDeniedHandler.javaorg.example.security 패키지에 작성한다.

package org.example.security;

@Log4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler{

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // TODO Auto-generated method stub
        log.error("Access Denied Handler");
        log.error("Redirect...");
            
        response.sendRedirect("/accessError");
    }
}

나머지 Controller와 뷰 코딩은 첫번째와 동일하다.

3. 커스텀 로그인 페이지

기본적으로 스프링이 로그인 폼을 제공하고, 처리도 해준다. 하지만 사용자가 원하는 폼과 서비스를 위해 커스터마이징 할 수 있다.

. 로그아웃 기능이 아직 미구현이므로, 로그아웃 상태를 만들기 위해 브라우저 개발도구 > Application > Cookie > SESSIONID 를 삭제한다.

security-context.xmlsecurity:form-login 네임스페이스에 직접 로그인 URL을 지정할 수 있다.

<security:http>
<!-- 생략 -->
    <!-- form-login 네임스페이스에 로그인 처리할 링크를 지정한다. -->
    <security:form-login login-page="/customLogin" authentication-success-handler-ref="customLoginSuccessHandler"/>
<!-- 생략 -->	
</security:http>

login-page에 연결된 URL을 코딩한다.

package org.example.controller;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class CommonController {

	@GetMapping("/accessError")
	public void accessDenied(Authentication auth, Model model) {
		log.info("access Denied : " + auth);
		model.addAttribute("msg", "Access Denied!");
	}
	
    // 커스텀 로그인 페이지 
	@GetMapping("/customLogin")
	public void customLogin(String error, String logout, Model model) {
		
		log.info("error:" + error);
		log.info("logout:" + logout);
		
		if(error != null) {
			model.addAttribute("error", "Login Error check Your Account");
		}
		
		if(logout != null) {
			model.addAttribute("logout", "Logout!!");
		}
	}
}

로그인 뷰 페이지 views/customLogin.jsp를 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1>Custom Login Page</h1>
	<h2><c:out value="${error}"/></h2>
	<h2><c:out value="${logout}"/></h2>
	
	<form method="post" action="/login">
		<div>
			<input type="text" name="username" value="admin">
		</div>
		<div>
			<input type="password" name="password" value="admin">
		</div>
		<div>
			<input type="submit">
		</div>
		<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
	</form>
</body>
</html>

. 요청방식은 꼭 post로 지정할 것! (CSRF 토큰 함께 보내야 함)

. 요청 주소는 꼭 /login 으로 할 것! ( 스프링 내부로직 호출)

. ${_csrf.token} 구문 꼭 함께 전송 할 것!

4. 로그인 성공과 AuthenticationSuccessHandler

로그인 성공 이후에, 특별한 동작을 하도록 제어하고 싶을 때, AuthenticationSuccessHandler 인터페이스를 구현해서 설정 할 수 있다.

(1) AuthenticationSuccessHandler을 구현한 CustomLoginSuccessHandler 클래스 작성한다.

package org.example.security;

@Log4j
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler{

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication auth) throws IOException, ServletException {
		// TODO Auto-generated method stub
		log.warn("Login Success!");
		
		List<String> roleNames = new ArrayList<>();
		auth.getAuthorities().forEach(authority -> {
			roleNames.add(authority.getAuthority());
		});
		
		log.warn("ROLE NAMES:" + roleNames);
		
		if(roleNames.contains("ROLE_ADMIN")) {
			response.sendRedirect("/sample/admin");
			return;
		}
		
		if(roleNames.contains("ROLE_MEMBER")) {
			response.sendRedirect("/sample/member");
			return;
		}
		
		response.sendRedirect("/");
	}
}

(2) security-context.xml 에 구현체를 빈으로 등록하고 form-login 속성에 추가한다.

<!-- 생략 -->
<bean id="customLoginSuccessHandler" class="org.example.security.CustomLoginSuccessHandler"></bean>
<security:http>
<!-- 생략 -->
    <security:form-login login-page="/customLogin" authentication-success-handler-ref="customLoginSuccessHandler"/>
    <!-- 생략 -->
</security:http>

. CustomLoginSuccessHandler 클래스 빈으로 등록하고, security:form-login 네임스페이스의 authentication-success-handler-ref 속성에 id 를 지정해 준다.

. 예제는, 로그인이 성공하면, 권한에 따라 특정 페이지로 강제로 이동하는 로직이다.

5. 로그아웃 처리와 LogoutSuccessHandler

특정 URL 을 지정하고, 로그아웃 성공 후, 직접 로직을 핸들링 할 수 있다.

(1) security-context.xml 에 security:logout 네임스페이스에 속성을 지정한다.

<bean></bean>
<security:http>
	<!-- 생략 -->
    <security:logout logout-url="/customLogout" invalidate-session="true"/>
    <!-- 생략 -->	
</security:http>

. logout-url 속성에 로그아웃 처리 URL을 지정할 수 있다.

. invalidate-session=true 는, 로그아웃하면, 세션을 삭제 처리한다.

(2) 로그아웃 버튼을 생성할 페이지에 로그아웃을 위한 form을 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<title>Insert title here</title>
</head>
<body>
<h1>/sample/admin page</h1>
<!-- <a href="/customLogout">Logout</a> -->
<form method="post" action="/customLogout">
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
	<button>로그아웃</button>
</form>
</body>
</html>

. action에는, security-context.xml 에서 지정한 /customLogout URL 을 지정한다.

. 전송방식은 꼭 post 로 해야하며, _csrf.token 을 함께 전송해야 한다.

. 실제 실행하여 로그아웃 버튼을 누르면, 스프링 내부 로직에 따라 로그인 페이지로 이동하는데 변경하고 싶으면 security:logoutlogout-success-url 속성에 로그아웃 후 이동할 URL을 입력한다.

. 로그아웃 처리 성공 후, 사용자가 추가적인 로직을 넣고 싶다면, LogoutSuccessHandler 인터페이스를 구현하여 빈에 등록하고, 그 ID 를 security:logoutsuccess-handler-ref 속성에 입력하면 된다. (로그인과 비슷하다.)

. ‘logout-success-url' 속성과 ‘success-handler-ref' 속성은, 둘중 하나만 사용할 수 있다.