抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

SpringBoot整合Shiro(一)

Shiro简介

​ Shiro是Apache下的一个开源项目,我们称之为Apache Shiro。它是一个很易用与Java项目的的安全框架,提供了认证、授权、加密、会话管理,与Spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。shiro属于轻量级框架,相对于security简单的多,也没有security那么复杂。所以我这里也是简单介绍一下shiro的使用。

Shiro的架构

Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。

● Authentication(认证):用户身份识别,通常被称为用户“登录”
● Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
● Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
● Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
同时Shiro还提供了其他特性来在不同的应用程序环境下使用强化以上的四大基石:
● Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
● 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
● 并发:Apache Shiro 支持多线程应用程序的并发特性。
● 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
● “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)。
● “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。

Shiro 的三大核心组件

● Subject其实代表的就是当前正在执行操作的用户,只不过因为“User”一般指代人,但是一个“Subject”可以是人,也可以是任何的第三方系统,服务账号等任何其他正在和当前系统交互的第三方软件系统。所有的Subject实例都被绑定到一个SecurityManager,如果你和一个Subject交互,所有的交互动作都会被转换成Subject与SecurityManager的交互。

● SecurityManager 是Shiro的核心,用于管理所有的Subject ,它主要用于协调Shiro内部各种安全组件,不过我们一般不用太关心SecurityManager,对于应用程序开发者来说,主要还是使用Subject的API来处理各种安全验证逻辑。

● Realm 这是用于连接Shiro和客户系统的用户数据的桥梁。一旦Shiro真正需要访问各种安全相关的数据(比如使用用户账户来做用户身份验证以及权限验证)时,他总是通过调用系统配置的各种Realm来读取数据。
正因为如此,Realm往往被看做是安全领域的DAO,他封装了数据源连接相关的细节,将数据以Shiro需要的格式提供给Shiro。当我们配置Shiro的时候,我们至少需要配置一个Realm来提供用户验证和权限控制方面的数据。我们可能会给SecurityManager配置多个Realm,但是不管怎样,我们至少需要配置一个。
Shiro提供了几种开箱即用的Realm来访问安全数据源,比如LDAP、关系数据库、基于ini的安全配置文件等等,如果默认提供的这几种Realm无法满足你的需求,那么你也可以编写自己的定制化的Realm插件。
和其他内部组件一样,SecurityManager决定了在Shiro中如何使用Realm来读取身份和权限方面的数据,然后组装成Subject实例。

Shiro完整架构

● Subject (org.apache.shiro.subject.Subject),如上所述.
● SecurityManager (org.apache.shiro.mgt.SecurityManager),如上所述.
● Authenticator(用户认证管理器), (org.apache.shiro.authc.Authenticator) 这个组件主要用于处理用户登录逻辑,他通过调用Realm的接口来判断当前登录的用户的身份。
用户认证策略,(org.apache.shiro.authc.pam.AuthenticationStrategy) 如果系统配置了多个Realm,则需要使用AuthenticationStrategy 来协调这些Realm以便决定一个用户登录的认证是成功还是失败。(比如,如果一个Realm验证成功了,但是其他的都失败了,怎么算?还是说都成功才算成功).
● Authorizer(权限管理器)(org.apache.shiro.authz.Authorizer)这个组件主要是用来做用户的访问控制。通俗来说就是决定用户能做什么、不能做什么。和Authenticator类似,Authorizer也知道怎么协调多个Realm数据源的数据,他有自己的一套策略。
● SessionManager(会话管理器) (org.apache.shiro.session.mgt.SessionManager) SessionManager知道如何创建会话、管理用户回话的声明周期以便在所有运行环境下都可以给用户提供一个健壮的回话管理体验。Shiro在任何运行环境下都可以在本地管理用户会话(即便没有Web或者EJB容器也可以)——这在安全管理的框架中算是独门绝技了。当然,如果当前环境中有会话管理机制(比如Servlet容器),则Shiro默认会使用该环境的会话管理机制。而如果像控制台程序这种独立的应用程序,本身没有会话管理机制,此时Shiro就会使用内部的会话管理器来给应用的开发提供一直的编程体验。SessionDAO允许用户使用任何类型的数据源来存储Session数据。
● SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO) 用于代替SessionManager执行Session相关的增删改查。这个接口允许我们将任意种类的数据存储方式引入到Session管理的基础框架中。
● CacheManager (org.apache.shiro.cache.CacheManager) CacheManager用于创建和维护一些在其他的Shiro组件中用到的Cache实例,维护这些Cache实例的生命周期。缓存用于存储那些从后端获取到的用户验证与权限控制方面的数据以提高性能,缓存是一等公民,在获取数据时,总是先从缓存中查找,如果没有再调用后端接口从其他数据源获取。Shiro允许用户使用其他更加现代的、企业级的数据源来替代内部的默认实现,以提供更高的性能和更好的用户体验。
● Cryptography 加密技术,(org.apache.shiro.crypto.*) 对于一个企业级的安全框架来说,加密算是其固有的一种特性。Shiro的crypto包中包含了一系列的易于理解和使用的加密、哈希(aka摘要)辅助类。这个包内的所有类都是经过精心设计,相比于java本身提供的那一套反人类的加密组件,Shiro提供的这套加密组件简直不要好用太多。
● Realm (org.apache.shiro.realm.Realm) 就如上文所提到的,Realm是连接Shiro和你的安全数据的桥梁。任何时候当Shiro需要执行登录或者访问控制的时候,都需要调用已经配置的Realm的接口去获取数据。一个应用程序可以配置一个或者多个Realm(最少配置一个)。

整合Shiro

使用springboot+mybatis+druid+thymleaf模板实现快速入门

前置工作

整合mybatis+durid

参考springboot 整合durid,springboot 整合mybatis

整合thymleaf

参考springboot 整合模板引擎

配置文件配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 数据库访问配置
# 主数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
######################### Druid连接池的配置信息 #################
#初始化连接大小
spring.druid.initialSize=5
#最小连接池数量
spring.druid.minIdle=5
#最大连接池数量
spring.druid.maxActive=20
#获取连接时最大等待时间,单位毫秒
spring.druid.maxWait=60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.druid.timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
spring.druid.minEvictableIdleTimeMillis=300000
#测试连接
spring.druid.validationQuery=SELECT 1 FROM DUAL
#申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性
spring.druid.testWhileIdle=true
#获取连接时执行检测,建议关闭,影响性能
spring.druid.testOnBorrow=false
#归还连接时执行检测,建议关闭,影响性能
spring.druid.testOnReturn=false
#是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭
spring.druid.poolPreparedStatements=false
#开启poolPreparedStatements后生效
spring.druid.maxPoolPreparedStatementPerConnectionSize=20
#配置扩展插件,常用的插件有=>stat:监控统计 log4j:日志 wall:防御sql注入
spring.druid.filters=stat,wall,log4j
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.druid.connectionProperties='druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000'

spring.redis.port=6379
spring.redis.host=127.0.0.1
# redis 数据库索引(默认为0)
spring.redis.database=0
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# reids 最大空闲连接
spring.redis.jedis.pool.max-idle=5
# 连接池最小空闲链接
spring.redis.jedis.pool.min-idle=0
# redis 超时时间(单位毫秒)
spring.redis.timeout=10000

mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.demo.entity

POM依赖导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>

数据库建表

user_info.sql(用户表)
1
2
3
4
5
6
7
8
9
10
11
12
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT '' COMMENT '用户名',
`password` varchar(256) DEFAULT NULL COMMENT '登录密码',
`name` varchar(256) DEFAULT NULL COMMENT '用户真实姓名',
`id_card_num` varchar(256) DEFAULT NULL COMMENT '用户身份证号',
`state` char(1) DEFAULT '0' COMMENT '用户状态:0:正常状态,1:用户被锁定',
PRIMARY KEY (`uid`),
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `id_card_num` (`id_card_num`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
sys_role.sql(角色表)
1
2
3
4
5
6
7
8
9
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`available` char(1) DEFAULT '0' COMMENT '是否可用0可用 1不可用',
`role` varchar(20) DEFAULT NULL COMMENT '角色标识程序中判断使用,如"admin"',
`description` varchar(100) DEFAULT NULL COMMENT '角色描述,UI界面显示使用',
PRIMARY KEY (`id`),
UNIQUE KEY `role` (`role`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
sys_user_role.sql(用户-角色表)
1
2
3
4
5
6
7
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`uid` int(11) DEFAULT NULL COMMENT '用户id',
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
KEY `uid` (`uid`) USING BTREE,
KEY `role_id` (`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
sys_permission.sql(权限表)
1
2
3
4
5
6
7
8
9
10
11
12
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`parent_id` int(11) DEFAULT NULL COMMENT '父编号,本权限可能是该父编号权限的子权限',
`parent_ids` varchar(20) DEFAULT NULL COMMENT '父编号列表',
`permission` varchar(100) DEFAULT NULL COMMENT '权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view',
`resource_type` varchar(20) DEFAULT NULL COMMENT '资源类型,[menu|button]',
`url` varchar(200) DEFAULT NULL COMMENT '资源路径 如:/userinfo/list',
`name` varchar(50) DEFAULT NULL COMMENT '权限名称',
`available` char(1) DEFAULT '0' COMMENT '是否可用0可用 1不可用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
sys_role_permission.sql(角色-权限表)
1
2
3
4
5
6
7
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
`permission_id` int(11) DEFAULT NULL COMMENT '权限id',
KEY `role_id` (`role_id`) USING BTREE,
KEY `permission_id` (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
初始化数据导入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#插入用户信息表
INSERT INTO user_info(uid,username,`password`,`name`,id_card_num) VALUES (null,'admin','123456','唐僧','133333333333333333');
INSERT INTO user_info(uid,username,`password`,`name`,id_card_num) VALUES (null,'test','123456','孙悟空','155555555555555555');
#插入用户角色表
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (null,0,'管理员','admin');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (null,0,'VIP会员','vip');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (null,1,'测试','test');
#插入用户_角色关联表
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (2,2);
#插入权限表
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (null,0,'用户管理',0,'0/','userInfo:view','menu','userInfo/view');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (null,0,'用户添加',1,'0/1','userInfo:add','button','userInfo/add');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (null,0,'用户删除',1,'0/1','userInfo:del','button','userInfo/del');
#插入角色_权限表
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);

创建实体类

用户信息
1
2
3
4
5
6
7
8
9
10
public class User {
private Integer uid;
private String username;
private String password;
private String name;
private String id_card_num;
private String state;
private Set<Role> roles = new HashSet<>();
//省略geter setter方法
}
角色信息
1
2
3
4
5
6
7
8
9
10
public class Role {
private Integer id;
private String role;
private String description;
private String available;
private Set<User> users = new HashSet<>();
private Set<Permission> permissions = new HashSet<>();
//省略geter setter方法

}
权限信息
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Permission {
private Integer id;
private Integer parent_id;
private String parent_ids;
private String permission;
private String resource_type;
private String url;
private String name;
private String available;
private Set<Role> roles = new HashSet<>();
//省略geter setter方法

}

编写mapper

UserMapper
1
2
3
4
5
6
@Mapper
public interface UserMapper {
User findByUserName(String userName);
int insert(User user);
int del(@Param("username") String username);
}
UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.mapper.UserMapper">

<!-- 查询用户信息 -->
<select id="findByUserName" resultType="com.demo.entity.User">
SELECT * FROM user_info WHERE username = #{userName}
</select>

<!-- 添加用户 -->
<!-- 创建用户 -->
<insert id="insert" parameterType="com.demo.entity.User">
<selectKey resultType="java.lang.Integer" keyProperty="uid" order="AFTER">
SELECT
LAST_INSERT_ID()
</selectKey>
insert into user_info
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="uid != null">
uid,
</if>
<if test="username != null and username != ''">
username,
</if>
<if test="password != null and password != ''">
password,
</if>
<if test="name != null and name != ''">
`name`,
</if>
<if test="id_card_num != null and id_card_num != ''">
id_card_num,
</if>
<if test="state != null and state != ''">
state,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="uid != null">
#{uid},
</if>
<if test="username != null and username != ''">
#{username},
</if>
<if test="password != null and password != ''">
#{password},
</if>
<if test="name != null and name != ''">
#{name},
</if>
<if test="id_card_num != null and id_card_num != ''">
#{id_card_num},
</if>
<if test="state != null and state != ''">
#{state},
</if>
</trim>
</insert>

<!-- 删除用户 -->
<delete id="del">
DELETE FROM user_info WHERE username = #{username}
</delete>

</mapper>

RoleMapper
1
2
3
4
@Mapper
public interface RoleMapper {
Set<Role> findRolesByUserId(@Param("uid") Integer uid);
}
RoleMapper.xml
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.mapper.RoleMapper">

<!-- 查询用户信息 -->
<select id="findRolesByUserId" resultType="com.demo.entity.Role">
SELECT r.* from sys_role r LEFT JOIN sys_user_role ur on r.id = ur.role_id where ur.uid = #{uid}
</select>

</mapper>
PermissionMapper
1
2
3
4
@Mapper
public interface PermissionMapper {
Set<Permission> findPermissionsByRoleId(@Param("roles") Set<Role> roles);
}
PermissionMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.mapper.PermissionMapper">

<!-- 查询用户权限信息 -->
<select id="findPermissionsByRoleId" resultType="com.demo.entity.Permission">
SELECT p.* from sys_permission p LEFT JOIN sys_role_permission rp on p.id = rp.permission_id WHERE rp.role_id IN
<foreach collection="roles" index="index" item="item" open="(" close=")" separator=",">
#{item.id}
</foreach>
</select>

</mapper>

Shiro 相关配置类

ShiroConfig

创建ShiroConfig.java配置类

我们需要定义一系列关于URL的规则和访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
@Configuration
public class ShiroConfig {


/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
*
* @param securityManager
* @return
*/
@Bean(name = "shirFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {

ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

//必须设置 SecurityManager,Shiro的核心安全接口
shiroFilterFactoryBean.setSecurityManager(securityManager);
//这里的/login是后台的接口名,非页面,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
//这里的/index是后台的接口名,非页面,登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面,该配置无效,并不会进行页面跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

//自定义拦截器限制并发人数,参考博客:
//LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//限制同一帐号同时在线的个数
//filtersMap.put("kickout", kickoutSessionControlFilter());
//shiroFilterFactoryBean.setFilters(filtersMap);

// 配置访问权限 必须是LinkedHashMap,因为它必须保证有序
// 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 一定要注意顺序,否则就不好使了
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//配置不登录可以访问的资源,anon 表示资源都可以匿名访问
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
//logout是shiro提供的过滤器
filterChainDefinitionMap.put("/logout", "logout");
//此时访问/userInfo/del需要del权限,在自定义Realm中为用户授权。
//filterChainDefinitionMap.put("/userInfo/del", "perms[\"userInfo:del\"]");

//其他资源都需要认证 authc 表示需要认证才能进行访问
filterChainDefinitionMap.put("/**", "authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return shiroFilterFactoryBean;
}

/**
* 配置核心安全事务管理器
*
* @param shiroRealm
* @return
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置自定义realm.
securityManager.setRealm(shiroRealm);
//配置记住我 参考博客:
//securityManager.setRememberMeManager(rememberMeManager());

//配置 redis缓存管理器 参考博客:
//securityManager.setCacheManager(getEhCacheManager());

//配置自定义session管理,使用redis 参考博客:
//securityManager.setSessionManager(sessionManager());

return securityManager;
}

/**
* 配置Shiro生命周期处理器
*
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}

/**
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
*
* @return
*/
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}

/**
* 必须(thymeleaf页面使用shiro标签控制按钮是否显示)
* 未引入thymeleaf包,Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect
*
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
Shiro内置的FilterChain
Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

anon:所有url都都可以匿名访问;
authc: 需要认证才能进行访问;
user:配置记住我或认证通过可以访问;

这几个是我们会用到的,在这里说明下,其它的请自行查询文档进行学习。

ShiroRealm

在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public class ShiroRealm extends AuthorizingRealm {

@Autowired
private UserMapper userMapper;

@Autowired
private RoleMapper roleMapper;

@Autowired
private PermissionMapper permissionMapper;

/**
* 验证用户身份
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

//获取用户名密码 第一种方式
//String username = (String) authenticationToken.getPrincipal();
//String password = new String((char[]) authenticationToken.getCredentials());

//获取用户名 密码 第二种方式
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
String password = new String(usernamePasswordToken.getPassword());

//从数据库查询用户信息
User user = this.userMapper.findByUserName(username);

//可以在这里直接对用户名校验,或者调用 CredentialsMatcher 校验
if (user == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
if ("1".equals(user.getState())) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}

//调用 CredentialsMatcher 校验 还需要创建一个类 继承CredentialsMatcher 如果在上面校验了,这个就不需要了
//配置自定义权限登录器 参考博客:

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(), getName());
return info;
}

/**
* 授权用户权限
* 授权的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>标签的时候调用的
* 它会去检测shiro框架中的权限(这里的permissions)是否包含有该标签的name值,如果有,里面的内容显示
* 如果没有,里面的内容不予显示(这就完成了对于权限的认证.)
*
* shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
* 当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
* 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。
*
* 在这个方法中主要是使用类:SimpleAuthorizationInfo 进行角色的添加和权限的添加。
* authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission());
*
* 当然也可以添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限
* authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions);
*
* 就是说如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[权限添加]");
* 就说明访问/add这个链接必须要有“权限添加”这个权限才可以访问
*
* 如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]");
* 就说明访问/add这个链接必须要有 "权限添加" 这个权限和具有 "100002" 这个角色才可以访问
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

//获取用户
User user = (User) SecurityUtils.getSubject().getPrincipal();

//获取用户角色
Set<Role> roles =this.roleMapper.findRolesByUserId(user.getUid());
//添加角色
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
for (Role role : roles) {
authorizationInfo.addRole(role.getRole());
}

//获取用户权限
Set<Permission> permissions = this.permissionMapper.findPermissionsByRoleId(roles);
//添加权限
for (Permission permission:permissions) {
authorizationInfo.addStringPermission(permission.getPermission());
}

return authorizationInfo;
}
}

编写Service

UserService
1
2
3
4
5
6
7
8
public interface UserService {
User findByUserName(String userName);

int insert(User user);

int del(@Param("username") String username);
}

UserServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class UserServiceImpl implements UserService {

private UserMapper userMapper;

@Override
public User findByUserName(String userName) {
return userMapper.findByUserName(userName);
}

@Override
public int insert(User user) {
return userMapper.insert(user);
}

@Override
public int del(String username) {
return userMapper.del(username);
}
}

RoleService
1
2
3
4
5
public interface RoleService {

Set<Role> findRolesByUserId(Integer uid);
}

RoleServiceIml
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class RoleServiceImpl implements RoleService {

@Autowired
private RoleMapper roleMapper;

@Override
public Set<Role> findRolesByUserId(Integer uid) {
return roleMapper.findRolesByUserId(uid);
}
}

PermissionService
1
2
3
4
5
public interface PermissionService {

Set<Permission> findPermissionsByRoleId(Set<Role> roles);
}

PermissionServiceImpl
1
2
3
4
5
6
7
8
9
10
11
@Service
public class PermissionServiceImpl implements PermissionService {

@Autowired
private PermissionMapper permissionMapper;

@Override
public Set<Permission> findPermissionsByRoleId(Set<Role> roles) {
return permissionMapper.findPermissionsByRoleId(roles);
}
}

编写Controller

LoginController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@Controller
public class LoginController {

/**
* 访问项目根路径
* @return
*/
@RequestMapping(value = "/",method = RequestMethod.GET)
public String root(Model model) {
Subject subject = SecurityUtils.getSubject();
User user=(User) subject.getPrincipal();
if (user == null){
return "redirect:/login";
}else{
return "redirect:/index";
}

}


/**
* 跳转到login页面
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String login(Model model) {
Subject subject = SecurityUtils.getSubject();
User user=(User) subject.getPrincipal();
if (user == null){
return "login";
}else{
return "redirect:index";
}

}

/**
* 用户登录
* @param request
* @param username
* @param password
* @param model
* @param session
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
public String loginUser(HttpServletRequest request, String username, String password, Model model, HttpSession session) {

//对密码进行加密
//password=new SimpleHash("md5", password, ByteSource.Util.bytes(username.toLowerCase() + "shiro"),2).toHex();
//如果有点击 记住我
//UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password,remeberMe);
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try {
//登录操作
subject.login(usernamePasswordToken);
User user=(User) subject.getPrincipal();
//更新用户登录时间,也可以在ShiroRealm里面做
session.setAttribute("user", user);
model.addAttribute("user",user);
return "index";
} catch(Exception e) {
//登录失败从request中获取shiro处理的异常信息 shiroLoginFailure:就是shiro异常类的全类名
String exception = (String) request.getAttribute("shiroLoginFailure");
model.addAttribute("msg",e.getMessage());
//返回登录页面
return "login";
}
}

@RequestMapping("/index")
public String index(HttpSession session, Model model) {
Subject subject = SecurityUtils.getSubject();
User user=(User) subject.getPrincipal();
if (user == null){
return "login";
}else{
model.addAttribute("user",user);
return "index";
}
}

/**
* 登出 这个方法没用到,用的是shiro默认的logout
* @param session
* @param model
* @return
*/
@RequestMapping("/logout")
public String logout(HttpSession session, Model model) {
Subject subject = SecurityUtils.getSubject();
subject.logout();
model.addAttribute("msg","安全退出!");
return "login";
}
}
UserController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@RestController
@RequestMapping("userInfo")
public class UserController {

@Autowired
private UserService userService;

/**
* 创建固定写死的用户
* @param model
* @return
*/
@RequestMapping(value = "/add",method = RequestMethod.GET)
@ResponseBody
public String login(Model model) {

User user = new User();
user.setName("王赛超");
user.setId_card_num("177777777777777777");
user.setUsername("baiyp");

userService.insert(user);

return "创建用户成功";

}

/**
* 删除固定写死的用户
* @param model
* @return
*/
@RequestMapping(value = "/del",method = RequestMethod.GET)
@ResponseBody
public String del(Model model) {

userService.del("baiyp");

return "删除用户名为baiyp用户成功";

}

@RequestMapping(value = "/view",method = RequestMethod.GET)
@ResponseBody
public String view(Model model) {

return "这是用户列表页";

}
}
UnifiedErrorController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 统一错误处理controller
*/
@Controller
public class UnifiedErrorController {

/**
* 跳转到无权限页面
*
* @param session
* @param model
* @return
*/
@RequestMapping("/unauthorized")
public String unauthorized(HttpSession session, Model model) {
return "unauthorized";
}


/**
* 跳转到无权限页面
*
* @param session
* @param model
* @return
*/
@RequestMapping("/404")
public String error404(HttpSession session, Model model) {
return "404";
}

/**
* 跳转到无权限页面
*
* @param session
* @param model
* @return
*/
@RequestMapping("/500")
public String error500(HttpSession session, Model model) {
return "500";
}
}

新建页面

login.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8"/>
<title>Insert title here</title>
</head>
<body>
<h1>欢迎登录</h1>
<h1 th:if="${msg != null }" th:text="${msg}" style="color: red"></h1>
<form action="/login" method="post">
用户名:<input type="text" name="username"/><br/>
&nbsp;&nbsp;码:<input type="password" name="password"/><br/>
<input type="submit" value="提交"/>
</form>
</body>
</html>

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8"/>
<title>Insert title here</title>
</head>
<body>
<h1 th:text="'欢迎' + ${user.username } + '光临!请选择你的操作'"></h1><br/>
<ul>
<h1 th:if="${msg != null }" th:text="${msg}" style="color: red"></h1>

<shiro:hasPermission name="userInfo:add"><a href="/userInfo/add">点击添加固定用户信息(后台写死,方便测试)</a></shiro:hasPermission>
<br/>
<shiro:hasPermission name="userInfo:del"><a href="/userInfo/del">点击删除固定用户信息(后台写死,方便测试)</a></shiro:hasPermission>
<br/>
<shiro:hasPermission name="userInfo:view"><a href="/userInfo/view">显示此内容表示拥有查看用户列表的权限</a></shiro:hasPermission>
<br/>


<!-- 用户没有身份验证时显示相应信息,即游客访问信息 -->
<shiro:guest>游客显示的信息</shiro:guest>
<br/>
<!-- 用户已经身份验证/记住我登录后显示相应的信息 -->
<shiro:user>用户已经登录过了</shiro:user>
<br/>
<!-- 用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的 -->
<shiro:authenticated>不是记住我登录</shiro:authenticated>
<br/>
<!-- 显示用户身份信息,通常为登录帐号信息,默认调用Subject.getPrincipal()获取,即Primary Principal -->
<shiro:principal></shiro:principal>
<br/>
<!--用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证,与guest标签的区别是,该标签包含已记住用户 -->
<shiro:notAuthenticated>已记住用户</shiro:notAuthenticated>
<br/>
<!-- 相当于Subject.getPrincipals().oneByType(String.class) -->
<shiro:principal type="java.lang.String"/>
<br/>
<!-- 相当于((User)Subject.getPrincipals()).getUsername() -->
<shiro:principal property="username"/>
<br/>
<!-- 如果当前Subject有角色将显示body体内容 name="角色名" -->
<shiro:hasRole name="admin">这是admin角色</shiro:hasRole>
<br/>
<!-- 如果当前Subject有任意一个角色(或的关系)将显示body体内容。 name="角色名1,角色名2..." -->
<shiro:hasAnyRoles name="admin,vip">用户拥有admin角色 或者 vip角色</shiro:hasAnyRoles>
<br/>
<!-- 如果当前Subject没有角色将显示body体内容 -->
<shiro:lacksRole name="admin">如果不是admin角色,显示内容</shiro:lacksRole>
<br/>
<!-- 如果当前Subject有权限将显示body体内容 name="权限名" -->
<shiro:hasPermission name="userInfo:add">用户拥有添加权限</shiro:hasPermission>
<br/>
<!-- 用户同时拥有以下两种权限,显示内容 -->
<shiro:hasAllPermissions name="userInfo:add,userInfo:view">用户同时拥有列表权限和添加权限</shiro:hasAllPermissions>
<br/>
<!-- 用户拥有以下权限任意一种 -->
<shiro:hasAnyPermissions name="userInfo:view,userInfo:del">用户拥有列表权限或者删除权限</shiro:hasAnyPermissions>
<br/>
<!-- 如果当前Subject没有权限将显示body体内容 name="权限名" -->
<shiro:lacksPermission name="userInfo:add">如果用户没有添加权限,显示的内容</shiro:lacksPermission>
<br/>
</ul>
<a href="/logout">点我注销</a>
</body>
</html>
unauthorized.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8"/>
<title>Insert title here</title>
</head>
<body>
<h1>对不起,您没有权限</h1>
</body>
</html>

500.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8"/>
<title>Insert title here</title>
</head>
<body>
<h1>对不起,服务器报错了</h1>
</body>
</html>

404.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8"/>
<title>Insert title here</title>
</head>
<body>
<h1>对不起,你的页面没有找到</h1>
</body>
</html>

进行身份验证测试

第一步: 访问http://localhost:8080/userInfo/add 发现自动跳转到登录页

第二步:使用admin登录

第三步:注销之后使用test登录

不同的用户登录,显示不同的功能,点击之后也可以调用后台服务,证明身份验证成功。

评论