1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > springBoot+security+mybatis 实现用户权限的数据库动态管理

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

时间:2021-06-21 02:10:28

相关推荐

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

[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 框架进行管理,供后边有需要的时候随时取用。[/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="/POM/4.0.0" xmlns:xsi="/2001/XMLSchema-instance"

xsi:schemaLocation="/POM/4.0.0 /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>

<!-- /artifact/com.google.code.gson/gson -->

<dependency>

<groupId>com.google.code.gson</groupId>

<artifactId>gson</artifactId>

<version>2.8.5</version>

</dependency>

<!-- /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相关的一些路径配置,以及一些数据源待用的参数,这个文件的配置参数会被系统默认加载,需要时直接取用,很方便。然后在主路径下创建一个含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]/upload/attachment/0130/5484/14df6d4c-a972-37af-897a-4f6763a43f90.png[/img]

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

[img]/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]/upload/attachment/0130/5488/ad42d585-2349-3eff-be56-15715c3a16f9.png[/img]

s_role

[img]/upload/attachment/0130/5490/395d1b9f-96a4-320a-a865-075280d43027.png[/img]

s_permission

[img]/upload/attachment/0130/5492/f3b345f7-f076-38c0-b5bc-89d84c7265f6.png[/img]

s_user_role

[img]/upload/attachment/0130/5494/8b6413cd-1684-3fdf-96c4-6a7e456fa744.png[/img]

s_role_permission

[img]/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="">

<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="/1999/xhtml"

xmlns:th=""

xmlns:sec="/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实现较为复杂逻辑的脚本,但是在相对简单逻辑的操作上,直接用注解的方式是清爽的不能再清爽;两者在实际运用中是等效的,也是可以一同使用。[/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 maSeparatedStringToAuthorityList(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类型实现这个权限。[/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.ponent;

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()方法,这个方法里可以定义登录登出等操作细节,以及一些静态资源的权限忽略之类的,甚至也是可以直接手动配权限的。[/size]

[size=medium] 一切完事儿,我们运行 SecApplication ,开始验证之旅:[/size]

[img]/upload/attachment/0130/5498/a7d068c6-abd4-37b7-a5fd-b643c5f05115.png[/img]

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

[img]/upload/attachment/0130/5500/7916b4d5-9123-3981-b86b-1fb35486d8ce.png[/img]

[img]/upload/attachment/0130/5502/990e7216-b53d-3b12-ab16-b44b1312d088.png[/img]

[img]/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="/1999/xhtml"

xmlns:th=""

xmlns:sec="/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="" 、 xmlns:sec="/thymeleaf-extras-springsecurity4" 等约束规范)重新启动后,分别用不同的用户,登录后跳转到hello页面查看,这时候可以看到,admin用户拥有较多权限,都可以看到, veiking 用户只能看到index和hello导航,而 xiaoming 用户什么都看不到了,并且他们都不能看到注销按钮,就是这个效果:[/size]

[img]/upload/attachment/0130/5506/932eef0f-1a57-344b-be21-31300a9c4337.png[/img]

[img]/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] 上边我们已尝试了经通过页面脚本和注解这两种方式的权限控制,接下来,我们尝试下通过数据库的权限配置,来过滤用户操作请求的。[/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.ponent;

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

/**

* 权限管理判断器|校验用户是否有权限访问请求资源

* @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.ponent;

/**

* 访问鉴权过滤器

* 该过滤器的作用就是,用户请求时,提供权限资源管理器和权限判断器工作的场所,实现鉴权操作

* @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]/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版本不同导致的一些细节差异,可能会有小坑,还是需要注意下的。文中所涉及代码最后都在附件中,感兴趣的同学可以自行下载。[/size]

[size=medium] 还有,喜欢的,扫下支付宝家电红包吧,哈哈哈![/size]

[img]/upload/attachment/0130/5513/25e7d164-24ca-3621-a0de-388ac2eacda5.jpg[/img]

代码附件:

[url]/upload/attachment/0130/5515/3b1eeefa-fa64-3dd2-97c0-753bd96c1acc.rar[/url]

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。