본문 바로가기

Spring 4/CustomAuthenticationProvider

Spring Security Custom AuthenticationProvider example

Spring Security 에서 커스텀 AuthenticationProvider를 사용하는 예


스프링에서 Spring Security 를 이용하면 사용자를 인증하고 권한에 따라서 특정 경로에 접근하는 것을 제한하는 기능을 설정으로 쉽게 적용할 수 있다. 그러나 설정만으로 모든 것을 처리할 수 있는 것은 아니므로 필요하다면 AuthenticationProvider 인터페이스를 구현하여 authenticate()메소드를 오버라이드하면 인증하는 부분의 로직을 개발자가 모두 작성할 수 있다.


이용자가 로그인 폼에서 데이터를 Spring Security 으로 전송하면 AuthenticationProvider의 authenticate()가 호출되어 인증절차를 진행하는데, 이 메소드를 개발자가 오버라이드하면 된다.


authenticate() 메소드의 원형

Authentication authenticate(Authentication authentication) throws AuthenticationException


authenticate() 메소드 오버라이드 규칙

- 파라미터로 전달된 authentication 객체에 대해서 인증처리를 지원하지 않는다면 null 을 리턴한다

- 인증에 성공하면 이용자의 상세정보를 Authentication 객체에 저장하여 리턴한다

- 인증에 실패하면 AuthenticationException 을 던진다

- BadCredentialsException, UsernameNotFoundException은 AuthenticationException의 하위 클래스이다


서블릿 설정파일에 다음과 같이 설정하면 콘트롤러 클래스를 작성하지 않고도 이용자의 요청을 바로 뷰에 연결할 수 있다

servlet-context.xml

	<view-controller path="/index" view-name="security/index" />
	<view-controller path="/sec/loginForm" view-name="security/loginForm" />
	<view-controller path="/sec/joinForm" view-name="security/joinForm" />
	<view-controller path="/admin/usermanager/main" view-name="security/usermanagerMain" />
	<view-controller path="/member/main" view-name="security/memberMain" />


Spring Security 관련 설정은 루트 컨텍스트 설정파일을 사용해야 한다. 별도의 파일을 생성하여 web.xml 에 루트 컨텍스트 설정파일을 하나더 추가할 수 있지만 여기서는 별도의 설정파일을 생성하지 않고 이미 작성한 root-context.xml 파일을 사용하기로 한다.

아래의 코드처럼 dataSource, component-scan 등도 여기에 선언해주어야 오류를 해결할 수 있다

root-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"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/security 
    		http://www.springframework.org/schema/security/spring-security-4.1.xsd
        	http://www.springframework.org/schema/jee 
        	http://www.springframework.org/schema/jee/spring-jee.xsd
        	http://www.springframework.org/schema/context 
        	http://www.springframework.org/schema/context/spring-context.xsd
        	http://www.springframework.org/schema/beans 
        	http://www.springframework.org/schema/beans/spring-beans.xsd">
        	
    <context:component-scan base-package="org.kdea.security" />
    <context:component-scan base-package="org.kdea.bcrypt" />
    
<!--     
	<beans:bean id="dataSource" class="oracle.jdbc.pool.OracleDataSource"> 
	    <beans:property name="dataSourceName" value="ds"/>
	    <beans:property name="URL" value="jdbc:oracle:thin:@localhost:1521:xe"/>
	    <beans:property name="user" value="scott"/>
	    <beans:property name="password" value="TIGER"/>
    </beans:bean>
  -->   
<!--   
    <beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <beans:property name="driverClassName"  value="oracle.jdbc.OracleDriver" />
        <beans:property name="url"  value="jdbc:oracle:thin:@localhost:1521:xe"  />
        <beans:property name="username"  value="scott"  />
        <beans:property name="password"  value="TIGER"/>
	</beans:bean>
 -->
     <beans:bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource">
        <beans:property name="driverClassName"  value="oracle.jdbc.OracleDriver" />
        <beans:property name="url"  value="jdbc:oracle:thin:@localhost:1521:xe"  />
        <beans:property name="username"  value="scott"  />
        <beans:property name="password"  value="TIGER"/>
	</beans:bean>
         
    <http auto-config='true'  use-expressions="true">
        <intercept-url pattern="/admin/usermanager/**"  access="hasAuthority('USER_ADMIN')" />
        <intercept-url pattern="/manager/**"  access="hasRole('USER_MANAGER')" />
        <intercept-url pattern="/member/**"  access="isAuthenticated()" />
        <intercept-url pattern="/**"  access="permitAll" />
        <form-login
			<!-- /sec/loginForm 으로 요청하면 로그인 폼이 나오도록 개발자가 준비해야 함 -->
        	login-page="/sec/loginForm" 
        		<!-- 임의로 지정하고 로그인 폼의 action 속성과 일치해야 함 -->
        	login-processing-url="/sec/login" 
        		<!-- 이용자 아이디에 해당하는 파라미터 이름 -->
        	username-parameter="id"				
        		<!-- 이용자 비밀번호에 해당하는 파라미터 이름 -->
        	password-parameter="pwd"			
        		<!-- 로그인 성공 후 이동 요청경로 -->
        	default-target-url="/index"			    
        		<!-- 로그인에 실패하면 폼이 다시 나오도록 함 -->
        	authentication-failure-url="/sec/loginForm?error=true"
        	/>
        <logout logout-success-url="/index" />  <!--로그아웃 성공시 스프링이 요청할 URL-->
        <csrf disabled="true"/>
    </http>
    
    <beans:bean id="customAuthenticationProvider"
 		class="org.kdea.security.CustomAuthenticationProvider"  />
 							
    <authentication-manager>
        <authentication-provider ref="customAuthenticationProvider"  />
    </authentication-manager>
    
    <beans:bean id="passwordEncoder"  
    		class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"  />
</beans:beans>


WEB-INF/views/security/index.jsp

<%@page import="java.nio.charset.Charset"%>
<%@ page contentType="text/html; charset=utf-8" pageEncoding="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>
<meta charset="utf-8">
<title>INDEX</title>
</head>
<body>

<h3>INDEX !</h3>

<ul>
<sec:authorize access="hasAuthority('USER_MANAGER')">
	<li><a href="<c:url value='/admin/usermanager/main' />">관리자</a></li>
</sec:authorize>

<sec:authorize access="hasAuthority('USER')">
	<li><a href="<c:url value='/member/main' />">회원 메인</a></li>
</sec:authorize>

<sec:authorize access="! isAuthenticated()">
	<li><a href="<c:url value='/sec/loginForm' />">로그인</a></li>
	<li><a href="<c:url value='/sec/joinForm' />">회원가입</a></li>
</sec:authorize>

<sec:authorize access="isAuthenticated()">
	<li><a href="<c:url value='/logout' />">로그아웃</a></li>
</sec:authorize>
</ul>

</body>
</html>


WEB-INF/views/security/loginForm.jsp

<%@ page contentType="text/html; charset=utf-8"  pageEncoding="utf-8"%>
<%@ taglib prefix="c"  uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>로그인 폼</title>
<style type="text/css">
body { text-align: center; }
#container { margin: 0px auto; width:400px; display:inline-block; padding:10px; }
form{ border:1px solid black; padding:10px;  }
</style>
</head>
<body>
<c:if test="${param.error==true }">
	로그인 실패
</c:if>
<div id="container">
<h3>로그인해주세요</h3>
<form action="<c:url value='/sec/login' />" method="post">
<input type="hidden" name="_csrf" value="${_csrf.token }">
ID <input type="text" name="id"  value="user01"><br>
PWD <input type="password" name="pwd"  value="pwd01"><br>
<button type="submit" >로그인</button>
</form>
</div>
</body>
</html>


WEB-INF/views/security/joinForm.jsp

<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib prefix="c"  uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>회원가입 폼</title>
<script type="text/javascript"  src="http://code.jquery.com/jquery-2.2.2.min.js"></script>
<script type="text/javascript">
function ajaxReq() {
	$.ajax({
		url:'<c:url value="/user/join" />',
		type:'post',
		data: $('#joinForm').serialize(),
		dataType:'json',
		success: function(added){
			if(added.ok){
				alert('회원가입 성공:'+added.pwd);
			}
		},
		error:function(hxr, status, msg){
			alert(msg);
		}
	});
}
</script>
</head>
<body>
<p>
<h3>회원가입 폼 작성</h3>
<div>
<form id="joinForm"  >
ID <input type="text" name="id" value="user"  ><br>
PWD <input type="password"  name="pwd" value="user"  ><br>
<button type="button"  onclick="ajaxReq();">저 장</button>
</form>
</div>
</body>
</html>


CustomAuthenticationProvider.java (로그인 폼이 전송되고 스프링에서 폼 데이터를 받으면 아래의 authenticate()가 호출된다)

authenticate()는 Authentication 객체를 리턴하는데 Authentication객체는 이용자의 아이디, 비밀번호, 권한 정보를 저장하고 있고 스프링은 이 객체를 이용자가 접속하는 경로에 대한 권한을 확인할 때 사용한다

package org.kdea.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
public class CustomAuthenticationProvider implements AuthenticationProvider 
{
	@Autowired
	private CustomUserDetailsService userService;
	
	@Autowired
	private BCryptPasswordEncoder passwordEncoder;
  
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException 
    {
        String user_id = (String)authentication.getPrincipal();    
        String user_pw = (String)authentication.getCredentials();
         
        System.out.printf("사용자 로그인정보: %s \n", user_id + "/" + user_pw);
        
        User user = (User) userService.loadUserByUsername(user_id);
        System.out.printf("사용자 DB 정보: %s \n", user.getUsername() + "/" + user.getPassword());

        // 화면에서 입력한 이용자의 비밀번호(평문)와 DB에서 가져온 이용자의 암호화된 비밀번호를 비교한다
        if(! passwordEncoder.matches(user_pw, user.getPassword())){
        	throw new BadCredentialsException("Bad credentials");
        }
        
        return new UsernamePasswordAuthenticationToken(user_id, user_pw, user.getAuthorities());
    }
    
    @Override
    public boolean supports(Class<?> authentication) 
    {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}



CustomUserDetailsService.java ( 이용자의 이름(ID)을 이용하여 데이터베이스에서 해당 이용자의 인증정보를 가져온다 )

AuthenticationProvider를 개발자가 직접 구현하는 지금과 같은 경우에는 반드시 UserDetailsService를 구현할 필요가 없고 일반 서비스 클래스와 DAO클래스를 사용해도 되지만 스프링에는 이미 이러한 경우에 사용할 수 있도록 UserDetailsService 인터페이스가 준비되어 있기 때문에 이를 구현하여 서비스 클래스를 작성하는 것이 좋겠다


loadUserByUsername() 를 오버라이드할 때의 규칙

DB에서 가져온 사용자 정보가 있으면 UserDetails 형의 객체로 리턴해야 한다

DB에서 가져온 사용자 정보가 있으나 아무런 권한도 없으면 UsernameNotFoundException을 던져야 한다

DB에서 가져온 사용자 정보가 없을 경우에는 UsernameNotFoundException을 던져야 한다 



package org.kdea.security;

import org.kdea.bcrypt.UserAuthDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService 
{
	@Autowired
	private UserAuthDAO userAuthDAO;
	
	@Override
	public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException 
	{
		User user = userAuthDAO.getUserDetails(id);
		if(user==null) throw new UsernameNotFoundException("["+id+"] 이름으로 검색된 결과가 없습니다");
		if(user.getAuthorities().size()==0) 
			throw new UsernameNotFoundException("["+id+"] 이용자는 아무런 권한이 없습니다");
		return user;
	}

}


UserAuthDAO.java

package org.kdea.bcrypt;

import java.sql.*;
import java.util.*;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Repository;

@Repository
public class UserAuthDAO 
{
	@Autowired
	DataSource dataSource;

	public boolean add(UserAuthVO user) 
	{
		Connection conn = null;
		PreparedStatement pstmt = null;
		try {
			conn = dataSource.getConnection();
			String sql = "INSERT INTO member VALUES(?,?, '1', 'USER')";
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, user.getId());
			pstmt.setString(2, user.getPwd());
			
			int rows = pstmt.executeUpdate();
			return rows>0 ? true : false;
		} catch (SQLException e) {
			e.printStackTrace();
		} finally{
				try {
					if(conn!=null) conn.close();
					if(pstmt!=null) pstmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
		}
		
		return false;
	}

	public UserAuthVO getUser(UserAuthVO user) 
	{
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			conn = dataSource.getConnection();
			String sql = "SELECT * FROM member WHERE id=?";
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, user.getId());
			
			rs = pstmt.executeQuery();
			if(rs.next()){
				UserAuthVO u = new UserAuthVO();
				u.setId(rs.getString("ID"));
				u.setPwd(rs.getString("PWD"));
				u.setEnabled(rs.getString("ENABLED").charAt(0));
				u.setAuthority(rs.getString("AUTHORITY"));
				return u;
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally{
				try {
					if(conn!=null) conn.close();
					if(pstmt!=null) pstmt.close();
					if(rs!=null) rs.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
		}
		return null;
	}

	public User getUserDetails(String userId) 
	{
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			conn = dataSource.getConnection();
			String sql = "SELECT * FROM member WHERE id=?";
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, userId);
			
			rs = pstmt.executeQuery();
			if(rs.next()){
				String id = rs.getString("ID");
				String pwd = rs.getString("PWD");
				String authority = rs.getString("AUTHORITY");
				SimpleGrantedAuthority role = new SimpleGrantedAuthority(authority);
				List<GrantedAuthority> roles = new ArrayList<>();
				roles.add(role);
				
				return new User(id, pwd, roles);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally{
				try {
					if(conn!=null) conn.close();
					if(pstmt!=null) pstmt.close();
					if(rs!=null) rs.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
		}
		return null;
	}
}