Shior了解与使用
简单了解
Apache Shiro是一个功能强大且易于使用的Java安全框架,用于执行身份验证,授权,加密和会话管理。使用Shiro易于理解的API,您可以快速且轻松地保护任何应用程序,从最小的移动应用程序到最大的Web和企业应用程序。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等,也支持单点登录、OAuth2 规范和用户并发登录管理功能,是一个比较完备的安全框架, 而且 Shiro 的 API 也非常简单。其基本功能点如下图所示:
Shiro 概念模型
关于软件的安全,首先有如下几个概念需要理解和区分(基本上都会在项目中使用):
Authentication: 认证 / 登录,检查用户是否有相应的身份。
Authorization: 授权,即权限验证,验证某个已认证的用户是否拥有某个权限,判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某 个资源是否具有某个权限。
Session Manager: 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的。
Cryptography: 密码模块(加密),Shiro 提高了一些常见的加密组件用于密码加密 / 解密。
除上述几个概念,Shiro 还有几个重要的对象需要了解。
Authenticator: 认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的 不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认 证通过了。
Authorizer: 授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控 制着用户能访问应用中的哪些功能。
CacheManager: 缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基 本上很少去改变,放到缓存中后可以提高访问的性能。
Shiro 框架工作方式以及对外的 API 简单了解
从应用程序的角度来看 ,Shiro 框架的工作方式为:
可以看到:
应用代码直接交互的对象是 Subject,也就是说 Shiro 框架的对外 API 核心就是 Subject
三大核心组件
-
Subject: 主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互 的任何东西都是 Subject(如网络爬虫,机器人等),即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;
-
SecurityManager: 安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互; 且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
-
Realm : 域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户数据进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限数据进行验证用户是否能进行操作。因此,可以通俗的把 Realm 看成 DataSource,即安全数据源。Realm 的实现方式可以有很多种,比如 JDBC、ini 文件、LDAP 或者内存实现。
认证( Authentication )
在 Shiro 中,认证指的是识别和证明操作者是一个合法用户。“用户”这个概念在框架中被抽象为主体(Subject)的概念。用户如果想要通过认证,需要提供 Principal (身份)和 Credentials(凭证),从而应用能验证用户身份。这些身份和凭证信息,在 Shiro 框架中以 Token (口令)的概念进行封装。Shiro 中的认证 Token 接口 AuthenticationToken 中封装了这两个信息。
Principal: 身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 手机号。
Credentials: 凭证 / 证明,即只有主体知道的安全值,如密码 / 数字证书等。
身份和凭证是两个比较抽象的概念,如果按简单项目中的概念说通俗一点,其实就是用户 名 / 密码。
基本认证步骤
快速上手
1.创建项目(Maven)
2. 添加依赖
该依赖为整合依赖,添加完成后,在后面的操作就可以不用添加新的依赖了
<dependencies>
<!--servlet-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!--springmvc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.1.6.RELEASE</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<!--log-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<!--添加tomcat插件-->
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</build>
</project>
3. 编写 shiro.ini 文件(resources文件夹下)
# 创建三个用户
[users]
# 用户账号=密码,角色(可以是多个)
admin=123456,admin
laoyang=12345,seller
heizhu=123,user
# 对角色权限进行配置
[roles]
# 角色=权限(*表示所有的权限)
admin=*
# user:* 表示拥有对员工的所有操作
seller=user:*
# user:query 只能做查询操作
user=user:query
[main]
# 如果没有登录则需要跳转至login页面
shiro.loginUrl = /user/login
# 如果用户权限不足则跳转至error页面
#shiro.unauthorizedUrl=/user/error
[urls]
# 路径=过滤器别名
# (anon表示不登录也可以访问 “/user/login” 这个路径)
# (authc表示必须要登录才可以访问)
/user/login = anon
# 表示必须要登录,然后能够访问 “/user/delete” 路径的角色为admin(其它角色无法访问,写多个角色会报错!)
#/user/delete = authc,roles["admin"]
4. 单元测试(控制台输出)
配置完 shiro.ini 文件后即可开始测试
注:在导包的时候一定要看清楚,导的包一定要是shiro的!!!
@SpringBootTest
public class ShiorTests {
//认证(这里使用laoyang来测试(角色为:seller;权限为:user:*)
@Test
public void test() {
//获取SecurityManager工厂,需要借助shior.ini文件进行初始化SecurityManager
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shior.ini");
//通过SecurityManager工厂拿到SecurityManager的实例
SecurityManager securityManager = factory.getInstance();
//把securityManager实例托管到SecurityUtiles工具类中,需要使用时直接调用SecurityUtils即可
SecurityUtils.setSecurityManager(securityManager);
//通过SecurityUtils工具类获得Subject,完成这一步后即可执行除加密以外的事了(比如:认证,会话管理,授权)
Subject subject = SecurityUtils.getSubject();
/**
* subject.isAuthenticated():是否登录
* subject.login():登录
* subject.logout():退出
*/
if (!subject.isAuthenticated()) {
try {
//构建身份令牌(这里的账号和密码就是从ini文件中获取的)
UsernamePasswordToken token = new UsernamePasswordToken("laoyang", "12345");
//登录(没有返回值,所以只要没有报错就表示登录成功,报错则表示登录失败)
subject.login(token);
//查看登录的用户名
System.err.println("----->用户名:"+subject.getPrincipal());
//角色效验(查看登录的用户中是否有对应的角色)
System.err.println("----->是否包含该角色:"+subject.hasRole("admin"));
//权限效验(查看登录的用户是否有对应的权限)
System.err.println("----->是否有该权限:"+subject.isPermitted("user:abc"));
//查看是否登录(true,false)
System.err.println("---->是否登录:"+subject.isAuthenticated());
//退出登录
subject.logout();
} catch (UnknownAccountException uae) {
System.err.println("账号输入错误");
} catch (IncorrectCredentialsException ice) {
System.err.println("密码输入错误");
}
}
}
}
此处只使用了验证账号和密码的异常,其实还有很多,比如:
-
UnsupportedTokenException:身份令牌异常,不支持的身份令牌
-
UnknownAccountException:未知账户/没找到帐号,登录失败
-
LockedAccountException:帐号锁定
-
DisabledAccountException:账户禁用
-
ConcurrentAccessException:用户多次登录异常
-
AccountException:账户异常
-
ExpiredCredentialsException:过期的凭据异常
-
IncorrectCredentialsException:密码错误,登录失败
-
CredentialsException:凭据异常
-
AuthenticationException:认证的父类
…
编写Web程序
快速上手
-
添加依赖(因为刚才那个依赖是整合的,所以可以省去这一个步骤,如果是重新创了项目的话可以在copy一次)
-
创建并编写 springmvc-servlet.xml 配置文件(resources文件夹下)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> <!-- 开启注解驱动 --> <mvc:annotation-driven></mvc:annotation-driven> <!-- 组件扫描 --> <context:component-scan base-package="cn.bdqn.shiro.controller"/> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <!-- 配置前后缀 --> <property name="prefix" value="/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 注册异常解析器 --> <bean class="cn.bdqn.shiro.config.MyExceptionResolver" /> </beans>
-
配置前端控制器(web.xml)
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 配置前端控制器 --> <servlet> <!-- dispatcherServlet:自己取的名称,可随意定义 --> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 加载指定配置文件 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc-servlet.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
-
编写前台页面(.jsp)
用户登录页面: login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>用户登录</title> </head> <body> <form action="/user/doLogin" method="post"> 用户名:<input type="text" name="usrName"> <br/> 密码:<input type="password" name="usrPassword"> <br/> <input type="submit" value="登录"> </form> </body> </html>
主页面: main.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>主页</title> </head> <body> <h1>登录成功!欢迎您ヾ(๑╹◡╹)ノ"</h1> </body> </html>
权限不足/错误页面: error.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>错误页面</title> </head> <body> 权限不足 </body> </html>
-
配置 ShiroFiter 过滤器(web.xml)
将以下代码放进 web.xml 文件中即可:
<!-- ShiroFilter过滤器,在启动项目的时候,会去初始化Shiro环境 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 在启动项目的时候,会去加载web-info或者classpath下的shiro.ini配置文件,然后构建WebSecurityManager --> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener>
-
编写控制层(controller)
配置完 web.xml 、spring-servlet.xml、过滤器后即可省略前三个步骤直接使用 Subject
@Controller @RequestMapping("/user") public class UserController { //跳转至用户登录页面 @RequestMapping(value = "/login",method = RequestMethod.GET) public String Login() { return "login"; } //登录验证 @RequestMapping(value = "/doLogin",method = RequestMethod.POST) public String toLogin(String usrName, String usrPassword) { /** * 在web.xml文件中配置了shiroFilter之后,程序启动的时候就已经帮我们把Shior的环境构建好了 * 就是意味着SecurityManager实例已经构建,并且成功托管到了SecurityUtils工具类中 */ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(usrName, usrPassword); subject.login(token); return "main"; } }
运行 Tomcat 进行测试:
Shiro标签
Shiro 的标签类似于 JSTL 的 c:if 标签
使用前也需要先引入头部声明:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
常用标签
1.游客:表示不需要登录也可以访问的路径和可执行的操作
<shiro:guest>
游客访问 <a href = "login.jsp"></a>
</shiro:guest>
2.获取用户名
欢迎您:<shiro:principal/>
3.已登录(不会记住我!)
<shiro:authenticted>
欢迎您:<shiro:principal/>
</shiro:authenticted>
4.未登录
<shiro:notAuthenticated>
您还未登录!
</shiro:notAuthenticated>
5.记住我(默认过期时间为365天)
<shiro:user>
欢迎您:<shiro:principal/>登录
</shiro:user>
6.判断是指定角色
<shiro:hashRole name = "admin">
用户<shiro:principal/>拥有角色admin
</shiro:hashRole>
7.判断不是指定角色
<shiro:lacksRole name = "admin">
用户<shiro:pricipal/>没有角色admin
</shiro:lacksRole>
8.判断是其中某一个角色
<shiro:hasAnyRoles name = "admin,seller">
用户<shiro:pricipal/>拥有角色admin 或者 seller
</shiro:hasAnyRoles>
9.判断拥有指定权限
<shiro:hashPermission name = "user:query">
用户<shiro:pricipal/>拥有权限user:query
</shiro:hashPermission>
10.判断不拥有指定权限
<shiro:lacksPermission name = "org:query">
用户<shiro:pricipal/>没有权限org:query
</shiro:lacksPermission>
...还有很多,这里就不一一举例了,有兴趣的小伙伴可以去网上找一找
Shiro加密
Shiro 加密支持 Hash加密,经常使用的不可逆加密就是 md5 和 sha 两种方式。
密码类型分为以下几种:
-
明文: 用户知道的密码,比如用户输入的是123,那么存入数据库的也就是123
-
密文: 用户不知道的面,比如用户输入的是123,但是存入数据库的是ICy5YqxZB1uWSwcVLSNLcA==
-
可逆加密: 明文加密后变为密文,根据密文可以推算出明文(说白了就是别人可以知道你的密码是什么),这就是可逆加密(一般常用于注册机),不是很安全。比如一个网站中有一万个用户,只要有一个用户的密文算法被破解,那么这一万个用户的密码就已经算是被破解了!(所以现在一般不会使用这种类型)
-
不可逆加密: 明文加密后变为密文,无法从密文推算出明文,比较安全(推荐使用)!
快速上手
// Shiro加密
public class Encryption {
public static void main(String[] args) {
//明文密码
String password = "123";
/**
* md5加密
* toString():十进制,比toBase64长一点
* toBase64():base64的编码格式,比toString短一点
*/
//方式一:
String pass1 = new Md5Hash(password).toString();
System.err.println("明文:"+password+",密文:"+pass1);
String pass2 = new Md5Hash(password).toBase64();
System.err.println("明文:"+password+",密文:"+pass2);
//方式二(在明文密码的基础上加点盐,以此来提高明文密码的复杂度):
//盐
String salt = UUID.randomUUID().toString();
//加密,加了盐之后会更加复杂,但是查看时看不出什么区别(类似于先加上了一个值在进行加密:pass+=salt)
String pass3 = new Md5Hash(password,salt).toBase64();
System.err.println("明文:"+password+",密文:"+pass3);
//方式三(在方式二的基础之上循环加密,从而提高加密的复杂度):
//循环加密次数
int count = 1000;
//加密,先把明文加上盐,然后拿到的密文就是加密过一次的密文...以此类推1000次
String pass4 = new Md5Hash(password,salt,count).toBase64();
System.err.println("明文:"+password+",密文:"+pass4);
/**
* sha 加密
* 与 md5 差不多
* sha 加密的话要注意中间的数字,数字越大密文复杂度越高(建议不要写太小)
*/
String pass5 = new Sha512Hash(password,salt,count).toBase64();
System.err.println("明文:"+password+",密文:"+pass5);
String pass6 = new Sha256Hash(password,salt,count).toString();
System.err.println("明文:"+password+",密文:"+pass6);
}
}
SpringBoot + Shiro 整合
前面是使用 Maven 项目编写 Shiro 的认证和加密等操作,但是看起来比较复杂,所以使用 SpringBoot 整合 Shiro 来实现不写配置文件实现这些操作!
快速上手
实现步骤:
- 创建项目(SpringBoot)
-
添加相关依赖(pom.xml)
<dependencies> <!-- redis依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- shiro+redis缓存插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>2.4.2.1-RELEASE</version> </dependency> <!-- 工具包(比如连接池) --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- jedis依赖(不加的话会导致redis权限不足而报错) --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!-- thymeleaf-shiro --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency> <!-- Mybatisplus依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.1</version> </dependency> <!-- shiro依赖,也是需要shiro-web和shiro-core,shiro-spring需要依赖上面的两个jar包 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.18</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
-
创建数据库和表(Mysql 5.*)
-- 用户表 DROP TABLE IF EXISTS `shiro_user`; CREATE TABLE `shiro_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(20) NOT NULL, `password` varchar(255) NOT NULL, `salt` varchar(255) NOT NULL, PRIMARY KEY (`user_id`), UNIQUE KEY `user_name` (`user_name`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `shiro_user`(`user_id`,`user_name`,`password`,`salt`) values (1,'laoyang','123456','123'); -- 角色表 DROP TABLE IF EXISTS `shiro_role`; CREATE TABLE `shiro_role` ( `role_id` int(11) NOT NULL AUTO_INCREMENT, `role_name` varchar(20) NOT NULL, PRIMARY KEY (`role_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `shiro_role`(`role_id`,`role_name`) values (1,'seller'); -- 权限表 DROP TABLE IF EXISTS `shiro_authority`; CREATE TABLE `shiro_authority` ( `authority_id` int(11) NOT NULL AUTO_INCREMENT, `authority_name` varchar(20) NOT NULL, PRIMARY KEY (`authority_id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; insert into `shiro_authority`(`authority_id`,`authority_name`) values (1,'user:query'),(2,'user:add'),(3,'user:delete'); -- 用户角色表 DROP TABLE IF EXISTS `shiro_user_role`; CREATE TABLE `shiro_user_role` ( `user_id` int(11) DEFAULT NULL, `role_id` int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; insert into `shiro_user_role`(`user_id`,`role_id`) values (1,1); -- 角色权限表 DROP TABLE IF EXISTS `shiro_role_authority`; CREATE TABLE `shiro_role_authority` ( `role_id` int(11) DEFAULT NULL, `authority_id` int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; insert into `shiro_role_authority`(`role_id`,`authority_id`) values (1,1),(1,2),(1,3);
-
编写 application.properties 配置文件(resources)
# 数据源 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/boot spring.datasource.username=root spring.datasource.password=123456 # 关闭thyeleaf的缓存 spring.thymeleaf.cache=false
-
编写业务类(entity、dao、service、service impl)
实体类(entity):
用户表:shiro_user
@Data @AllArgsConstructor @NoArgsConstructor @TableName("shiro_user") public class ShiroUser implements Serializable { @TableId(value = "user_id", type = IdType.AUTO) private Integer userId; @TableField(value = "user_name") private String userName; private String password; private String salt; }
角色表:shiro_role
@Data @AllArgsConstructor @NoArgsConstructor @TableName("shiro_role") public class ShiroRole implements Serializable { @TableId(value = "role_id", type = IdType.AUTO) private Integer roleId; @TableField(value = "role_name") private String roleName; }
权限表:shiro_authorityService
@Data @AllArgsConstructor @NoArgsConstructor @TableName("shiro_authority") public class ShiroAuthority implements Serializable { @TableId(value = "authority_id", type = IdType.AUTO) private Integer authorityId; @TableField(value = "authority_name") private String authorityName; }
数据库访问层(dao):
写sql的时候千万要注意空格!!!
/** * (ShiroUser)表数据库访问层 */ @Mapper public interface ShiroUserDao extends BaseMapper<ShiroUser> { // 认证:根据用户名查找对应的用户信息(因为继承了BaseMapper所以可以直接使用提供的方法实现) }
/** * (ShiroRole)表数据库访问层 */ @Mapper public interface ShiroRoleDao extends BaseMapper<ShiroRole> { //授权:根据用户名查找对应的角色名 @Select("SELECT role_name FROM shiro_role r " + "INNER JOIN shiro_user_role ur on r.role_id = ur.role_Id " + "INNER JOIN shiro_user u on u.user_id = ur.user_id " + "WHERE u.user_name=#{userName}") public Set<String> findRoleByUserName(@Param("userName") String userName); }
/** * (ShiroAuthority)表数据库访问层 */ @Mapper public interface ShiroAuthorityDao { //授权:根据用户查找对应的权限名 @Select("SELECT authority_name FROM shiro_authority a " + "INNER JOIN shiro_role_authority ra on a.authority_id = ra.authority_id " + "INNER JOIN shiro_role r on r.role_id = ra.role_id " + "INNER JOIN shiro_user_role ur on r.role_id = ur.role_id " + "INNER JOIN shiro_user u on u.user_id = ur.user_id " + "WHERE u.user_name=#{userName}") public Set<String> findAuthorityByUserName(@Param("userName") String userName); }
PS: 使用Set的原因:因为使用list在传值的时候可能会传不过去
业务逻辑层(service):
/** * (ShiroUser)表服务接口 */ public interface ShiroUserService { //根据用户名查询对应用户 public ShiroUser getfindByuserName(String userName); //注册 public int getRegister(ShiroUser user); }
/** * (ShiroRole)表服务接口 */ public interface ShiroRoleService { //授权:根据用户名查找对应的角色名 public Set<String> getfindRoleByUserName(String userName); }
/** * (ShiroAuthority)表服务接口 */ public interface ShiroAuthorityService { //授权:根据用户查找对应的权限名 public Set<String> getfindAuthorityByUserName(String userName); }
业务逻辑层(service impl):
/** * (ShiroUser)表服务实现类 */ @Service public class ShiroUserServiceImpl implements ShiroUserService { @Resource private ShiroUserDao shiroUserDao; //根据用户名查询对应用户 @Override public ShiroUser getfindByuserName(String userName) { QueryWrapper<ShiroUser> queryWrapper = new QueryWrapper<>(); //第一个值是数据库字段名,第二个是参数名 queryWrapper.eq("user_name", userName); return shiroUserDao.selectOne(queryWrapper); } //注册 @Override public int getRegister(ShiroUser user) { //迭代次数 int count = 1000; //获得盐 String salt = UUID.randomUUID().toString(); user.setSalt(salt); //加密,toBase64 或 toString都可以 String password = new Md5Hash(user.getPassword(), salt, count).toBase64(); user.setPassword(password); return shiroUserDao.insert(user); } }
/** * (ShiroRole)表服务实现类 */ @Service public class ShiroRoleServiceImpl implements ShiroRoleService { @Resource private ShiroRoleDao shiroRoleDao; //授权:根据用户名查找对应的角色名 @Override public Set<String> getfindRoleByUserName(String userName) { return shiroRoleDao.findRoleByUserName(userName); } }
/** * (ShiroAuthority)表服务实现类 */ @Service public class ShiroAuthorityServiceImpl implements ShiroAuthorityService { @Resource private ShiroAuthorityDao shiroAuthorityDao; //授权:根据用户查找对应的权限名 @Override public Set<String> getfindAuthorityByUserName(String userName) { return shiroAuthorityDao.findAuthorityByUserName(userName); } }
-
编写自定义 Realm(config)
SecurityManager 默认的 Realm 是IniRealm,这个Realm只能去加载 shiro.ini 文件中的数据,但是这个项目中并没有这个文件,而是把这个文件的用户、角色、权限等信息都存入到了数据库中,所以 IniRealm 就已经没有意义了,需要自己定义一个去查询数据库的 Realm ,然后在注入给 SecurityManager。/** * 自定义Realm * 继承AuthorizingRealm,然后实现里面的两个方法 */ public class MyRealm extends AuthorizingRealm { @Resource private ShiroUserService shiroUserService; @Resource private ShiroRoleService shiroRoleService; @Resource private ShiroAuthorityService shiroAuthorityService; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //做授权需要先知道是谁登录了(登录之后拿到的用户名) String userName = (String) principalCollection.getPrimaryPrincipal(); //调用业务逻辑层,根据用户名查询对应角色和权限 Set<String> roles = shiroRoleService.getfindRoleByUserName(userName); Set<String> auths = shiroAuthorityService.getfindAuthorityByUserName(userName); //获得角色 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(roles); //获得权限 authorizationInfo.setStringPermissions(auths); return authorizationInfo; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //获取用户登录的用户名 String userName = (String) authenticationToken.getPrincipal(); //调用业务逻辑层(根据用户名查询对应用户) ShiroUser user = shiroUserService.getfindByuserName(userName); if (user == null) { //因为程序是先验证用户名然后在验证密码,所以当用户名为空或错误时就可直接结束程序 return null; } //因为传入进来的盐是String类型所以需要使用ByteSource.Util.bytes()的方式 return new SimpleAuthenticationInfo(user.getUserName(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName()); } }
-
编写 Shiro 配置类(config)
/** * Shiro配置类 */ @Configuration public class ShiroConfig { //注册自定义Realm,把自定义的Realm加入到Bean容器中 @Bean public MyRealm myRealm() { MyRealm myRealm = new MyRealm(); //把自定义的密码比对器注册给Realm myRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myRealm; } //Filter工厂,设置对应的过滤条件和跳转条件 @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { //创建Filter工厂 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //Shiro.ini中的 [urls] //设置条件 Map<String, String> map = new HashMap<>(); //登出 map.put("/user/logout", "logout"); //匿名登录 map.put("/user/login", "anon"); //需要登录才能访问的路径:main map.put("/user/main", "authc"); //删除,只有 seller 才可以执行(注:这里的中括号格式不要加单引号,否则识别不出来!) map.put("/user/delete", "roles[seller]"); //还可以加其他的...比如:不登录也可以访问的路径 map.put("/user/register", "anon"); // map.put("/*","authc"); 这个可以看情况来写,因为范围比较广 //Shiro.ini文件中的 [main] //配置未登录时跳转的路径 shiroFilterFactoryBean.setLoginUrl("/user/login"); //角色权限不足时跳转的路径 shiroFilterFactoryBean.setUnauthorizedUrl("/user/error"); //退出跳转的路径 shiroFilterFactoryBean.setSuccessUrl("/user/login"); //设置过滤(map条件) shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } }
-
实现用户注册功能
注册页面(register.html):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用户注册</title> </head> <body> <form action="/user/toRegister" method="post"> <table border="1"> <tr> <th colspan="2"> <h1>用户注册</h1> </th> </tr> <tr> <th>用户名:</th> <td> <input type="text" name="userName"> </td> </tr> <tr> <th>密码:</th> <td> <input type="password" name="password"> </td> </tr> <tr> <th colspan="2"> <input type="submit" value="注册"> </th> </tr> </table> </form> </body> </html>
登录页面(login.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用户登录</title> </head> <body> <form action="/user/toLogin" method="post"> <table border="1"> <tr> <th colspan="2"> <h1>登录</h1> </th> </tr> <tr> <th>用户名:</th> <td> <input type="text" name="userName"> </td> </tr> <tr> <th>密码:</th> <td> <input type="password" name="password"> </td> </tr> <tr> <th colspan="2"> <input type="submit" value="登录"> </th> </tr> </table> </form> </body> </html>
主页面(mian.html)
<!DOCTYPE html> <!-- 添加 thymeleaf 和 shiro 的头部声明 xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" --> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <meta charset="UTF-8"> <title>主页</title> </head> <body> <h1> 登录成功,欢迎您! </h1> </body> </html>
错误提示页面(error.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>页面访问失败</title> </head> <body> <h1>账号或密码错误/权限不足,请重新进行操作!</h1> <!-- 可以添加一个返回按钮 --> </body> </html>
编写控制层(controller)
/** * (ShiroUser)表控制层 */ @Controller @RequestMapping("/user") public class ShiroUserController { /** * 服务对象 */ @Resource private ShiroUserService shiroUserService; //跳转至注册页面 @GetMapping(value = "/register") public String add() { return "register"; } //注册功能实现 @PostMapping(value = "/toRegister") public String toRegister(ShiroUser user) { shiroUserService.getRegister(user); //注册成功后跳转回登录页面进行登录 return "redirect:/user/login"; } //跳转至登录页面 @GetMapping(value = "/login") public String login() { return "Login"; } //登录功能实现 @PostMapping(value = "/toLogin") public String toLogin(ShiroUser user) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword()); //在登录前设置实现记住我的功能,默认为365天(每次登录都会删除上一次登录的剩余时间,然后重新设置为365天) token.setRememberMe(true); subject.login(token); return "redirect:/user/main"; } //跳转至主页面(需要先登录) @GetMapping(value = "/main") public String main() { return "main"; } //删除功能实现(因为这里只是为了演示给大家看,所以就不真的去删除数据啦) @GetMapping(value = "/delete") @ResponseBody public String delete() { return "删除成功"; } }
一般来说完成以上就可以正常运行使用了,但是因为我们注册的时候使用了加密的方式,所以在使用明文登录的时候会导致密码匹配错误,因为明文 != 密文,所以在我们登录前需要配置一个密码比对器!
deldete(删除) 方法就给大家简单的演示一下之前设置的权限是否生效吧(就不真的实现删除啦),我们在前面的 ShiroConfig 配置类中配置了权限,需要先登录才可以访问这个删除路径,没有登录的话会跳转至指定路径,大家可以根据自己的需求来更改。密码比对器(修改ShiroConfig):
/** * Shiro配置类 */ @Configuration public class ShiroConfig { //...省略之前的配置 //注册自定义Realm,把自定义的Realm加入到Bean容器中 @Bean public MyRealm myRealm() { MyRealm myRealm = new MyRealm(); //把自定义的密码比对器注册给Realm myRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myRealm; } //注册密码比对器(以什么方式加密、迭代次数、是否采用十进制),执行完后会注入进容器中 @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //设置加密方式 hashedCredentialsMatcher.setHashAlgorithmName("MD5"); //设置加密迭代次数 hashedCredentialsMatcher.setHashIterations(SpringTools.ENCRYPT_COUNT); //是否采用了十进制 hashedCredentialsMatcher.setStoredCredentialsHexEncoded(false); return hashedCredentialsMatcher; } }
这样就可以实现正常登录了,如果觉得在用户不小心输入错误账号或密码时跳至404或500之类的报错页面(对用户来说不太友好),则可以自己编写一个自定义异常解析器指定报错时调往的页面,对用户进行提示,增加用户体验感。
自定义异常解析器(config):
/** * 自定义异常解析器 * 当程序报错时跳转至指定页面提示用户,增加用户体验 */ public class MyExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) { e.printStackTrace(); ModelAndView view = new ModelAndView(); if (e instanceof UnknownAccountException || e instanceof IncorrectCredentialsException) { view.setViewName("redirect:/user/error"); } return view; } }
配置自定义异常解析器后需要在配置类中进行注册!
注册自定义异常解析器(修改ShiroConfig):
注册之后就配置完成了
/** * Shiro配置类 */ @Configuration public class ShiroConfig { //...省略之前的配置 //注册自定义异常解析器 @Bean public MyExceptionResolver myExceptionResolver() { return new MyExceptionResolver(); } }
记住我功能讲解和使用
//在控制层中的登录验证方法中添加以下语句即可实现
token.setRememberMe(true);
在登录之后,把用户的信息记录在 Cookie 中,然后在过期前访问就可以不用登录也可以进行一些常规操作(默认过期时间为 365 天,并且在下一次登录后会刷新默认时间!),但是在执行一些敏感操作时还是需要用户手动登录验证的(比如在网上购物时、绑定身份证信息等)。里面还有两个同名的 Cookie( rememberMe ),其中 value 为deleteMe 的 Cookie时为了登录的时候先把之前存在的 Cookie 删除,然后在写入最新的 Cookie ,保持每次登录之后 Cookie都是最新的!
查看过期时间:
-
运行项目,打开浏览器输入对应地址
-
在浏览器打开的页面 使用快捷键(Fn+F12)或 右键然后点击检查
-
登录后会出现一个toLogin(也就是自己在控制层设置的路劲名),出现后点击即可
- 点击 toLogin 后会显示下面的效果,然后在 Cookies 中可以看到过期时间
自定义过期时间
如果觉得默认的过期时间太长了或者有需求,那么可以自己自定义一个过期时间。
注册自定义RememberMe-Cookie配置(修改ShiroConfig):
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
//...省略之前的配置
//配置SecurityManager
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//把自定义的Realm注入给SecurityManager
securityManager.setRealm(myRealm());
//把自定义的rememberMe管理器注入给SecurityManager
securityManager.setRememberMeManager(cookieRememberMeManager());
return securityManager;
}
//注册自定义RememberMe-Cookie配置
@Bean
public SimpleCookie simpleCookie() {
//创建名为rememberMe的Cookie(rememberMe是浏览器cookie的名称,不可以乱写!)
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置过期时间(默认为365天; 单位:秒),我们这里修改为十天(10*24*60*60)
cookie.setMaxAge(864000);
//可以设置为只有http请求才可访问(也可以不写,因为默认就是为true)
cookie.setHttpOnly(true);
return cookie;
}
//定义好后将它放进Cookie管理对象中(不做下面这一步的话就无法生效!)
//Cookie的管理对象(rememberMe管理器)
@Bean
public CookieRememberMeManager cookieRememberMeManager() {
CookieRememberMeManager meManager = new CookieRememberMeManager();
//给CookieRememberMeManager注入自定义的Cookie
meManager.setCookie(simpleCookie());
//也可以加入一些一起的配置(比如加密之类的,这里就不给大家一一讲解了)
return meManager;
}
}
Shiro标签的使用
因为之前是使用 Maven 项目和 ini 配置文件编写的,所以就没给大家演示标签效果了,这里就给大家简单的看一下效果和如何使用。
需要的依赖(前面的依赖中已经包含了,这里就让大家了解一下):
<!-- thymeleaf-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
配置方言(修改ShiroConfig):
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
//...省略之前的配置
//配置Shiro方言,让thymeleaf支持shiro标签
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
完成以上两个步骤后即可开始在页面中使用,这里就用之前创好的 main.html 页面来进行演示
修改主页面(main.html):
<!DOCTYPE html>
<!--
添加thymeleaf和shiro的头部声明
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
-->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<!-- 各种模块的使用场景,为了看效果可以先把shiroconfig中的/user/main访问权限设置为anon-->
<!-- 1.游客模块(在程序重新启动后,第一次直接访问main页面时会显示) -->
<h1 shiro:guest="">
您现在是游客状态!
</h1>
<!-- 2.已登录模块(在用户每一次登录进main页面的时候显示,之后如果是直接访问则不会显示) -->
<h1>
欢迎您:<span shiro:principal=""></span>!
</h1>
<!-- 3.已经登录或者记住我(在用户成功登录后显示,只要不过期就会一直显示,对用户比较友好) -->
<h1 shiro:user="">
我记住你了:<span shiro:principal=""></span>
</h1>
</body>
</html>
如果是想看 “记住我” 的效果,可以先正常登录一遍,这样会显示第二模块和第三模块,然后把浏览器关闭在打开直接访问主页面(mian.html),然后就可以看到页面中只会显示第三模块(只要不过期就会一直存在)!
Shiro会话管理
Shiro 提供了完整的企业级会话还礼功能,不依赖与底层容器(如 web 容器 Tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对web的透明支持,SSO 单点登录的支持等特性。
为什么需要使用 session ?
session 对象是一种会话对象,用来记录每个客户端的访问状态,因为HTTP协议是一种无状态协议,也就是客户端向服务器发送一个请求 request,然后服务器返回一个相应 response,之后这个连接就会被关闭,两者之间也就没有任何关系了,也就是服务器中不会存储此次请求的有关信息,再次请求时服务器就没办法知道这次请求和上次请求是否是一个用户了。所以我们就需要采用会话 session 保持连接。
Shiro 为什么需要使用 session ?
Shiro 是一个安全框架,所以对状态保持是必须的!Shiro 提供了一整套 session 管理方案。
核心对象
- SimpleSession:复责完成 Session 的基本功能。
- SimpleSessionFactory:负责生产 SimpleSession 的工厂。
- SessionDao:类似 DAO,负责 Session 的增删改查。
- DefaultSessionManager:Session 管理者,管理 SimpleSessionFactory 和 SessionDAO
快速上手
之前的时候是直接在控制层(controller)中使用 HttpSession 来存储用户信息之类的,然后这次给大家讲解和使用的是 Shiro 的会话管理(Session Management),默认过期时间为 30 分钟。
我们先来修改它的过期时间让大家看一下效果
自定义Session-Cookie配置(修改ShiroConfig):
在之前的查看过期页面可以发现有一个 JSESSIONID 的 Cookie 名,这个名称就是 Session ,所以我们可以copy之前写好的 ”记住我“ 的 Cookie 配置,然后更改 Cookie 对于的名称和过期时间即可(步骤基本一致)。
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
//...省略之前的配置
//配置SecurityManager
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//把自定义的Realm注入给SecurityManager
securityManager.setRealm(myRealm());
//把自定义的rememberMe管理器注入给SecurityManager
securityManager.setRememberMeManager(cookieRememberMeManager());
//把自定义的Session管理器注入给SecurityManager
securityManager.setSessionManager(defaultWebSessionManager());
return securityManager;
}
//自定义Session-Cookie配置
@Bean
public SimpleCookie SessionCookie() {
//创建名为JSESSIONID的Cookie(JSESSIONID是浏览器cookie的名称,不可以乱写!)
SimpleCookie cookie = new SimpleCookie("JSESSIONID");
//设置过期时间(默认为-1,表示当前会话有效,0表示删除当前cookie; 单位:秒)
cookie.setMaxAge(-1);
//可以设置为只有http请求才可访问(也可以不写,因为默认就是为true)
cookie.setHttpOnly(true);
return cookie;
}
//Cookie的管理对象(JSESSIONID管理器)
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
//给DefaultWebSessionManager注入自定义的Cookie
webSessionManager.setSessionIdCookie(SessionCookie());
//session全局超时时间,我们这里设置为10秒,默认为30分钟,单位:毫秒
webSessionManager.setGlobalSessionTimeout(10000);
return webSessionManager;
}
}
配置好后运行打开浏览器进入检查页面,然后正常登录会显示出已登录的数据,然后过了十秒钟(也就是自己设置的过期时间)后,刷新页面就会直接跳转回登录页面,因为过期后系统就不知道你是谁,所以就会让你重新登录认证!
PS: 需要把之前的 /user/main(在ShiroConfig配置类中) 的访问权限调整至 authc(必须登录才可访问),这样效果比较明显,不然等过期了刷新页面还是在主页,效果就不是很明显。
Session 监听
session 监听就是监控 session 的3个状态(创建、过期、停止)。
过期:
判断过期的方式是在 session 被创建的时候记录创建时间,然后跟现在的时间进行对比,判断这个 session 是否已经过期,session 不会自动告诉报告已过期,需要再次访问或者通过检测器才会识别这个 session 是否已经过期!
停止:
只有在 logout 或 session.stop() 时才会触发,直接关闭浏览器是不会触发的!
自定义监听器(config):
可以在每个状态中进行不同的操作,一般用于打印日志
/**
* 自定义Session监听器:实时监控Session的创建、过期和停止
*/
public class MySessionListener extends SessionListenerAdapter {
//Session创建时触发
@Override
public void onStart(Session session) {
System.err.println("----->Session创建成功!");
}
//Session停止时触发
@Override
public void onStop(Session session) {
System.err.println("----->Session已停止!");
}
//Session过期时触发
@Override
public void onExpiration(Session session) {
System.err.println("----->Session已过期!");
}
}
注册自定义监听器(修改ShiroConfig):
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
//...省略之前的配置
//注册自定义Session监听器
@Bean
public List<SessionListener> sessionListener() {
List<SessionListener> listeners = new ArrayList<>();
listeners.add(new MySessionListener());
//...如果有多个监听器,只需要在下面添加即可
return listeners;
}
//Cookie的管理对象(JSESSIONID管理器)
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
//给DefaultWebSessionManager注入自定义的Cookie
webSessionManager.setSessionIdCookie(SessionCookie());
//session全局超时时间,默认为30分钟,单位:毫秒
webSessionManager.setGlobalSessionTimeout(10000);
//给Session管理者注入自定义Session监听器
webSessionManager.setSessionListeners(sessionListener());
return webSessionManager;
}
}
配置完后运行程序,然后进行登录,登录 或 退出时注意控制台打印的东西!
Session 检测
用户直接关闭浏览器,不会知道 session 是否过期,也就意味着 session 不能停止(不停止就会一直占着空间)!所以 Shiro 提供了 session 的检测机制,可以定时的发送检测,识别 session 是否已经过期。Session 检测机制是默认开启的,但是默认时间是 1 小时(太长了,只是测试的话效果不明显),所以我们可以自己自定义配置一下。
配置 Session 检测(修改ShiroConfig):
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
//...省略之前的配置
//Cookie的管理对象(JSESSIONID管理器)
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
//给DefaultWebSessionManager注入自定义的Cookie
webSessionManager.setSessionIdCookie(SessionCookie());
//session全局超时时间,默认为30分钟,单位:毫秒
webSessionManager.setGlobalSessionTimeout(10000);
//给Session管理者注入自定义Session监听器
webSessionManager.setSessionListeners(sessionListener());
//开启Session检测,默认为开启,可以省略
// webSessionManager.setSessionValidationSchedulerEnabled(true);
//设置检测的时间间隔,默认为1小时,这里我们为了看效果,所以设置为15秒一次(单位:毫秒,15000L=15秒)
webSessionManager.setSessionValidationInterval(15000L);
return webSessionManager;
}
}
Shiro 注解
这里的注解只是一个标识,不会自动生效!所以在使用前需要为注解做一些配置!
-
@RequiresAuthentication
表示当前 Subject 已经通过 login 进行了身份验证;即 Subject. isAuthenticated() 返回 true。
-
@RequiresUser
表示当前 Subject 已经身份验证或者通过记住我登录的。
-
@RequiresGuest
表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。
-
@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)
表示当前 Subject 需要角色 admin 和 user
-
@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)
表示当前Subject需要权限 user:a 或 user:b
快速上手
由于是为了给大家看一下效果,所以就直接使用刚才创建好了的 SpringBoot 项目来演示
开启Shiro注解(修改ShiroConfig):
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
//...省略之前的配置
//开启Shiro注解,需要借助AOP来实现
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
//开启Shiro注解
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
//开启AOP注解支持,用于扫描shiro注解的类(协助shiro注解完成功能)
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
//给AuthorizationAttributeSourceAdvisor注入SecurityManager
attributeSourceAdvisor.setSecurityManager(securityManager);
return attributeSourceAdvisor;
}
}
添加控制层(ShiroRoleController):
/**
* (ShiroRole)表控制层
* 使用Shiro注解不会报错,但是会发生逻辑错误
* 比如在类上使用需要登录才可以访问的注解,而方法上使用不需要登录就可以访问则会发生逻辑错误,一直需要登录才可访问!
*/
@Controller
@RequestMapping("/role")
//如果把注解放在类上,则表示这个类下面所有的路径都需要满足这个注解的条件,所以一定要注意逻辑!!
//@RequiresAuthentication
public class ShiroRoleController {
@GetMapping(value = "/query1")
@ResponseBody
//@RequiresAuthentication: 表示用户需要登入才可以访问的路径
@RequiresAuthentication
public String query1() {
/**
* 如果在未登录的情况下直接访问该路径,会导致报错
* 所以需要先访问 /user/login 进行登录,登录成功后再访问 /role/query1
*/
return "用户已登录";
}
@GetMapping(value = "/query2")
@ResponseBody
//@RequiresUser: 表示当前 Subject 已经身份验证或者通过记住我登录的。
@RequiresUser
public String query2() {
/**
* ”记住我“ 这样测试的话效果不是很明显,因为不管有没有记住我都不会报错
* 如果想要效果明显一点可以自己写一个简单查询方法进行测试
*/
return "记住我";
}
@GetMapping(value = "/query3")
@ResponseBody
//@RequiresRoles: 表示当前 Subject 需要角色 admin 和 user
@RequiresRoles(value={"admin", "seller"}, logical= Logical.AND)
public String query4() {
/**
* 这个方法也可以使用一个查询方法来进行测试,然后用数据库中的角色来验证
* 也可以使用 shiro.ini 配置文件然后使用令牌的方式
*/
return "角色为 admin 或 seller";
}
@GetMapping(value = "/query4")
@ResponseBody
//表示当前Subject需要权限 user:a 或 user:b
@RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR)
public String query5() {
/**
* 这个方法也可以使用一个查询方法来进行测试,然后用数据库中的角色来验证
* 也可以使用 shiro.ini 配置文件然后使用令牌的方式
*/
return "角色为 admin 或 seller";
}
}
Shiro + Redis集成
快速上手
需要的依赖:
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
<!-- 工具包(比如连接池),根据自己情况来加(可以不加) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- jedis依赖(不加的话会导致redis权限不足而报错) -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
Redis相关配置(修改application. properties ):
# Redis相关配置
# IP地址(必须要配)
spring.redis.host=localhost
# 端口号(必须要配)
spring.redis.port=6379
# 最大连接数(默认为8)
spring.redis.lettuce.pool.max-active=8
# 等待的最大连接数(默认为-1,表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
# 空闲的等待数量(默认为0)
spring.redis.lettuce.pool.min-idle=0
# 最大的等待数量(默认为8)
spring.redis.lettuce.pool.max-idle=8
# 超时时间
spring.redis.timeout=10000
添加配置(修改ShiroConfig):
- 注入Redis参数
- Shiro的Redis管理器
- Shiro的缓存管理器
- Shiro的RedisSessionDAO
/**
* Shiro配置类
*/
@Configuration
public class ShiroConfig {
//...省略之前的配置
//注入Redis参数
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
//配置Shiro的Redis管理器
@Bean
public RedisManager redisManager() {
RedisManager manager = new RedisManager();
manager.setHost(host);
manager.setPort(port);
return manager;
}
//配置Shiro的缓存管理器
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager();
//给RedisCacheManager注入自定义的Redis管理器
cacheManager.setRedisManager(redisManager());
return cacheManager;
}
//配置Shiro的RedisSessionDAO
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
//给RedisSessionDAO注入自定义Redis管理器
sessionDAO.setRedisManager(redisManager());
return sessionDAO;
}
//Cookie的管理对象(JSESSIONID管理器)
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
//给DefaultWebSessionManager注入自定义的Cookie
webSessionManager.setSessionIdCookie(SessionCookie());
//session全局超时时间,默认为30分钟,单位:毫秒
webSessionManager.setGlobalSessionTimeout(10000);
//给Session管理者注入自定义Session监听器
webSessionManager.setSessionListeners(sessionListener());
//开启Session检测,默认为开启,可以省略
// webSessionManager.setSessionValidationSchedulerEnabled(true);
//设置检测的时间间隔,默认为1小时(单位:毫秒,15000L=15秒)
webSessionManager.setSessionValidationInterval(15000L);
//给DefaultWebSessionManager注入自定义的RedisSessionDAO
webSessionManager.setSessionDAO(redisSessionDAO());
return webSessionManager;
}
}
配置完成后即可启动项目开始测试,登录后可以看到 redis 中会出现一个值,然后当 session 过期后这个值会自动删除