본문 바로가기

서버/Spring
[Spring Security] 로그인, 로그아웃, 계정 정보

// 스프링 시큐리티, Spring Security
    - 기존 Servlet/JSP > 인증(증명, 인증 티켓) + 허가(권한) > 세션 기반 인증
    - 동작 방식 > 서블릿 필터와 스프링 인터셉터를 사용하여 보안을 처리

    - 일괄 설정(pom.xml, ojdbc6, web.xml, root-context.xml, https://log4jdbc.log4j2.properties, log4j(log level))

    - 스프링 시큐리티 설정

        ~ pom.xml : 의존성 4개 추가

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-web</artifactId>
   <version>5.0.7.RELEASE</version>
</dependency>

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-config</artifactId>
   <version>5.0.7.RELEASE</version>
</dependency>

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-core</artifactId>
   <version>5.0.7.RELEASE</version>
</dependency>

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-taglibs</artifactId>
   <version>5.0.7.RELEASE</version>
</dependency>


        ~ /webapp/WEB-INF/spring/ > security-context.xml
            - root-context.xml, servlet-context/xml 처럼 스프링을 설정하는 파일(Context)
            - 기존 컨텍스트와 분리해서 설정할 수 있다. > 단독 설정
            - Spring Bean Configuration File
            - xsi:schemaLocation에서 5.0 버전 지우기

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

</beans>


        ~ web.xml
            - 스프링 시큐리티가 스프링 MVC에서 사용 > 필터를 사용해서 스프링 시큐리티가 스프링 동작에 관여할 수 있도록 설정(필터 등록)
                1. security-context.xml 인식 > web.xml 추가
                2. security-context.xml 기본 구문 설정

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

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

	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>*</url-pattern>
	</filter-mapping>
	
	<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>

</web-app>

// 인증, Authentication & 허가(권한부여, Authorization)


~ AuthenticationManager
    - 인증 매니저
    - 가장 중심 + 가장 중요한 역할
    - 인증을 담당한다.

~ AuthenticationProvider
    - 인증 제공자
    - 실제로 인증 작업을 진행한다.

~ UserDetailsService
    - 사용자 정보 + 사용자 권한 관리


// 테스트를 위한 URI 설계
- 통제가 필요한 URI를 설계 + 스프링 시큐리티를 적용

- /index.do  > 로그인(O), 로그인(X) > 모든 사용자 접근 가능
- /member.do > 로그인(O) > 회원만 접근 가능
- /admin.do  > 로그인(O) > 회원 중 관리자 권한이 있는 사용자만 접근 가능


- 파일 생성
~ "com.test.controller" > Test.Controller.java

package com.test.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class TestController {

	@GetMapping("/index.do")
	public String index() {
		
		log.info("TestController > 모든 사용자 페이지");
		return "index";
	}
	
	@GetMapping("/member.do")
	public String member() {
		
		log.info("TestController > 회원 전용 페이지");
		return "member";
	}
	
	@GetMapping("/admin.do")
	public String admin() {
		
		log.info("TestController > 관리자 전용 페이지");
		return "admin";
	}
	
	@GetMapping("/template.do")
	public String template() {
		
		return "template";
	}
	
}


~ views > index.jsp
             > member.jsp
             > admin.jsp

<%@ page language="java" 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>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>

</style>
</head>
<body>

	<%@ include file="/WEB-INF/views/inc/header.jsp" %>
	
	<h2>
		<span class="material-symbols-outlined">favorite</span>
		Index Page
	</h2>
	
	<div>모든 사용자가 접근 가능합니다.</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>

</script>
</body>
</html>

 

<%@ page language="java" 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>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>

</style>
</head>
<body>

	<%@ include file="/WEB-INF/views/inc/header.jsp" %>
	
	<h2>
		<span class="material-symbols-outlined">heart_plus</span>
		Member Page
	</h2>
	
	<div>회원만 접근 가능합니다.</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>

</script>
</body>
</html>

 

<%@ page language="java" 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>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>

</style>
</head>
<body>

	<%@ include file="/WEB-INF/views/inc/header.jsp" %>
	
	<h2>
		<span class="material-symbols-outlined">heart_check</span>
		Admin Page
	</h2>
	
	<div>관리자만 접근 가능합니다.</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>

</script>
</body>
</html>


~ views > "inc" > header.jsp

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

<header>
	<h1>Spring Security</h1>
	<ul>
		<li><a href="/security/index.do">Index</a></li>
		<li><a href="/security/member.do">Member</a></li>
		<li><a href="/security/admin.do">Admin</a></li>
		<li class="divider"></li>
		<li><a href="/security/customlogin.do">Login</a></li>
		<li><a href="/security/customlogout.do">Logout</a></li>
	</ul>
</header>

// 로그인, 로그아웃 > 인증

- 단순 로그인 처리
- 자동 생성 로그인 페이지 사용
- 계정 정보 > XMl 정의 > 메모리 상에서 관리 
    ~ security-context.xml 수정

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

	<security:http>
		<!-- 특정 URI + 액세스 설정 -->
		<!--  
			pattern: URI 패턴
			access: 권한 > 표현식(권장★★★) or 권한명
			
			인증 > Role(자격 - 관리자,매니저,팀장,담당자)
				> Authority(권한 - 글쓰기,열람,승인) 
		-->
		<security:intercept-url pattern="/index.do" access="permitAll"/> 
		<security:intercept-url pattern="/member.do" access="hasRole('ROLE_MEMBER')"/> 
		<security:intercept-url pattern="/admin.do" access="hasRole('ROLE_ADMIN')"/> 
			
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider>
		
			<security:user-service>
				<security:user name="hong" password="{noop}1111" authorities="ROLE_MEMBER"/>
				<security:user name="admin" password="{noop}1111" authorities="ROLE_ADMIN, ROLE_MEMBER"/>
			</security:user-service>
			
		</security:authentication-provider> 
	</security:authentication-manager>

</beans>


- There is no PasswordEncoder mapped for the id "null"
- 스프링 4버전까지는 암호 유무 선택
- 스프링 5버전부터는 반드시 구현 > PasswordEncoder 사용

- 관리자 사용자 설정
    1. 비회원(X)
    2. 회원('ROLE_MEMBER') 
    3. 관리자('ROLE_MEMBER', 'ROLE_ADMIN')


// 접근 권한 메시지 처리



- 로그인을 한 상태에서 접근 불가능한 URI를 접근하면 에러가 발생 > 403
    1. AccessDeniedHandler 직접 구현
        ~ "com.test.auth" > CustomAccessDeniedHandler.java

package com.test.auth;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import lombok.extern.log4j.Log4j;

@Log4j
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
	
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
		AccessDeniedException accessDeniedException) throws IOException, ServletException {
		// 403 발생 > request, response 조작
		
		log.error("Access Denied Handler");
		log.error("Redirect..");
		
		response.sendRedirect("/security/accesserror.do");
		
	}

}


        ~ security-context.xml 수정

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

	<!-- 403 처리 담당자 -->
	<bean id="customAccessDenied" class="com.test.auth.CustomAccessDeniedHandler"></bean>

	<security:http>
		<!-- 특정 URI + 액세스 설정 -->
		<!--  
			pattern: URI 패턴
			access: 권한 > 표현식(권장★★★) or 권한명
			
			인증 > Role(자격 - 관리자,매니저,팀장,담당자)
				> Authority(권한 - 글쓰기,열람,승인) 
		-->
		<security:intercept-url pattern="/index.do" access="permitAll"/> 
		<security:intercept-url pattern="/member.do" access="hasRole('ROLE_MEMBER')"/> 
		<security:intercept-url pattern="/admin.do" access="hasRole('ROLE_ADMIN')"/> 
		<!-- ref에 bean id 넣기 -->
		<security:access-denied-handler ref="customAccessDenied"/> 

	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider>
		
			<security:user-service>
				<security:user name="hong" password="{noop}1111" authorities="ROLE_MEMBER"/>
				<security:user name="admin" password="{noop}1111" authorities="ROLE_ADMIN, ROLE_MEMBER"/>
			</security:user-service>            
			
		</security:authentication-provider> 
	</security:authentication-manager>

</beans>

    2. 단순 URI 지정 > forward

        - 에러 페이지 작성
           ~ "com.test.controller" > AuthController.java

package com.test.controller;

import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class AuthController {

	@GetMapping("/accesserror.do")
	public String accesserror(Authentication auth, Model model) {
		
		log.info("Access Denied: " + auth);
		
		model.addAttribute("msg", "Access Denied: " + auth);
		
		return "accesserror";
	}
}


           ~ views > accesserror.jsp

<%@ page language="java" 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>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>

</style>
</head>
<body>

	<%@ include file="/WEB-INF/views/inc/header.jsp" %>
	
	<h2>Access Denied Page</h2>

	<div>${SPRING_SECURITY_403_EXCEPTION.getMessage()}</div>
	
	<div>${msg}</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>

</script>
</body>
</html>


           ~ security-context.xml 수정

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

	<security:http>
		<!-- 특정 URI + 액세스 설정 -->
		<!--  
			pattern: URI 패턴
			access: 권한 > 표현식(권장★★★) or 권한명
			
			인증 > Role(자격 - 관리자,매니저,팀장,담당자)
				> Authority(권한 - 글쓰기,열람,승인) 
		-->
		<security:intercept-url pattern="/index.do" access="permitAll"/> 
		<security:intercept-url pattern="/member.do" access="hasRole('ROLE_MEMBER')"/> 
		<security:intercept-url pattern="/admin.do" access="hasRole('ROLE_ADMIN')"/> 
		
		<security:access-denied-handler error-page="/accesserror.do"/>

	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider>
		
			<security:user-service>
				<security:user name="hong" password="{noop}1111" authorities="ROLE_MEMBER"/>
				<security:user name="admin" password="{noop}1111" authorities="ROLE_ADMIN, ROLE_MEMBER"/>
			</security:user-service>
			
		</security:authentication-provider> 
	</security:authentication-manager>

</beans>

// 로그인 페이지
- 직접 로그인 페이지 구현 > URI 지정



~ AuthController.java>  로그인 페이지 추가

package com.test.controller;

import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class AuthController {
	
	@GetMapping("/customlogin.do")
	public String customlogin(String error, String logout, Model model) {
		
		log.error("error: " + error) ;
		log.info("logout: " + logout);
		
		model.addAttribute("error", error);
		model.addAttribute("logout", logout);
		
		return "customlogin";
	}
}


~ views > customlogin.jsp

<%@ page language="java" 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>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>

</style>
</head>
<body>

	<%@ include file="/WEB-INF/views/inc/header.jsp" %>
	
	<h2>Custom Login Page</h2>

	<div class="message">${error}</div>
	<div class="message">${logout}</div>
	
	
	<form method="POST" action="/security/login">
	<table>
		<tr>
			<th>아이디</th>
			<td><input type="text" name="username" required></td>
		</tr>
		<tr>
			<th>암호</th>
			<td><input type="password" name="password" required></td>
		</tr>
	</table>
	<div>
		<button class="in">로그인</button>
	</div>
	
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
	
	</form>	

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>

</script>
</body>
</html>


- <form>의 action이 "/login"이다. + POST
- <input name="username">, <input name="password"> 의 name이 예약어이다.★★★★★

※ 스프링 시큐리티에서 username의 의미★★★ 
    - 일반 시스템(userid) == 스프링 시큐리티(username)
    - '홍길동' != username
    - 'hong' == username

- <input type="hidden" name="_csrf" value="1219d8e6-7f5c-4391-89b0-9ca958ef675e">

※ CSRF, Cross-site request forgery
    - CSRT 공격 > 토큰
    - <security:csrf disabled="true" />

    - "/login" + GET 요청 > /customlogin.do 호출


// 로그인 후속 동작
- 로그인 성공 후에 특정한 동작을 하고 싶을 때
- AuthenticationSuccessHandler 인터페이스를 구현

~ "com.test.auth" > CustomLoginSuccessHandler.java

package com.test.auth;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

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;

import lombok.extern.log4j.Log4j;

@Log4j
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException {
		
		log.info("Login Success");
		
		//권한별 조치
		// - 일반회원 > "/member.do"
		// - 관리자 > "/admin.do"
		
		List<String> roleNames = new ArrayList<String>();
		
		authentication.getAuthorities().forEach(authority -> {
			roleNames.add(authority.getAuthority());
		});

		log.info("Role");
		
		if(roleNames.contains("ROLE_ADMIN")) {
			
			response.sendRedirect("/security/admin.do");
			return;
		}
		
		if(roleNames.contains("ROLE_MEMBER")) {
			
			response.sendRedirect("/security/member.do");
			return;
		}
		
		response.sendRedirect("/security/index.do");
		
	}
	
}


~ security-context.xml 수정

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

	<!-- 403 처리 담당자 -->
	<bean id="customAccessDenied" class="com.test.auth.CustomAccessDeniedHandler"></bean>

	<!-- 로그인 성공 처리 담당자 -->
	<bean id="customLoginSuccess" class="com.test.auth.CustomLoginSuccessHandler"></bean>
	
	<security:http>
		<!-- 특정 URI + 액세스 설정 -->
		<!--  
			pattern: URI 패턴
			access: 권한 > 표현식(권장★★★) or 권한명
			
			인증 > Role(자격 - 관리자,매니저,팀장,담당자)
				> Authority(권한 - 글쓰기,열람,승인) 
		-->
		<security:intercept-url pattern="/index.do" access="permitAll"/> 
		<security:intercept-url pattern="/member.do" access="hasRole('ROLE_MEMBER')"/> 
		<security:intercept-url pattern="/admin.do" access="hasRole('ROLE_ADMIN')"/> 
		<security:form-login login-page="/customlogin.do"
			authentication-success-handler-ref="customLoginSuccess"/>
		
		<!-- ref에 bean id 넣기 -->
		<security:access-denied-handler ref="customAccessDenied"/> 
        
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider>
		
			<security:user-service>
				<security:user name="hong" password="{noop}1111" authorities="ROLE_MEMBER"/>
				<security:user name="admin" password="{noop}1111" authorities="ROLE_ADMIN, ROLE_MEMBER"/>
			</security:user-service> 
			
		</security:authentication-provider> 
	</security:authentication-manager>

</beans>

// 로그아웃
- 로그인처럼 URI 지정, 핸들러 등록


~ AuthController.java > 메소드 추가

package com.test.controller;

import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j
public class AuthController {

	@GetMapping("/customlogout.do")
	public String customlogout() {
		
		log.info("custom logout");
		
		return "customlogout";
	}
	
}


~ views > customlogout.jsp

<%@ page language="java" 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>Insert title here</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
<style>

</style>
</head>
<body>

	<%@ include file="/WEB-INF/views/inc/header.jsp" %>
	
	<h2>Custom Logout Page</h2>
	
	<form method="POST" action="/security/customlogout.do">
		<div>
			<button class="out">로그아웃</button>
		</div>
		
		<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
	
	</form>	

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>

</script>
</body>
</html>

// 계정 정보
- XML > <security:user name="hong" password="{noop}1111" authorities="ROLE_MEMBER">
- InMemoryUserDetailsManager를 사용한 방식


- 데이터베이스를 활용한 계정 정보

1.미리 정해진 구조의 스키마 사용 > 편해짐 > 제약 심함
    ~ SecurityTest > script.sql

create table users (
   username varchar2(50) not null primary key,
   password varchar2(50) not null,
   enabled char(1) default '1'
);

create table authorities (
   username varchar2(50) not null,
   authority varchar2(50) not null,
   constraint fk_authorities_users foreign key(username) references users(username)
);

create unique index ix_auth_username on authorities (username, authority);

insert into users (username, password) values('hong', '1111');
insert into users (username, password) values('test', '1111');
insert into users (username, password) values('admin', '1111');

insert into authorities (username, authority) values('hong', 'ROLE_MEMBER');
insert into authorities (username, authority) values('test', 'ROLE_MEMBER');
insert into authorities (username, authority) values('admin', 'ROLE_MEMBER');
insert into authorities (username, authority) values('admin', 'ROLE_ADMIN');


    ~ "com.test.auth" > CustomNoOpPasswordEncoder.java

package com.test.auth;

import org.springframework.security.crypto.password.PasswordEncoder;

import lombok.extern.log4j.Log4j;

@Log4j
public class CustomNoOpPasswordEncoder implements PasswordEncoder {
	
	@Override
	public String encode(CharSequence rawPassword) {
		
		log.info("before encode: " + rawPassword);
		
		return rawPassword.toString();
	}
	
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		
		log.info("matches: " + rawPassword + ":" + encodedPassword);
		
		return rawPassword.toString().equals(encodedPassword);
	}

}


~ security-context.xml 수정

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

	<!-- 403 처리 담당자 -->
	<bean id="customAccessDenied" class="com.test.auth.CustomAccessDeniedHandler"></bean>

	<!-- 로그인 성공 처리 담당자 -->
	<bean id="customLoginSuccess" class="com.test.auth.CustomLoginSuccessHandler"></bean>
	
	<!-- 암호화 객체 -->
	<!-- <bean id="customPasswordEncoder" class="com.test.auth.CustomNoOpPasswordEncoder"></bean> -->

	<security:http>
		<!-- 특정 URI + 액세스 설정 -->
		<!--  
			pattern: URI 패턴
			access: 권한 > 표현식(권장★★★) or 권한명
			
			인증 > Role(자격 - 관리자,매니저,팀장,담당자)
				> Authority(권한 - 글쓰기,열람,승인) 
		-->
		<security:intercept-url pattern="/index.do" access="permitAll"/> 
		<security:intercept-url pattern="/member.do" access="hasRole('ROLE_MEMBER')"/> 
		<security:intercept-url pattern="/admin.do" access="hasRole('ROLE_ADMIN')"/> 
		<security:form-login login-page="/customlogin.do"
			authentication-success-handler-ref="customLoginSuccess"/>
		
		<!-- <security:access-denied-handler error-page="/accesserror.do"/> -->
		
		<!-- ref에 bean id 넣기 -->
		<security:access-denied-handler ref="customAccessDenied"/> 
		
		<!-- 로그아웃 -->
		<security:logout logout-url="/customlogout.do" invalidate-session="true" logout-success-url="/index.do"/>		
		
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider>
			
			<security:jdbc-user-service data-source-ref="dataSource"/>
			<security:password-encoder ref="customPasswordEncoder"/>
			
		</security:authentication-provider> 
	</security:authentication-manager>

</beans>

2. 사용자가 직접 만든 구조의 스키마 사용 > 불편함 > 제약 약함
    - 스프링 시큐리티에서 제공하는 기본 스키마 말고, 사용자가 직접 설계한 스키마로 적용
 
  ~ SecurityTest > script.sql 

create table tbl_member (
   userid varchar2(50) not null primary key,
   userpw varchar2(100) not null,
   username varchar2(100) not null,
   regdate date default sysdate,
   updatedate date default sysdate,
   enabled char(1) default '1'
);

create table tbl_member_auth (
   userid varchar2(50) not null,
   auth varchar2(50) not null,
   constraint fk_member_auth foreign key(userid) references tbl_member(userid)
);

select * from tbl_member;

select * from tbl_member_auth;


    ※ 스프링 시큐리티
        - BCryptPasswordEncoder 클래스를 사용
        - bcrypt(Blowfish > 암호화 해시 함수)
        - 암호화 가능, 복호화 불가능

~ src/test/java > com.test.security > MemberTest.java (인증을 하는 데 사용)

package com.test.security;

import java.sql.Connection;
import java.sql.PreparedStatement;

import javax.sql.DataSource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml", "file:src/main/webapp/WEB-INF/spring/security-context.xml"})
public class MemberTest {

   @Autowired
   private DataSource ds;
   
   @Autowired
   private PasswordEncoder encoder;
   
   @Test
   public void testInsertMember() {
      
      String sql = "insert into tbl_member(userid, userpw, username) values (?, ?, ?)";
      
      try {
         
         Connection conn = ds.getConnection();
         PreparedStatement stat = conn.prepareStatement(sql);
         
         stat.setString(1, "dog");
         stat.setString(2, encoder.encode("1111"));
         stat.setString(3, "강아지");
         
         stat.executeUpdate();
         
         stat.setString(1, "cat");
         stat.setString(2, encoder.encode("1111"));
         stat.setString(3, "고양이");
         
         stat.executeUpdate();
         
         stat.setString(1, "lion");
         stat.setString(2, encoder.encode("1111"));
         stat.setString(3, "사자");
         
         stat.executeUpdate();
         
      } catch (Exception e) {
         e.printStackTrace();
      }
      
   }
   
}

 

~ src/test/java > com.test.security > MemberTest2.java (권한을 확인하는 데 사용)

package com.test.security;

import java.sql.Connection;
import java.sql.PreparedStatement;

import javax.sql.DataSource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml", "file:src/main/webapp/WEB-INF/spring/security-context.xml"})
public class MemberTest2 {

   @Autowired
   private DataSource ds;
   
   @Autowired
   private PasswordEncoder encoder;
   
   @Test
   public void testInsertMember() {
      
      String sql = "insert into tbl_member_auth(userid, auth) values (?, ?)";
      
      try {
         
         Connection conn = ds.getConnection();
         PreparedStatement stat = conn.prepareStatement(sql);
         
         stat.setString(1, "dog");
         stat.setString(2, "ROLE_MEMBER");
         
         stat.executeUpdate();
         
         stat.setString(1, "cat");
         stat.setString(2, "ROLE_MEMBER");
         
         stat.executeUpdate();
         
         stat.setString(1, "lion");
         stat.setString(2, "ROLE_MEMBER");
         
         stat.executeUpdate();
         
         stat.setString(1, "lion");
         stat.setString(2, "ROLE_ADMIN");
         
         stat.executeUpdate();
         
      } catch (Exception e) {
         e.printStackTrace();
      }
      
   }
   
}


~ security-context.xml 수정

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

	<!-- 403 처리 담당자 -->
	<bean id="customAccessDenied" class="com.test.auth.CustomAccessDeniedHandler"></bean>

	<!-- 로그인 성공 처리 담당자 -->
	<bean id="customLoginSuccess" class="com.test.auth.CustomLoginSuccessHandler"></bean>
	
	<!-- 암호화 객체(사용자 정의) -->
	<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>

	<security:http>
		<!-- 특정 URI + 액세스 설정 -->
		<!--  
			pattern: URI 패턴
			access: 권한 > 표현식(권장★★★) or 권한명
			
			인증 > Role(자격 - 관리자,매니저,팀장,담당자)
				> Authority(권한 - 글쓰기,열람,승인) 
		-->
		<security:intercept-url pattern="/index.do" access="permitAll"/> 
		<security:intercept-url pattern="/member.do" access="hasRole('ROLE_MEMBER')"/> 
		<security:intercept-url pattern="/admin.do" access="hasRole('ROLE_ADMIN')"/> 
		<security:form-login login-page="/customlogin.do"
			authentication-success-handler-ref="customLoginSuccess"/>
		
		<!-- ref에 bean id 넣기 -->
		<security:access-denied-handler ref="customAccessDenied"/> 
		
		<!-- 로그아웃 -->
		<security:logout logout-url="/customlogout.do" invalidate-session="true" logout-success-url="/index.do"/>		
		
	</security:http>
	
	<security:authentication-manager>
		<security:authentication-provider>
			<security:jdbc-user-service data-source-ref="dataSource" users-by-username-query="select userid username, userpw password, enabled from tbl_member where userid = ?" authorities-by-username-query="select userid username, auth authority from tbl_member_auth where userid = ?" />
			
			<security:password-encoder ref="bcryptPasswordEncoder"/>
			
		</security:authentication-provider> 
	</security:authentication-manager>

</beans>