Spring Spring Security

使用Spring Method Security保護資料安全

江直穎 2019/12/26 14:17:24
2575

1. 前言

大部分開發者導入Spring Security想必是因為它有許多強大且多元的驗證機制,並可能經常使用到頁面層級(Page-Level)的身分驗證,例如:可透過設定權限(一般使用者/系統管理員)來決定該段網址的請求是否被允許。

但是,若有細節尚未設定完全,則可能會被有心人士透過特殊方式破解權限進入該網址,進而取得防護層後方的資料。

而針對此問題 Spring Security 提供多個方法層級(Method-Level)的驗證以及資料過濾機制,能讓安全性防護能夠更加穩固,只要透過Annotation方法設定於Class Method上,即可簡單地使用。

 

目前Spring官方推薦使用,方法層級的驗證有兩種設定方式

@PreAuthorize :

於進入設定的Method 前 執行身分驗證動作,驗證成功則正常執行,若驗證失敗則底層拋出exception 以便開發者可定義後續流程。

@PostAuthorize :

於進入設定的Method 後 執行身分驗證動作,可針對回傳結果資料加上條件判斷,驗證成功則正常執行,若驗證失敗則底層拋出exception 。

 

於方法層級篩選資料使用的過濾器也是有兩種設定方式:

@PreFilter :

於進入設定的Method 前 執行資料篩選動作,可指定資料過濾條件及使用者權限,決定進入流程中的資料內容,只能針對一個集合參數(Collection) 做過濾條件設定。

@PostFilter :

於進入設定的Method 後 執行資料篩選動作,可指定資料過濾條件及使用者權限,在返回流程的資料集合(Collection)中移除特定資料。

 

2.目的

本文將藉由實作畫面,從Spring Boot 工具建立Spring Security專案、載入相關套件、服務邏輯建立、透過JSP查詢和修改功能畫面,以四種簡單的情境,來說明如何使用 Method Security 相關 Annotation ,並於類別方法上加入設定,以達到安全性驗證和資料過濾功能。

 

3.實作前準備

3-1.新增一個Spring Starter Project

 

3-2.勾選Spring Security與Spring Web

 

3-3.載入相關Maven dependency

專案建立完成後,打開maven pom.xml檔案,並加入相關library ,其中本文章實作的部份會參考spring-security-core-5.2.1.RELEASE.jar

而它會參照 spring-boot-starter-security 這個 dependency 設定,所以必須要確認是否有被引入至 maven repository 中;

而其他設定還需要 spring-security-web 以及邏輯處理有用到spring core 、網頁JSP相關呈現,這邊也須載入spring-security-web 、

spring-core 以及JSP相關lib

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/>
	</parent>
	<groupId>com.owen.demo</groupId>
	<artifactId>SpringSecurityMethodDemo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringSecurityMethodDemo</name>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
		</dependency>
        
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-taglibs</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>

		<!-- JSTL -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
		</dependency>

		<!-- Need this to compile JSP -->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>

		<!-- Need this to compile JSP -->
		<dependency>
			<groupId>org.eclipse.jdt.core.compiler</groupId>
			<artifactId>ecj</artifactId>
			<version>4.6.1</version>
			<scope>provided</scope>
		</dependency>
		
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			
		</plugins>
	</build>

</project>

 

3-4.設定 Security 相關Configuration 檔案

需繼承Spring 提供之 WebSecurityConfigurerAdapter 抽象類別,其中需注意的部分如下圖紅框處

 @EnableGlobalMethodSecurity(prePostEnabled = true)  設定必須定義打開,才可使用本實作中方法層級的驗證

並於同檔案設定網站中可驗證登入的使用者(user/admin)、密碼、角色

以及定義使用者驗證登入前後、登入成功/失敗導向頁面設定

 

3-5.定義WEB MVC view 部分設定,使用JstlView 依照回傳字串組合前後綴詞找到指定JSP頁面並呈現

 

3-6.建立Controller 程式,定義頁面入口網址:/loginPage  以及登入成功後轉跳網址: /list  其中/list 入口為查詢員工列表清單,

如同上方3-1.所提到,可以允許訪問的權限有 ROLE_USER 或是 ROLE_ADMIN

定義修改資料頁面入口網址: /editEmp 這邊各別使用Http GET 與 POST 來區分進入修改資料頁面,或送出修改資料頁面

 

3-7.建立需要存取的資料物件Employee

 

3-8.建立查詢/更新資料的Interface EmployeeService 以及實作類別 EmployeeServiceImpl

 

3-9.建立相關頁面JSP (登入 , 登入失敗 , 尚未授權 ,員工列表 , 修改員工資料 , 修改成功 等頁面 , 這邊只列出檔案清單)

 

4.實際情境模擬並驗證測試

4-1. 情境一:使用@PreAuthorize 設定頁面訪問權限

經過上方準備步驟後,我們即可使用Spring Boot 啟動內建tomcat 執行測試網頁。

首先使用一般使用者 user 登入

登入可以看到員工名單頁面

按下修改連結後可以進入修改頁面

但其實一般使用者需定義不能進入修改資料才對(或是只能修改相同權限之資料),所以我們試著加入@PreAuthorize 設定於Controller程式中,於修改員工資料的入口 /editEmp上,將權限角色判斷為ADMIN時才允許通過驗證,阻擋一般使用者訪問修改資料的頁面

修改後重新啟動Spring Boot,再次使用user登入並進入修改頁,則會發現雖然url轉跳仍然是editEmp 但實際上已經被Spring Security攔截,並轉跳至尚未授權之頁面

 

4-2.情境二:使用@PostAuthorize 設定更新頁面資料存取權限

本次設定將Annotation放置於Service層Method上,@PostAuthorize 設定用來攔截執行該Method後返回物件資料前,可以用來判斷該資料並限制存取權限。如下圖: 設定該方法(更新員工資料前查詢)存取權限為ADMIN角色 或是 返回物件中權限欄位資料為該使用者,意指ADMIN角色可以修改全部員工資料,而一般使用者只能修改user權限之員工資料

首先使用user帳號做登入並測試

於第一筆權限為admin的員工資料並點選修改連結,則會被導頁至尚未授權頁面

點選第三筆權限為user的員工資料並點選修改連結,則可以正常進入修改畫面

接下來使用admin帳號做登入並測試,點選第一筆權限為admin的員工資料並點選修改連結,則可以正常進入修改頁面

點選第三筆權限為user的員工資料並點選修改連結,也可以正常進入修改頁面

 

4-3.情境三:使用@PreFilter 設定篩選新增特定條件資料

@PreFilter 設定代表可以攔截執行該Method所傳入參數(必須為Collection集合),並賦予權限或篩選特定條件。

首先於Controller新增一入口功能為一次建立多筆員工資料Method , 固定新增三筆員工資料(其中1位admin權限、2位user權限)

於Service也加入一次建立多筆員工資料的Method,並在上方加入@PreFilter 設定。如下圖: 設定規則為允許ADMIN權限 或 沒有ADMIN權限的人但可以新增非特定條件的物件資料;意指如果使用者admin執行此功能會一次新增三筆記錄,使用者user執行會是傳入篩選特定條件後的2筆非admin資料

使用admin登入並點選員工名單頁新增的功能連結:一次建立多筆員工資料

點選後跳轉回員工名單,加入的資料為下方三筆

接著使用user登入並點選新增連結

點選後跳轉回員工名單,加入的資料為下方兩筆,發現權限為admin的員工資料並未被加入名單中,表示上述設定的規則成立

 

4-4.情境四:使用@PostFilter 設定篩選回傳特定條件資料

@PostFilter 設定代表可以攔截執行該Method之後所回傳內容(必須為Collection集合),並賦予權限或篩選特定條件。

於Service層查詢員工名單列表Method上加入此設定。如下圖: 規則為允許ADMIN權限 或 沒有ADMIN權限的人但只能撈取特定條件的物件資料;

意指如果使用者admin執行此功能會查詢出全部資料,使用者user查詢會只顯示篩選特定條件後的2筆user資料

使用admin登入則會看見全部的員工名單資料

使用user登入則發現員工權限為admin的資料則不顯示

 

5.相關資料參考

Spring 官方文件 - https://www.baeldung.com/spring-security-method-security

API範例 - 

https://www.programcreek.com/java-api-examples/index.php?api=org.springframework.security.access.prepost.PreAuthorize

https://www.programcreek.com/java-api-examples/index.php?api=org.springframework.security.access.prepost.PostAuthorize

https://www.programcreek.com/java-api-examples/index.php?api=org.springframework.security.access.prepost.PreFilter

https://www.programcreek.com/java-api-examples/?api=org.springframework.security.access.prepost.PostFilter

 

江直穎