docker 環境變量注入,springBoot+security+mybatis 實現用戶權限的數據庫動態管理

 2023-10-21 阅读 31 评论 0

摘要:[b][size=large]一、Spring Security 應用的概述[/size][/b] [size=medium] 鑒于目前微服務的興起,Spring周邊方案的普及,以及 Spring Security 強大的和高度可定制的優良特性,最近關注了一下相關內容,順便留個筆記心得,希望對大家
[b][size=large]一、Spring Security 應用的概述[/size][/b]

[size=medium] 鑒于目前微服務的興起,Spring周邊方案的普及,以及 Spring Security 強大的和高度可定制的優良特性,最近關注了一下相關內容,順便留個筆記心得,希望對大家有所幫助。[/size]

[size=medium] Spring Security 權限方案針對不同場景需求有多種不同的使用方法,在此,我們最終描述的是如何采用數據庫存儲配置,并通過自定義過濾器的實現方式,來進行對權限的權利,希望這個過程能加深對Spring Security的理解,如有初學者閱讀,建議先簡單了解下Spring Security 框架,以避免遭遇太多的疑惑。[/size]

[size=medium] 先說大概,Spring Security,包括絕大部分的安全框架,都可以簡單理解為兩個核心:一個是認證,即看看這個請求用戶存在不存在啊,密碼對不對啊等,認證,來確保請求用戶的合法性;另一個就是鑒權,即看看這個訪問的資源,有沒有權限,這個決定用戶能做什么,不能做什么。敲黑板,兩個重點核心:認證!鑒權!下面,我們將嘗試下,看看在 Spring Security 框架內是如何完成這些功能的。[/size]

[size=medium] 在這里,我們不準備剖析 Spring Security 底層的基本邏輯,有些還需要就源碼進行解讀,這里只講應用層面的東西。 [/size]

[size=medium] 先說認證,與本次實現密切相關的幾個類或接口,是UserDetails、UserDetailsService、AuthenticationProvider,我們可以這么理解:UserDetails是用來封裝用戶的,用戶的帳號信息啊、一些權限啊,帳號狀態啊等信息,從數據庫那里拿到,首先是要封裝成UserDetails的樣子,才可以在Spring Security框架中使用的;UserDetailsService,顧名思義,處理UserDetails的Service,它是提供去查詢賬號信息并封裝成UserDetails的服務;AuthenticationProvider的主要工作是負責認證,從登錄請求那里拿到帳號密碼之類,然后再跟從數據庫資源那里得到的UserDetails進行對比確認,如果發現不對勁兒,該報錯報錯,該提示提示,如果OK,則把這些信息揉巴成一團,封裝成一個包含所有信息的認證對象,交給 Spring Security 框架進行管理,供后邊有需要的時候隨時取用。docker 環境變量注入,[/size]

[size=medium] 接下來說鑒權,Spring Security 的鑒權方式有多種,我們大概捋一下,這里我們重點講述如何通過自定義過濾器的鑒權方式,來實現數據庫配置權限的動態管理,與此密切相關的幾個核心類或接口分別是:AbstractSecurityInterceptor(Filter)、FilterInvocationSecurityMetadataSource和AccessDecisionManager。我們可以這么理解,FilterInvocationSecurityMetadataSource是權限資源管理器,它的主要工作就是根據請求的資源(路徑),從數據庫獲取相對應的權限信息;AccessDecisionManager類似權限管理判斷器,負責校驗當前認證用戶的權限,是否可以訪問;AbstractSecurityInterceptor就是前邊這兩個角色負責表演的地方,拿到訪問資源所需的權限,和認證用戶的權限,對比,出結果,如果出現對比不成功,分分鐘拋要一個拒絕訪問的異常,403forbidden了![/size]

[size=medium] 在這里先把這幾個類或者接口,默默的混個眼熟,認證相關:UserDetails、UserDetailsService、AuthenticationProvider;鑒權相關:AbstractSecurityInterceptor(Filter)、FilterInvocationSecurityMetadataSource和AccessDecisionManager,誰是干啥的,誰跟誰什么關系,大概就是那么個意思了,也能猜出 Spring Security 是怎么工作的。[/size]

[size=medium] 接下來還會介紹下 Spring Security 的核心配置類:WebSecurityConfigurerAdapter,它的主要職責就是配置配置哪些資源不需要權限限制啊,哪些需要啊等等,以及做一些綜合性的配置操作,以及 Spring Security 本身的注冊等。[/size]

[size=medium] 以上是 Spring Security 應用的一個概述,目的是有個簡單的了解,提前混個眼熟,便于思路連續性的展開。[/size]

[b][size=large]二、springBoot項目初建[/size][/b]

[size=medium] 在eclipse上怎么創建maven項目,我們就不多說了,方式很多種;這里講,本次 Spring Security 的實現要用到的依賴主要有 Spring MVC、Spring Security、Mybatis、thymeleaf,我們用自己最熟悉的方式建個maven項目,然后修改pom.xml文件如下:[/size]

pom.xml

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<groupId>sec_test</groupId>
<artifactId>sec</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.0.1.RELEASE</spring-boot.version>
</properties>

<dependencies>
<!-- spring-boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.19</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<finalName>${project.name}</finalName>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>


[size=medium] Spring boot下,各個版本一般都是向下兼容略有不同,在這種簡單的應用上基本體現不出太大的差異,我們遵循各自習慣去配置,開心就好,注意pom文件中,除了幾個核心的,額外還有gson和lombok的引入,gson是為了方便輸出對象日志;lombok是為了省去bean類中set/get方法,這個可以讓代碼看起來稍微簡練些,首次使用需要提前安裝下lombok的插件之類,感興趣的可以自行百度下,也可以根據自己的習慣決定是否使用。[/size]

[size=medium] 接下來我們在 src/main/resources 中創建一個 application.yml 作為springBoot項目的主配置文件,注意,這個.yml和.properties的配置方式,雖各有優劣長短,但功效是一樣的,我們這里將采用 .yml 的方式,文件內容如下:[/size]

application.yml

server:
port: 8090
application:
name: sec

spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
content-type: text/html
cache: false #開發時關閉緩存,不然沒法看到實時頁面!
prefix: classpath:/public/ #配置頁面文件路徑
suffix: .html #配置頁面默認后綴

datasource:
url: jdbc:mysql://127.0.0.1:3306/sec?useUnicode=true&characterEncoding=UTF-8
username: root
password: ******


[size=medium] 這個配置文件就是設定一下服務端口啊,服務名稱啊,還有thymeleaf相關的一些路徑配置,以及一些數據源待用的參數,這個文件的配置參數會被系統默認加載,需要時直接取用,很方便。Mybatis框架。然后在主路徑下創建一個含main方法的SecApplication類,做啟動入口,如下:[/size]

SecApplication.java

package com.veiking;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 項目啟動入口
* @author Veiking
*/
@SpringBootApplication
public class SecApplication {
public static void main(String[] args) {
SpringApplication.run(SecApplication.class, args);
}
}


[size=medium] 注意加標簽@SpringBootApplication,表示這將是按照 Spring boot 項目的形式運行。然后直接右鍵運行啟動,留意下輸出窗口,看看什么情況,啟動成功,注意,輸出欄的日志里很突兀的大了這樣一行代碼:Using generated security password: XXXX7e44-e83c-460a-aeef-94249316XXXX ,這個是 Spring Security 自帶默認的,用戶名為user,密碼就是這串UUID一樣的串兒,接下來,我們瀏覽器輸入:http://localhost:8090,敲回車,自動跳轉到了http://localhost:8090/login的路徑,我們可以看到一個框架本身自帶的登錄頁面:[/size]

[img]http://dl2.iteye.com/upload/attachment/0130/5484/14df6d4c-a972-37af-897a-4f6763a43f90.png[/img]

[size=medium] 我們在窗口輸入默認的用戶名密碼,提交,就得到了這樣一個頁面:[/size]

[img]http://dl2.iteye.com/upload/attachment/0130/5486/0562907e-ea8a-3f5a-a395-77abdfd17e16.png[/img]

[size=medium] 好了,初步的 Spring Security 項目驗證通過,項目創建完成。[/size]

[b][size=large]三、數據庫信息的創建[/size][/b]

[size=medium] 這一波操作我們要創建本次實現要用的數據庫表了,按照一般節奏,我們先來五張表:s_user、s_role、s_permission 和 s_user_role、s_role_permission,簡單介紹下,就是用戶、角色、權限資源和他們的關聯關系表,他們結構如下:[/size]

s_user
[img]http://dl2.iteye.com/upload/attachment/0130/5488/ad42d585-2349-3eff-be56-15715c3a16f9.png[/img]
s_role
[img]http://dl2.iteye.com/upload/attachment/0130/5490/395d1b9f-96a4-320a-a865-075280d43027.png[/img]
s_permission
[img]http://dl2.iteye.com/upload/attachment/0130/5492/f3b345f7-f076-38c0-b5bc-89d84c7265f6.png[/img]
s_user_role
[img]http://dl2.iteye.com/upload/attachment/0130/5494/8b6413cd-1684-3fdf-96c4-6a7e456fa744.png[/img]
s_role_permission
[img]http://dl2.iteye.com/upload/attachment/0130/5496/bcc7cb77-3ca8-3bc2-add2-808c965f436f.png[/img]

[size=medium] 我們順便貼上結構代碼,以便使用:[/size]


-- ----------------------------
-- Table structure for `s_user`
-- ----------------------------
DROP TABLE IF EXISTS `s_user`;
CREATE TABLE `s_user` (
`id` int(11) NOT NULL,
`name` varchar(32) DEFAULT NULL,
`password` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for `s_user_role`
-- ----------------------------
DROP TABLE IF EXISTS `s_user_role`;
CREATE TABLE `s_user_role` (
`fk_user_id` int(11) DEFAULT NULL,
`fk_role_id` int(11) DEFAULT NULL,
KEY `union_key` (`fk_user_id`,`fk_role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for `s_role`
-- ----------------------------
DROP TABLE IF EXISTS `s_role`;
CREATE TABLE `s_role` (
`id` int(11) NOT NULL,
`role` varchar(32) DEFAULT NULL,
`describe` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for `s_role_permission`
-- ----------------------------
DROP TABLE IF EXISTS `s_role_permission`;
CREATE TABLE `s_role_permission` (
`fk_role_id` int(11) DEFAULT NULL,
`fk_permission_id` int(11) DEFAULT NULL,
KEY `union_key` (`fk_role_id`,`fk_permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for `s_permission`
-- ----------------------------
DROP TABLE IF EXISTS `s_permission`;
CREATE TABLE `s_permission` (
`id` int(11) NOT NULL,
`permission` varchar(32) DEFAULT NULL,
`url` varchar(32) DEFAULT NULL,
`describe` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


[size=medium] 接下來我們新增一些用戶數據,admin、veiking、xiaoming,添加一些記錄,大概意思是,admin擁有所有權限,veiking只有hello、index相關權限,xiaoming什么權限都沒有,添加數據記錄的腳本如下:[/size]


-- ----------------------------
-- Records of s_user
-- ----------------------------
INSERT INTO `s_user` VALUES ('1', 'admin', 'admin');
INSERT INTO `s_user` VALUES ('2', 'veiking', 'veiking');
INSERT INTO `s_user` VALUES ('3', 'xiaoming', 'xiaoming');

-- ----------------------------
-- Records of s_user_role
-- ----------------------------
INSERT INTO `s_user_role` VALUES ('1', '1');
INSERT INTO `s_user_role` VALUES ('2', '2');

-- ----------------------------
-- Records of s_role
-- ----------------------------
INSERT INTO `s_role` VALUES ('1', 'R_ADMIN', '大總管,所有權限');
INSERT INTO `s_role` VALUES ('2', 'R_HELLO', '說hello相關的權限');

-- ----------------------------
-- Records of s_role_permission
-- ----------------------------
INSERT INTO `s_role_permission` VALUES ('1', '1');
INSERT INTO `s_role_permission` VALUES ('1', '2');
INSERT INTO `s_role_permission` VALUES ('1', '3');
INSERT INTO `s_role_permission` VALUES ('2', '1');
INSERT INTO `s_role_permission` VALUES ('2', '3');

-- ----------------------------
-- Records of s_permission
-- ----------------------------
INSERT INTO `s_permission` VALUES ('1', 'P_INDEX', '/index', 'index頁面資源');
INSERT INTO `s_permission` VALUES ('2', 'P_ADMIN', '/admin', 'admin頁面資源');
INSERT INTO `s_permission` VALUES ('3', 'P_HELLO', '/hello', 'hello頁面資源');

[size=medium] 好了,數據庫表相關的內容是準備完成。[/size]

[b][size=large]四、測試頁面的準備[/size][/b]

[size=medium] 緊接著我們創建一些用來測試檢驗效果的頁面:login.html、index、admin、hello 等頁面,其中 login.html 是用來檢驗登錄效果的,代碼如下:[/size]

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html;charset=UTF-8"/>
<title>登錄</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}"/>
<style type="text/css">
body { padding: 20px; }
.starter-template { width:350px; padding: 0 40px; text-align: center; }
</style>
</head>
<body>

<a th:href="@{/index}"> INDEX</a>
<a th:href="@{/admin}"> | ADMIN</a>
<a th:href="@{/hello}"> | HELLO</a>
<br/>


<hr/>
<div class="starter-template">
<p th:if="${param.logout}" class="bg-warning">已成功注銷
有錯誤,請重試
<h2>使用用戶名密碼登錄</h2>
<form name="form" th:action="@{/login}" action="/login" method="POST">
<div class="form-group">
<label for="username">賬號</label>
<input type="text" class="form-control" name="username" value="" placeholder="賬號" />
</div>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" class="form-control" name="password" placeholder="密碼" />
</div>
<div class="form-group">
<input type="submit" id="login" value="登錄" class="btn btn-primary" />
</div>

</form>
</div>
</body>
</html>


[size=medium] index、admin、hello等頁面內容都差不多,就是不同導航鏈接頁面,到時候會用來測試權限控制的一些效果,其中 index.html 的內容如下:[/size]

index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>主頁</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}"/>
<style type="text/css">
body { padding: 40px; }
</style>
</head>
<body>
<h1>INDEX</h1>
<br/>你好:<a sec:authentication="name"></a>

<a th:href="@{/index}"> INDEX</a>
<a th:href="@{/admin}"> | ADMIN</a>
<a th:href="@{/hello}"> | HELLO</a>
<br/><hr/>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="注銷"/>
</form>


</body>
</html>


[size=medium] 好了,頁面也準備好了,接著下一步。[/size]


[b][size=large]五、基礎類及查詢接口的創建[/size][/b]

[size=medium] 所需數據是準備好了,接下來我們要創建一系列的數據對象,和對應的查詢接口,來供 Spring Security 使用,先來創建一波數據 bean 類:SUser、SRole、SPermission,這幾個分別是用戶、角色、權限資源類,代碼如下:[/size]

SUser.java

package com.veiking.sec.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用戶名密碼信息
* @author Veiking
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SUser {
private int id;
private String name;
private String password;

public SUser(SUser sUser) {
this.id = sUser.getId();
this.name = sUser.getName();
this.password = sUser.getPassword();
}
}


SRole.java

package com.veiking.sec.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* 角色信息
* @author Veiking
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SRole {
private int id;
private String role;
private String describe;
}

SPermission.java

package com.veiking.sec.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 訪問資源信息
* @author Veiking
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SPermission {
private int id;
private String permission;
private String url;
private String describe;
}


[size=medium] 注意@Data、@NoArgsConstructor、@AllArgsConstructor這些注解,都是lombok幫助處理set/get和全參無參構造方法的,如果不喜歡,自行替換即可。[/size]

[size=medium] 然后來處理查詢接口,我們這里采用的是 mybatis 框架的方式,好了,創建幾個對應的dao,代碼如下:[/size]

SUserDao.java

package com.veiking.sec.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.veiking.sec.bean.SUser;

/**
* 用戶信息查詢
* @author Veiking
*/
@Mapper
public interface SUserDao {
/**
* 根據用戶名獲取用戶
*
* @param name
* @return
*/
@Select(value = " SELECT su.* FROM s_user su WHERE su.name = #{name} ")
public SUser findSUserByName(String name);

}

SRoleDao.java

package com.veiking.sec.dao;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.veiking.sec.bean.SRole;
/**
* 角色信息查詢
* @author Veiking
*/
@Mapper
public interface SRoleDao {
/**
* 根據用戶ID獲取角色列表
* @param sUserId
* @return
*/
@Select(value=" SELECT sr.* FROM s_role sr " +
" LEFT JOIN s_user_role sur ON sr.id = sur.fk_role_id " +
" LEFT JOIN s_user su ON sur.fk_user_id = su.id " +
" WHERE su.id = #{sUserId} ")
public List<SRole> findSRoleListBySUserId(int sUserId);

/**
* 根據資源路徑獲取角色列表
* @param sPermissionUrl
* @return
*/
@Select(value=" SELECT sr.* FROM s_role sr " +
" LEFT JOIN s_role_permission srp ON sr.id = srp.fk_role_id " +
" LEFT JOIN s_permission sp ON srp.fk_permission_id = sp.id " +
" WHERE sp.url = #{sPermissionUrl} ")
public List<SRole> findSRoleListBySPermissionUrl(String sPermissionUrl);
}


SPermissionDao.java

package com.veiking.sec.dao;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import com.veiking.sec.bean.SPermission;
/**
* 資源權限信息查詢
* @author Veiking
*/
@Mapper
public interface SPermissionDao {
/**
* 根據用戶ID獲取資源權限列表
* @param sUserId
* @return
*/
@Select(value=" SELECT * FROM s_permission sp " +
" LEFT JOIN s_role_permission srp ON sp.id = srp.fk_permission_id " +
" LEFT JOIN s_role sr ON srp.fk_role_id = sr.id " +
" LEFT JOIN s_user_role sur ON sr.id = sur.fk_role_id " +
" LEFT JOIN s_user su ON sur.fk_user_id = su.id " +
" WHERE su.id = #{sUserId} ")
public List<SPermission> findSPermissionListBySUserId(int sUserId);

/**
* 根據資源路徑獲取資源權限列表
* @param sPermissionUrl
* @return
*/
@Select(value=" SELECT * FROM s_permission sp WHERE sp.url = #{sUserId} ")
public List<SPermission> findSPermissionListBySPermissionUrl(String sPermissionUrl);
}



[size=medium] 請注意,這里的幾個Dao查詢接口是使用注解的方式實現誰,當然,一般mybatis框架通常使用的方式是dao接口+xml腳本,當然個人也是習慣用xml實現較為復雜邏輯的腳本,但是在相對簡單邏輯的操作上,直接用注解的方式是清爽的不能再清爽;兩者在實際運用中是等效的,也是可以一同使用。spring security5,[/size]

[size=medium] 這幾個接口的主要作用是:通過用戶名(登錄名)來獲取用戶信息;通過用戶ID、資源路徑(請求路徑)來獲取角色列表和權限資源列表。緊接著,本著編程習慣,我們再搞一個服務接口,將上邊幾個dao的功能整合,統一對外提供數據服務:[/size]

SecurityDataService.java

package com.veiking.sec.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.veiking.sec.bean.SPermission;
import com.veiking.sec.bean.SRole;
import com.veiking.sec.bean.SUser;
import com.veiking.sec.dao.SPermissionDao;
import com.veiking.sec.dao.SRoleDao;
import com.veiking.sec.dao.SUserDao;

/**
* Security 數據服務
*
* @author Veiking
*/
@Service
public class SecurityDataService {
@Autowired
private SUserDao sUserDao;
@Autowired
private SRoleDao sRoleDao;
@Autowired
private SPermissionDao sPermissionDao;

public SUser findSUserByName(String name) {
return sUserDao.findSUserByName(name);
}

public List<SRole> findSRoleListBySUserId(int sUserId) {
return sRoleDao.findSRoleListBySUserId(sUserId);
}

public List<SRole> findSRoleListBySPermissionUrl(String sPermissionUrl) {
return sRoleDao.findSRoleListBySPermissionUrl(sPermissionUrl);
}

public List<SPermission> findSPermissionListBySUserId(int sUserId) {
return sPermissionDao.findSPermissionListBySUserId(sUserId);
}

public List<SPermission> findSPermissionListBySPermissionUrl(String sPermissionUrl) {
return sPermissionDao.findSPermissionListBySPermissionUrl(sPermissionUrl);
}
}



[size=medium] 這個service沒有額外的操作,僅僅是傳遞dao的功能,OK,到此,Spring Security 需要用的數據服務等一些準備部分,我們都已經準備好了,下面的環節,就是重點了。[/size]

[b][size=large]六、重點:Spring Security之用戶認證[/size][/b]

[size=medium] 經過一番相當羅嗦的鋪墊,終于迎來了正題,我們將在接下來的環節里,講述 Spring Security 認證有關的東西。[/size]

[size=medium] 首先,再次回顧,Spring Security 認證有關的重要類或接口:UserDetails、UserDetailsService、AuthenticationProvider,我們將嘗試自定義封裝UserDetails,經由UserDetailsService提供給AuthenticationProvider,然后和請求消息中獲取的用戶信息進行對比認證。[/size]

[size=medium] 首先,為了刻意的來區分認證和鑒權這里啊范疇,我們先來賣個關子,在包主路徑下創建倆包:authentication、authorization,這倆單詞簡直是很像了,也是特意才用這兩個單詞,是看到有位前輩在博客中調侃了他們,印象深刻:authentication即認證,authorization即鑒權,注意字母微小的差異下在邏輯實現中不同的含義。[/size]

[size=medium] 好,在authentication包下來完成我們 Spring Security 的認證,先新建一個 VUserDetails 類來實現 UserDetails(注:在此,所有的重新實現,都將在原類或接口名稱前綴加大寫的V,此處僅為示例,如有仿例操作,請根據個人習慣;包括之前的類或接口名,也不是很符合java推薦的命名規則,這只是為了在名稱上強調而強調,勿在意,更勿仿效),代碼如下:[/size]

VUserDetails.java

package com.veiking.sec.authentication;

import java.util.Collection;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import com.google.gson.Gson;
import com.veiking.sec.bean.SPermission;
import com.veiking.sec.bean.SRole;
import com.veiking.sec.bean.SUser;
/**
* 用戶信息的封裝,包含用戶名稱密碼及用戶狀態、權限等信息
* @author Veiking
*/
public class VUserDetails extends SUser implements UserDetails{

private static final long serialVersionUID = 1L;
Gson gson = new Gson();
Logger logger = LoggerFactory.getLogger(this.getClass());
//用戶角色列表
private List<SRole> sRoleList = null;
//用戶資源權限列表
private List<SPermission> sPermissionList = null;
/**
* 注意后邊的這兩個參數:sRoleList、sPermissionList
* @param sUser
* @param sRoleList
* @param sPermissionList
*/
public VUserDetails(SUser sUser, List<SRole> sRoleList, List<SPermission> sPermissionList) {
super(sUser);
this.sRoleList = sRoleList;
this.sPermissionList = sPermissionList;
}
/**
* 獲取用戶權限列表方法
* 可以理解成,返回了一個List<String>,之后所謂的權限控制、鑒權,其實就是跟這個list里的String進行對比
* 這里處理了角色和資源權限兩個列表,可以這么理解,
* 角色是權限的抽象集合,是為了更方便的控制和分配權限,而真正顆粒化細節方面,還是需要資源權限自己來做
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
StringBuilder authoritiesBuilder = new StringBuilder("");
List<SRole> tempRoleList = this.getsRoleList();
if (null != tempRoleList) {
for (SRole role : tempRoleList) {
authoritiesBuilder.append(",").append(role.getRole());
}
}
List<SPermission> tempPermissionList = this.getsPermissionList();
if (null != tempPermissionList) {
for (SPermission permission : tempPermissionList) {
authoritiesBuilder.append(",").append(permission.getPermission());
}
}
String authoritiesStr = "";
if(authoritiesBuilder.length()>0) {
authoritiesStr = authoritiesBuilder.deleteCharAt(0).toString();
}
logger.info("VUserDetails getAuthorities [authoritiesStr={} ", authoritiesStr);
return AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesStr);
}

@Override
public String getPassword() {
return super.getPassword();
}

@Override
public String getUsername() {
return super.getName();
}

/**
* 判斷賬號是否已經過期,默認沒有過期
*/
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return true;
}

/**
* 判斷賬號是否被鎖定,默認沒有鎖定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}

/**
* 判斷信用憑證是否過期,默認沒有過期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}

/**
* 判斷賬號是否可用,默認可用
*/
@Override
public boolean isEnabled() {
return true;
}

public List<SRole> getsRoleList() {
return sRoleList;
}

public void setsRoleList(List<SRole> sRoleList) {
this.sRoleList = sRoleList;
}

public List<SPermission> getsPermissionList() {
return sPermissionList;
}

public void setsPermissionList(List<SPermission> sPermissionList) {
this.sPermissionList = sPermissionList;
}

}


[size=medium] 注意這個VUserDetails,它繼承SUser并實現了UserDetails,這個類主要功能就是封裝用戶信息,包括從SUser繼承來的用戶名密碼等屬性,還有兩個角色和權限的列表,注意這個 getAuthorities(),這個方法主要工作是提供一組框架定義的權限列表,可以留意下源碼,這個并沒有定義具體類型,我們這里就用String類型實現這個權限。spring4 security。[/size]

[size=medium] 這里還要解釋下,我們在getAuthorities方法里里分別循環了兩個列表來加工 Spring Security 需要權限信息,即 tempRoleList 和 tempPermissionList,可以這樣子理解,角色和權限的概念,角色本身是權限的抽象集合,是協助我們開發管理的東西,真正意義的東西還是顆粒細小的權限。添個插曲,在本人最初接觸到權限設計的時候,總是傻傻的被二者的關系搞暈,加上一些實際應用的系統還樂此不疲的在權限命名上"ROLE"來"ROLE"去的,甚至一些方法命名本身也在混淆這二者(懷疑可能是英語的使用習慣之類的原因),導致早先的我常常常常陷入對二者的理解困惑上,當然現在清晰的很多: 在大塊兒整體性的權限控制上,角色控制為主;細化到頁面小塊兒、按鈕級別的,權限控制為主;一般再加上訪問URL的過濾鑒權,基本上一套強壯的權限控制體系是穩穩的在這兒了。[/size]

[size=medium] 最后注意下代碼里的幾個isXXX方法,這些是一些細節補充,一般默認,也可以重寫控制下邏輯;緊接著我們新建一個 VUserDetailsService 類,來實現UserDetailsService,代碼如下:[/size]

VUserDetailsService.java

package com.veiking.sec.authentication;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
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;

import com.veiking.sec.bean.SPermission;
import com.veiking.sec.bean.SRole;
import com.veiking.sec.bean.SUser;
import com.veiking.sec.service.SecurityDataService;
/**
* 提供用戶信息封裝服務
* @author Veiking
*/
@Service
public class VUserDetailsService implements UserDetailsService {

@Autowired
SecurityDataService securityDataService;
/**
* 根據用戶輸入的用戶名返回數據源中用戶信息的封裝,返回一個UserDetails
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SUser sUser = securityDataService.findSUserByName(username);
//用戶角色列表
List<SRole> sRoleList = securityDataService.findSRoleListBySUserId(sUser.getId());
//用戶資源權限列表
List<SPermission> sPermissionList = securityDataService.findSPermissionListBySUserId(sUser.getId());
return new VUserDetails(sUser, sRoleList, sPermissionList);
}

}


[size=medium] 這個類基本上沒啥好說的,服務提供者,就是一個搬運工,看這個loadUserByUsername()方法,拿到用戶基本信息、角色列表和資源權限列表后,構造一個 VUserDetails 對象,OK返回。接下來是一個小重點,我們創建一個 VAuthenticationProvider 類來實現 AuthenticationProvider,代碼如下:[/size]

VAuthenticationProvider.java

package com.veiking.sec.authentication;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.stereotype.Component;

import com.google.gson.Gson;
/**
* 認證提供者,校驗用戶,登錄名密碼,以及向系統提供一個用戶信息的綜合封裝
* @author Veiking
*/
@Component
public class VAuthenticationProvider implements AuthenticationProvider {
Gson gson = new Gson();
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
VUserDetailsService vUserDetailsService;
/**
* 首先,在用戶登錄的時候,系統將用戶輸入的的用戶名和密碼封裝成一個Authentication對象
* 然后,根據用戶名去數據源中查找用戶的數據,這個數據是封裝成的VUserDetails對象
* 接著,將兩個對象進行信息比對,如果密碼正確,通過校驗認證
* 最后,將用戶信息(含身份信息、細節信息、密碼、權限等)封裝成一個對象,此處參考UsernamePasswordAuthenticationToken
* 最最后,會將這個對象交給系統SecurityContextHolder中(功能類似Session),以便后期隨時取用
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
logger.info("VAuthenticationProvider authenticate login user [username={}, password={}]", username, password);
VUserDetails vUserDetails = (VUserDetails)vUserDetailsService.loadUserByUsername(username);
logger.info("VAuthenticationProvider authenticate vUserDetails [vUserDetails={}]", gson.toJson(vUserDetails));
if(vUserDetails == null){
throw new BadCredentialsException("用戶沒有找到");
}
if (!password.equals(vUserDetails.getPassword())) {
logger.info("VAuthenticationProvider authenticate BadCredentialsException [inputPassword={}, DBPassword={}]", password, vUserDetails.getPassword());
throw new BadCredentialsException("密碼錯誤");
}
//認證校驗通過后,封裝UsernamePasswordAuthenticationToken返回
return new UsernamePasswordAuthenticationToken(vUserDetails, password, vUserDetails.getAuthorities());
}

@Override
public boolean supports(Class<?> authentication) {
return true;
}

}


[size=medium] 這個實現類的核心就是authenticate方法,一步步看,系統會將用戶在登錄請求操作的時候,把輸入的用戶名密碼等,封裝到一個Authentication對象中,我們從這個對象里拿到用戶名,通過 VUserDetailsService 獲取到 VUserDetails 對象,然后拿這個對象的密碼屬性,和請求Authentication對象中獲取的密碼進行對比,如果一切OK,驗證功過,然后再將這些所有信息,整體封裝成一個Authentication對象(這里邊我們用的是UsernamePasswordAuthenticationToken),交給系統框架,后期可以隨時取用。[/size]

[size=medium] 好了,經過上面的工作,用戶認證的邏輯已經完事兒了,我們要做訪問工作,這里還要做些配置操作,這里分別新建倆類,代碼如下:[/size]


WebMvcConfig.java

package com.veiking.sec;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 訪問路徑配置類
* 可以理解成做簡單訪問過濾的,轉發到相應的視圖頁面
* @author Veiking
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/").setViewName("index");
registry.addViewController("/index").setViewName("index");
}
}

PageController.java

package com.veiking.sec.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 請求頁面分發,注意和WebMvcConfig的對比,功能類似
* @author Veiking
*/
@Controller
public class PageController {

@RequestMapping("/admin")
public String admin(Model model, String tt) {
return "admin";
}

@RequestMapping("/hello")
public String hello(Model model, String tt) {
return "hello";
}
}



[size=medium] WebMvcConfig 是一個簡單的路徑映射,功能跟在 PageController中實現的差不多,之所以多寫一個PageController,是因為后邊會有其他的功能演示。[/size]

[size=medium] 然后我們還需創建一個 WebSecurityConfig 類來繼承 WebSecurityConfigurerAdapter,代碼如下:[/size]

WebSecurityConfig.java

package com.veiking.sec;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security 主配置文件
* @author Veiking
*/
@Configuration
@EnableWebSecurity //開啟Spring Security的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/**
* 定義不需要過濾的靜態資源(等價于HttpSecurity的permitAll)
*/
@Override
public void configure(WebSecurity webSecurity) throws Exception {
webSecurity.ignoring().antMatchers("/css/**");
}

/**
* 安全策略配置
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
// 對于網站部分資源需要指定鑒權
//.antMatchers("/admin/**").hasRole("ADMIN")
// 除上面外的所有請求全部需要鑒權認證
.anyRequest().authenticated().and()
// 定義當需要用戶登錄時候,轉到的登錄頁面
.formLogin().loginPage("/login").defaultSuccessUrl("/index").permitAll().and()
// 定義登出操作
.logout().logoutSuccessUrl("/login?logout").permitAll().and()
.csrf().disable()
;
// 禁用緩存
httpSecurity.headers().cacheControl();
}
}



[size=medium] 這個類是使用 Spring Security 的主配置入口,在這個配置文件中,正式啟用 Spring Security 包括我們之前所講的所有功能,這里主要留意一下負責安全策略配置的 configure()方法,這個方法里可以定義登錄登出等操作細節,以及一些靜態資源的權限忽略之類的,甚至也是可以直接手動配權限的。springsecurity中文、[/size]

[size=medium] 一切完事兒,我們運行 SecApplication ,開始驗證之旅:[/size]

[img]http://dl2.iteye.com/upload/attachment/0130/5498/a7d068c6-abd4-37b7-a5fd-b643c5f05115.png[/img]

[size=medium] 在登錄頁面,輸入用戶名密碼:admin/admin,登錄看看,隨便點點跳跳,換成veiking/veiking試試,也可以輸錯試試,再試下登出:[/size]

[img]http://dl2.iteye.com/upload/attachment/0130/5500/7916b4d5-9123-3981-b86b-1fb35486d8ce.png[/img]
[img]http://dl2.iteye.com/upload/attachment/0130/5502/990e7216-b53d-3b12-ab16-b44b1312d088.png[/img]
[img]http://dl2.iteye.com/upload/attachment/0130/5504/f1513035-e8d9-3462-a031-51e0be5be2aa.png[/img]

[size=medium] 好了,這個簡單的用戶認證功能看來是可以了,我們接下來看看如何控制權限。[/size]

[b][size=large]七、重點:Spring Security之鑒權-初試[/size][/b]

[size=medium] 認證OK,回想下,是否還記得,在VAuthenticationProvider的校驗環節,我們在封裝返回給系統的Authentication對象里,是提供了vUserDetails.getAuthorities()這個認證列表的,接下來看看,這個被交給系統的認證列表,是怎么體現的。[/size]

[size=medium] 我們打開 hello.html 頁面,在其中的幾個導航跳轉的信息上,添加一個 sec:authorize="hasAuthority('XXX')" 的代碼,這樣子的腳本,大概意思就是,只有名為‘XXX’的角色或者權限的用戶,登錄之后才可以看到,如下:[/size]

hello.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta content="text/html;charset=UTF-8"/>
<title>HELLO</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}"/>
<style type="text/css">
body { padding: 40px; }
</style>
</head>
<body>
<h1>HELLO</h1>
<br/>你好:<a sec:authentication="name"></a>

<a sec:authorize="hasAuthority('P_INDEX')" th:href="@{/index}"> INDEX</a>
<a sec:authorize="hasAuthority('P_ADMIN')" th:href="@{/admin}"> | ADMIN</a>
<a sec:authorize="hasAuthority('P_HELLO')" th:href="@{/hello}"> | HELLO</a>
<br/><hr/>
<form th:action="@{/logout}" method="post" sec:authorize="hasAuthority('R_ADMIN')">
<input type="submit" class="btn btn-primary" value="注銷"/>
</form>


</body>
</html>


(注意,在頁面中使用 Spring Security 相關腳本,要在<html>標簽處添加 xmlns:th="http://www.thymeleaf.org" 、 xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4" 等約束規范)重新啟動后,分別用不同的用戶,登錄后跳轉到hello頁面查看,這時候可以看到,admin用戶擁有較多權限,都可以看到, veiking 用戶只能看到index和hello導航,而 xiaoming 用戶什么都看不到了,并且他們都不能看到注銷按鈕,就是這個效果:[/size]

[img]http://dl2.iteye.com/upload/attachment/0130/5506/932eef0f-1a57-344b-be21-31300a9c4337.png[/img]
[img]http://dl2.iteye.com/upload/attachment/0130/5508/88ba11a8-0483-3c76-9c79-231e3b02980c.png[/img]

[size=medium] 上邊是從頁面層面來進行權限控制的,注意hasAuthority('XXX')中,有用到R_ADMIN、P_ADMIN、P_HELLO不同類型的權限字眼,包含角色和權限,這個控制的顆粒度沒有絕對的,只要設計成規律可循、操作可行方案即可。[/size]

[size=medium] 接下來,打開 PageController,在/admin處添加標簽:@PreAuthorize("hasAuthority('R_ADMIN')"),如下:[/size]

PageController.java

package com.veiking.sec.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 請求頁面分發,注意和WebMvcConfig的對比,功能類似
* @author Veiking
*/
@Controller
public class PageController {

@RequestMapping("/admin")
@PreAuthorize("hasAuthority('R_ADMIN')")
public String admin(Model model, String tt) {
return "admin";
}

@RequestMapping("/hello")
public String hello(Model model, String tt) {
return "hello";
}
}



[size=medium] 注意,這個操作還需要在 WebSecurityConfig 類中加 @EnableGlobalMethodSecurity(prePostEnabled=true) 標簽來,開啟注解控制權限,然后配置 authenticationManagerBean 以供支持,代碼如下:[/size]

WebSecurityConfig.java

package com.veiking.sec;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Security 主配置文件
* @author Veiking
*/
@Configuration
@EnableWebSecurity //開啟Spring Security的功能
@EnableGlobalMethodSecurity(prePostEnabled=true)//開啟注解控制權限
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/**
* 定義不需要過濾的靜態資源(等價于HttpSecurity的permitAll)
*/
@Override
public void configure(WebSecurity webSecurity) throws Exception {
webSecurity.ignoring().antMatchers("/css/**");
}

/**
* 安全策略配置
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
// 對于網站部分資源需要指定鑒權
//.antMatchers("/admin/**").hasRole("ADMIN")
// 除上面外的所有請求全部需要鑒權認證
.anyRequest().authenticated().and()
// 定義當需要用戶登錄時候,轉到的登錄頁面
.formLogin().loginPage("/login").defaultSuccessUrl("/index").permitAll().and()
// 定義登出操作
.logout().logoutSuccessUrl("/login?logout").permitAll().and()
.csrf().disable()
;
// 禁用緩存
httpSecurity.headers().cacheControl();
}

/**
* 開啟注解控制權限
* 見Controller 中 @PreAuthorize("hasAuthority('XXX')")
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}



[size=medium] 然后再次啟動,用veiking登錄,INDEX頁面點擊ADMIN導航 ——好,403 Forbidden了,對,被攔了,就是這個效果。[/size]

[size=medium] 以上這些,是簡單的對 Spring Security 鑒權操作的一些嘗試,當然,如果是小規模功能開發,這些是可以滿足的,如果想追求更為靈活的控制,就要重新是實現下過濾機制,接下來我們就嘗試下從對數據庫層面的配置,實現權限的動態管理。[/size]

[b][size=large]八、重點:Spring Security之鑒權-過濾器[/size][/b]

[size=medium] 上邊我們已嘗試了經通過頁面腳本和注解這兩種方式的權限控制,接下來,我們嘗試下通過數據庫的權限配置,來過濾用戶操作請求的。docker 權限,[/size]

[size=medium] 跟認證對應,我們新建一個包,authorization,然后在這個里面來實現過濾請求方式的鑒權:先寫一個 VFilterInvocationSecurityMetadataSource 類,來實現 FilterInvocationSecurityMetadataSource,這個可以簡單理解成權限資源管理器,它的工作是通過用戶的請求地址,來獲取訪問這個地址所需的權限,代碼如下:[/size]

FilterInvocationSecurityMetadataSource.java

package com.veiking.sec.authorization;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import com.google.gson.Gson;
import com.veiking.sec.bean.SPermission;
import com.veiking.sec.bean.SRole;
import com.veiking.sec.service.SecurityDataService;
/**
* 權限資源管理器
* 根據用戶請求的地址,獲取訪問該地址需要的所需權限
* @author Veiking
*/
@Component
public class VFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
Gson gson = new Gson();
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
SecurityDataService securityDataService;

@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//獲取請求起源路徑
String requestUrl = ((FilterInvocation) object).getRequestUrl();
logger.info("VFilterInvocationSecurityMetadataSource getAttributes [requestUrl={}]", requestUrl);
//登錄頁面就不需要權限
if ("/login".equals(requestUrl)) {
return null;
}
//用來存儲訪問路徑需要的角色或權限信息
List<String> tempPermissionList = new ArrayList<String>();
//獲取角色列表
List<SRole> sRoleList = securityDataService.findSRoleListBySPermissionUrl(requestUrl);
logger.info("VFilterInvocationSecurityMetadataSource getAttributes [sRoleList={}]", gson.toJson(sRoleList));
for(SRole sRole : sRoleList) {
tempPermissionList.add(sRole.getRole());
}
//徑獲取資源權限列表
List<SPermission> sPermissionList = securityDataService.findSPermissionListBySPermissionUrl(requestUrl);
logger.info("VFilterInvocationSecurityMetadataSource getAttributes [sPermissionList={}]", gson.toJson(sPermissionList));
for(SPermission sPermission : sPermissionList) {
tempPermissionList.add(sPermission.getPermission());
}
//如果沒有權限控制的url,可以設置都可以訪問,也可以設置默認不許訪問
if(tempPermissionList.isEmpty()) {
return null;//都可以訪問
//tempPermissionList.add("DEFAULT_FORBIDDEN");//默認禁止
}
String[] permissionArray = tempPermissionList.toArray(new String[0]);
logger.info("VFilterInvocationSecurityMetadataSource getAttributes [permissionArray={}]", gson.toJson(permissionArray));
return SecurityConfig.createList(permissionArray);
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
// TODO Auto-generated method stub
return null;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}

}


[size=medium] 接著訪問需要的權限資源拿到了,就得有判斷的地方,新建一個 VAccessDecisionManager 來實現 AccessDecisionManager ,這個類可以理解成權限管理判斷器,他的主要工作就是鑒權,通過拿到的訪問路徑所需的權限,和用戶所擁有的權限進行對比,判斷用戶是否有權限訪問,代碼如下:[/size]

VAccessDecisionManager.java

package com.veiking.sec.authorization;

import java.util.Collection;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
/**
* 權限管理判斷器|校驗用戶是否有權限訪問請求資源
* @author Veiking
*/
@Component
public class VAccessDecisionManager implements AccessDecisionManager {

@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
//當前用戶所具有的權限
Collection<? extends GrantedAuthority> userAuthorityList = authentication.getAuthorities();
//訪問資源所需的權限信息
Collection<ConfigAttribute> needAuthoritieList = configAttributes;
//依次循環對比,發現有匹配的即返回
for(ConfigAttribute needAuthoritie: needAuthoritieList) {
String needAuthoritieStr = needAuthoritie.getAttribute();
for (GrantedAuthority userAuthority : userAuthorityList) {
String userAuthorityStr = userAuthority.getAuthority();
if (needAuthoritieStr.equals(userAuthorityStr)) {
return;
}
}
}
//執行到這里說明沒有匹配到應有權限
throw new AccessDeniedException("權限不足!");
}

@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}

}


[size=medium] 最后,要寫一個過濾器,提供上邊這些功能的工作場所,創建 VFilterSecurityInterceptor 類,繼承 AbstractSecurityInterceptor 并實現 Filter,這就是個鑒權過濾器,代碼如下:[/size]

VFilterSecurityInterceptor.java

package com.veiking.sec.authorization;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
/**
* 訪問鑒權過濾器
* 該過濾器的作用就是,用戶請求時,提供權限資源管理器和權限判斷器工作的場所,實現鑒權操作
* @author Veiking
*/
@Component
@ServletComponentScan
@WebFilter(filterName="vFilterSecurityInterceptor",urlPatterns="/*")
public class VFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

@Autowired
private VFilterInvocationSecurityMetadataSource vFilterInvocationSecurityMetadataSource;

@Autowired
public void setMyAccessDecisionManager(VAccessDecisionManager vAccessDecisionManager) {
super.setAccessDecisionManager(vAccessDecisionManager);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
invoke(filterInvocation);
}

public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// filterInvocation里面有一個被攔截的url
// 里面調用VFilterInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取filterInvocation對應的所有權限
// 再調用VAccessDecisionManager的decide方法來校驗用戶的權限是否足夠
InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(filterInvocation);
try {
// 執行下一個攔截器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.afterInvocation(interceptorStatusToken, null);
}
}

@Override
public void destroy() {
}

@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.vFilterInvocationSecurityMetadataSource;
}

}


[size=medium] 這里邊注意 @ServletComponentScan 和 @WebFilter(filterName="vFilterSecurityInterceptor",urlPatterns="/*") 注解,這個是確保過濾器自動注冊并工作,否則過濾器無效。[/size]

[size=medium] 接下來啟動項目,用個個用戶登進去看看,尤其是veiking和xiaoming用戶,訪問沒有權限的連接時果斷遭到限制,403 Forbidden![/size]

[img]http://dl2.iteye.com/upload/attachment/0130/5510/5d650109-06c4-3eb3-a8cc-369acb1c7223.png[/img]

[b][size=large]九、結語[/size][/b]

[size=medium] 好了,經過這么一番折騰,學習應用 Spring Security 框架該接觸到的一些知識點,都有所體現了。權限控制的本質,就是對比校驗,其一般體現方式,就是過濾器。Spring Security 是提供了一種相對比較好的表現形式,給出了一個優良的范式,敲示例代碼的本身,更重要的應該是為了幫助理解和學習,而不是為了實現而實現。[/size]

[size=medium] 本文是足夠羅嗦,也是個人為了加深在理解記憶,但有些地方甚至也是錯誤的、不合乎規范的,希望大家不要被誤導,這只能說是一個提供理解的參考,幫助大家從懵懂到懂;還有需要注意的是,因spring版本不同導致的一些細節差異,可能會有小坑,還是需要注意下的。文中所涉及代碼最后都在附件中,感興趣的同學可以自行下載。spring jdbc。[/size]


[size=medium] 還有,喜歡的,掃下支付寶家電紅包吧,哈哈哈![/size]

[img]http://dl2.iteye.com/upload/attachment/0130/5513/25e7d164-24ca-3621-a0de-388ac2eacda5.jpg[/img]


代碼附件:
[url]http://dl2.iteye.com/upload/attachment/0130/5515/3b1eeefa-fa64-3dd2-97c0-753bd96c1acc.rar[/url]

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://808629.com/163025.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 86后生记录生活 Inc. 保留所有权利。

底部版权信息