`

springBoot+security+mybatis 实现用户权限的数据库动态管理

阅读更多

一、Spring Security 应用的概述

    鉴于目前微服务的兴起,Spring周边方案的普及,以及 Spring Security 强大的和高度可定制的优良特性,最近关注了一下相关内容,顺便留个笔记心得,希望对大家有所帮助。

    Spring Security 权限方案针对不同场景需求有多种不同的使用方法,在此,我们最终描述的是如何采用数据库存储配置,并通过自定义过滤器的实现方式,来进行对权限的权利,希望这个过程能加深对Spring Security的理解,如有初学者阅读,建议先简单了解下Spring Security 框架,以避免遭遇太多的疑惑。

    先说大概,Spring Security,包括绝大部分的安全框架,都可以简单理解为两个核心:一个是认证,即看看这个请求用户存在不存在啊,密码对不对啊等,认证,来确保请求用户的合法性;另一个就是鉴权,即看看这个访问的资源,有没有权限,这个决定用户能做什么,不能做什么。敲黑板,两个重点核心:认证!鉴权!下面,我们将尝试下,看看在 Spring Security 框架内是如何完成这些功能的。

    在这里,我们不准备剖析 Spring Security 底层的基本逻辑,有些还需要就源码进行解读,这里只讲应用层面的东西。

    先说认证,与本次实现密切相关的几个类或接口,是UserDetails、UserDetailsService、AuthenticationProvider,我们可以这么理解:UserDetails是用来封装用户的,用户的帐号信息啊、一些权限啊,帐号状态啊等信息,从数据库那里拿到,首先是要封装成UserDetails的样子,才可以在Spring Security框架中使用的;UserDetailsService,顾名思义,处理UserDetails的Service,它是提供去查询账号信息并封装成UserDetails的服务;AuthenticationProvider的主要工作是负责认证,从登录请求那里拿到帐号密码之类,然后再跟从数据库资源那里得到的UserDetails进行对比确认,如果发现不对劲儿,该报错报错,该提示提示,如果OK,则把这些信息揉巴成一团,封装成一个包含所有信息的认证对象,交给 Spring Security 框架进行管理,供后边有需要的时候随时取用。

    接下来说鉴权,Spring Security 的鉴权方式有多种,我们大概捋一下,这里我们重点讲述如何通过自定义过滤器的鉴权方式,来实现数据库配置权限的动态管理,与此密切相关的几个核心类或接口分别是:AbstractSecurityInterceptor(Filter)、FilterInvocationSecurityMetadataSource和AccessDecisionManager。我们可以这么理解,FilterInvocationSecurityMetadataSource是权限资源管理器,它的主要工作就是根据请求的资源(路径),从数据库获取相对应的权限信息;AccessDecisionManager类似权限管理判断器,负责校验当前认证用户的权限,是否可以访问;AbstractSecurityInterceptor就是前边这两个角色负责表演的地方,拿到访问资源所需的权限,和认证用户的权限,对比,出结果,如果出现对比不成功,分分钟抛要一个拒绝访问的异常,403forbidden了!

    在这里先把这几个类或者接口,默默的混个眼熟,认证相关:UserDetails、UserDetailsService、AuthenticationProvider;鉴权相关:AbstractSecurityInterceptor(Filter)、FilterInvocationSecurityMetadataSource和AccessDecisionManager,谁是干啥的,谁跟谁什么关系,大概就是那么个意思了,也能猜出 Spring Security 是怎么工作的。

    接下来还会介绍下 Spring Security 的核心配置类:WebSecurityConfigurerAdapter,它的主要职责就是配置配置哪些资源不需要权限限制啊,哪些需要啊等等,以及做一些综合性的配置操作,以及 Spring Security 本身的注册等。

    以上是 Spring Security 应用的一个概述,目的是有个简单的了解,提前混个眼熟,便于思路连续性的展开。

二、springBoot项目初建

    在eclipse上怎么创建maven项目,我们就不多说了,方式很多种;这里讲,本次 Spring Security 的实现要用到的依赖主要有 Spring MVC、Spring Security、Mybatis、thymeleaf,我们用自己最熟悉的方式建个maven项目,然后修改pom.xml文件如下:

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>


    Spring boot下,各个版本一般都是向下兼容略有不同,在这种简单的应用上基本体现不出太大的差异,我们遵循各自习惯去配置,开心就好,注意pom文件中,除了几个核心的,额外还有gson和lombok的引入,gson是为了方便输出对象日志;lombok是为了省去bean类中set/get方法,这个可以让代码看起来稍微简练些,首次使用需要提前安装下lombok的插件之类,感兴趣的可以自行百度下,也可以根据自己的习惯决定是否使用。

    接下来我们在 src/main/resources 中创建一个 application.yml 作为springBoot项目的主配置文件,注意,这个.yml和.properties的配置方式,虽各有优劣长短,但功效是一样的,我们这里将采用 .yml 的方式,文件内容如下:

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: ******


    这个配置文件就是设定一下服务端口啊,服务名称啊,还有thymeleaf相关的一些路径配置,以及一些数据源待用的参数,这个文件的配置参数会被系统默认加载,需要时直接取用,很方便。然后在主路径下创建一个含main方法的SecApplication类,做启动入口,如下:

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);
	}
}


    注意加标签@SpringBootApplication,表示这将是按照 Spring boot 项目的形式运行。然后直接右键运行启动,留意下输出窗口,看看什么情况,启动成功,注意,输出栏的日志里很突兀的大了这样一行代码:Using generated security password: XXXX7e44-e83c-460a-aeef-94249316XXXX ,这个是 Spring Security 自带默认的,用户名为user,密码就是这串UUID一样的串儿,接下来,我们浏览器输入:http://localhost:8090,敲回车,自动跳转到了http://localhost:8090/login的路径,我们可以看到一个框架本身自带的登录页面:



    我们在窗口输入默认的用户名密码,提交,就得到了这样一个页面:



    好了,初步的 Spring Security 项目验证通过,项目创建完成。

三、数据库信息的创建

    这一波操作我们要创建本次实现要用的数据库表了,按照一般节奏,我们先来五张表:s_user、s_role、s_permission 和 s_user_role、s_role_permission,简单介绍下,就是用户、角色、权限资源和他们的关联关系表,他们结构如下:

s_user

s_role

s_permission

s_user_role

s_role_permission


    我们顺便贴上结构代码,以便使用:

-- ----------------------------
-- 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;


    接下来我们新增一些用户数据,admin、veiking、xiaoming,添加一些记录,大概意思是,admin拥有所有权限,veiking只有hello、index相关权限,xiaoming什么权限都没有,添加数据记录的脚本如下:

-- ----------------------------
-- 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页面资源');

    好了,数据库表相关的内容是准备完成。

四、测试页面的准备

    紧接着我们创建一些用来测试检验效果的页面:login.html、index、admin、hello 等页面,其中 login.html 是用来检验登录效果的,代码如下:

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>


    index、admin、hello等页面内容都差不多,就是不同导航链接页面,到时候会用来测试权限控制的一些效果,其中 index.html 的内容如下:

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>


    好了,页面也准备好了,接着下一步。


五、基础类及查询接口的创建

    所需数据是准备好了,接下来我们要创建一系列的数据对象,和对应的查询接口,来供 Spring Security 使用,先来创建一波数据 bean 类:SUser、SRole、SPermission,这几个分别是用户、角色、权限资源类,代码如下:

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;
}


    注意@Data、@NoArgsConstructor、@AllArgsConstructor这些注解,都是lombok帮助处理set/get和全参无参构造方法的,如果不喜欢,自行替换即可。

    然后来处理查询接口,我们这里采用的是 mybatis 框架的方式,好了,创建几个对应的dao,代码如下:

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);
}



    请注意,这里的几个Dao查询接口是使用注解的方式实现谁,当然,一般mybatis框架通常使用的方式是dao接口+xml脚本,当然个人也是习惯用xml实现较为复杂逻辑的脚本,但是在相对简单逻辑的操作上,直接用注解的方式是清爽的不能再清爽;两者在实际运用中是等效的,也是可以一同使用。

    这几个接口的主要作用是:通过用户名(登录名)来获取用户信息;通过用户ID、资源路径(请求路径)来获取角色列表和权限资源列表。紧接着,本着编程习惯,我们再搞一个服务接口,将上边几个dao的功能整合,统一对外提供数据服务:

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);
	}
}



    这个service没有额外的操作,仅仅是传递dao的功能,OK,到此,Spring Security 需要用的数据服务等一些准备部分,我们都已经准备好了,下面的环节,就是重点了。

六、重点:Spring Security之用户认证

    经过一番相当罗嗦的铺垫,终于迎来了正题,我们将在接下来的环节里,讲述 Spring Security 认证有关的东西。

    首先,再次回顾,Spring Security 认证有关的重要类或接口:UserDetails、UserDetailsService、AuthenticationProvider,我们将尝试自定义封装UserDetails,经由UserDetailsService提供给AuthenticationProvider,然后和请求消息中获取的用户信息进行对比认证。

    首先,为了刻意的来区分认证和鉴权这里啊范畴,我们先来卖个关子,在包主路径下创建俩包:authentication、authorization,这俩单词简直是很像了,也是特意才用这两个单词,是看到有位前辈在博客中调侃了他们,印象深刻:authentication即认证,authorization即鉴权,注意字母微小的差异下在逻辑实现中不同的含义。

    好,在authentication包下来完成我们 Spring Security 的认证,先新建一个 VUserDetails 类来实现 UserDetails(注:在此,所有的重新实现,都将在原类或接口名称前缀加大写的V,此处仅为示例,如有仿例操作,请根据个人习惯;包括之前的类或接口名,也不是很符合java推荐的命名规则,这只是为了在名称上强调而强调,勿在意,更勿仿效),代码如下:

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;
	}

}


    注意这个VUserDetails,它继承SUser并实现了UserDetails,这个类主要功能就是封装用户信息,包括从SUser继承来的用户名密码等属性,还有两个角色和权限的列表,注意这个 getAuthorities(),这个方法主要工作是提供一组框架定义的权限列表,可以留意下源码,这个并没有定义具体类型,我们这里就用String类型实现这个权限。

    这里还要解释下,我们在getAuthorities方法里里分别循环了两个列表来加工 Spring Security 需要权限信息,即 tempRoleList 和 tempPermissionList,可以这样子理解,角色和权限的概念,角色本身是权限的抽象集合,是协助我们开发管理的东西,真正意义的东西还是颗粒细小的权限。添个插曲,在本人最初接触到权限设计的时候,总是傻傻的被二者的关系搞晕,加上一些实际应用的系统还乐此不疲的在权限命名上"ROLE"来"ROLE"去的,甚至一些方法命名本身也在混淆这二者(怀疑可能是英语的使用习惯之类的原因),导致早先的我常常常常陷入对二者的理解困惑上,当然现在清晰的很多: 在大块儿整体性的权限控制上,角色控制为主;细化到页面小块儿、按钮级别的,权限控制为主;一般再加上访问URL的过滤鉴权,基本上一套强壮的权限控制体系是稳稳的在这儿了。

    最后注意下代码里的几个isXXX方法,这些是一些细节补充,一般默认,也可以重写控制下逻辑;紧接着我们新建一个 VUserDetailsService 类,来实现UserDetailsService,代码如下:

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);
	}

}


    这个类基本上没啥好说的,服务提供者,就是一个搬运工,看这个loadUserByUsername()方法,拿到用户基本信息、角色列表和资源权限列表后,构造一个 VUserDetails 对象,OK返回。接下来是一个小重点,我们创建一个 VAuthenticationProvider 类来实现 AuthenticationProvider,代码如下:

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;
	}

}


    这个实现类的核心就是authenticate方法,一步步看,系统会将用户在登录请求操作的时候,把输入的用户名密码等,封装到一个Authentication对象中,我们从这个对象里拿到用户名,通过 VUserDetailsService 获取到 VUserDetails 对象,然后拿这个对象的密码属性,和请求Authentication对象中获取的密码进行对比,如果一切OK,验证功过,然后再将这些所有信息,整体封装成一个Authentication对象(这里边我们用的是UsernamePasswordAuthenticationToken),交给系统框架,后期可以随时取用。

    好了,经过上面的工作,用户认证的逻辑已经完事儿了,我们要做访问工作,这里还要做些配置操作,这里分别新建俩类,代码如下:


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";
	}
}



    WebMvcConfig 是一个简单的路径映射,功能跟在 PageController中实现的差不多,之所以多写一个PageController,是因为后边会有其他的功能演示。

    然后我们还需创建一个 WebSecurityConfig 类来继承 WebSecurityConfigurerAdapter,代码如下:

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();
	}
}



    这个类是使用 Spring Security 的主配置入口,在这个配置文件中,正式启用 Spring Security 包括我们之前所讲的所有功能,这里主要留意一下负责安全策略配置的 configure()方法,这个方法里可以定义登录登出等操作细节,以及一些静态资源的权限忽略之类的,甚至也是可以直接手动配权限的。

    一切完事儿,我们运行 SecApplication ,开始验证之旅:



    在登录页面,输入用户名密码:admin/admin,登录看看,随便点点跳跳,换成veiking/veiking试试,也可以输错试试,再试下登出:





    好了,这个简单的用户认证功能看来是可以了,我们接下来看看如何控制权限。

七、重点:Spring Security之鉴权-初试

    认证OK,回想下,是否还记得,在VAuthenticationProvider的校验环节,我们在封装返回给系统的Authentication对象里,是提供了vUserDetails.getAuthorities()这个认证列表的,接下来看看,这个被交给系统的认证列表,是怎么体现的。

    我们打开 hello.html 页面,在其中的几个导航跳转的信息上,添加一个 sec:authorize="hasAuthority('XXX')" 的代码,这样子的脚本,大概意思就是,只有名为‘XXX’的角色或者权限的用户,登录之后才可以看到,如下:

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]




    上边是从页面层面来进行权限控制的,注意hasAuthority('XXX')中,有用到R_ADMIN、P_ADMIN、P_HELLO不同类型的权限字眼,包含角色和权限,这个控制的颗粒度没有绝对的,只要设计成规律可循、操作可行方案即可。

    接下来,打开 PageController,在/admin处添加标签:@PreAuthorize("hasAuthority('R_ADMIN')"),如下:

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";
	}
}



    注意,这个操作还需要在 WebSecurityConfig 类中加 @EnableGlobalMethodSecurity(prePostEnabled=true) 标签来,开启注解控制权限,然后配置 authenticationManagerBean 以供支持,代码如下:

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();
	}
}



    然后再次启动,用veiking登录,INDEX页面点击ADMIN导航 ——好,403 Forbidden了,对,被拦了,就是这个效果。

    以上这些,是简单的对 Spring Security 鉴权操作的一些尝试,当然,如果是小规模功能开发,这些是可以满足的,如果想追求更为灵活的控制,就要重新是实现下过滤机制,接下来我们就尝试下从对数据库层面的配置,实现权限的动态管理。

八、重点:Spring Security之鉴权-过滤器

    上边我们已尝试了经通过页面脚本和注解这两种方式的权限控制,接下来,我们尝试下通过数据库的权限配置,来过滤用户操作请求的。

    跟认证对应,我们新建一个包,authorization,然后在这个里面来实现过滤请求方式的鉴权:先写一个 VFilterInvocationSecurityMetadataSource 类,来实现 FilterInvocationSecurityMetadataSource,这个可以简单理解成权限资源管理器,它的工作是通过用户的请求地址,来获取访问这个地址所需的权限,代码如下:

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;
	}

}


    接着访问需要的权限资源拿到了,就得有判断的地方,新建一个 VAccessDecisionManager 来实现 AccessDecisionManager ,这个类可以理解成权限管理判断器,他的主要工作就是鉴权,通过拿到的访问路径所需的权限,和用户所拥有的权限进行对比,判断用户是否有权限访问,代码如下:

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;
	}

}


    最后,要写一个过滤器,提供上边这些功能的工作场所,创建 VFilterSecurityInterceptor 类,继承 AbstractSecurityInterceptor 并实现 Filter,这就是个鉴权过滤器,代码如下:

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;
	}

}


    这里边注意 @ServletComponentScan 和 @WebFilter(filterName="vFilterSecurityInterceptor",urlPatterns="/*") 注解,这个是确保过滤器自动注册并工作,否则过滤器无效。

    接下来启动项目,用个个用户登进去看看,尤其是veiking和xiaoming用户,访问没有权限的连接时果断遭到限制,403 Forbidden!



九、结语

    好了,经过这么一番折腾,学习应用 Spring Security 框架该接触到的一些知识点,都有所体现了。权限控制的本质,就是对比校验,其一般体现方式,就是过滤器。Spring Security 是提供了一种相对比较好的表现形式,给出了一个优良的范式,敲示例代码的本身,更重要的应该是为了帮助理解和学习,而不是为了实现而实现。

    本文是足够罗嗦,也是个人为了加深在理解记忆,但有些地方甚至也是错误的、不合乎规范的,希望大家不要被误导,这只能说是一个提供理解的参考,帮助大家从懵懂到懂;还有需要注意的是,因spring版本不同导致的一些细节差异,可能会有小坑,还是需要注意下的。文中所涉及代码最后都在附件中,感兴趣的同学可以自行下载。


    还有,喜欢的,扫下支付宝家电红包吧,哈哈哈!




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



  • 大小: 4.3 KB
  • 大小: 2.8 KB
  • 大小: 5.2 KB
  • 大小: 5 KB
  • 大小: 5.8 KB
  • 大小: 4 KB
  • 大小: 3.8 KB
  • 大小: 9.8 KB
  • 大小: 5 KB
  • 大小: 10.5 KB
  • 大小: 10.2 KB
  • 大小: 3.7 KB
  • 大小: 3.1 KB
  • 大小: 7.4 KB
  • 大小: 76.1 KB
  • sec.rar (79.2 KB)
  • 下载次数: 15
1
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics