> Hello World !!!

     

@syaku

스프링 시큐리티 로그인 #1 : Spring Framework Security Login 스프링프레임워크 #1

written by Seok Kyun. Choi. 최석균

스프링 시큐리티 로그인

스프링 시큐리티 연재 포스팅
2014/08/29 - [개발노트/Spring] - 스프링 시큐리티 로그인 #1 : Spring Framework Security Login 스프링프레임워크 #1
2014/09/04 - [개발노트/Spring] - 스프링 시큐리티 로그인 커스텀 #2 : Spring Framework Security Login Custom 스프링프레임워크 #2
2014/09/04 - [개발노트/Spring] - 스프링 시큐리티 로그인 Handler Ajax #3 : Spring Framework Security Login Ajax 스프링프레임워크 #3
2014/10/25 - [개발노트/Spring] - 스프링 시큐리티 커스텀 로그인 : Spring Security Custom Login UserDetailsService AuthenticationProvider #4 스프링프레임워크/Spring Framework

개발환경

Mac OS X 10.9.4
JAVA 1.6
Apache Tomcat 7.x
Spring 3.1.1
Spring Tool Suite 3.5.1
Maven 2.5.1

스프링 시큐리티(Spring Security)는 스프링 서브 프로젝트 중 하나로 스프링 기반의 어플리케이션을 보호하기 위한 필수적인 프레임워크이다. 스프링을 사용하면서 자체적으로 세션을 이용한 인증방식을 구현한다면 바보같은 짓일 것이다. 스프링 시큐리티는 보안을 체계적으로 관리하며 개발한 스프링 어플리케이션들과 유연하게 연결된다. 그리고 오랜기간 다양한 피드백으로 개발되어 신뢰도가 높을 것이다.
스프링에 최적화된 스프링 시큐리티 보다 안정적인 프레임워크는 아마 없을 것이다. 무엇보다 스프링에서는 스프링 시큐리티를 표준으로 정의하고 있다.

공식 사이트 : http://projects.spring.io/spring-security

스프링 시큐리티의 기본 예제를 먼저 알아보도록하겠다. 실무에서 사용할 수 없겠지만 스프링 시큐리트가 어떻게 구현되는 지 알 수 있는 예제이다.
프로젝트를 생성하고 스프링 시큐리티 라이브러리를 설치한다. Maven 설정에 2개의 dependency 를 추가한다.

@소스 pom.xml

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

<!— CGLib —>
<dependency>
     <groupId>cglib</groupId>
     <artifactId>cglib</artifactId>
     <version>3.1</version>
     <type>jar</type>
     <scope>compile</scope>
</dependency>

스프링 시큐리티를 사용하기 위해 아래와 같이 환경설정을 추가한다.

@소스 web.xml

<context-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>
     /WEB-INF/spring/root-context.xml,
     classpath*:com/syaku/config/security-context.xml
     </param-value>
</context-param>

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

<filter-name>는 springSecurityFilterChain 이름으로 해야한다. 스프링에서 의존하는 필터이기 때문이다.

다음은 스프링에 스프링 시큐리티 설정정보를 추가한다. /src/main/resources/com/syaku/config/security-context.xml 파일을 생성한다.

@소스 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">
          <intercept-url pattern="/**" access="ROLE_USER" />
     </http>

     <authentication-manager>
          <authentication-provider>
               <user-service>
                    <user name="guest" password="guest" authorities="ROLE_USER"/>
               </user-service>
          </authentication-provider>
     </authentication-manager>

</beans:beans>

테스트를 위해 웹페이지에서 아래와 같이 접속을 하면 로그인 화면이 출력된다.

http://localhost:8080/security

security-context.xml 에서 설정한 계정과 암호를 입력하여 로그인한다. authentication-manager 태그를 확인하면 된다.

http 태그는 접근 권한설정하는 부분이고, authentication-manager 태그는 접근 권한을 부여하는 부분이다.
그래서 guest 계정으로 로그인 하면 ROLE_USER 라는 권한을 부여받게 된다.

스프링 시큐리티에는 기본적인 로그인 부분을 제공하고 있기때문에 기본 설정만으로 로그인 로직을 구현할 수 있다. 하지만 실무에 사용하긴 힘들고 스프링 서큐리티가 어떻게 작동하는 지 알 수 있는 참고용으로 사용할 수 있다.

만약 다양한 접근 권한을 구현하기를 원한다면 인터셉터를 더많이 추가하면 된다. 현재는 모든 애플리케이션에 ROLE_USER 권한이 있는 사용자만 접근할 수 있게 설정된 것이다.

관리자 계정을 추가하고 관리자 계정만 접근할 수 있는 애플리케이션을 만들어 테스트해보기로 한다.

@소스 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">
          <intercept-url pattern="/" access="ROLE_USER" />
          <intercept-url pattern="/admin" access="ROLE_ADMIN" />
     </http>

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

</beans:beans>

여기서 또 중요한 것이 access 의 첫문자 패턴이 ROLE_ 시작해야한다. 변경이 가능하나, 구지 그럴필요가 없기때문에 모든 권한은 ROLE_ 시작할 수 있게 구성하도록한다.

권한을 추가하였다면 admin 컨트롤러도 추가한다. HomeController.java 파일을 열어 아래의 메서드를 추가한다.

@소스 HomeController.java

@RequestMapping(value = "/admin", method = RequestMethod.GET)
public String admin() {
     return "admin";
}

그리고 컨트롤러의 뷰페이지 파일을 추가한다. src/main/webapp/WEB-INF/views/admin.jsp 소스 내용은 알아서 추가한다.
http://localhost:8080/admin 으로 접근하면 권한이 없다는 오류메세지가 출력된다.

근데 문제는 admin/ 접근하면 권한 체크를 하지 않고 접속이 가능해진다. 이럴때 패턴을 /admin/** 주면된다. 하지만 하위 폴더에는 다른 권한을 줘야할 경우가 생길수가 있다. 이럴때는 최상 권한을 제일 하위에 등록하고 그 위에 추가할 경로의 권한을 추가하면 된다.


<intercept-url pattern="/" access="ROLE_USER" />
<intercept-url pattern="/admin/test" access="ROLE_USER" />
<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />

이처럼 접근 권한은 위에서 아래로 순차적으로 인터셉터가 처리하게 된다. 그리고 intercept-url 의 pattern 은 말 그대로 경로의 패턴 값을 입력하는 것이다. 경로 값으로 오해하지 않도록한다.

Spring EL 표현식

http 설정에서 use-expressions=true 한 경우 SpEL(Spring EL expressions) 를 사용할 수 있다. 기본 값은 false 이다.

스프링 표현 언어(SpEL) 에 대한 자세한 설명은 아래와 같다.

Expression Description
hasRole([role]) Returns true if the current principal has the specified role.
hasAnyRole([role1,role2]) Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings)
principal Allows direct access to the principal object representing the current user
authentication Allows direct access to the current Authentication object obtained from the SecurityContext
permitAll Always evaluates to truedenyAllAlways evaluates to false
isAnonymous() Returns true if the current principal is an anonymous user
isRememberMe() Returns true if the current principal is a remember-me user
isAuthenticated() Returns true if the user is not anonymous
isFullyAuthenticated() Returns true if the user is not an anonymous or a remember-me user

출처 : 스프링 프레임워크 시큐리티 도움말 3.1.x

SpEL 방식이 아닌 경우 아래와 같은 설정할 수 있다.

AUTHORITY DESCRIPTION
IS_AUTHENTICATED_ANONYMOUSLY 익명 사용자
IS_AUTHENTICATED_REMEMBERED REMEMBERED 사용자
IS_AUTHENTICATED_FULLY 인증된 사용자

출처 : 전자정부프레임워크 도움말

스프링 서큐리티 어노테이션

좀 더 유연하게 접근 권한을 설정하기 위해 어노테이션을 사용할 수 있다. 한 곳에서 권한을 설정한다면 어플리케이션이 추가될때마다 수정되어야 하는 문제가 발생한다. 여러 어플리케이션을 개발하는 프로젝트라면 각각의 어플리케이션에 맞게 개별적으로 설정하는 것이 효과적이다.
[참고] 스프링 서큐리티 어노테이션을 사용하기 위해 CGLib 가 필요하다.

기존에 생성했던 security-context.xml 에 http 태그를 <http auto-config="true" /> 수정한다.
security-context.xml 파일은 공통적인 보안에 대한 설정을 담당하는 파일이라고 생각하면 될 것 같다.
그리고 servlet-context.xml 파일을 열어 아래와 같이 수정한다.

@소스 servlet-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:beans="http://www.springframework.org/schema/beans"
     xmlns:context="http://www.springframework.org/schema/context"
     xmlns:security="http://www.springframework.org/schema/security"
     xsi:schemaLocation="
     http://www.springframework.org/schema/mvc
     http://www.springframework.org/schema/mvc/spring-mvc.xsd
     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
     ">

     <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->

     <!-- Enables the Spring MVC @Controller programming model -->
     <annotation-driven />

     <security:global-method-security secured-annotations="enabled" />

     <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
     <resources mapping="/resources/**" location="/resources/" />

     <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
     <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
          <beans:property name="prefix" value="/WEB-INF/views/" />
          <beans:property name="suffix" value=".jsp" />
     </beans:bean>

     <context:component-scan base-package="com.syaku.security" />

</beans:beans>

기본 소스에서 security 스키마와 <security:global-method-security secured-annotations="enabled" /> 설정이 추가되었고, @Secured 어노테이션을 사용할 수 있다. 본 어노테이션에는 SpEL 표현식은 사용할 수 없다. 기본적인 룰과 생성한 룰만을 사용할 수 있고, 여러가지 룰을 적용하려면 배열을 사용하면 된다. @Secured({"ROLE_ADMIN","ROLE_USER"})

컨트롤러 파일을 열어 접근 권한 어노테이션을 추가하면 된다.

@소스 HomeController.java

package com.syaku.security;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

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

/**
 * Handles requests for the application home page.
 */
@Controller
public class HomeController {

     private static final Logger logger = LoggerFactory.getLogger(HomeController.class);

     /**
      * Simply selects the home view to render by returning its name.
      */
     @Secured("ROLE_USER")
     @RequestMapping(value = "/", method = RequestMethod.GET)
     public String home(Locale locale, Model model) {
          logger.info("Welcome home! The client locale is {}.", locale);

          Date date = new Date();
          DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

          String formattedDate = dateFormat.format(date);

          model.addAttribute("serverTime", formattedDate );

          return "home";
     }

     @Secured("ROLE_ADMIN")
     @RequestMapping(value = "/admin", method = RequestMethod.GET)
     public String admin() {
          return "admin";
     }

     @Secured("ROLE_USER")
     @RequestMapping(value = "/admin/test", method = RequestMethod.GET)
     public String admin_test() {
          return "admin";
     }

}

기존 소스에서 @Secured 가 각 메서드마다 추가되었다. 접근 권한에 대해서는 이전에 설명했던것 처럼 룰을 적용하면 된다.

만약 로그인도 했고 해당 메서드에 권한도 있는 데… 접근할 수 없는 곳이 있다면?
쉽게말해… 게시판을 예로들어 글에 대한 제어 권한은 작성한 사용자만이 글을 수정 및 삭제할 수 있게 해야한다면 @Secured 만으로 구현할 수 없다. @Secured 는 SpEL 표현식을 지원하지 않기 때문이다.

그래서 스프링 시큐리티에서 지원하는 새로운 어노테이션을 사용해야 한다.
어노테이션을 사용하기 위해 다음과 같이 설정해야 한다.

<security:global-method-security pre-post-annotations="enabled" />

@Pre* : 메서드 인자값에 접근할 수 있다.
@Post* : 메서드 반환값에 접근할 수 있다.

어노테이션 설명
@PreFilter 메서드 인자 값을 필터한다.
@PreAuthorize 메서드 인자 값을 검증한다.
@PostFilter 메서드 반환 값을 필터한다.
@PostAuthorize 메서드 반환 값을 검증한다.

@PerAuthorize

간단한 예제로 계정을 비교하는 프로그램을 아래와 같이 만들어 본다.

Service 와 VO 두개의 클래스 파일을 생성한다.

@소스 UserVO.java

package com.syaku.security;

public class UserVO {
     private String user_name;
     private String password;

     public void setUser_name(String user_name) {
          this.user_name = user_name;
     }

     public String getUser_name() {
          return this.user_name;
     }

     public void setPassword(String password) {
          this.password = password;
     }

     public String getPassword() {
          return this.password;
     }
}

@소스 HomeService.java

package com.syaku.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class HomeService {
     private static final Logger logger = LoggerFactory.getLogger(HomeService.class);

     @PreAuthorize("#userVO.user_name == authentication.name or hasRole(‘ROLE_ADMIN')")
     public void getUser(UserVO userVO) {
          // 테스트를 위한 로그 출력
          logger.info("getUser success");
     }
}

만약 디비로 구성한다면 @Service 계층에 CURD 메서드들을 구성하면 된다. 이제 컨트롤러에서 서비스 메서드를 호출하면 된다.
테스트를 위해 동적인 계정을 얻기 위해 파라메터 값을 사용해 비교하였다.

@소스 HomeController.java


@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Locale locale, Model model,@RequestParam(value="user", defaultValue="", required=true) String user) {
     logger.info("Welcome home! The client locale is {}.", locale);

     // 파라메터 값을 얻어 삽입
     UserVO userVO = new UserVO();
     userVO.setUser_name(user);

     // 서비스 호출
     homeService.getUser(userVO);

     Date date = new Date();
     DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

     String formattedDate = dateFormat.format(date);

     model.addAttribute("serverTime", formattedDate );

     return "home";
}

http://localhost:8080/security/ 를 접속하여 테스트한다. 로그인은 guest 로 한다.
위 경로로 접속하면 익셉션이 발생한다. 비교대상인 계정의 파라메터가 없기 때문이다. 하지만 admin 계정으로 접속하면 정상적인 페이지가 출력된다.

http://localhost:8080/security/?user=guest 접속하면 @Service 계층의 테스트 로그가 출력되고 정상적인 화면이 출력된다. 로그인한 계정과 파라메터로 넘긴 계정이 일치하기 때문이다.

로그인 정보 얻기

3가지 방법이 있고, 조금식 다른 방식과 결과를 출력한다. 그에 맞게 사용하면 될 것 같다.

@소스 HomeController.java

@Secured({"ROLE_USER","ROLE_ADMIN"})
@RequestMapping(value = "/user", method = RequestMethod.GET)
public String user(Principal principal) {

     // 첫번째 방법
     Authentication auth = SecurityContextHolder.getContext().getAuthentication();
     logger.info(auth.toString());

     // 두번째 방법
     User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
     logger.info(user.toString());

     // 세번째 방법
     logger.info(principal.toString());

     return "home";
}

첫번째 결과

INFO : com.syaku.security.HomeController - org.springframework.security.authentication.UsernamePasswordAuthenticationToken@be01b6d0: Principal: org.springframework.security.core.userdetails.User@5e22dd8: Username: guest; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffe9938: RemoteIpAddress: 0:0:0:0:0:0:0:1%0; SessionId: 02543D109730FC954434CFFE9B3E4D5E; Granted Authorities: ROLE_USER

두번째 결과

INFO : com.syaku.security.HomeController - org.springframework.security.core.userdetails.User@5e22dd8: Username: guest; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER

세번째 결과

INFO : com.syaku.security.HomeController - org.springframework.security.authentication.UsernamePasswordAuthenticationToken@be01b6d0: Principal: org.springframework.security.core.userdetails.User@5e22dd8: Username: guest; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffe9938: RemoteIpAddress: 0:0:0:0:0:0:0:1%0; SessionId: 02543D109730FC954434CFFE9B3E4D5E; Granted Authorities: ROLE_USER

첫번째와 세번째 결과는 같다. 하지만 호출방식이 틀리니 참고한다.

참고 : http://www.mkyong.com/spring-security/get-current-logged-in-username-in-spring-security/

posted syaku blog

Syaku Blog by Seok Kyun. Choi. 최석균.

http://syaku.tistory.com