8장 스프링 시큐리티: 로그인/로그아웃 페이지 만들기

­이주현 (Joo Hyun Lee)·2023년 4월 10일
0
post-thumbnail

들어가기

들어가기에 앞서

(1) sts bundle을 사용하는 경우, pom.xml의 <org.springframework-version> 버전을 5.0.7 이상으로 바꿔야 한다.

<org.springframework-version>5.0.7.RELEASE</org.springframework-version>

(2) Controller의 method 이름이 겹칠 경우 mapping 오류가 생길 수 있다. 이 경우, src/main/java의 컨트롤러들을 src/test/java로 옮겨 실행해야 한다.

이 장에서 다룰 핵심 내용

  • 스프링 시큐리티 개요
  • 접근 권한과 사용자 권한 설정
  • 뷰 페이지에 사용하는 시큐리티 태그
    (실습) 스프링 시큐리티 태그로 도서 등록 페이지에 접근 권한 설정하기
  • 로그인과 로그아웃 처리
    (실습) 스프링 시큐리티 태그로 로그인/로그아웃 페이지 구현하기

8.1 스프링 시큐리티 개요

웹 애플리케이션에서는 허가된 사용자만 특정 웹 페이지에 접근할 수 있도록 제한하는 보안 기능이 필수입니다. 웹 서비스를 개발할 때 보안 기능을 손쉽게 구현할 수 있는 스프링 시큐리티를 알아봅니다. 스프링 시큐리티를 이용하여 로그인 및 로그아웃 페이지를 만들어 봅시다.

8.1.1 스프링 시큐리티

스프링 시큐리티(security)는 스프링 기반 애플리케이션의 보안(인증과 권한)을 담당하는 프레임워크입니다. 스프링 시큐리티를 이용하면 웹 애플리케이션에 로그인할 때 아이디와 비밀번호를 입력하여 사용자를 인증(authentication)하고 로그인한 후 접근 가능한 경로를 제한할 수 있는 권한 부여(authorization) 작업 등을 효율적으로 구현할 수 있습니다. 구현 시간도 줄일 수 있습니다.
스프링 시큐리티를 이용하려면 반드시 다음과 같은 환경 설정이 필수입니다.

pom.xml 파일에 의존 라이브러리 등록하기

스프링 MVC에서 스프링 시큐리티를 사용하려면 pom.xml 파일에 spring-security-web.jar을 spring-security-config.jar과 의존 라이브러리로 등록해야 합니다.

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>${org.springframework-version}</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>${org.springframework-version}</version>
</dependency>

• spring-security-web.jar: 필터 및 웹 보안 인프라 관련 코드를 포함합니다. 서블릿 API 종속성이 있는 모든 것, 스프링 시큐리티 웹 인증 서비스 및 URL 기반 액세스를 제어하는 경우에 필요한 모듈입니다. 기본 패키지는 org.springframework.security.web입니다.

• spring-security-config.jar: 보안 네임 스페이스 구문을 분석하는 코드를 포함합니다. 구성을 위해 스프링 시큐리티 XML 네임 스페이스를 사용하는 경우에 필요한 모듈입니다. 기본 패키지는 org.springframework.security.config입니다.

web.xml 파일에 시큐리티 필터 등록하기

스프링 MVC에서 서블릿 필터로 스프링 시큐리티를 동작하여 모든 웹 요청에 대해 권한을 확인하도록 하려면 스프링 시큐리티 필터를 등록해야 합니다. web.xml 파일에 다음과 같이 서블릿 필터 DelegatingFilterProxy를 등록합니다.

<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

web.xml 파일에 스프링 시큐리티 설정 파일

시큐티리 설정 파일은 스프링 시큐리티에 사용되는 빈을 위한 파일로, web.xml 파일의 < context-param > 요소에 시큐리티 설정 파일을 등록하여 읽게 합니다.
시큐리티 설정 파일이 security-context.xml이라면 다음과 같이 등록합니다.

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/spring/root-context.xml
				 /WEB-INF/spring/security-context.xml
	</param-value>
</context-param>

8.1.2 스프링 시큐리티 태그

시큐리티 태그는 접근 권한을 위한 태그와 사용자 권한을 위한 태그로 분류할 수 있습니다.
접근 권한 태그는 허가된 사용자만 특정 페이지에 접근하게 하고, 인증을 처리하는 로그인 페이지를 호출하거나 로그아웃을 처리하도록 설정하는 데 사용합니다. 사용자 권한 태그는 인증을 처리하기 위해 사용자 정보를 가져오는 데 사용합니다.
다음은 접근 권한을 위한 태그와 사용자 권한을 위한 태그로 사용하는 주요 시큐리티 태그 유형입니다.

주요 스프링 시큐리티 태그의 종류

< http > : 시큐리티의 시작과 끝을 나타내는 데 사용합니다.
< intercept-url > : 시큐리티가 감시해야 할 URL과 그 URL에 접근 가능한 권한을 정의하는 데 사용합니다.
< form-login > : 로그인 관련 설정을 하는 데 사용합니다.
< logout > : 로그아웃 관련 설정을 하는 데 사용합니다.
< authentication-manager > : 사용자 권한 서비스의 시작과 끝을 나타내는 데 사용합니다.
< authentication-provider > : 사용자 정보를 인증 요청하는 데 사용합니다.
< user-service > : 사용자 정보를 가져오는 데 사용합니다.
< user > : 사용자 정보를 나타내는 데 사용합니다.

8.2 접근 권한과 사용자 권한 설정

접근 권한과 사용자 권한을 위한 스프링 시큐리티 태그를 알아봅시다. 스프링 시큐리티 태그를 사용하여 도서 등록 페이지의 접근 권한 및 사용자 권한 설정을 만들어 보겠습니다.

8.2.1 접근 권한을 설정하는 시큐리티 태그

인증이 허가된 특정 사용자에 따라 특정 경로에 접근할 수 있게 설정하는 태그로 < http >와 < intercept-url >이 있습니다.

< http > 태그

< http > 태그는 스프링 시큐리티 설정의 핵심으로 시작과 끝 태그(< http >...</ http >) 안에 스프링 시큐리티와 관련된 내용을 포함하는 최상위 태그입니다.

< http > 태그의 속성

auto-config : 일반적인 웹 애플리케이션에 필요한 기본 보안 서비스를 자동으로 설정합니다.
use-expressions : < intercept-url > 태그의 access 속성에서 스프링 표현 언어(SpEL)를 사용할 수 있습니다.

< http > 태그 적용 예

<http auto-config='true' use-expressions="true">
	// 생략
</http>

• auto-config: 기본 로그인 페이지, HTTP 기본 인증, 로그아웃 기능 등 제공 여부를 알려 줍니다.
• use-expressions: 스프링 표현 언어의 사용 여부를 알려 줍니다.

  • Note ≣ 스프링 표현 언어를 알려 주세요!
    스프링 표현 언어(Spring Expression Language, SpEL for Short)는 런타임 시점에서 특정 객체 정보에 접근하거나 조작하는 것을 지원하는 강력한 표현 언어 중 하나입니다. 스프링 표현 언어는 웹 프로그래밍 언어(JSP나 JSF)의 표현 언어 구문이 모두 다르기 때문에 이를 해결하는 통합 표현 언어라고 할 수 있습니다. 또한 독립적으로 사용할 수 있어 해당 애플리케이션에서 사용하거나 다른 자바빈에 주입하여 사용할 수 있습니다.
    구문은 Unified EL(웹 페이지에 표현식을 포함시키고자 자바 웹 응용 프로그램에서 주로 사용되는 특수 용도 프로그래밍 언어)과 유사하며 추가 기능, 특히 메서드 호출 및 기본 문자열 템플릿 기능을 제공합니다.

< intercept-url > 태그

< intercept-url > 태그는 접근 권한에 대한 URL 패턴을 설정할 때 사용됩니다. < http > 태그 안에 여러 개 설정할 수 있으며, 선언된 순서대로 접근 권한이 적용됩니다.

< intercept-url > 태그의 속성

pattern : ant 경로 패턴(?(문자 한 개와 매칭), #(0개 이상의 문자와 매칭), ##(0개 이상의 디렉터리와 매칭))을 사용하여 접근 경로를 설정합니다.
access : pattern 속성에 설정된 경로 패턴에 접근할 수 있도록 사용자 권한을 설정합니다.
requires-channel : 정의된 패턴 URL로 접근하면 설정된 옵션 URL로 리다이렉션합니다. 옵션으로는 http, https, any가 있습니다.

< intercept-url > 태그 적용 예

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans...>

	<http use-expressions="true">
    <intercept-url pattern="/admin/**" access="hasAuthority('ROLE_ADMIN')"/><intercept-url pattern="/manager/**" access="hasRole('ROLE_MANAGER')"/><intercept-url pattern="/member/**" access="IsAuthenticated()"/><intercept-url pattern="/**" access="permitAll"/></http>

... 
</beans:beans>	

➊ /admin/ 경로는 ROLE_ADMIN 권한이 있는 사용자만 접근할 수 있습니다.
웹 요청 URL이 http://.../admin이거나 http://.../admin/main이라면 ROLE_ADMIN 권한이 있는 사용자만 접근할 수 있으며, 그 외에는 접근할 수 없습니다.
➋ /manager/
경로는 ROLE_MANAGER 권한이 있는 사용자만 접근할 수 있습니다.
웹 요청 URL이 http://.../manager/이거나 http://.../manager/main이라면 ROLE_MANAGER 권한이 있는 사용자만 접근할 수 있으며, 그 외에는 접근할 수 없습니다.
➌ /member/ 경로는 인증된 사용자만 접근할 수 있습니다.
웹 요청 URL이 http://.../member이거나 http://.../member/main이라면 인증된 사용자만 접근할 수 있으며, 그 외에는 접근할 수 없습니다.
➍ /
경로는 권한에 상관없이 모두 접근할 수 있습니다.
웹 요청 URL이 http://.../이거나 http://.../home이거나 http://.../home/main이라면 누구든지 접근할 수 있습니다.

< intercept-url > 태그의 access 속성에 스프링 표현 언어를 사용하려면 < http > 태그 안에 use-expressions를 true로 설정해야 합니다.

스프링 표현 언어

hasRole([role])
• 현 권한자가 지정된 [role]을 가졌다면 true로 반환합니다.
• [role]에서 ‘ROLE_’ 접두어를 생략할 수 있습니다.

hasAnyRole([role1, role2])
• 현 권한자가 지정된 [role1, role2]에서 하나라도 가졌다면 true를 반환합니다.
• 콤마로 구분하여 표현하고 ‘ROLE_’ 접두어를 생략할 수 있습니다.

hasAuthority([authority])
• 현 권한자가 지정된 [authority]를 가졌다면 true로 반환합니다.
• [authority]에서 ‘ROLE_’ 접두어를 생략할 수 있습니다.

hasAnyAuthority([authority1, authority2])
• 현 권한자가 지정된 [authority1, authority2]에서 하나라도 가졌다면 true를 반환합니다.
• 콤마로 구분하여 표현하고 ‘ROLE_’ 접두어를 생략할 수 있습니다.

principal
현 사용자를 나타내는 주요 객체에 직접 접근할 수 있도록 허락합니다.

authentication
SecurityContext에서 얻은 현 인증 객체에 직접 접근할 수 있도록 허락합니다.

permitAll
현 권한자에 상관없이 항상 true입니다.

denyAll
현 권한자에 상관없이 항상 false입니다.

isAnonymous()
현 권한자가 익명의 사용자이면 true를 반환합니다.

isRememberMe()
현 권한자가 기억된 사용자이면 true를 반환합니다.

isAuthenticated()
사용자가 익명이 아니면 true를 반환합니다.

isFullyAuthenticated()
익명의 사용자이거나 기억된 사용자가 아니면 true를 반환합니다.

[role]과 [authority]의 기본 권한 표현

ROLE_ADMIN : 관리자
ROLE_USER : 일반 사용자
ROLE_ANONYMOUS : 모든 사용자
ROLE_RESTRICTED : 제한된 사용자
IS_AUTHENTICATED_FULLY : 인증된 사용자
IS_AUTHENTICATED_ANONYMOUSLY : 익명 사용자
IS_AUTHENTICATED_REMEMBERED REMEMBERED : 사용자

8.2.2 사용자 권한을 설정하는 시큐리티 태그

사용자 권한(authentication) 서비스 태그는 허가된 사용자의 아이디와 비밀번호 등 사용자 정보를 직접 설정하는 데 사용하며, 태그 유형은 다음과 같습니다.

사용자 권한 태그의 유형

< authentication-manager > : 사용자 권한 인증을 위한 최상위 태그입니다.

< authentication-provider > : 사용자 정보를 인증 요청 처리할 경우 사용합니다.

< user-service > : 사용자 정보(사용자 ID, 사용자 암호, 권한 등)를 가져올 때 사용합니다.

< user > : name, password, authorities 속성으로 사용자 정보를 나타낼 때 사용합니다.

사용자 권한 태그 예

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans...>
...
<authentication-manager>
	<authentication-provider>
    	<user-service>
        	<user name="admin" password="{noop}1234" authorities="ROLE_ADMIN, ROLE_USER"/>; ➊
            <user name="manager" password="{noop}1235" authorities="ROLE_MANAGER"/><user name="guest" password="{noop}1236" authorities="ROLE_USER"/></user-service>
    </authentication-provider>
</authentication-manager>
</beans:beans>

➊ < user > 태그에서 사용자 이름이 admin이고 비밀번호가 1234인 사용자는 관리자 및 일반 사용자 권한인 ROLE_ADMIN과 ROLE_USER를 가집니다.
➋ < user > 태그에서 사용자 이름이 manager고 비밀번호가 1235인 사용자는 매니저 권한인 ROLE_MANAGER를 가집니다.
➌ < user > 태그에서 사용자 이름이 guest고 비밀번호가 1236인 사용자는 일반 사용자 권한인 ROLE_USER를 가집니다.

  • Note ≣ < user-service > 태그에 사용자 정보를 가져오는 방법
    • < user > 태그로 사용자 정보 가져오기
    < user > 태그 안에 사용자 이름, 비밀번호, 권한 정보를 직접 작성하여 가져옵니다.
<authentication-manager>
	<authentication-provider>
    <user-service>
        	<user name="user1" password="111" authorities="ROLE_USER"/>;
            <user name="user2" password="222" authorities="ROLE_USER"/>
    </user-service>
    </authentication-provider>
</authentication-manager>

• 메시지 리소스 파일(*.properties)로 사용자 정보 가져오기
메시지 리소스 파일에 사용자 이름, 비밀번호, 권한 정보 등을 작성합니다. 그다음 < user-service > 태그 안에 메시지 리소스 파일을 설정하여 사용자 정보를 가져옵니다.

<authentication-manager>
	<authentication-provider>
    <user-service properties="WEB-INF/user/users.properties"/>
    </authentication-provider>
</authentication-manager>

메시지 리소스 파일(*.properties) 작성 예

admin=admin123, ROLE_ADMIN, ROLE_USER
user1=111,ROLE_USER
user2=222,ROLE_USER

• 데이터베이스에서 사용자 정보 가져오기
< authentication-provider > 태그 안에 < jdbc-user-service > 태그를 사용하여 데이터베이스에서 사용자 정보를 조회하여 가져옵니다.

<authentication-manager>
	<authentication-provider>
    <jdbc-user-service data-source-ref="dataSource"/>
    </authentication-provider>
</authentication-manager>

Example01Controller.java

package com.springmvc.chap08;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Example01Controller {
	@GetMapping("/exam01")
	public String requestMethod(Model model) {
		return "webpage08_01";
	}
	
	@GetMapping("/admin/main")
	public String requestMethod2(Model model) {
		model.addAttribute("data", "/webpage01/adminPage.jsp");
		return "webpage01/adminPage";
	}
	
	@GetMapping("/manager/main")
	public String requestMethod3(Model model) {
		model.addAttribute("data", "/webpage01/managerPage.jsp");
		return "webpage01/managerPage";
	}
	
	@GetMapping("/member/main")
	public String requestMethod4(Model model) {
		model.addAttribute("data", "/webpage01/memberPage.jsp");
		return "webpage01/memberPage";
	}
	
	@GetMapping("/home/main")
	public String requestMethod5(Model model) {
		model.addAttribute("data", "/webpage01/homePage.jsp");
		return "webpage01/homePage";
	}
}

접근 권한과 사용자 권한을 설정하는 예입니다.

adminPage.jsp, managerPage.jsp, memberPage.jsp, homePage.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!DOCTYPE html>
<html>
<head>
<title>Security</title>
</head>
<body>
	<h3>접근 권한과 사용자 권한 설정 예</h3>
	<p>뷰 페이지는 ${data} 입니다.
	<p><a href="<c:url value='/exam01' />">[웹 요청 URL /exam01로 이동하기]</a>
</body>
</html>

웹 요청 URL이 http://.../admin/main, http://.../manager/main, http://.../member/main, http://.../home/main일 때 Example01Controller 컨트롤러의 요청 처리 메서드 requestMethod2(), requestMethod3(), requestMethod4(), requestMethod5()에 매핑되어 출력되는 뷰 페이지입니다.

views 폴더 아래에 webpage01 폴더를 만들고, 그 안에 adminPage.jsp, managerPage.jsp, memberPage.jsp, homePage.jsp들이 존재해야 로딩이 가능하므로 주의해야 합니다.

webpage08_01.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!DOCTYPE html>
<html>
<head>
<title>Security</title>
</head>
<body>
<h3>스프링 시큐리티 예제</h3>
<ul>
	<li>웹 요청 URL : <a href="<c:url value='/home/main' />">/home/main</a></li>
	<li>웹 요청 URL : <a href="<c:url value='/member/main' />">/member/main</a></li>
	<li>웹 요청 URL : <a href="<c:url value='/manager/main' />">/manager/main</a></li>
	<li>웹 요청 URL : <a href="<c:url value='/admin/main' />">/admin/main</a></li>
</ul>
</body>
</html>

웹 요청 URL이 http://.../exam01일 때 Example01Controller 컨트롤러의 요청 처리 메서드 requestMethod()에 매핑되어 출력되는 뷰 페이지입니다.

실행 결과

웹 요청 URL이 http://.../exam01일 때 Example01Controller 컨트롤러의 요청 처리 메서드 requestMethod()에 매핑되어 webpage08_01.jsp 파일이 출력됩니다. 이때 누구나 /home/main 경로로 접근할 수 있습니다. 로그인 인증을 하게 되면 ROLE_USER와 ROLE_ADMIN 권한은 /member/main 경로에, ROLE_MANAGER 권한은 /manager/main 경로에, ROLE_ADMIN 권한은 /admin/main 경로에 접근할 수 있습니다.

manager 권한으로 user 페이지 열람이 가능했는데, 원칙대로라면 열람이 가능하지 않아야 하는 것 같습니다.

8.3 뷰 페이지에 사용하는 시큐리티 태그

스프링 시큐리티는 JSP 뷰 페이지에서 보안 정보에 접근하고, 보안 제약 조건을 적용하는 태그 라이브러리를 지원합니다. 이런 태그 중 하나를 사용하려면 JSP에 다음과 같이 태그 라이브러리를 선언해야 합니다.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

앞의 코드에서 prefix="sec"는 뷰 페이지 내 어느 곳에서든 sec 이름을 사용하면 uri에 적힌 http://~~/tags 라이브러리의 태그를 사용할 수 있다는 것을 나타냅니다.
스프링 시큐리티 태그를 사용하려면 pom.xml 파일에 spring-security-taglibs.jar을 의존 라이브러리로 등록해야 합니다.

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-taglibs</artifactId>
	<version>5.6.3</version>
</dependency>

8.3.1 권한 태그: <sec:authorize>

권한 태그는 태그 안의 내용을 평가할지 여부를 결정하며, <sec:authorize> 태그로 표현하여 사용합니다.

< sec:authorize > 태그의 속성

access : 접근 권한 설정을 위한 정규 표현식을 설정합니다.
url : 접근 권한이 설정된 사용자만 접근하도록 경로를 설정합니다.
var : 접근 권한이 설정된 사용자를 변수로 재정의하여 설정합니다.

// 사용자가 설정된 권한일 때
<sec:authorize access="hasRole('ROLE_ADMIN')">...</sec::authorize>

// 사용자가 설정 권한을 가지고 있지 않을 때
<sec:authorize access="!hasRole('ROLE_ADMIN')">...</sec::authorize>

// 사용자가 둘 중 하나의 권한을 가질 때
<sec:authorize access="hasAnyRole('ROLE_ADMIN','ROLE_MANAGER')"">...</sec:authorize>

// 사용자가 로그인할 때 <sec:authorize access="isAuthenticated()">...</sec:authorize>

// 사용자가 로그인하지 않을 때
<sec:authorize access="isAnonymous()">...</sec:authorize>

Example02Controller.java

웹 요청 URL이 http://.../exam02, http://.../manager/tag일 때 Example02Controller 컨트롤러의 요청 처리 메서드 requestMethod(), requestMethod2()로 출력되는 뷰 페이지 코드입니다.

package com.springmvc.chap08;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Example02Controller {
	
	@GetMapping("/exam02")
	public String requesMethod(Model model) {
		return "webpage08_02";
	}
	
	@GetMapping("/manager/tag")
	public String requesMethod2(Model model) {
		return "webpage08_02";
	}
}

교재와 다르게 "/manager/tag"라고 정의해야 mapping이 가능했습니다.

webpage08_02.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<!DOCTYPE html>
<html>
<head>
<title>Security</title>
</head>
<body>
<h2>스프링 시큐리티 태그 예</h2>
<sec:authorize access="hasRole('ROLE_MANAGER')" var="isAdmin">
	<p><h3>매니저 권한 화면입니다.</h3>
</sec:authorize>
<c:choose>
	<c:when test="${isAdmin}">
		<p>ROLE_MANAGER 권한 로그인 중입니다.
		<p><a href="<c:url value='/exam02' />">[웹 요청 URL /exam02로 이동하기]</a>
	</c:when>
	<c:otherwise>
		<p>로그인 중이 아닙니다.
		<p><a href="<c:url value='/manager/tag' />">[웹 요청 URL /manager/tag로 이동하기]</a>
	</c:otherwise>
</c:choose>
</body>
</html>

실행 결과

8.3.2 인증 태그: <sec:authentication>

인증 태그는 시큐리티 설정 파일에 저장된 현재 authentication 객체에 대한 접근을 허용합니다. <security:authentication>으로 표현하여 사용합니다. 그리고 JSP 뷰 페이지에서 property 속성을 사용하여 현재 authentication 객체에 직접 접근할 수 있습니다.

<security:authentication> 태그의 속성

property : 접근 권한이 설정된 현재 authentication 객체 이름입니다.
scope : 접근 권한이 설정된 영역입니다.
var : 접근 권한이 설정된 사용자를 변수로 재정의하여 설정합니다.

Example03Controller.java

package com.springmvc.chap08;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Example03Controller {
	@GetMapping("/exam03")
	public String requesMethod(Model model) {
		return "webpage08_03";
	}
	
	@GetMapping("/admin/tag")
	public String requesMethod2(Model model) {
		return "webpage08_03";
	}
}

웹 요청 URL이 http://.../exam03, http://.../admin/tag일 때 컨트롤러 Example03Controller의 요청 처리 메서드 requestMethod(), requestMethod2()로 출력되는 뷰 페이지 코드입니다.

webpage08_03.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<!DOCTYPE html>
<html>
<head>
<title>Security</title>
</head>
<body>
<h2>스프링 시큐리티 태그 예</h2>
<sec:authorize access="hasRole('ROLE_ADMIN')" var="isAdmin">
	<p><h3>관리자 권한 화면입니다.</h3>
</sec:authorize>
<c:choose>
	<c:when test="${isAdmin}">
		<p>로그인 중입니다.
		<p>비밀번호 : <sec:authentication property="principal.password" />
		<sec:authentication property="authorities" var="roles" scope="page" />
		<p>권한 :
			<ul>
				<c:forEach var="role" items="${roles}">
					<li>${role}</li>
				</c:forEach>
			</ul>
		<p>이름 : <sec:authentication property="principal.username" />
		<p><a href="<c:url value='/exam03' />">[웹 요청 URL /exam03로 이동하기]</a>
	</c:when>
	<c:otherwise>
		<p>로그인 중이 아닙니다.
		<p><a href="<c:url value='/admin/tag' />">[웹 요청 URL /admin/tag로 이동하기]</a>
	</c:otherwise>
</c:choose>
</body>
</html>

실행 결과

8.3.3 (실습) 스프링 시큐리티 태그로 도서 등록 페이지에 접근 권한 설정하기

스프링 시큐리티 태그를 사용하여 도서 등록 페이지에 접근할 수 있도록 사용자 정보를 설정하고 도서 등록 페이지에 접근 권한 설정을 구현해 보겠습니다.

pom.xml

  1. 메이븐 관련 환경 설정 파일 pom.xml에 스프링 시큐리티 의존 라이브러리를 등록합니다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"...>
	...
	<properties>
		<java-version>11</java-version>
		<org.springframework-version>5.3.19</org.springframework-version>
		<org.aspectj-version>1.9.9.1</org.aspectj-version>
		<org.slf4j-version>1.7.36</org.slf4j-version>
		<security.version>5.6.3</security.version>
	</properties>
	<dependencies>
		<!-- Spring -->
		...
		
		<!-- Spring Security -->
        <dependency>
        	<groupId>org.springframework.security</groupId>
        	<artifactId>spring-security-web</artifactId>
        	<version>${security.version}</version>
    	</dependency>
    	<dependency>
        	<groupId>org.springframework.security</groupId>
        	<artifactId>spring-security-config</artifactId>
        	<version>${security.version}</version>
    	</dependency>
    		
		<!-- AspectJ -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>
...
</project>

web.xml

  1. 웹 프로젝트 설정 파일 web.xml에 시큐리티 필터를 등록합니다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
	...
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
		
	<filter>
		<filter-name>springSecurityFilterChain</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>springSecurityFilterChain</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
</web-app>

web.xml

  1. 계속해서 web.xml 파일에 시큐리티 설정 파일(security-context.xml)의 위치 경로를 등록합니다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app...>

	<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml
		/WEB-INF/spring/security-context.xml
		</param-value>
	</context-param>
	...
</web-app>
  • Note ≣ web.xml 파일에 시큐리티 설정 파일을 등록하는 다른 방법
    다음과 같이 디스패처 서블릿 클래스 내에 시큐리티 설정 파일을 등록할 수 있습니다.
<web-app...>
<servlet>
	<servlet-name>appServlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/appServlet/servlet-context.xml
        			/WEB-INF/spring/security-context.xml // 시큐리티 설정 파일 등록
		</param-value>
	</init-param>
</servlet>
	... 
</web-app>

security-context.xml

  1. src/webapp/WEB-INF/spring 폴더에 스프링 시큐리티 설정 파일 security-context.xml을 생성하고 다음과 같이 작성합니다. 시큐리티 태그를 사용하여 접근 권한 사이트를 설정하고, 사용자 권한 서비스 태그로 권한을 부여합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
	xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"><http auto-config='true' use-expressions="true">
		<intercept-url pattern="/books/add" access="hasAuthority('ROLE_ADMIN')"/><form-login/>
		<csrf/>
		<logout/>
	</http><authentication-manager>
		<authentication-provider>
			<user-service>
				<user name="Admin" password="{noop}Admin1234" authorities="ROLE_ADMIN"/></user-service>
		</authentication-provider>
	</authentication-manager>
</beans:beans>

➊ 시큐리티 태그로 접근 권한을 설정합니다.
➋ ROLE_ADMIN 권한을 가진 사용자만 /books/add에 접근할 수 있습니다.
➌ 시큐리티 태그로 사용자 권한을 설정합니다.
➍ 사용자 이름은 Admin, 비밀번호는 Admin1234인 ROLE_ADMIN 권한을 정의합니다.

실행 결과


5. 웹 브라우저 주소창에 ‘http://localhost:8080/BookMarket/books/add’를 입력하면 실행 결과를 확인할 수 있습니다.
시큐리티 설정 파일에 설정한 사용자 이름 Admin과 비밀번호 Admin1234를 입력한 후 Sign in 버튼을 누릅니다. 사용자 인증에 성공하면 /books/add 경로로 이동하여 뷰 페이지인 addBook.jsp 파일을 출력합니다.


6. 다음은 사용자 아이디와 비밀번호를 잘못 입력하여 사용자 인증에 실패해 봅니다. 다음과 같은 실행 결과를 확인할 수 있습니다.

8.4 로그인과 로그아웃 처리

로그인과 로그아웃 설정을 위한 스프링 시큐리티 태그를 알아봅니다. 스프링 시큐리티 태그를 사용하여 로그인과 로그아웃을 설정하고, 로그인으로 인증된 사용자만 도서 등록 페이지에 접근할 수 있는 권한을 가지고 로그아웃하면 인증된 사용자가 해제되도록 만들어 봅시다.

8.4.1 < form-login > 태그

< form-login > 태그는 인증되지 않은 사용자가 특정 경로에 접근하거나 사용자 인증이 필요할 때 로그인 페이지를 보여 주는 데 사용됩니다. 이때 다음 속성들을 사용하여 로그인 페이지를 출력하고 로그인하는 사용자 정보를 전송합니다.

< form-login > 태그의 속성

login-page : 로그인 페이지 경로를 지정합니다.
login-processing-url : 로그인 요청 처리 경로를 지정합니다. < form > 태그의 action 속성 값을 설정합니다.
default-target-url : 로그인에 성공하면 이동할 기본 경로를 지정합니다.
always-use-default-target : ture 값으로 설정하면 항상 default-target-url 속성의 설정된 경로로 시작합니다.
authentication-failure-url : 로그인에 실패하면 이동할 경로를 지정합니다. 기본값은 /login?error입니다.
username-parameter : 로그인할 때 사용자 계정 이름에 대한 파라미터 이름을 설정합니다.
password-parameter : 로그인할 때 사용자 비밀번호에 대한 파라미터 이름을 설정합니다.

< form-login > 태그를 사용한 예

<?xml version="1.0" encoding="UTF-8"?> 
<http...>     
	<form-login login-page="/login"                  
		login-processing-url="/login"                  
		default-target-url="/admin"                  
		username-parameter="username"                  
		password-parameter="password"                  
		authentication-failure-url="/user/loginform?error=true"      
	/> 
</http>

➊ 설정된 경로인 /login으로 사용자 인증을 위한 로그인 페이지를 호출합니다.
➋ 설정된 경로인 /login으로 로그인 요청을 처리합니다.
➌ 설정된 경로인 /admin으로 로그인에 성공하면 자동으로 이동합니다.
➍ 로그인 페이지에서 입력된 사용자 계정 이름을 설정된 사용자 계정 이름 username으로 전달받습니다.
➎ 로그인 페이지에서 입력된 사용자 비밀번호를 설정된 비밀번호 password로 전달받습니다.
➏ 설정된 경로 /user/loginform으로 로그인에 실패하면 자동으로 이동합니다.

8.4.2 < logout > 태그

< logout > 태그는 로그아웃을 처리하는 데 사용되며, 다음 속성들로 로그아웃 페이지를 출력합니다.

< logout > 태그의 속성

delete-cookies : 로그아웃에 성공할 때 삭제할 쿠키 이름을 지정합니다. 콤마로 구분합니다.
invalidate-session : 로그아웃할 때 세션을 제거할지 지정합니다. 기본값은 true입니다.
logout-success-url : 로그아웃에 성공할 때 이동할 경로를 지정합니다. 기본값은 /login?logout입니다.
logout-url : 로그아웃 요청 처리 경로를 지정합니다. < form > 태그의 action 속성의 지정 값을 설정합니다.
success-handler-ref : 로그아웃에 성공할 때 이동을 제어하려면 LogoutSuccessHandler를 지정합니다.

< logout > 태그를 사용한 예

<http>     
	<logout logout-url="/logout"              
		logout-success-url="/logout"      
	/> 
</http>

➊ 설정된 경로인 /logout으로 로그아웃 페이지를 호출합니다.
➋ 로그아웃에 성공할 때 설정된 경로인 /logout으로 자동 이동합니다.

Example04Controller.java

package com.springmvc.chap08;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Example04Controller {
	
	@GetMapping("/login")
	public String requesMethod(Model model) {
		return "loginform";
	}
	
	@GetMapping("/admin/tag")
	public String requesMethod2(Model model) {
		return "webpage08_04";
	}
	
	@GetMapping("/logout")
	public String requesMethod3(Model model) {
		return "loginform";
	}
}

loginform.jsp

<%@ page contentType="text/html; charset=utf-8" %>

<!DOCTYPE html>
<html>
<head>
<title>로그인</title>
</head>
<body>
<h1>로그인</h1>
<form action="./login" method="post">
	<p>사용자명 <input type="text" name="username" placeholder="username">
	<p>비밀번호 <input type="password" name="password" placeholder="password">
	<p><button tupe="submit">로그인</button>
	<input type="hidden" name="${_csrf.parametherName}" value="${_csrf.token}"/>
</form>
</body>
</html>

webpage08_04.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<!DOCTYPE html>
<html>
<head>
<title>Security</title>
</head>
<body>
<h2>스프링 시큐리티 예</h2>

<sec:authorize access="isAuthenticated()">
	<h5><sec:authentication property="principal.username"/>님, 반갑습니다.</h5>
	<form action="./logout" method="POST">
	<button type="submit">LOGOUT</button>
		<input name="${_csrf.parametherName}" type="hidden" value="${_csrf.token}"/>
	</form>
</sec:authorize>
</body>
</html>

실행 결과

8.4.3 (실습) 스프링 시큐리티 태그로 로그인 페이지 구현하기

스프링 시큐리티 태그를 사용하여 로그인과 로그아웃 정보를 설정해 봅시다. 그리고 도서 등록 페이지의 접근 권한을 인증하는 로그인과 로그아웃 페이지를 구현합니다. 로그인 인증에 실패하면 오류 메시지가 출력되도록 해 보겠습니다.

security-context.xml

  1. 스프링 시큐리티 설정 파일 security-context.xml에 다음과 같이 작성합니다. 시큐리티 태그를 사용하여 < form-login > 태그로 로그인 관련 내용을 설정합니다.
<?xml version="1.0" encoding="UTF-8"?> 
<beans:beans...>
	...     
	<http use-expressions="true">         
		<intercept-url pattern="/books/add" access="hasRole('ROLE_ADMIN')"/>         
		<form-login login-page="/login"                      
			default-target-url="/books/add"                      
			authentication-failure-url="/loginfailed"               
            username-parameter="username"                      
			password-parameter="password"/><csrf/>         
		<logout/>     
	</http>         
		<authentication-manager>             
			<authentication-provider>                 
				<user-service>                     
					<user name="Admin" password="{noop}Admin1234"                           authorities="ROLE_ADMIN"/>                 
				</user-service>             
			</authentication-provider>         
		</authentication-manager> 
</beans:beans>

➊ 로그인 페이지의 경로입니다.
➋ 인증에 성공할 때의 경로입니다.
➌ 인증에 실패할 때의 경로입니다.
➍ 사용자 계정 이름입니다.
➎ 사용자 계정의 비밀번호입니다.

LoginController.java

  1. com.springmvc.controller 패키지에 LoginController 클래스를 생성하고 다음과 같이 작성합니다. 시큐리티 설정 파일에 선언한 /login, /loginfailed 경로에 대한 매핑 요청 처리 메서드입니다.
package com.springmvc.controller;  

import org.springframework.stereotype.Controller; 
import org.springframework.ui.Model; 
import org.springframework.web.bind.annotation.GetMapping; 
@Controller 
public class LoginController {      

	@GetMapping("/login") ➊      
➋         
	public String login() {         
		return "login"; ➌     
	}                

	@GetMapping("/loginfailed") ➍      
➎         
	public String loginerror(Model model) {          
		model.addAttribute("error", "true"); ➏         
		return "login"; ➐     
	}      
}

➊ 시큐리티 설정 파일에 login-page=“/login”으로 요청할 때 매핑합니다.
➋ 웹 요청 URL이 /login일 때의 요청 처리 메서드입니다.
➌ 뷰 이름 login으로 반환하여 login.jsp 파일을 출력합니다.
➍ 시큐리티 설정 파일에 authentication-failure-url=“/loginfailed”로 요청할 때 매핑합니다.
➎ 웹 요청 URL이 /loginfailed일 때의 요청 처리 메서드입니다.
➏ 모델 속성 error에 true 값을 저장합니다.
➐ 뷰 이름 login으로 반환하여 login.jsp 파일을 출력합니다.

login.jsp

  1. src/main/webapp/WEB-INF/views 폴더에 login.jsp 파일을 생성하고 다음을 작성합니다.
<%@ page contentType="text/html; charset=utf-8" %> 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>  

<html> 
<head> 
<link href="<c:url value="/resources/css/bootstrap.min.css"/>" rel="stylesheet"> 
<title>로그인</title> 
</head> 
<body>     
	<nav class="navbar navbar-expand navbar-dark bg-dark">         
		<div class="container">             
			<div class="navbar-header">                 
				<a class="navbar-brand" href="./home">Home</a>     
			</div>         
		</div>     
	</nav>     
	<div class="jumbotron">         
		<div class="container">             
			<h1 class="display-3">로그인</h1>         
		</div>     
	</div>     
	<div class="container col-md-4">         
		<div class="text-center">             
			<h3 class="form-signin-heading">Please login</h3>         
		</div><c:if test="${not empty error}">             
			<div class="alert alert-danger">                 
				UserName과 Password가 올바르지 않습니다.<br />           
			</div>         
		</c:if>             
		<form class="form-signin" action="<c:url value="/login"/>" method="post"> ➋             
			<div class="form-group row">                 
				<input type="text" name="username" class="form-control" placeholder="User Name" required autofocus></div>             
			<div class="form-group row">                 
				<input type="password" name="password" class="form-control"  placeholder="Password" required></div>             
			<div class="form-group row">                 
				<button class="btn btn-lg btn-success btn-block" type="submit">로그인</button>                 
				<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/></div>         
		</form>     
	</div> 
</body>
</html> 

➊ 인증에 실패했을 때 모델 속성 error에 저장된 값이 있다면 오류를 출력합니다.
➋ < form > 태그 선언 및 로그인 인증을 위한 요청 경로를 설정합니다.
➌ < input > 태그로 사용자 계정 이름을 설정합니다.
➍ < input > 태그로 사용자 비밀번호를 설정합니다.
➎ CSRF 공격을 방어하려고 설정합니다.

실행 결과


4. 웹 브라우저 주소창에 ‘http://localhost:8080/BookMarket/login’을 입력해서 로그인 페이지로 이동합니다. 사용자 이름과 비밀번호를 임의로 입력해서 다음과 같이 사용자 인증에 실패하는 것을 확인해 봅니다.


5. 이번에는 사용자 이름 ‘Admin’과 비밀번호 ‘Admin1234’를 입력합니다. 로그인 버튼을 눌러 사용자 인증이 성공하면 /books/add 경로로 이동하여 뷰 페이지인 addBook.jsp 파일을 출력합니다.

  • Note ≣ CSRF 공격에 대해 알아 두세요!
    스프링 시큐리티 3.2 버전부터는 CSRF 공격에 대한 방어 기능을 제공합니다. CSRF(Cross Site Request Forgery) 공격이란 한 번 인증된 세션 정보로 악의적인 목적에서 똑같이 구성된 다른 페이지로 요청을 보내는 것을 의미합니다.
    웹 쇼핑몰에서 CSRF 공격을 당하는 경우를 소개하겠습니다. 고객이 웹 쇼핑몰에 로그인한 후 로그아웃하지 않고 다른 웹 쇼핑몰을 방문할 때가 있습니다. 새로 방문한 웹 쇼핑몰의 HTML 코드가 로그아웃하지 않은 이전 웹 쇼핑몰과 동일하다면 고객 의도와는 다르게 다른 사람의 정보를 사용할 수 있습니다.
    따라서 웹 쇼핑몰 서버에서는 고객의 요청 폼이 해당 웹 쇼핑몰의 올바른 폼인지 구분할 수 있는 고유한 값을 표현하는 방법인 Syncronized Token 패턴을 사용합니다. 이는 모든 요청에 세션 쿠키와 더불어 임의로 생성되는 토큰을 HTTP 파라미터로 제공하는 방법입니다. 이에 스프링 시큐리티는 CSRF 공격을 막고자 랜덤 토큰 인증 방식을 제공합니다.
    스프링 시큐리티 3.2 이상 버전에서는 웹 요청을 할 때 적절한 CSRF 토큰을 포함하지 않으면 다음 오류가 발생합니다.

    CSRF 토큰 오류를 해결하려면 호출하는 URL의 파라미터에 토큰 값을 추가해야 합니다.

CSRF 토큰 전송 방법

<form action="/login?${_csrf.parameterName}=${_csrf.token}" method="GET">     
	... 
</form> 
/login?${_csrf.parameterName}=${_csrf.token}

또는 CSRF 토큰 전송 방법으로 두 가지 예를 추가합니다.

<a href="/login?${_csrf.parameterName}=${_csrf.token}">login</a>

예제처럼 GET 방식이나 URL에 토큰 파라미터를 추가하면 토큰 값이 노출되므로 다음과 같이 < input > 태그 안에 hidden 타입을 설정하여 POST 방식으로 전송해야 합니다.

<form action="/login" method="POST">     
	...     
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> 
</form>

8.4.4 (실습) 스프링 시큐리티 태그로 로그아웃 페이지 구현하기

security-context.xml

  1. security-context.xml 파일에 다음과 같이 작성합니다. 시큐리티 태그를 사용하여 < form-logout > 태그로 로그아웃 처리를 설정합니다.
<?xml version="1.0" encoding="UTF-8"?> 
<beans:beans...>     
	...     
    <http use-expressions="true">         
    	<intercept-url pattern="/books/add" access="hasRole('ROLE_ADMIN')"/>         
        <form-login login-page="/login"                     
        	default-target-url="/books/add"                     
            authentication-failure-url="/loginfailed"                     username-parameter="username"                     
            password-parameter="password"/>          
        <csrf/>         
        <logout logout-success-url="/logout"/></http>
... 
</beans:beans>

➊ 로그아웃할 때 이동할 경로입니다.

LoginController.java

  1. 시큐리티 설정 파일에 선언된 /logout 경로에 대한 매핑 요청 처리 메서드를 LoginController 클래스에 작성합니다.
package com.springmvc.controller; 
... 
@Controller 
public class LoginController {     
	...     
	@GetMapping("/loginfailed")         
    public String loginerror(Model model) {         
        model.addAttribute("error", "true");         
        return "login";     
    }     
    @GetMapping("/logout")      
➊         
	public String logout(Model model) {         
    	return "login"; ➋     
    }     
}

➊ 웹 요청 URL이 /logout일 때 요청 처리 메서드입니다.
➋ 뷰 이름 login으로 반환하여 login.jsp 파일을 출력합니다.

addBook.jsp

  1. [Logout] 버튼을 설정하려면 addBook.jsp 파일을 다음과 같이 추가해야 합니다.
<%@ page contentType="text/html; charset=utf-8" %> 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>     
	...     
    <div class="jumbotron">         
    	<div class="container">             
        	<h1 class="display-3">도서 등록</h1>         
        </div>
    </div>    
    
    <div class="container">
    	<div class="float-right"><form:form action="${pageContext.request.contextPath}/logout" method="POST">
            	<input type="submit" class="btn btn-sm btn-success" value="Logout"/>             
           </form:form>
       </div>
       <br><br>
       <form:form modelAttribute="NewBook" class="form-horizontal"> ...

실행 결과


4. 사용자 인증이 성공하면 /books/add 경로로 이동하여 뷰 페이지인 addBook.jsp 파일을 출력합니다. 그리고 [Logout] 버튼이 추가된 것을 확인할 수 있습니다.

나가기

스프링 시큐리티 태그가 무엇인지 살펴보고 이를 적용하여 도서 쇼핑몰의 로그인과 로그아웃 페이지를 구현해 보았습니다. 또 인증된 사용자만 도서 등록 페이지에 접근 권한을 가질 수 있도록 구현해 보았습니다.

0개의 댓글