스프링

 

 1. 스프링프레임웍 - Spring Security(1) : 기본 설정

출처: http://pentode.tistory.com/141 [쉬고 싶은 개발자]

스프링 시큐리티는 지금까지 직접 구현해왔던 아이디/비밀번호를 입력하고 로그인하여 사용자를 인증(Authentication)하고, 로그인후 프로그램의 각각의 기능에 대한 권한을 체크(Authorization)하는 작업을 구현해둔 보안 프레임웍 입니다.

 

프로그램외에 리소스(이미지 등)에 대한 접근도 제어할 수 있고, CSRF(Cross Site Request Forgery) 공격 방어, 세션 고정(Session Fixation) 공격 방어 및 다중 접속 방지 등도 간단하게 구현할 수 있습니다.

 

이제부터 스프링 시큐리티를 사용하는 방법에 대해 알아 보도록 하겠습니다. 이 글에서는 스프링 시큐리티 4.2.1 버전으로 테스트 해 봅니다.

 

1. 의존성 등록(pom.xml)

스프링 시큐리티 라이브러리를 포함 시킵니다.

 

        <!-- 시큐리티 최소 의존성 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>4.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>4.2.1.RELEASE</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>4.2.1.RELEASE</version>
        </dependency>


 

2. 필터를 등록합니다.(web.xml)

 

스프링 시큐리티는 서블릿 필터로 동작을 하여서 요청의 앞단에서 필요한 체크를 수행합니다.

 

	<!-- Spring Security -->
	<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>


 

3. 스프링 시큐리티 설정파일 로딩하도록 지정(web.xml)

 

스프링 시큐리티에 사용되는 빈들을 위한 설정파일을 스프링 프레임웍이 읽어들이도록 설정 파일 지정을 추가 합니다. security-context.xml 파일 입니다.


 

	<!-- 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/appServlet/security-context.xml
		</param-value>
	</context-param>

 

 

4. 스프링 시큐리티 <http> 설정(security-content.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:context="http://www.springframework.org/schema/context"
	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/context 
	http://www.springframework.org/schema/context/spring-context.xsd 
	http://www.springframework.org/schema/security 
	http://www.springframework.org/schema/security/spring-security.xsd ">


	<http>
		<intercept-url pattern="/login" access="hasRole('USER')" />
		<form-login />
		<logout logout-url="/logout" />
	</http> <!-- provider -->
	
		
	<authentication-manager>
		<authentication-provider>
			<user-service>
				<user name="user" password="password" authorities="ROLE_USER" />
			</user-service>
		</authentication-provider>
	</authentication-manager>


</beans:beans>




 

 

5. 로그아웃 기능 입니다.(home.jsp)

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page session="false"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>Home</title>
</head>
<body>
	<h1>Hello world!</h1>

	<P>The time on the server is ${serverTime}.</P>

<p>

	<form:form action="${pageContext.request.contextPath}/logout"
		method="POST">
		<input type="submit" value="로그아웃" />
	</form:form>



</body>
</html>

 

 

6. 기본설정으로 실행해본 화면입니다.

user: user

password: password

 

 

 

 

로그인 화면은 직접 만들지 않았지만 기본값으로 스프링 시큐리티가 제공해주는 것이 보여집니다.
 

 

7. 설정을 알아보겠습니다.(security-content.xml)

설정파일 내의 <http> 요소에서 웹 관련 기능을 설정합니다. 최소 설정이므로 기본값들이 많이 사용되었습니다. 위의 설정에서 사용된 기본값들을 더 적어 보면 다음과 같을 것입니다.
 

<http use-expressions="true">
 <intercept-url pattern="/**" access="hasRole('USER')" /> 
<form-login login-page="/login" />
 <logout logout-url="/logout" /> 
<csrf disabled="false"/> 

</http>

 

use-expressions 속성은 <intercept-url>의 access 속성에 표현식을 사용할 수 있다는 뜻입니다. 이 값이 "false"이면 access="USER" 로 값을 바로 사용해야 합니다.

 

<intercept-url> 요소는 pattern 속성으로 주어인 URL 에 대해 access속성의 권한을 요구합니다. 위의 의미는 모든 URL(/**)을 보호하고, 액세스하기 위해서는 ROLE_USER 역할을 요구합니다. 접두어 "ROLE_" 가 없어도 동일한 의미 입니다.(접두어를 붙여도 됩니다.)

 

<intercept-url>요소는 서로 다른 요구사항을 나타내는 여러개가 나올 수 있습니다. 여러개가 나올 경우 순서대로 매칭되어져서 첫번째가 매칭된것이 사용되어 집니다. 그러므로 가장 특수한 경우를 위쪽에 놓고 일반적인 경우를 아래쪽에 놓아야 합니다.

 

<form-login> 요소는 로그인을 위해서 폼을 사용합니다. 로그인처리 URL의 기본값은 "/login" 입니다.

 

<logout> 요소는 로그아웃 URL을 제공한다는 뜻입니다. 기본값은 "/logout" 입니다.

 

<csrf disabled="false"/> 는 CSRF(Cross Site Resquest Fosery) 공격을 방어하기 위한 처리를 하겠다는 뜻입니다. 이게 기본값입니다. CSRF 방어를 위해 POST 방식으로 값을 넘기는 곳에는 모두 <input name="_csrf" type="hidden" value="ae10ba14-8ca9-4171-9f0d-dd472321a08d" /> 와 같은 숨겨진 값을 보내게 됩니다.

 

프로바이더 부분은 <user name="user" password="password" authorities="ROLE_USER" /> 부분에서 사용자 정보를 제공하고 있습니다. 실제로 사용할때는 이 정보를 데이터베이스에서 가져오도록 설정하게 될것 입니다.

 

 

CSRF 방어를 사용하도록 되어 있다면 로그아웃을 POST 방식으로 해야 합니다. CSRF 방어를 사용하지 않도록 되어있다면 GET 방식도 되므로 단순 링크로 처리할 수 있습니다.

 

 

<a href="<c:url value='/logout' />">로그아웃</a>

 

지금까지 스프링 시큐리티를 최소기능만을 가지고 테스트해보았습니다. 이걸 가지고 아직 할 수 있는게 없습니다. 앞으로 조금씩 알아나가도록 하겠습니다.

 

 



 2. 스프링프레임웍 - Spring Security(2) : 커스텀 로그인 화면 및 권한에 따른 접근 제어

출처: http://pentode.tistory.com/144 [쉬고 싶은 개발자]

 

위 사이트에서 참조하였으나 변경한 부분이 있다.

 

메인 페이지이다.

소개페이지 또는 관리자 홈 페이지 클릭시

로그인이 안 되어 있으면  다음과 같이 로그인 페이지로 이동한다.

 

 

 

아이디와 비밀번호를 잘못 입력했을 때 다음과 같은

메시지가 나온다.

그리고 url  에서 파라미터  error 가 존재한다.

 

스프링시큐리티에서는 이동하고자하는 페이지를 값을 자동으로 저장 한다.

메인 페이지에서 소개페이지 로 이동시 로그인 이 안되어 있으면 로그인 화면으로 이동한다. 

그리고 로그인 후에는  소개 페이지로  이동한다.

 

관리자도 마찬가지이다.  메인에서 관리자 홈 클릭시 관리자로 로그인이 안되어 있으면 로그인 페이지로 

이동한고 로그인을 한 후에는 자동으로 이동하고자 하는 관리자 홈 페이지로 이동한다.

 



 

위화 같은 형태로 구동 되고 소스 는 

https://github.com/braverokmc79/spring_simple_blog  

의 test 프로젝트에 존재한다.

 

web.xml

<!-- 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/appServlet/security-context.xml
		</param-value>
	</context-param>

 

1. 스프링 시큐리티 태그라이브러리를 사용하기 위한 의존성을 추가합니다.

 

메인화면에서 로그인이 되어 있지 않으면 로그인 링크를 보여주고, 로그인이 되어 있으면 로그아웃 링크를 보여주기 위해서 사용됩니다.

 

		<!-- 시큐리티 최소 의존성 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
			<version>4.2.1.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>4.2.1.RELEASE</version>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-taglibs</artifactId>
			<version>4.2.1.RELEASE</version>
		</dependency>

 

2. 커스텀 로그인 화면을 사용하기 위한 설정을 추가 합니다.

 

커스텀 로그인 화면 제공 및 권한(역할)에 따른 접근 제어도 테스트 해봅니다.

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:context="http://www.springframework.org/schema/context"
	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/context 
	http://www.springframework.org/schema/context/spring-context.xsd 
	http://www.springframework.org/schema/security 
	http://www.springframework.org/schema/security/spring-security.xsd ">
<!--
 	<http>
		<intercept-url pattern="/login" access="hasRole('USER')" />
		<form-login />
		<logout logout-url="/logout" />
	</http> 
	
		
	<authentication-manager>
		<authentication-provider>
			<user-service>
				<user name="user" password="password" authorities="ROLE_USER" />
			</user-service>
		</authentication-provider>
	</authentication-manager>
 
-->


<!-- access="permitAll" - 로그인 하지 않아도 접근 permitAll 항상 true로 평가 됩니다. -->
<!-- /admin/**  access="hasRole('ADMIN')" URL 은 ADMIN 권한이 있어야 접근할 수 있도록 합니다. -->

<!-- hasAnyRole([role1,role2])
현재 로그인된 사용자가 콤마(,)로 분리하여 주어진
 role들 중 하나라도 가지고 있으면 true를 반환합니다. 
 제공된 role이 'ROLE_'로 시작하지 않으면 기본적으로 'ROLE_'를 추가합니다. 
 이것은 DefaultWebSecurityExpressionHandler에서 defaultRolePrefix를 수정하여 커스터마이즈할 수 있습니다.
 -->


<!-- 
<logout> 요소를 커스터마이징 합니다.

  + logout-url="/logout" : 로그아웃에 사용될 페이지를 지정합니다. 기본값은 "/logout" 입니다.

  + logout-success-url="/home.do" : 로그아웃에 성공하였을 때 이동할 페이지를 지정합니다.
  
  
 -->
	<!-- 검사 URL -->
	<http use-expressions="true">
		<intercept-url pattern="/sample/**" access="permitAll"/>	
		<intercept-url pattern="/login/loginForm.do" access="permitAll"/>  	
		<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> 
		<intercept-url pattern="/login" access="hasRole('USER')" />
		<intercept-url pattern="/" access="permitAll"/>
		<intercept-url pattern="/user/**" access="hasAnyRole('USER, ADMIN')" />
		
		<form-login login-page="/login/loginForm.do" 
					default-target-url="/"
					authentication-failure-url="/login/loginForm.do?error"
					username-parameter="id"
					password-parameter="password"	
		/>		
		<logout logout-url="/logout"    
				logout-success-url="/" />
		<access-denied-handler error-page="/login/accessDenied.do"/>			
	</http>

	<authentication-manager>
		<authentication-provider>
			<user-service>
				<user name="user" password="password" authorities="ROLE_USER" />
				<user name="admin" password="password" authorities="ROLE_ADMIN" />
			</user-service>
		</authentication-provider>	
	</authentication-manager>
	
	
	
</beans:beans>




 

provider 를 통해 ROLE_USER, ROLE_ADMIN 두 개의 역할을 가지는 사용자를 추가 합니다. 각각 아이디가 "user"와 "admin" 입니다.

 

<intercept-url pattern="/login/loginForm.do" access="permitAll" /> : 로그인 폼 URL에는 로그인 하지 않아도 접근할 수 있도록 합니다.

<intercept-url pattern="/" access="permitAll" /> : 메인화면도 로그인 없이 접근할 수 있도록 합니다.

<intercept-url pattern="/admin/**" access="hasRole('ADMIN')" /> : /admin/** URL 은 ADMIN 권한이 있어야 접근할 수 있도록 합니다.

<intercept-url pattern="/**" access="hasAnyRole('USER, ADMIN')" /> : 그 외 나머지는 USER 또는 ADMIN중 하나로도 권한이 있으면 접근할 수 있습니다.

 

<form-login> 요소를 커스터마이징 합니다.

  + login-page="/login/loginForm.do" : 커스텀 로그인 페이지를 지정합니다.

  + default-target-url="/" : 로그인 후에 기본으로 보여질 페이지 입니다. 특정 페이지를 클릭해서 로그인 화면이 나왔다면 로그인 성공후 앞서 클릭한 페이지로 이동합니다.

  + authentication-failure-url="/login/loginForm.do?error" : 로그인 실패시 보여질 페이지를 지정합니다. 여기서는 로그인 페이지를 다시 보여줍니다. 기본값은 /login?error 입니다.

  + username-parameter="id" : 로그인 폼에 아이디 입력 필드에 사용될 name 입니다. 기본값은 "username"입니다.

  + password-parameter="password" : 로그인 폼에 비밀번호 입력 필드에 사용될 name 입니다. 기본값은 "password" 입니다.

 

<logout> 요소를 커스터마이징 합니다.

  + logout-url="/logout" : 로그아웃에 사용될 페이지를 지정합니다. 기본값은 "/logout" 입니다.

  + logout-success-url="/home.do" : 로그아웃에 성공하였을 때 이동할 페이지를 지정합니다.

 

<access-denied-handler error-page="/login/accessDenied.do" /> : 로그인 하였으나 권한이 없는 요청을 하였을 경우 보여지는 페이지를 지정합니다.

 

 

 

3. 추가된 페이지들에 대해서 간단히 알아보겠습니다.

 

- 메인화면인 home.jsp 파일 입니다.

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

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

<html>
<head>
<title>Home</title>
</head>
<body>
	<h1>Home!</h1>
	<sec:authorize access="isAnonymous()">
		<p>
			<a href="<c:url value="/login/loginForm.do" />">로그인</a>
		</p>
	</sec:authorize>
	<sec:authorize access="isAuthenticated()">
		<form:form action="${pageContext.request.contextPath}/logout"
			method="POST">
			<input type="submit" value="로그아웃" />
		</form:form>
	</sec:authorize>
	<h3>
		[<a href="<c:url value="/user/introduction.do" />">소개 페이지</a>] [<a
			href="<c:url value="/admin/adminHome.do" />">관리자 홈</a>]
	</h3>


</body>
</html>

- 스프링 시큐리티 태그를 사용하기 위해서 추가합니다.

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

 

- 로그인 안된 상태를 확인합니다.

<sec:authorize access="isAnonymous()"></sec:authorize>

 

- 로그인 된 상태를 확인합니다.

<sec:authorize access="isAuthenticated()"></sec:authorize>

 

- 로그인폼 페이지입니다.(loginForm.jsp)


 

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

<!DOCTYPE html>
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<!-- 합쳐지고 최소화된 최신 CSS -->
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
	<!-- 부가적인 테마 -->
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
	<!-- 합쳐지고 최소화된 최신 자바스크립트 -->
	<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>

<title>로그인 페이지</title>
</head>

<body onload="document.f.id.focus();">

<div class="row">
	<div class="col-xl-3 col-sm-3"></div>
	<div class="col-xl-4 col-sm-4">
	
	<h3>아이디와 비밀번호를 입력해주세요.</h3>

	<c:url value="/login" var="loginUrl" />

	<form:form name="f" action="${loginUrl}" method="post" >
		<c:if test="${param.error != null}">
			<p>아이디와 비밀번호가 잘못되었습니다.</p>
		</c:if>
		<c:if test="${param.logout != null}">
			<p>로그아웃 하였습니다.</p>
		</c:if>
		<p>
			<label for="username">아이디</label> <input type="text" id="id" name="id" class="form-control" />
		</p>
		<p>
			<label for="password">비밀번호</label> <input type="password" id="password" name="password" class="form-control" />	
		</p>
		<%-- <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> --%>
		<button type="submit" class="btn">로그인 </button>
	</form:form>


	</div>
</div>

</body>
</html>


 

 

- 아이디 입력 필드의 name 을 기본값이 "username"이 아니라 "id" 로 변경하였습니다.

- 로그인에 실패했을 경우 param.error 가 따로 오므로 메세지를 보여줍니다.

- param.logout 는 로그아웃 페이지에서 사용할 수 있습니다. 현재 예제는 로그아웃 성공시 /home.do 로 가므로 사용되지 않습니다.

 

 

4.  에러 페이지 이동 페이지


accessDenied.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Access Denied</title>
</head>

<body>

<h1>Access Denied!</h1>

<h3>[<a href="<c:url value="/" />">홈</a>]</h3>

</body>
</html>

 

 

5. user 화면 페이지

introduction.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>소개 페이지</title>
</head>

<body>

<h1>소개 페이지 입니다.</h1>

<p><a href="<c:url value="/" />">홈</a></p>

</body>
</html>

 

 

6. 관리자 페이지

adminHome.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>관리자 홈</title>
</head>

<body>

<h1>관리자 홈 화면!</h1>

<h3>[<a href="<c:url value="/" />">홈</a>]</h3>

</body>
</html>


 

 

7. 컨트롤 

 

LoginController

/**
* <pre>
* 1. 패키지명 : net.macaronics.web.controller
* 2. 타입명 : LoginController.java
* 3. 작성일 : 2017. 11. 21. 오후 7:17:08
* 4. 저자 : 최준호
* 
* </pre>
*
*/
package net.macaronics.web.controller;

import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value="/login")
public class LoginController {
	
	private static final Logger logger=LoggerFactory.getLogger(LoginController.class);
	
	@RequestMapping(value="/loginForm.do", method=RequestMethod.GET)
	public String loginForm(Locale locale, Model model){
		logger.info("Welcome Login Form!");
		return "login/loginForm";
	}
	
	@RequestMapping(value="/accessDenied.do", method=RequestMethod.GET)
	public String accessDenied(Locale locale, Model model){
		logger.info("Welcome Access Denied!");
		return "login/accessDenied";
	}
	
}

 

UserController

/**
* <pre>
* 1. 패키지명 : net.macaronics.web.controller
* 2. 타입명 : UserController.java
* 3. 작성일 : 2017. 11. 22. 오후 5:10:15
* 4. 저자 : 최준호
* 
* </pre>
*
*/
package net.macaronics.web.controller;

import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value="/user/")
public class UserController {

	private static Logger logger=LoggerFactory.getLogger(UserController.class);
	
	@RequestMapping(value = "introduction.do", method = RequestMethod.GET)
	public String introduction(Locale locale, Model model) {
		logger.info("Welcome Introduction!");
		
		return "user/introduction";
	}
	
	
	
}



 

AdminController

/**
* <pre>
* 1. 패키지명 : net.macaronics.web.controller
* 2. 타입명 : AdminController.java
* 3. 작성일 : 2017. 11. 21. 오후 7:11:30
* 4. 저자 : 최준호
* 
* </pre>
*
*/
package net.macaronics.web.controller;

import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class AdminController {
	private static final Logger logger=LoggerFactory.getLogger(AdminController.class);
	
	@RequestMapping(value="/admin/adminHome.do", method=RequestMethod.GET)
	public String home(Locale locale, Model model){
		logger.info("Welcome Admin Home!");
		return "admin/adminHome";
	}
	
}

 

※ 일반적인 내장 표현식을 정리하였습니다.

 

표현식설명

hasRole([role])  현재 로그인된 사용자가 지정된 role을 가지고 있으면 true를 반환합니다. 제공된 role이 'ROLE_'로 시작하지 않으면 기본적으로 'ROLE_'를 추가합니다. 이것은 DefaultWebSecurityExpressionHandler에서 defaultRolePrefix를 수정하여 커스터마이즈할 수 있습니다.

hasAnyRole([role1,role2])  현재 로그인된 사용자가 콤마(,)로 분리하여 주어진 role들 중 하나라도 가지고 있으면 true를 반환합니다. 제공된 role이 'ROLE_'로 시작하지 않으면 기본적으로 'ROLE_'를 추가합니다. 이것은 DefaultWebSecurityExpressionHandler에서 defaultRolePrefix를 수정하여 커스터마이즈할 수 있습니다.

hasAuthority([authority])  현재 로그인된 사용자가 지정된 권한이 있으면 true를 반환합니다.

hasAnyAuthority([authority1,authority2])현재 로그인된 사용자가 콤마(,)로 분리하여 주어진 권한들중 하나라도 가지고 있으면 true를 반환합니다.

principal현재 사용자를 나타내는 principal 객체에 직접 접근할 수 있습니다.

authenticationSecurityContext로 부터 얻은 Authentication 객체에 직접 접근할 수 있습니다.

permitAll 항상 true로 평가 됩니다.

denyAll 항상 false로 평가 됩니다.

isAnonymous()현재 사용자가 익명사용자(로그인 안됨) 사용자이면 true를 반환합니다.

isRememberMe()현재 로그인된 사용자가 remember-me 사용자이면 true를 반환합니다.(로그인 정보 기억 기능에 의한 사용자)

isAuthenticated()현재 사용자가 로그인된 사용자라면 true를 반환합니다.

isFullyAuthenticated()로그인 정보 기억(remember-me)이 아니라 아이디/비밀번호를 입력하여 로그인 했다면 true를 반환합니다.

hasPermission(Object target, Object permission)   사용자가 주어진 권한으로 제공된 대상에 액세스 할 수 있으면 true 를 반환합니다. 예, hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)   사용자가 주어진 권한으로 제공된 대상에 액세스 할 수 있으면 true 를 반환합니다. 예, hasPermission(1, 'com.example.domain.Message', 'read')

 

 

 

 


 3. 스프링프레임웍 - Spring Security(3) : 사용자/권한 정보 DB사용하기

 

DB 사용 방법은  위 내용 1 ~ 2에서 DB  테이블 추가 와 authentication-manager 만 변경하면 된다.

1. DB 테이블 추가 

테이블은 mysql 과  oracle 두가지가 소스에  있다.

http://pentode.tistory.com/146 의 소스를 참조해서 만들었다.

mysql 소스 

/**********************************/
/* Table Name: 회원 */
/**********************************/
CREATE TABLE users(
        username                              VARCHAR(20)         NOT NULL COMMENT '아이디' ,
        password                              VARCHAR(100)         NULL COMMENT '비밀번호',
        enabled                               INTEGER         NULL COMMENT '계정사용여부'
);



ALTER TABLE users ADD CONSTRAINT IDX_users_PK PRIMARY KEY (username);

/**********************************/
/* Table Name: 권한 */
/**********************************/
CREATE TABLE authorities(
        username                              VARCHAR(20)         NOT NULL COMMENT '회원아이디',
        authority                             VARCHAR(20)         NOT NULL COMMENT '권한'
);



ALTER TABLE authorities ADD CONSTRAINT IDX_authorities_PK PRIMARY KEY (username, authority);
ALTER TABLE authorities ADD CONSTRAINT IDX_authorities_FK0 FOREIGN KEY (username) REFERENCES users (username);

/**********************************/
/* Table Name: 그룹 */
/**********************************/
CREATE TABLE groups(
        id                                    VARCHAR(20)         NOT NULL COMMENT '그룹 아이디',
        group_name                            VARCHAR(20)         NULL COMMENT '그룹 명'
);


ALTER TABLE groups ADD CONSTRAINT IDX_groups_PK PRIMARY KEY (id);

/**********************************/
/* Table Name: 그룹 권한 관계 */
/**********************************/
CREATE TABLE group_authorities(
        group_id                              VARCHAR(20)         NOT NULL COMMENT '그룹 아이디',
        authority                             VARCHAR(20)         NOT NULL COMMENT '권한'
);



ALTER TABLE group_authorities ADD CONSTRAINT IDX_group_authorities_PK PRIMARY KEY (group_id, authority);
ALTER TABLE group_authorities ADD CONSTRAINT IDX_group_authorities_FK0 FOREIGN KEY (group_id) REFERENCES groups (id);

/**********************************/
/* Table Name: 그룹 회원 관계 */
/**********************************/
CREATE TABLE group_members(
        group_id                              VARCHAR(20)         NOT NULL COMMENT '그룹 아이디',
        username                              VARCHAR(20)         NOT NULL COMMENT '회원 아이디'
);



ALTER TABLE group_members ADD CONSTRAINT IDX_group_members_PK PRIMARY KEY (group_id, username);
ALTER TABLE group_members ADD CONSTRAINT IDX_group_members_FK0 FOREIGN KEY (username) REFERENCES users (username);
ALTER TABLE group_members ADD CONSTRAINT IDX_group_members_FK1 FOREIGN KEY (group_id) REFERENCES groups (id);


-- 회원 데이터 입력
INSERT INTO users (username, password, enabled) VALUES ('user', '1', 1);
INSERT INTO users (username, password, enabled) VALUES ('admin', '1', 1);

-- 회원 권한 입력
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');
INSERT INTO authorities (username, authority) VALUES ('admin', 'ROLE_ADMIN');
INSERT INTO authorities (username, authority) VALUES ('admin', 'ROLE_USER');

-- 그룹
INSERT INTO groups (id, group_name) VALUES ('G01', '관리자 그룹');
INSERT INTO groups (id, group_name) VALUES ('G02', '사용자 그룹');

-- 그룹 권한
INSERT INTO group_authorities (group_id, authority) VALUES ('G01', 'ROLE_ADMIN');
INSERT INTO group_authorities (group_id, authority) VALUES ('G01', 'ROLE_USER');
INSERT INTO group_authorities (group_id, authority) VALUES ('G02', 'ROLE_USER');

-- 그룹 회원
INSERT INTO group_members (group_id, username) VALUES ('G01', 'admin');
INSERT INTO group_members (group_id, username) VALUES ('G02', 'user');

 

 

 

 

2. 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:context="http://www.springframework.org/schema/context"
    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/context 
    http://www.springframework.org/schema/context/spring-context.xsd 
    http://www.springframework.org/schema/security 
    http://www.springframework.org/schema/security/spring-security.xsd ">

    <!-- 검사 URL -->
    <http use-expressions="true">
        <intercept-url pattern="/sample/**" access="permitAll"/>    
        <intercept-url pattern="/login/loginForm.do" access="permitAll"/>      
        <intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> 
        <intercept-url pattern="/login" access="hasRole('USER')" />
        <intercept-url pattern="/" access="permitAll"/>
        <intercept-url pattern="/user/**" access="hasAnyRole('USER, ADMIN')" />
        
        <form-login login-page="/login/loginForm.do" 
                    default-target-url="/"
                    authentication-failure-url="/login/loginForm.do?error"
                    username-parameter="id"
                    password-parameter="password"    
        />        
        <logout logout-url="/logout"    
                logout-success-url="/" />
        <access-denied-handler error-page="/login/accessDenied.do"/>            
    </http>
        
    <!--  provider  -->
    <authentication-manager>
        <authentication-provider>
        <jdbc-user-service 
            data-source-ref="dataSource"
            role-prefix=""
            users-by-username-query="select username, password, enabled from users where username = ?"
            authorities-by-username-query="select username, authority from authorities where username = ?"
            group-authorities-by-username-query="select g.id, g.group_name, 
            ga.authority from groups g, group_members gm, group_authorities ga where
             gm.username = ? and g.id = ga.group_id and g.id = gm.group_id"
        />
        </authentication-provider>
    </authentication-manager>
   
    
    
    
</beans:beans>





   

 

http://pentode.tistory.com/146 페이지 

내용 ->

 

앞서 "스프링프레임웍 - Spring Security(2) : 커스텀 로그인 화면 및 권한에 따른 접근 제어"에서 로그인 화면을 원하는 형태로 만드는 방법에 대해서 알아 보았습니다.

 

지금까지의 기본설정과 화면 커스터마이징에서는 사용자 정보와 권한 정보가 모두 security-context.xml 파일 안에 있었습니다. 이번에는 이 정보들을 DB 저장하고 이용할 수 있도록 해 보겠습니다. 데이터베이스는 Oracle을 사용합니다.

 

먼저 사용자 정보와 권한정보를 저장할 테이블 구조 입니다. 이 테이블들은 스프링 시큐리티의 DB 지원 기본 구조를 키구조만 조금 바꾼것 입니다.


 

 

1. 사용자/권한 테이블

 

사용자 권한 처리를 위해서 총 5개의 테이블로 구성됩니다. 테이블을 생성하고 기본 데이터를 입력하는 쿼리파일은 글 하단 전체소스내의 doc폴더에 sample.sql 파일에 있습니다. 각각의 테이블에 대해 알아 보도록 하겠습니다.

 

i. users - 회원 정보 테이블

 

username이 아이디입니다. 여기에서는 비밀번호가 암호화 되지 않았습니다.

enabled는 boolean 값으로 계정 사용여부를 나타냅니다.

 

 

ii. authorities - 회원, 권한 관계 테이블

 

- 회원(username)이 가지는 권한정보가 있는 테이블 입니다.

authority는 "ROLE_USER", "ROLE_ADMIN" 이 될 것입니다.

 

 

iii. gropus - 그룹 테이블 입니다.

 

- 그룹에 권한을 할당하고, 사용자에게 그룹을 할당하는 방식으로 권한을 부여할 수 있습니다.

 

 

iv. group_authorities - 그룹이 가지는 권한 정보 테이블

 

groupu_id는 groups테이블의 id와 관계되는 외래키 입니다.

authority는 "ROLE_USER", "ROLE_ADMIN" 이 될 것입니다.

 

 

v. group_member - 그룹과 회원의 관계 테이블

 

group_id 는 groups 테이블의 id 입니다.

username 은 회원 아이디 입니다.

 

 

2. 권한 부여방식

 

위의 테이블들을 가지고 회원에게 권한을 부여하는 방법은 두 가지가 있습니다.

 

첫 번째는 users와 authorities 테이블을 사용하여 권한을 부여 하는 것입니다. users테이블의 정보로 로그인하고, authorities 의 권한정보를 접근 제어를 합니다.

 

두 번째 방법은 users 와 (group_members, groups, group_authorities) 로 처리하는 방법입니다. 역시 users 정보로 로그인하고, 권한은 회원이 속한 그룹에 할당된 권한으로  접근제어를 하는 것입니다.

 

스프링 시큐리티는 먼저 authorities 테이블에서 권한을 확인하고, 다음에 group_members 테이블에서 권한을 확인합니다. 그러므로 두 가지 방법중 하나만 사용하면 됩니다. 즉,  authorities 테이블에 정보가 없어도 group_members 테이블에 정보가 있으면 권한이 부여됩니다. 그 반대로 마찬가지 입니다.

 

 

3. security-context.xml 설정파일을 보겠습니다.

 

회원 정보와 권한 정보를 데이터베이스에서 가져오도록 설정하는 부분입니다.


 

    <!--  provider  -->
    <authentication-manager>
        <authentication-provider>
        <jdbc-user-service 
            data-source-ref="dataSource"
            role-prefix=""
            users-by-username-query="select username, password, enabled from users where username = ?"
            authorities-by-username-query="select username, authority from authorities where username = ?"
            group-authorities-by-username-query="select g.id, g.group_name, 
            ga.authority from groups g, group_members gm, group_authorities ga where
             gm.username = ? and g.id = ga.group_id and g.id = gm.group_id"
        />
        </authentication-provider>
    </authentication-manager>
   
	

 

 

인증 정보를 가져오는 기능을 하는 인터페이스가 org.springframework.security.core.userdetails.UserDetailsService 입니다. 데이터베이스에서 정보를 가져오도록 이 인터페이스를 구현해둔 클래스가 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl 입니다. 위 코드의 id가 "userDetailsService"인 빈이 이것 입니다. 이 빈에 대한 정의는 넣지 않아도 됩니다.

 

이 빈은 <jdbc-user-service> 요소로 사용되어 집니다. 각 속성들을 알아보겠습니다.

 

data-source-ref="dataSource" : 데이터베이스 연결을 나타내는 dataSource 입니다. root-contxt.xml 파일에 정의 되어 있습니다.

role-prefix : "ROLE_" 와 같은 롤 앞에 붙는 prefix 를 지정합니다. 권한 체크시 여기에 지정된 값을 붙여서 확인합니다. 데이터베이스에 ROLE_USER 형식으로 데이터를 넣어 둿으므로 여기서는 값을 주지 않았습니다.

users-by-sername-query="" : 아이디로 사용자 정보를 가져오는 쿼리 입니다. users 테이블에서 정보를 가져옵니다.

authorities-by-username-query="" : authorities 테이블로부터 권한정보를 가져옵니다.

group-authorities-by-username-query="" : 그룹/회원 관계로부터 권한정보를 가져옵니다.

 

 

이것으로 해서 회원/권한 정보를 데이터베이스로부터 가져오는 처리를 해보았습니다. 하지만 아직 한가지가 처리되지 않을 것이 있습니 다. URL별로 권한을 지정하는 부분이 아직도 xml 파일에 남아 있습니다. 다음 코드 입니다.

 

<intercept-url pattern="/login/loginForm.do" access="permitAll" /> 
<intercept-url pattern="/home.do" access="permitAll" /> 
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')" />
 <intercept-url pattern="/**" access="hasAnyRole('USER, ADMIN')" />

 

 

 

 

* 간단한게 Spring Security - username 정보 가져오는 방법은 다음과 같다.


아래와 같이 하면 Spring Security를 사용할 때 User와 관련된 정보(username, password, authorities)를 얻어올 수 있다.

User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println("username = " + user.getName());
 

home.jsp

<%@page import="net.macaronics.web.domain.UserVO"%>
<%@page import="org.springframework.security.core.userdetails.User"%>
<%@page import="org.springframework.security.core.context.SecurityContextHolder"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page session="false"%>

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

<html>
<head>
<title>Home</title>
</head>
<body>
	<h1>Home!</h1>
	<sec:authorize access="isAnonymous()">
		<p>
			<a href="<c:url value="/login/loginForm.do" />">로그인</a>
		</p>
	</sec:authorize>
	<sec:authorize access="isAuthenticated()">
		<form:form action="${pageContext.request.contextPath}/logout"
			method="POST">
			<input type="submit" value="로그아웃" />
		</form:form>
	</sec:authorize>
	<h3>
		[<a href="<c:url value="/user/introduction.do" />">소개 페이지</a>] [<a
			href="<c:url value="/admin/adminHome.do" />">관리자 홈</a>]
	</h3>
	
		
	<sec:authorize access="isAuthenticated()">
<%


User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if(user!=null){
	System.out.println("username = " + user.toString());	
	out.write(user.getUsername());
}



%>

</sec:authorize>



</body>
</html>

 

콘솔에 다음과 같이 출력 된다.


*******  Macaroncis AOP Annotation.beforeTargetMethod executed. => 
INFO : config.aop.LogAdvice -  MVC 영역 - Controller      :  ,  Type - net.macaronics.web.controller.HomeController 
INFO : config.aop.LogAdvice - 클래스 명 - HomeController, 메서드 명 - home 
INFO : config.aop.LogAdvice - 파라미터 값 : [ko_KR, {}, null]
INFO : net.macaronics.web.controller.HomeController - Welcome home! The client locale is ko_KR.
username = org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER

 

 

 

about author

PHRASE

Level 60  라이트

No gains without pains. (노력 없이는 이득도 없다.)

댓글 ( 4)

댓글 남기기

작성