읽은 책 정리/코드로 배우는 스프링 웹 프로젝트

[Spring] 31 로그인과 로그아웃 처리

포포015 2021. 2. 6. 01:03

스프링 시큐리티의 내부 구조는 상당히 복잡하지만, 실제 사용은 약간의 설정만으로 처리가 가능하다..!

 

접근제한설정

security-context.xml 에 접근제한 설정 추가 한다.

특정한 URI에 접근할때 인터셉터를 이용해서 접근을 제한하는 설정은 <security:intercept-url> 를 이용한다.

pattern 이라는 속성(URI의 패턴을 의미) 과 access(권한체크)라는 속성을 지정해야한다.

access의 속성값으로 사용되는 문자열은 . 1) 표현식과 2)권한명을 의미하는 문자열을 이용한다.

기본설정이 표현식을 이용하는것인데, 단순 문자열을 이용할수 있지만 권장x 이기때문에 표현식을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
    <!-- 시큐리티의 시작점 -->
    <security:http>
        <!-- 접근 제한설정 pattern 은 uri 패턴 의미, access는 권한체크 (security:http)는 기본설정이 표현식을 이용한다.  -->
        <security:intercept-url pattern="/sample/all" access="permitAll"/>
        
        <security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/>
        
        
        <security:form-login/>
        
    </security:http>
cs

 

설정하고 /sample/member로 접근하면 /sample/all과 달리 로그인 페이지로 강제 이동한다.

(신기한점은 해당하는 컨트롤러나 웹페이지를 제작한적이없다. * 스프링 시큐리티가 기본으로 제공하는 페이지이다.)

 

단순 로그인 처리

화면은 보여지나 로그인을 할수없는 상황이므로, /sample/member에 접근할수 없다.

추가 설정을 통해 지정된 아이디와 패스워드로 로그인이 가능하도록 설정을 한다.

인증과 권한에 대해 실제 처리는 UserDetailsService 라는 것을 이용해 처리하는데 XML 에선 아래와 같이 지정할수있다.

1
2
3
4
5
6
7
8
9
10
11
    <!-- 스프링 시큐리티 동작하기위해 필요한 존재 -->
    <security:authentication-manager>
    
        <!-- 실제처리는 UserDetailsSerivce를 이용해 처리 하는데 아래는 단순한 로그인을 위한 지정 -->
        <security:authentication-provider>
            <security:user-service>
                <security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/>
            </security:user-service>
        </security:authentication-provider>
    
    </security:authentication-manager>
cs

패스워드에 {noop} 이라고 설정 되있는건 스프링 시큐리티 5버전 부터 PasswordEncoder 라는 존재를

이용하도록 변경(강제) 되었는데, 임시로 인코딩 처리 없이 사용 하기위해 선언한것이다.

 

로그아웃 확인

로그아웃하고 새롭게 로그인 해야 하는상황이 자주 발생한다.

가장 확실한 방법은 브라우저에서 유지하고 있는 세션과 관련된 정보를 삭제하는것이다.

개발자 도구에서 Application 탭을 확인해보면 'Cookies' 항목에 'JSESSIONID'와 같이 세션을 유지하는데

사용되는 세션 쿠키의 존재를 확인할수 있다. 쿠키를 강제로 삭제처리후 같은 URI을 호출 한다면 로그인이 필요하다.

 

접근 제한 메시지의 처리

특정한 사용자가 로그인은 했지만 URI를 접근할수 있는 권한이 없는 상황이 발생할경우

접근 제한 메시지리를 보게된다,

예제의 경우 member라는 권한을 가진 사용자는 /sample/member에 접근가능하지만

/sample/admin은 접근 할수없다 (이럴땐 403 에러가 나온다)

접근 제한에 대해서 AccessDeniedHandle를 직접 구현하거나 특정한 URI을 지정할수 있다.

 

security.context.xml 파일의 설정을 추가한다

<security:access-denied-handler>는

1. org.springframework.security.web.access.AccessDeniedHandler 인터페이스의 구현체를 지정하거나

2. error-page를 지정할수 있다. (아래의 경우는 uri로 접근제한시 보이는 화면을 처리한다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    <!-- 시큐리티의 시작점 -->
    <security:http>
        <!-- 접근 제한설정 pattern 은 uri 패턴 의미, access는 권한체크 (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:form-login/>
        
        <security:access-denied-handler error-page="/accessError"/>
        
    </security:http>
cs

 

org.zerock.controller에 CommonController 클래스를 생성해서 /accessError를 처리하도록 지정한다.

1
2
3
4
5
6
7
8
9
10
11
@Controller
@Log4j
public class CommonController {
 
    @GetMapping("/accessError")
    public void accessDenied(Authentication auth, Model model) {
        
        log.info("access Denied" + auth);
        
        model.addAttribute("msg""403 접근제한!");
    }
cs

간단히 사용자가 알아볼수 있는 에러 메시지만을 Model에 추가하고,

/accessError는 Authentication 타입의 파라미터를 받도록 설계해서 필요한경우 사용자의 정보를 확인할수 있도록함.

 

accessError.jsp를 생성해서 정보를 확인한다.( 이전의 403 에러 메시지 대신 아래의 .jsp 내용이 보이게 된다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ 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>
<!-- Access Deined의 경우 403 에러 메시지가 발생한다. 그럴 경우 아래의 메시지가 출력이 된다. -->
<h2><c:out value="${SPRING_SECURITY_403_EXCEPTION.getMessage()}"></c:out></h2>
<h2><c:out value="${msg}"></c:out></h2>
</body>
</html>
cs

 

 

 

 

AccessDeniedHandler 인터페이스를 구현하는경우 (접근제한이 된경우 다양한처리를위한 인터페이스)

위의 처리와 같이 error-page="/accessError" 와 같이 error-page 만을 제공 하는경우,

사용자가 접근했던 URI 자체의 변화는 없다.

접근 제한이 된경우 다양한 처리를 하고싶다면 직접 인터페이스를 구현하는 편이좋다.

예를 들어 접근 제한이 되었을때, 쿠키나 세션에 특정한 작업을 하거나,

 HttpServletResponse에 특정한 헤더 정보를 추가하는 등의 행위를 할경우 직접 구현하는 방식이 권장된다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.zerock.security;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
 
import lombok.extern.log4j.Log4j;
 
@Log4j
// 쿠키나 , 세션에 특정한 작업을 하거나 , HttpServletResponse에 특정한 헤더정보를 추가할경우 상속받아 구현
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
 
        log.error("Access denied handler");
        
        log.error("Redircet.......");
        
        //접근 제한에 걸리는 경우 리다이렉트하는방식 
        response.sendRedirect("/accessError");
    }
 
}
 
cs

security 패키지를 생성해서 ,CommonController 클래스는 AccessDeniedHandler를 상속받아 직접 구현 했다.

서블릿 api를 사용해 처리가 가능하다. 위의 코드는 로그를 찍고 리다이렉트를 하는 방식으로 지정 했다.

 

security-context.xml 에서는 error-page 속성 대신 CustomAccessDeniedHandler를 빈으로 등록해 사용한다.

<security:access-denied-handler>는 error-page 속성과 ref 속성 둘중 하나만을 사용한다.

1
2
3
   <!-- AccessDeniedHandler 인터페이스를 직접 구현체를 빈으로 등록  403에러페이지->
    <bean id="customAccessDenied" class="org.zerock.security.CustomAccessDeniedHandler"></bean>

<!-- 시큐리티의 시작점 -->
<security:http>
<!-- 접근 제한설정 pattern 은 uri 패턴 의미, access는 권한체크 (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:form-login/>

<!--  <security:access-denied-handler error-page="/accessError"/>  -->
  <security:access-denied-handler ref="customAccessDenied"/>  


</security:http>
 
cs

 

커스텀 로그인 페이지

스프링 시큐리티에서 기본적으로 로그인 페이지를 제공하지만, 디자인등 문제로 사용하기 불편해서

대부분 별도의 URI을 이용해 로그인 페이지를 다시 제작해 사용 한다

(접근 제한 페이지와 유사하게 직접 특정한 URI를 지정할수 있다)

1
2
<security:http> 
  <!--     <security:form-login/>  -->
        <security:form-login login-page="/customLogin"/>

</security:http>
cs

login-page 속성의 URI는 반드시 GET방식으로 접근하는 URI를 지정한다.

org.zerock.controller 패키지의 CommonController에 /customLogin에 해당하는 메서드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @GetMapping("/customLogin")
    public void loginInput(String error, String logout, Model model) {
        
        log.info("error" + error);
        log.info("logout" + logout);
       
// 값이 있을경우
        if(error != null) {
            model.addAttribute("error""로그인 오류");
        }
        
        if(logout != null) {
            model.addAttribute("logout""로그아웃!~");
        }
    }
cs

loginInput는 get방식으로 접근하고, 에러메시지와 로그아웃 메시지를 파라미터로 사용할수 있다.

 

customLogin.jsp 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h1>로그인 페이지</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>
cs

코드를 저장하고 브라우저에서 로그인 정보를 삭제후 /sample/admin과 같이 접근 제한이 필요한 

URI에 접근하면 작성된 페이지의 내용을 볼수 있다.

중요한 점이 있는데,

우선<form>태그의 action 속성은, 실제로 로그인 처리 작업은 '/login'을 통해 이루어지고 반드시 post방식으로 전송.!!

<input>태그의 name 속성은 기본적으로 username과 password 속성을 이용 한다.

마지막으로 <input type="hidden">태그는 특이하게 ${_csrf.parameterName }로 처리한다.

실제 브라우저에선 아래와 같이 태그와 값이 생성된다. (value는 임의의 값이 지정된다)

만일 패스워드 등을 잘못입력하는 경우 자동으로 다시 로그인 페이지로 이동한다

 

CSRF(Cross-site request forgery) 공격과 토큰

스프링 시큐리티에서 POST 방식을 이용하는 경우 기본적으로 CSRF 토큰 이라는것을 이용한다.

별도로 설정이 없다면 시큐리티가 적용된 사이트의 모든 POST 방식은 CSRF 토큰이 사용된다.

'사이트간 위조방지'를 목적으로 특정한 값의 토큰을 사용하는 방식.

CSRF공격은 어떤 출처에서 호출이 진행되었는지 따지지 않기때문에 생기는 허점을 노리는 공격방식.

공격을 막기 위해선 여러가지 방식이 있다 ( 출처를 의미하는 referer 헤더를 체크하거나, REST방식에서 사용되는 PUT,DELETE와 같은 방식을 이용하는 등 방식을 고려할수 있다)

 

CSRF 토큰

사용자가 임의로 변하는 특정한 토큰값을 서버에서 체크하는 방식.

(서버에는 브라우저에 데이터를 전송할때 CSRF 토큰을 같이 전송함) POST방식등 특정한 작업을 할때는

브라우저에서 전송된 CSRF 토큰의 값과 서버가 보관하고 있는 토큰의 값을 비교. 토큰값이 다르다면 작업처리 X

 

CSRF설정

CSRF 토큰은 세션을 통해 보관하고, 브라우저에서 전송된 CSRF 토큰값을 검사하는 방식으로 처리.

아래와 같이 처리한다면 비활성화가 된다

<security:csrf disabled="true" />

 

 

로그인 성공과 AuthenticationSuccessHandler 인터페이스 (로그인 성공처리후, 특정한 동작을 하도록 제어)

로그인 성공처리후, 특정한 동작을 하도록 제어하고 싶은경우

AuthenticationSuccessHandler 인터페이스를 구현해서 설정할수있다

어떤경로로 페이지 들어오건 특정 uri로 이동 하게하던지..

모든권한을 문자열로 체크해서 해당 권한을 가졌다면 해동 uri로 이동하게하는방식.

 

상속받을 클래스를 추가한다.( 로그인한 사용자에게 부여된 권한 AuthenticationSuccessHandler를 이용해 

사용자의 모든 권한을 문자열로 체크한다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.zerock.security;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
 
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.AuthenticationSuccessHandler;
 
import lombok.extern.log4j.Log4j;
 
@Log4j
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler{
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        
        log.warn("로그인 성공");
        
        List<String> roleNames = new ArrayList<>();
        
        authentication.getAuthorities().forEach(authority ->{
            
            roleNames.add(authority.getAuthority());
        });
        
        log.warn("로그인 이름" + roleNames);
        
        if(roleNames.contains("ROLE_ADMIN")) {
            response.sendRedirect("/sample/admin");
            return;
        }
 
        if(roleNames.contains("ROLE_MEMBER")) {
            response.sendRedirect("/sample/member");
            return;
        }
        response.sendRedirect("/");
 
    }
 
}
 
cs

security-context.xml 에 빈을 등록하고, 로그인 성공후 처리를 담당하는 핸들러로 지정한다.

(권한에 따라 페이지이동이 이루어진다.)

1
2
3
4
5
<!-- 접근제어 메시지, 로그인성공시 인터페이스 구현 (객체등록) -->
<bean id="customAccessDenied" class="org.zerock.security.CustomAccessDeniedHandler"></bean>  
 <bean id="customLoginSuccess" class="org.zerock.security.CustomLoginSuccessHandler"></bean>    
 
 
<!-- 로그인 성공시 호출 -->
        <security:form-login login-page="/customLogin" authentication-success-handler-ref="customLoginSuccess"/>
cs

 

로그아웃의 처리와 LogoutSuccessHandler

로그인과 마찬가지로 특정한 URI을 지정하고 ,로그아웃후 직접 로직을 처리할수 있는 핸들러를 등록할수 있다

(로그아웃시 세션을 무효화 시키는 설정이나 특정한 쿠키를 지우는 작업을 지정할수 있다.)

1
    <security:logout logout-url="/customLogout" invalidate-session="true"/>    
cs

 

 

로그아웃 역시 로그인과 동일하게 Controller에 GET방식으로 로그아웃을 하는 페이지에 대한

메소드를 선언해두고 , 아래와같이 post 방식으로 처리한다 (CSRF 토큰값 같이지정)

추가적인 작업이 필요할경우 LogoutSuccessHandler 인터페이스를 정의해서 처리한다

1
2
3
4
5
<form action="/customLogout" method="post">
 
<input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token }">
<button>로그아웃</button>
</form>
cs

 

스프링 시큐리티가 어차피 필터들의 연속이므로 이 연속된 흐름에서 기존의 LogoutFilter의 동작 시점에 LogoutHandler를 끼워 넣는 형태로 작성해야 한다.) 

 

 

흐름)

/sample/admin 호출  

if 로그인 x 권한x 

로그인페이지

정상실행

/sample/admin 화면 이동

로그아웃 get방식 화면

로그아웃 post 누르면 (로그아웃)