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
댓글 ( 4)
댓글 남기기