Pages

2014년 1월 29일 수요일

[SPRING] Spring Security를 이용한 인증 처리

Spring Framework를 통해서 인증(Authentication)과 허가(Authorization)에 관련된 작업을 한다면 여러 방법이 있을 수 있겠지만. 일반적으로 Spring의 서브 프로젝트인 Spring Security를 사용하게 된다. Spring Security는 필터기반으로 동작하므로 Spring MVC의 구현과 완전히 분리되고 Spring과의 밀접한 연동으로 메서드 보안등의 여러가지 장점이 있다. 또한 Role 기반의 허가를 지원하므로 경로별, 권한별 리소스 제한에 대해서도 많은 기능을 제공한다. Spring Security를 사용할 때 기본적인 Form 인증을 사용하는 경우에 대해서 정리해본다.

1. Spring Framework를 구성한다.

2. Spring Security 사이트에서 배포본을 다운로드 받아 그 안의 jar 파일을 라이브러리에 등록한다.(http://www.springsource.org/spring-security#download) Spring Framework의 버전에 따라서 다운로드 받아야 할 Spring Security의 배포본 버전이 다르므로 주의.

3. 다음의 필터 정의를 web.xml에 추가한다.
   <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>

이 필터 정의를 추가함으로서 해당 사이트의 모든 요청은 Spring Security를 통해서 처리된다.

4. 다음의 설정 파일을 추가한다. 관리상 Spring Framework의 설정 파일과 분리하는 편이 좋겠다. 여기서는 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/beanshttp://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/securityhttp://www.springframework.org/schema/security/spring-security-3.1.xsd">
     <http pattern="/login" security="none"></http>
 <http auto-config="true" access-denied-page="/login?denied=true">
     <intercept-url pattern="/-*" access="ROLE_USER" />
  <form-login
   login-page="/login"
   authentication-success-handler-ref="loginSuccessHandler"
   authentication-failure-handler-ref="loginFailureHandler"
  />
  <logout logout-success-url="/login" />
 </http>

 <beans:bean id="loginSuccessHandler" class="com.preludeb.auth.core.LoginSuccessHandler"></beans:bean>
 <beans:bean id="loginFailureHandler" class="com.preludeb.auth.core.LoginFailureHandler"></beans:bean>

 <beans:bean id="preludebUserService" class="com.preludeb.auth.core.PreludebUserService"></beans:bean>
 <beans:bean id="encoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>

 <authentication-manager>
  <authentication-provider user-service-ref="preludebUserService">
   <password-encoder ref="encoder" />   
  </authentication-provider>
 </authentication-manager>
</beans:beans>

설정 파일의 내용을 설명해 보면

 <http pattern="/login" security="none"></http>

위 코드는 login 요청에 대해서는 보안을 적용하지 않는다는 의미이다. Spring Security를 사용할 때 인증에 관련없이 보여주어야 할 부분(주로 로그인 페이지, 이미지 등의 정적인 리소스등)은 <http pattern="/s-ripts/-*" security="none"></http> 같은 형태로 패턴을 추가해 주면 된다. 이 패턴 설정은 위부터 순서대로 적용되고 패턴매칭이 완료 되면 아래의 패턴은 무시 되므로 보안 설정을 무시하는 경로는 아래에 나오는 보안 설정 이전에 나와야 한다.

 <http auto-config="true" access-denied-page="/login?denied=true">
      <intercept-url pattern="/-*" access="ROLE_USER" />
  <form-login
   login-page="/login"
   authentication-success-handler-ref="loginSuccessHandler"
   authentication-failure-handler-ref="loginFailureHandler"
  />
  <logout logout-success-url="/login" />
 </http>

위 코드는 실질적으로 웹사이트 경로에 대한 권한 설정을 하는 부분이다. auto-config="true"를 통해서 일반적으로 설정되는 많은 설정 부분이 자동으로 설정된다. access-denied-page="/login?denied=true" 부분은 인증을 통과 했지만 해당 요청에 맞는 권한이 없는 경우 보내지는 페이지 경로를 설정한다.

<intercept-url pattern="/-*" access="ROLE_USER" /> 는 웹사이트 모든 경로에 대해서 ROLE_USER 권한이 있어야 접근할 수 있다는 내용이다. 인증된 사용자에 대하여 ROLE_USER 권한이 없다면 access-denied-page에 지정된 페이지로 리다이렉트 된다.

<form-login login-page="/login" authentication-success-handler-ref="loginSuccessHandler" authentication-failure-handler-ref="loginFailureHandler" /> 는 폼 인증을 사용하겠다는 정의이며, 로그인 페이지는 /login. 인증이 성공할때는 loginSuccessHandler 핸들러로 처리. 인증이 실패할때는 loginFailureHandler 핸들러로 처리하겠다는 의미이다.

<logout logout-success-url="/login" /> 는 로그아웃시 리다이렉트 될 페이지를 정의한다.

<beans:bean id="loginSuccessHandler" class="com.preludeb.auth.core.LoginSuccessHandler"></beans:bean>는 로그인 성공시 처리할 핸들러 빈의 정의이다. 해당 구현은 잠시 후 살펴본다.

<beans:bean id="loginFailureHandler" class="com.preludeb.auth.core.LoginFailureHandler"></beans:bean>는 로그인 실패시 처리할 핸들러 빈의 정의이다. 해당 구현은 잠시 후 살펴본다.

<beans:bean id="preludebUserService" class="com.preludeb.auth.core.PreludebUserService"></beans:bean>는 로그인 처리 과정에서 UserDetails 객체를 생성하는 UserDetailsService의 빈의 정의이다. 해당 구현은 잠시 후 살펴본다.

<beans:bean id="encoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>는 Spring Security에서 제공하는 패스워드 인코더의 빈 정의이다. 이 인코더는 random salt를 적용하는 단 방향 해시이며 일반적인 해시와 동일한 형태로 사용하면 된다. 구버전 Spring Security의 경우에는 이 인코더를 지원하지 않으므로 SHA나 MD5 해싱을 이용한다.

 <authentication-manager>
  <authentication-provider user-service-ref="preludebUserService">
   <password-encoder ref="encoder" />   
  </authentication-provider>
 </authentication-manager> 

위 코드는 인증 매니저를 설정한다. 이 부분에서 설정한 구성을 사용해 Spring Security는 인증 객체를 가져오고 인증 과정을 처리한다.

5. UserDetails 인터페이스를 구현하는 계정정보를 담는 클래스를 구현한다. 클래스의 이름은 각자 환경에 맞게 하면 된다.

package com.preludeb.auth.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class PreludebUser implements UserDetails
{
    private String username;
    private String password;
  
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();  
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
      
        return authorities;
    }
  
    public void setPassword(String password)
    {
        this.password = password;
    }
  
    public String getPassword()
    {
        return password;
    }
  
    public void setUsername(String username)
    {
        this.username = username;
    }
  
    public String getUsername()
    {
        return username;
    }
  
    public boolean isAccountNonExpired()
    {
        return true;
    }
  
    public boolean isAccountNonLocked()
    {
        return true;
    }
  
    public boolean isCredentialsNonExpired()
    {
        return true;
    }
  
    public boolean isEnabled()
    {
        return true;
    }
}

UserDetails 인터페이스는 위의 클래스에서 정의된 각각의 get, is 메서드를 호출해 인증 과정에서 필요한 정보를 UserDetailsService에 노출한다. 필요에 따라서 username, password 등의 변수를 생성하고 그 변수에 값을 입력하는 getter 메서드를 추가하면 된다. 여기에서는 username, password만을 입력하도록 구현하였다. 권한처리가 필요한 경우에는 getAuthorities()에 대응하는 List<Authority> 변수를 설정하거나 계정 잠금의 경우에는 isNonLocked() 메서드에 대응하는 boolean 값을 추가하면 된다. UserDetails 인터페이스는 각 설정을 확인하기 위한 getter 메서드만을 필요로 한다.

username과 password의 경우에는 일반적인 형태의 getter, setter 메서드를 사용하였고 권한을 반환하는 getAuthorities() 메서드에서는 인증 후 필요한 기본 권한인 ROLE_USER 권한을 반환하도록 구현 하였다. 그외의 부분은 아무런 입력 없이도 허가가 통과하도록 true를 반환하도록 되어 있다. 계정 잠금, 계정 만료, 계정 사용여부 등으로 계정 사용을 처리하고자 한다면 해당 부분을 사용하면 된다.

6. 인증 객체를 생성하는 UserDetailsService 클래스를 구현한다.
package com.preludeb.auth.core;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.StandardPasswordEncoder;

public class PreludebUserService implements UserDetailsService
{
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        StandardPasswordEncoder encoder = new StandardPasswordEncoder();
      
        PreludebUser entazUser = new PreludebUser();
      
        entazUser.setUsername(username);
        entazUser.setPassword(encoder.encode("abcd"));
      
        return entazUser;
    }
}

Spring Security는 인증 과정이 시작되면 UserDetailsService 클래스의 loadUserByUsername()을 호출하게 된다. 이 메서드의 파라마터는 사용자가 로그인 정보로 입력한 j_username이 전달된다. 일반적인 경우에는 전달받은 username으로 데이터베이스등에 쿼리를 하고 그 결과 전달받은 패스워드, 권한 정보등을 UserDetails를 구현하는 클래스(여기서는 PreludebUser)에 저장하고 객체를 반환하게 된다. 여기에서는 간단한 구현을 위해 username은 전달받은 username, 패스워드는 abcd의 해시값을 입력하였다. 실제 데이터베이스를 사용하는 경우에는 회원 가입시 입력받은 패스워드를 StandardPasswordEncoder.encode()를 거쳐 저장하고 이 메서드에서는 쿼리결과를 바로 setter 메서드로 보내는 형태가 될 것이다. 만약 username으로 쿼리를 날려서 결과가 없다면 UsernameNotFoundException 예외를 throw 하면 된다.

7. 인증 성공시 처리하는 핸들러를 구현한다.

package com.preludeb.auth.core;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

public class LoginSuccessHandler implements AuthenticationSuccessHandler
{
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException
    {
     response.sendRedirect(request.getContextPath() + "/index");
    }
}

성공시 처리하는 핸들러는 AuthenticationSuccessHandler 인터페이스를 구현한다. Spring Security에서 인증 과정을 거친 후 인증이 성공하면 onAuthenticationSuccess() 를 호출한다. 여기에서는 단순히 /index 로 리다이렉트 하지만 일반적인 경우에는 계정에 대한 초기화 작업이나 로그 기록등의 작업을 진행하게 된다.

8. 인증 실패시 처리하는 핸들러를 구현한다.

package com.preludeb.auth.core;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

public class LoginFailureHandler implements AuthenticationFailureHandler
{
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException auth) throws IOException, ServletException
    {
     response.sendRedirect(request.getContextPath() + "/login");
    }
}

실패시 처리하는 핸들러는 AuthenticationFailureHandler 인터페이스를 구현한다. Spring Security에서 인증 과정을 거친 후 인증이 실패하면 onAuthenticationFailure() 를 호출한다. 여기에서는 단순히 /login 로 리다이렉트 하지만 일반적인 경우에는 로그인 실패에 대한 로그 기록등을 하게 된다.

9. 로그인 과정을 처리하기 위한 컨트롤러 코드를 구현한다.

package com.preludeb.auth.controller;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import com.preludeb.auth.core.PreludebUser;

@Controller
public class IndexController
{
    public PreludebUser getUser()
    {
        return (PreludebUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
  
    @RequestMapping(value = "/index", method = RequestMethod.GET)
    public ModelAndView index()
    {
        ModelAndView view = new ModelAndView();
        view.setViewName("index");
        view.addObject("username", getUser().getUsername());
      
        return view;
    }
  
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView login()
    {
        ModelAndView view = new ModelAndView();
        view.setViewName("login");
      
        return view;
    }
}

별다른 내용이 없는 컨트롤러 코드이다. login의 경우에는 로그인 view 페이지를 쓰고 있고, index의 경우에는 인증 객체에서 username을 가져와 view페이지에 전달하는 간단한 구현을 하였다.

10. 로그인을 위한 JSP페이지를 구현한다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<head></head>
<body>
<form action="j_spring_security_check" method="post">
 <table border="0">
  <tr>
   <td>ID:</td>
   <td><input type="text" name="j_username"></td>
  </tr>
  <tr>
   <td>패스워드:</td>
   <td><input type="password" name="j_password"></td>
  </tr>
  <tr>
   <td colspan="2">
    <input type="submit" value="Login">
   </td>
  </tr>
 </table>
</form>
</body>
</html>

form에서 action을 j_spring_security_check로 로그인 내용을 보내면 Spring Security가 인증 과정을 처리하게 된다. action과 form의 name은 예약되어 있는 이름으로 그대로 사용하면 된다.

11. 로그인 후 리다이렉트 되는 JSP페이지를 구현한다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<head></head>
<body>
Hello ${username}!<br>
<a href="<%= request.getContextPath() %>/j_spring_security_logout">[로그아웃]</a>
</body>
</html>

앞에서 구현한 Controller 클래스에서의 내용처럼 이 뷰 페이지에는 로그인 계정명이 전달된다. 아래 로그아웃 부분에서처럼 j_spring_security_logout 페이지를 호출하면 Spring Security에서 로그아웃 처리를 하고 설정 파일에 정의된 로그아웃 후 페이지로 리다이렉트 된다.

사이트 구성이 끝났다면 웹사이트를 시작하고 로그인 페이지에서 ID와 패스워드에 'abcd'를 입력하면 Hello ID!가 출력됨을 확인할 수 있다.

[출처:http://preludeb.egloos.com/4738521]

댓글 없음:

댓글 쓰기