1、Shiro简介
1.1、什么是Shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理,Web集成,缓存等。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
1.2、shiro的功能
Shiro 的 API 也是非常简单;其基本功能点如下图所示:

- Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web 支持,可以非常容易的集成到 Web 环境;
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
- Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
==注:Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。==
1.3、Shiro外部框架
Shiro外部框架具有非常简单易于使用的 API,且 API 契约明确。下图从应用程序角度展示了如何使用Shiro完成工作:

可以看到:应用代码直接交互的对象是 Subject,==也就是说 Shiro 的对外 API 核心就是 Subject==;其每个 API 的含义:
-
Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;
-
SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出==SecurityManager是 Shiro 的核心==,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
-
Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
-
也就是说对于我们而言,最简单的一个 Shiro 应用:
-
应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
-
我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
-
从以上也可以看出,Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入。
1.4、Shiro内部框架
Shiro内部框架是一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。

- Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
- SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
- Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;==所以我们一般在应用中都需要实现自己的 Realm;==
- SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
- SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
- CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
- Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的。
2、Shiro的快速开始
2.1、环境搭建
新建一个maven项目,导入依赖:
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.1</version>
</dependency>
<!-- configure logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
log4j.properties:
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
# General Apache libraries
log4j.logger.org.apache=WARN
# Spring
log4j.logger.org.springframework=WARN
# Default Shiro logging
log4j.logger.org.apache.shiro=INFO
# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
Shiro配置文件shiro.ini:
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
2.2、测试类
测试类里面测试了一些API方法,主要是Subject的使用。
import com.sun.org.omg.CORBA.InitializerSeqHelper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
/* 设置环境:
* 使用配置创建 Shiro SecurityManager 的最简单方法:
* 通过ini文件(shiro.ini)加载领域,用户,角色,和权限配置
* IniSecurityManagerFactory通过读取.ini配置文件,返回
*/
// Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// SecurityManager securityManager = factory.getInstance();
SecurityManager securityManager = new IniSecurityManagerFactory("classpath:shiro.ini").getInstance();
// 对于这个简单的示例快速入门,使 SecurityManager 可作为 JVM 单例访问。
// 大多数应用程序不会这样做,而是依赖于它们的容器配置或 web.xml 的 webapps。
// 这超出了这个简单快速入门的范围,所以我们只会做最起码的事情,这样你就可以继续感受事物。
SecurityUtils.setSecurityManager(securityManager);
// 现在已经设置了一个简单的 Shiro 环境,让我们看看可以做什么:
// 获取当前正在执行的用户Subject:
Subject currentUser = SecurityUtils.getSubject();
// 通过当前用户拿到session(shiro里的session)
Session session = currentUser.getSession();
// session存值和取值
session.setAttribute("someKey", "万里顾一程");
String value = (String) session.getAttribute("someKey");
if (value.equals("万里顾一程")) {
log.info("Subject--》session [" + value + "]");//Subject--》session [万里顾一程]
}
// 判断当前用户是否认证
if (!currentUser.isAuthenticated()) {
//验证用户的用户名和密码(和.ini配置文件里的用户信息对比),如果认证成功就给用户生成一个token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken("root", "secret");
token.setRememberMe(true);//开启记住我功能
try {
currentUser.login(token);//执行登录操作
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// 总的异常
catch (AuthenticationException ae) {
}
}
// 打印当前用户名
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
// 测试用户的角色
if (currentUser.hasRole("admin")) {//如果当前用户有admin这个角色
log.info("May the admin be with you!");
} else {
log.info("Hello, mere mortal.");
}
// 测试角色权限(非实例级),粗粒度
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
// 一个(非常强大的)实例级权限,细粒度
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
// 注销
currentUser.logout();
// 结束
System.exit(0);
}
}
启动测试类测试,查看日志输出:

上面只是一个简单的控制台输出的测试,下面我们用Springboot来集成shiro。
3、Springboot集成Shiro
3.1、搭建环境
新建一个springboot项目,导入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</dependency>
<!--导入tomcat解析jsp的依赖-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>
自定义Realm
package com.cheng.shiro.realm;
import jdk.nashorn.internal.ir.CallNode;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
编写Shiro配置类
package com.cheng.config;
import com.cheng.shiro.realm.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
//1.创建ShiroFilter,拦截所有的请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
return bean;
}
//2.创建SecurityManager
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
//3.创建自定义的Realm
@Bean(name = "realm")
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
return customerRealm;
}
}
3.2、实现登录拦截
登录页面
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>登录页面</h1>
<form action="" method="post">
用户名<input type="text" name="username"><br>
密码<input type="text" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
用户主页
<%--解决乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1 style="color: red">用户主页v1.0</h1>
<ul>
<li><a href="">用户管理</a></li>
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">职工管理</a></li>
</ul>
</body>
</html>
controller
@Controller
@RequestMapping("/user")
public class UserController {
}
我们要实现过滤功能,就在配置类添加相应的过滤器即可。
package com.cheng.config;
import com.cheng.shiro.realm.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
//1.创建ShiroFilter,拦截所有的请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
/*添加过滤器
* anon:无需认证就可以访问
* authc:必须认证了才能访问
* user:必须拥有 记住我 功能才能使用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
* */
//创建一个map,map中决定哪些资源是受限的,哪些资源是公共的
HashMap<String, String> map = new HashMap<String, String>();
//访问index.jsp需要认证
map.put("/index.jsp","authc");
//将拦截的请求放入过滤器
bean.setFilterChainDefinitionMap(map);
//默认认证的界面路径,当登录失败时自动跳转到该页面进行认证
bean.setLoginUrl("/login.jsp");
return bean;
}
//2.创建SecurityManager
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
//3.创建自定义的Realm
@Bean(name = "realm")
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
return customerRealm;
}
}
启动程序测试,当我们尝试访问主页时,跳转到登录界面,拦截成功!
3.3、实现用户认证和退出
用户认证功能需要使用我们自定义的Realm
首先用户在登录界面提交用户信息,再用Controller接收。
Controller接收
@RequestMapping("/login")
public String login(String username,String password){
Subject subject = SecurityUtils.getSubject();
//封装前端提交的用户信息
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);//执行登录方法,执行成功就跳转主页面
return "redirect:/index.jsp";
//捕获可能出现的异常
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
return "redirect:/login.jsp";
}
登录界面
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>登录页面</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
用户名<input type="text" name="username"><br>
密码<input type="text" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
接收到用户提交的信息后,封装成token,然后把token和自定义的Realm中保存的用户信息进行比对,比对成功就进行登录操作,比对失败就抛出异常。
CustomerRealm.java
package com.cheng.shiro.realm;
import jdk.nashorn.internal.ir.CallNode;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("====进入了认证方法====");
//获得身份信息
String principal = (String) token.getPrincipal();
//虚拟一个用户数据用来测试,
if ("wanli".equals(principal)){//如果身份信息认证成功
return new SimpleAuthenticationInfo(principal,"123",this.getName());
}
return null;
}
}
启动程序进行测试:
访问登录界面,输入错误的用户名,测试用户名认证功能

然后输入错误的密码,测试密码认证功能

最后输入正确的用户名和密码,进行登录,成功进入首页!
用户注销
在主页添加
<a href="${pageContext.request.contextPath}/user/logout">注销登录</a>
然后编写controller
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.jsp";
}
3.4、连接数据库实现基于MD5盐值加密的注册功能
为了防止用户密码被获取,在用户注册时,我们就应该将用户提交的明文密码在保存到数据库之前,使用shiro为我们提供的MD5算法对明文密码进行加密,并且加盐处理之后,我们才能保存到数据库,日后在认证的时候,我们才能在数据库中读取加密的密码。
连接数据库
创建一个shiro数据库,然后建一张user表
CREATE DATABASE `shiro`CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE `shiro`.`t_user
( `id` INT(6) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(60), `password` VARCHAR(60),
`salt` VARCHAR(30),
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
导入依赖
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!--导入mysql的依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
编写数据库配置文件
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=19990802
mybatis.type-aliases-package=pojo
mybatis.mapper-locations=classpath:mapper/*.xml
编写生成随机盐的工具类
package com.cheng.utils;
import java.util.Random;
//生成随机盐的工具类
public class SaltUtils {
public static String getSalt(int n){
//定义一个数组,随机盐从这里生成
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-=+.?".toCharArray();
/*StringBuffer与StringBuilder之间区别
* StringBuffer 字符串变量(线程安全)多线程操作字符串
* StringBuilder 字符串变量(非线程安 单线程操作字符串
* */
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
//每次都在在chars范围内随机返回一个数字,执行n次,Random的区间是[a,b)
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
//测试一下
public static void main(String[] args) {
System.out.println(getSalt(6));//.8mv+#
}
}
实现注册业务
dao层接口
@Mapper
@Repository
public interface UserDao {
//用户注册
public void save(User user);
}
service层接口
public interface UserService {
public void register(User user);
}
service层实现类,实现具体的业务
package com.cheng.service;
import com.cheng.dao.UserDao;
import com.cheng.pojo.User;
import com.cheng.utils.SaltUtils;
import lombok.experimental.Accessors;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional//声明式事务管理,开启正常提交事务,异常回滚事务
public class UserServiceImpl implements UserService{
//service调用dao层
@Autowired
private UserDao userDao;
@Override
public void register(User user) {
//处理业务
//1.生成随机盐 8位
String salt = SaltUtils.getSalt(8);
//2.将随机盐保存到数据库
user.setSalt(salt);
//3.将明文密码进行MD5 + salt随机盐 + hash散列加密, 散列次数1024
Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
//将加密后的密码保存到数据库
user.setPassword(md5Hash.toHex());//toHex()字符串转换为十六进制编码
userDao.save(user);
}
}
启动程序,实现用户注册,用户信息保存到数据库中,注册成功!

3.5、连接数据库实现基于MD5盐值加密的认证功能
实现认证功能,需要通过用户提交的用户名查询数据库中是否有这个用户,所以我们需要写一个根据用户名查询用户的方法:
dao层
//根据用户名查询用户
User queryUserByName(String username);
service层
//根据用户名查询用户
User queryUserByName(String username);
service层实现类
@Override
public User queryUserByName(String username) {
return userDao.queryUserByName(username);
}
因为我们在上面对明文密码进行了MD5盐值加密,当shrio进行密码匹配时会非对称匹配,所以我们要在配置类中修改密码校验匹配器
//3.创建自定义的Realm
@Bean(name = "realm")
public CustomerRealm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//修改密码凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
在自定义的realm中进行用户认证的实现
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获得身份信息
String principal = (String) token.getPrincipal();
User user = userService.queryUserByName(principal);
if (!ObjectUtils.isEmpty(user)){//ByteSource提供了一个内部方法,可以将字符串转换为对应的盐值信息
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
}
return null;
}
启动程序测试,认证成功!
3.6、授权的基本使用
授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。
在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role):
- 主体,即访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。
- 资源, 在应用中用户可以访问的URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。
- 权限,代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro 不会去做这件事情,而是由实现人员提供。
- 角色,角色代表了操作集合,可以理解为权限的集合,一般情况下我们赋予用户的是角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。不同的角色拥有一组不同的权限。
shiro授权流程分析

流程如下:
- 首先调用 Subject.isPermitted / hasRole接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
- Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:update”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
- 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
- Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。
授权方式
Shiro 支持三种方式的授权:
1.编程式:通过写 if/else 授权代码块完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
2.注解式:通过在执行的 Java 方法上放置相应的注解完成:
@RequiresRoles("admin")
public void hello() {
//有权限
}
没有权限将抛出相应的异常;
3.JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限 —>
</shiro:hasRole>
3.6.1、使用Jsp标签实现授权
在页面头部引入shiro标签
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
1.基于角色的权限管理
先给用户添加一个角色:
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户认证时的主身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
//根据主身份信息获得角色 和 权限信息
if ("xiaowei".equals(primaryPrincipal)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//给用户添加角色
simpleAuthorizationInfo.addRole("user");
return simpleAuthorizationInfo;
}
return null;
}
然后在jsp页面定义角色授权规则
<ul>
<shiro:hasAnyRoles name="user,admin"><%--里面的资源可以被多个角色访问--%>
<li><a href="">用户管理</a></li>
</shiro:hasAnyRoles>
<shiro:hasRole name="admin"><%--里面的资源只有admin角色才能访问--%>
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">职工管理</a></li>
</shiro:hasRole>
</ul>
我们给当前用户赋予了一个user角色,启动程序访问主页面:

因为当前用户角色是user,只有访问用户管理的权限。
下面我们将当前用户的角色修改为admin,再启动程序访问主页面:

2.基于权限字符串的授权管理
权限字符串编写规则:“资源标识符:操作:对象实例 ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串,“:”表示资源/操作/实例的分割;“,”表示操作的分割;“*”表示任意资源/操作/实例。
给当前用户授予权限:
//权限字符串书写规则 user:*:* user表示模块,第一个*表示所有操作,第二个*表示所有资源
simpleAuthorizationInfo.addStringPermission("user:*:*");
然后在jsp页面定义权限字符串授权规则:
<li><a href="">用户管理</a>
<ul>
<%--下面资源需要对应的权限才能访问--%>
<shiro:hasPermission name="user:add:*">
<li><a href="">添加</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:*">
<li><a href="">删除</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:*">
<li><a href="">修改</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:find:*">
<li><a href="">查询</a></li>
</shiro:hasPermission>
</ul>
</li>
==注:在页面中对权限字符串的控制,shiro不提供像shiro:hasAnyRoles这样的多个控制,因为权限字符串可以写通配符。==
当前用户为user,权限为user:*:*,启动程序访问主页:

下面我们修改用户的权限为user:add:*和user:delete:*
simpleAuthorizationInfo.addStringPermission("user:add:*");
simpleAuthorizationInfo.addStringPermission("user:delete:*");
再启动程序访问主页:此时用户只对添加和删除操作有访问权限

3.Shiro 对权限字符串缺失部分的处理
-
如“user:view”等价于“user:view:”;而“organization”等价于“organization:”或者“organization::”。可以这么理解,这种方式实现了前缀匹配。
-
另外如“``use
r:”可以匹配如“user:delete”、“user:delete”可以匹配如“user:delete:1”、“user::1”可以匹配如“user:view:1”、“user”可以匹配“user:view”或“user:view:1”等。即可以匹配所有,不加可以进行前缀匹配;但是如“:view”不能匹配“system:user:view”,需要使用“::view`”,即后缀匹配必须指定前缀(多个冒号就需要多个来匹配)。
3.6.2、使用编程式实现授权
编程式:通过写 if/else 授权代码块完成:
1.基于角色的权限管理
给当前用户赋予admin角色,然后在代码里面判断是否有该角色
@RequestMapping("save")
public String saveOrder(){
//获得当前主体对象
Subject subject = SecurityUtils.getSubject();
//如果当前主体对象有admin角色
if (subject.hasRole("admin")){
//处理业务
System.out.println("保存订单成功");
}else{
System.out.println("无权访问");
}
return "redirect:/index.jsp";
}
2.基于权限字符串的授权管理
给当前用户赋予admin角色,并赋予user:update:*权限,编写一个修改用户的controller来进行测试
@RequestMapping("save")
public String addUser(){
//获得当前主体对象
Subject subject = SecurityUtils.getSubject();
//判断当前主体对象是否有user:update:*权限
if (subject.isPermitted("user:update:*")){
//处理业务
return "redirect:/update.jsp";
}else{
return "redirect:/index.jsp";
}
}
编写修改用户的jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>修改用户</h1>
</body>
</html>
在首页面控制跳转
<shiro:hasPermission name="user:update:*">
<li><a href="${pageContext.request.contextPath}/user/update">修改</a></li>
</shiro:hasPermission>
启动程序测试,因为当前用户有user:update:*权限,所以我们点击修改链接时,成功跳转!

shiro也提供了需要多个权限才能访问一个资源的方法isPermittedAll;
subject.isPermittedAll("user:delete:*","user:update:*")
3.6.3、使用注解式实现授权
通过在执行的 Java 方法上放置相应的注解完成
1.基于角色的授权管理
给当前用户赋予admin角色
simpleAuthorizationInfo.addRole("admin");
controller:
@RequestMapping("/update")
@RequiresRoles("admin")//判断当前用户是否有该角色,如果有才能执行下面操作
public String updateUser(){
//处理业务
return "redirect:/update.jsp";
我们再写一个controller,这个方法需要有user角色才能执行
@RequestMapping("/delete")
@RequiresRoles("user")//如果当前用户没有该角色,则会报异常
public String deleteUser(){
//处理业务
return "redirect:/delete.jsp";
}
启动程序进行测试,访问该请求:不能访问!

@RequiresRoles注解还可以指定多个role,如:
@RequiresRoles(value={"user","admin"})//必须拥有全部角色才能访问
2.基于权限字符串的权限管理
给当前用户赋予admin角色,并赋予user:update:*权限,编写一个修改用户的controller来进行测试
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("user:update:*");
controller
@RequestMapping("/update")
@RequiresPermissions("user:update:01")//拥有该权限才能执行下面操作,
public String updateUser(){
//处理业务
return "redirect:/update.jsp";
}
@RequiresPermissions的使用与@RequiresRoles注解类似
3.7、授权数据持久化
我们上面的授权数据都是写死了都,但在正常开发环境下,我们的授权数据肯定是来源于数据库,并且是持久化的。下面我们就把授权数据持久化到数据库中。
权限模型的库表关系:
3.7.1、搭建环境
根据上面的库表关系来构建对应的库表结构
先给上面的user表注册两个用户
用户名:xiaowei 密码:123,id为1
用户名:xiaopeng 密码:123,id为2
建角色表t_role
CREATE TABLE `shiro`.`t_role`(
`id` INT(6) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(60),
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
t_role实体类Role
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)//开启链式编程
public class Role {
private int id;
private String name;
}
插入数据
INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES ('1', 'admin');
INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES ('2', 'user');
INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES ('3', 'product');
建权限表t_perms
CREATE TABLE `shiro`.`t_perms`(
`id` INT(6) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(60),
`url` VARCHAR(255),
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
t_perms实体类perms
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)//开启链式编程
public class Perms {
private int id;
private String name;
private String url;
}
插入数据
INSERT INTO `shiro`.`t_perms` (`id`, `name`, `url`) VALUES ('1', 'user:*:*', 'user/*');
INSERT INTO `shiro`.`t_perms` (`id`, `name`, `url`) VALUES ('2', 'product:*:*', 'product/*');
建用户角色表t_user_role
CREATE TABLE `shiro`.`t_user_role`(
`id` INT(6) NOT NULL,
`userid` INT(6),
`roleid` INT(6),
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
插入数据
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES ('1', '1', '1');
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES ('2', '1', '2');
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES ('3', '2', '2');
一号用户xiaowei角色是admin和user
二号用户xiaopeng角色是user
建角色权限表t_role_perms
CREATE TABLE `shiro`.`t_role_perms`(
`id` INT(6),
`roleid` INT(6),
`permsid` INT(6)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
插入数据
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('1', '1', '1');
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('2', '1', '2');
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('3', '2', '1');
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('4', '3', '2');
一号角色admin有user:*:*和product:*:*权限
二号角色user有user:*:*权限
三号角色product有product:*:*权限
3.7.2、角色信息在数据库中的获取
因为用户和角色是一对多关系,再给User实体类增加一个属性:
//定义角色的的集合
private List<Role> roles;
定义资源的角色授权规则:
<ul>
<shiro:hasAnyRoles name="user,admin"><%--里面的资源可以被多个角色访问--%>
<li><a href="">用户管理</a>
<ul>
<%--下面资源需要对应的权限才能访问--%>
<shiro:hasPermission name="user:add:*">
<li><a href="">添加</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:*">
<li><a href="${pageContext.request.contextPath}/user/delete">删除</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:*">
<li><a href="${pageContext.request.contextPath}/user/update">修改</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:find:*">
<li><a href="${pageContext.request.contextPath}/user/find">查询</a></li>
</shiro:hasPermission>
</ul>
</li>
</shiro:hasAnyRoles>
<shiro:hasRole name="admin"><%--里面的资源需要admin角色才能访问--%>
<li><a href="">商品管理</a></li>
<li><a href="">订单管理</a></li>
<li><a href="">职工管理</a></li>
</shiro:hasRole>
</ul>
根据用户名查询数据库中用户对应的角色:
dao层
//根据用户名查询角色
User queryRoleByName(String username);
UserDao.xml
<resultMap id="userMap" type="User">
<result column="uid" property="id"></result>
<result column="username" property="username"></result>
<!--角色信息-->
<collection property="roles" ofType="Role" javaType="list">
<result column="ird" property="id"/>
<result column="rname" property="name"/>
</collection>
</resultMap>
<select id="queryRoleByName" resultMap="userMap" parameterType="String">
select tu.id uid,tu.username,tr.id rid,tr.name rname
from shiro.t_user tu left join shiro.t_user_role tur on tu.id=tur.userid
left join shiro.t_role tr on tr.id=tur.roleid
where username=#{username}
</select>
service层
//根据用户名查询角色
User queryRoleByName(String username);
service层实现类
@Override
public User queryRoleByName(String username) {
return userDao.queryRoleByName(username);
}
在自定义的Realm中实现从数据库获取用户角色,并进行认证
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户认证时的主身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
User user = userService.queryRoleByName(primaryPrincipal);
List<Role> roles = user.getRoles();
//如果角色信息不为空
if (!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role->{ //遍历出所有的role
simpleAuthorizationInfo.addRole(role.getName());
});
return simpleAuthorizationInfo;
}
return null;
}
启动程序,先登录用户xiaowei,角色是admin和user

再登录用户xiaopeng,角色是user

3.7.3、权限字符串在数据库中的获取
因为用户和角色是一对多关系,再给Role实体类增加一个属性:
//定义权限的集合
private List<Perms> perms;
定义资源的权限字符串授权规则:
<ul>
<shiro:hasAnyRoles name="user,admin"><%--里面的资源可以被多个角色访问--%>
<li><a href="">用户管理</a>
<ul>
<%--下面资源需要对应的权限才能访问--%>
<shiro:hasPermission name="user:add:*">
<li><a href="">添加</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:*">
<li><a href="${pageContext.request.contextPath}/user/delete">删除</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:*">
<li><a href="${pageContext.request.contextPath}/user/update">修改</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:find:*">
<li><a href="${pageContext.request.contextPath}/user/find">查询</a></li>
</shiro:hasPermission>
</ul>
</li>
</shiro:hasAnyRoles>
<shiro:hasRole name="admin"><%--里面的资源需要admin角色才能访问--%>
<li><a href="">商品管理</a>
<ul>
<%--下面资源需要对应的权限才能访问--%>
<shiro:hasPermission name="product:add:*">
<li><a href="">添加</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="product:delete:*">
<li><a href="${pageContext.request.contextPath}/product/delete">删除</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="product:update:*">
<li><a href="${pageContext.request.contextPath}/product/update">修改</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="product:find:*">
<li><a href="${pageContext.request.contextPath}/product/find">查询</a></li>
</shiro:hasPermission>
</ul>
</li>
<li><a href="">订单管理</a></li>
<li><a href="">职工管理</a></li>
</shiro:hasRole>
</ul>
根据用户角色查询权限信息:
dao层
//根据角色id查询权限信息
List<Perms> findAllPermsByRoleId(int id);
dao层的xml文件
<select id="findAllPermsByRoleId" resultType="Perms" parameterType="int">
select tp.id,tp.name,tp.url,tr.name tname from t_role tr
left join t_role_perms trp on tr.id=trp.roleid
left join t_perms tp on trp.permsid = tp.id
where tr.id=#{id}
</select>
service层
//根据角色id查询权限信息
List<Perms> findAllPermsByRoleId(int id);
service实现类
@Override
public List<Perms> findAllPermsByRoleId(int id) {
return userDao.findAllPermsByRoleId(id);
}
自定义的realm
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取用户认证时的主身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("primaryPrincipal-->"+primaryPrincipal);
User user = userService.queryRoleByName(primaryPrincipal);
System.out.println(user);
List<Role> roles = user.getRoles();
//如果角色信息不为空
if (!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {//遍历出所有的role
simpleAuthorizationInfo.addRole(role.getName());//把角色信息放进Authorizer进行比对
//获取权限信息
List<Perms> perms = userService.findAllPermsByRoleId(role.getId());
System.out.println(perms);
if (!CollectionUtils.isEmpty(perms)&& perms.get(0)!=null ){
perms.forEach(perm -> {
simpleAuthorizationInfo.addStringPermission(perm.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
启动程序测试:
先登录用户xiaowei,角色为admin和user,admin权限为user/*/*和product/*/*,user为user/*/*

再登录用户xiaopeng,角色为user,权限为user/*/*

打这里,我们就完成了权限数据持久化的功能!
3.8、整合springboot缓存
从shiro的内部框架图可以很清晰地看到,CacheManager也是Shiro架构中的主要组件之一,Shiro正是通过``CacheManager`组件实现权限数据缓存。
当权限信息存放在数据库中时,对于每次前端的访问请求都需要进行一次数据库查询。特别是在大量使用shiro的jsp标签的场景下,对应前端的一个页面访问请求会同时出现很多的权限查询操作,这对于权限信息变化不是很频繁的场景,每次前端页面访问都进行大量的权限数据库查询是非常不经济的。因此,非常有必要对权限数据使用缓存方案。
Shiro只提供了一个可以支持具体缓存实现(如:Hazelcast, Ehcache, OSCache, Terracotta, Coherence, GigaSpaces, JBossCache等)的抽象API接口,这样就允许Shiro用户根据自己的需求灵活地选择具体的CacheManager。
缓存简单流程:

3.8.1、shiro中使用Ehcache实现缓存
导入依赖
<!--shiro和ehcache整合的依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
在shiro配置(shiroConfig)里面设置缓存管理器
//3.创建自定义的Realm
@Bean(name = "realm")
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//修改密码凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
//开启缓存ehcache管理
customerRealm.setCacheManager(new EhCacheManager());
//开启全局缓存管理
customerRealm.setCachingEnabled(true);
//开启认证缓存
customerRealm.setAuthenticationCachingEnabled(true);
//设置认证缓存名
customerRealm.setAuthenticationCacheName("authenticationCache");
//开启授权缓存
customerRealm.setAuthorizationCachingEnabled(true);
//设置授权缓存名
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
设置了缓存后,以后只有在第一次查询时会操作数据库,再次查询将不会操作数据库,有效的减轻了数据库的负担。但是如果我们的应用程序重启后,再次查询的时候第一次还是要操作数据库。
3.9、图片验证码实现
1.配置验证码工具类
package com.cheng.utils;
import java.io.IOException;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;
public class VerifyCodeUtils {
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 使用系统默认字符源生成验证码
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize){
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources){
if(sources == null || sources.length() == 0){
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for(int i = 0; i < verifySize; i++){
verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
}
return verifyCode.toString();
}
/**
* 生成随机验证码文件,并返回验证码值
* @param w
* @param h
* @param outputFile
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}
/**
* 输出随机验证码图片流,并返回验证码值
* @param w
* @param h
* @param os
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}
/**
* 生成指定验证码图像文件
* @param w
* @param h
* @param outputFile
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, File outputFile, String code) throws IOException{
if(outputFile == null){
return;
}
File dir = outputFile.getParentFile();
if(!dir.exists()){
dir.mkdirs();
}
try{
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch(IOException e){
throw e;
}
}
/**
* 输出指定验证码图片流
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW };
float[] fractions = new float[colors.length];
for(int i = 0; i < colors.length; i++){
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h-4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h-4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for(int i = 0; i < verifySize; i++){
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
public static void main(String[] args) throws IOException {
//获取验证码
String s = generateVerifyCode(4);
//将验证码放入图片中
outputImage(260,60,new File("/Users/chenyannan/Desktop/安工资料/aa.jpg"),s);
System.out.println(s);
}
}
2.实现验证码方法
//验证码方法
@RequestMapping("/getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
//调用验证码工具类生成验证码,4位验证码
String code = VerifyCodeUtils.generateVerifyCode(4);
//把验证码存入session,登录时进行验证码比较
session.setAttribute("code",code);
//把验证码放入图片
ServletOutputStream os = response.getOutputStream();
//设置响应类型
response.setContentType("image/png");
VerifyCodeUtils.outputImage(220,60,os,code);
}
3.在登录页面添加验证码输入框
请输入验证码<input type="text" name="code" ><img src="${pageContext.request.contextPath}/user/getImage" alt=""><br>
4.在shiro配置shiroConfig中放行验证码请求
map.put("/user/getImage","anon");//公共资源
5.在认证方法中比较验证码
@RequestMapping("/login")
public String login(String username,String password,String code,HttpSession session){
//验证码比较
String code1 = (String)session.getAttribute("code");//拿到session中的验证码
//如果验证码比较正确,进行登录操作
try {
if (code1.equals(code)){
Subject subject = SecurityUtils.getSubject();
//封装前端提交的用户信息
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);//执行登录方法,执行成功就跳转主页面
return "redirect:/index.jsp";
//捕获可能出现的异常
}else {
throw new RuntimeException("验证码错误");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
return "redirect:/login.jsp";
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}catch (Exception e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return "redirect:/login.jsp";
}
启动程序,测试:

成功登录,OK!