一、什么是shiro
shiro是一种易于使用的Java安全框架,shiro可以用来完成:认证、授权、加密、会话管理、与Web集成、缓存等。
二、基本功能
(1)Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
(2)Authorization:授权,验证已认证的用户是否拥有某个权限,能否进行相关的操作;
(3)Seesion Manager:会话管理,用户登录之后就是一次会话,没有退出之前,它的所有信息都保存在会话中;会话可以是普通的JavaSE环境,也可以是WEB环境;
(4)Cryptography:加密,保护数据的安全性,如密码加密存储到数据库
(5)Web Support:Web支持,可以非常容易的集成到Web环境
(6)Caching : 缓存,可以缓存用户登录后的信息、角色以及权限
(7)Concurrency:Shiro支持多线程应用的并发验证
(8)Testing:提供测试支持
(9)Run As:允许用户假装为另外的用户的身份进行访问
(10)Remember Me:记住我,下次登录就不需要进行验证
三、Shiro架构
从外部来看,Shiro最主要的组件是Subject、SecurityManager以及Realm
(1)Subject:Shiro对外的接口,使用Shiro,就得拥有Subject对象,我们对Shiro的操作都是从Subject对象开始,subject对象可以理解为操作Shiro的媒介;
(2)SecurityManager:安全管理器,与安全相关的交互都会与SecurityManager交互,它同时也管理着所有的Subject,相当于Spring MVC中的DispatchServlet的角色
(3)Realm:Shiro从Realm获取安全数据,SecurityManager要想验证用户的身份信息,就需要从Realm中获取用户的信息,或者用户所拥有的权限/角色,SecurityManager才能判断该用户是否能够进行下一步的操作。
从Shiro内部来看:
(1)Subject:任何可以与应用交互的“用户”
(2)SecurityManager:相当于Shiro的心脏,所有的具体交互都要通过SecurityManager来进行管理,它管理着所有 Subject、且负责认证、授权、会话以及缓存的管理。
(3)Authentication:负责Subject认证,可以自定义实现,可以使用认证策略,及什么情况下该用户算通过认证
(4)Authorizer:授权器,控制着用户能够访问那些应用,是用户鉴权的核心
(5)Realm:可以有1个或者多个Realm,可以认为是安全的实体数据源,用与获取安全实体的,可以是JDBC实现,也可以是内存实现,由用户提供;所以一般在应用中都需要实现自己的Realm;
(6)SessionManager:管理Session生命周期的组件,
(7)CacheManager:缓存
(8)Cryptography:密码加密模块,对数据库中的用户密码进行加密处理
四、登录认证的概念
(1)身份验证:验证你是否是我的用户、一般通过用户名/密码来证明身份。
(2)在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,使应用能够验证用户身份:
principals:身份,能够唯一标识主体的属性,比如手机号、邮箱、用户名等
credentials:证明/凭证,只有主体知道,比如密码
最常见的principals和credentials组合就是用户名/密码。
五、登录认证的基本流程
(1)得到用户的登录信息,比如用户名、密码;
(2)调用调用Subject.login进行登录,如果失败则得到相应的AuthenticationException异常信息,否则登录成功
(3)创建自定义的Realm类,继承shiro中的AuthenticatingRealm类,实现doGetAuthenticationInfo()方法
身份认证的基本流程:
(1)首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
(2)SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份
验证;
(3)Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此
处可以自定义插入自己的实现;
(4)Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份
验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm
身份验证;
(5) Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如
果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序
及策略进行访问。
使用Shiro登录案例
Shiro 获取权限相关信息可以通过数据库获取,也可以通过 ini 配置文件获取,我们通过ini文件获取。
创建简单的maven工程,在resource下创建shiro.ini文件,在该文件中定义登录用户的用户名和密码
注意shiro.ini的配置文件中,【user】更改为【users】,下图没有更改,后续测试会报错。
导入依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
创建测试类:
import com.sun.org.omg.CORBA.InitializerSeqHelper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
public class ShiroLoginText {
public static void main(String[] args) {
//1.初始化获取SecurityManager
IniSecurityManagerFactory factory=new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager instance = factory.getInstance();
SecurityUtils.setSecurityManager(instance);
//2.获取Subject对象
Subject subject = SecurityUtils.getSubject();
//3.创建token对象,web应用从前端传递用户名和密码
AuthenticationToken token=new UsernamePasswordToken("zhangsan","zss");
//4.完成登录
try {
subject.login(token);
System.out.println("登录成功" );
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
}
}
输入正确的用户名、密码:
输入错误的用户名:
输入错误的密码:
六、角色、鉴权
判断用户是否拥有角色,使用subject.hasRole()方法完成、或者使用注解@RequiresRoles(“角色名称”)、或者在JSP/GSP标签中,使用<shiro:hasRole name=“角色名称进行判断”>
案例扩展
在shiro.ini文件中添加角色和角色对应的权限信息
登录成功之后,判断该用户是否用户“admin”这个角色,同时判断是否拥有user:insert插入数据的权限
校验校色使用:subject.hasRoles()
校验权限使用:subject.isPermitted()
import com.sun.org.omg.CORBA.InitializerSeqHelper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
public class ShiroLoginText {
public static void main(String[] args) {
//1.初始化获取SecurityManager
IniSecurityManagerFactory factory=new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager instance = factory.getInstance();
SecurityUtils.setSecurityManager(instance);
//2.获取Subject对象
Subject subject = SecurityUtils.getSubject();
//3.创建token对象,web应用从前端传递用户名和密码
AuthenticationToken token=new UsernamePasswordToken("zhangsan","zss");
//4.完成登录
try {
subject.login(token);
System.out.println("登录成功" );
//验证是否拥有 角色admin
boolean admin = subject.hasRole("admin");
System.out.println("是否拥有admin角色 " + admin);
//拥有该角色,验证是否拥有 User:insert权限
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有该权限 = " + permitted);
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户不存在");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
}
}
运行结果:
看到他没有这个角色和权限,检查一下是不是我们的参数写错了:
果然写错了,我们登录的账户是zhangsan,结果角色给lisi了,肯定就没有这个权限和角色了,在实际开发中,这种情况就可以拦截这个请求了,或者直接return一个空值结束请求。
我们继续把admin角色给张三
再次运行查看结果:
这次就拥有这个角色和权限啦。
7.Shrio加密
我们在数据库中保存账户名和密码时,一般都需要对账户的密码进行加密,不可能保存明文密码吧,使用shiro可以很简单的对数据进行加密
例如:
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
public class shiroMD5 {
public static void main(String[] args) {
String password="12345"; //比如这是密码
//使用MD5加密
Md5Hash md5Hash=new Md5Hash(password);
System.out.println("MD5加密 = " + md5Hash.toHex());
//带盐的MD5加密,就是给你的密码加点料,在你的密码后面拼接字符串,在进行M加密
Md5Hash md5Hash1=new Md5Hash(password,"jialiao");//很明显,多的这个字符串参数就是加的料
System.out.println("带盐(加料)的加密= " + md5Hash1.toHex());
//你以为这就完了?加一次还可能被别人破解,那我多加几次,多迭代几次,多加点料
Md5Hash md5Hash2=new Md5Hash(password,"jialiao",3);
System.out.println("迭代三次盐(加了三次料)的加密 = " + md5Hash2.toHex());
//还可以使用父类加密,Md5Hash它的父类是SimpleHash
//使用父类需要额外指定加密方式
SimpleHash simpleHash=new SimpleHash("MD5",password,"jialiao",3);
System.out.println("使用父类,指定加密方式为MD5,迭代三次盐(加了三次料)的加密 = "+simpleHash.toHex());
}
}
运行结果:
八、Spring Boot 整合Shiro
创建Spring Boot项目、导入相关依赖,自定义登录认证
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency
在yml文件配置进行配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
# MySQL数据源
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shirodb?characterEncoding=utf-8&useSSL=false
username: *****
password: *****
type: com.zaxxer.hikari.HikariDataSource
#日期格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
#登录接口
shiro:
loginUrl: /myController/login
创建登录表
CREATE DATABASE IF NOT EXISTS `shirodb` CHARACTER SET utf8mb4;
USE `shirodb`;
CREATE TABLE `user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` VARCHAR(30) DEFAULT NULL COMMENT '用户名',
`pwd` VARCHAR(50) DEFAULT NULL COMMENT '密码',
`rid` BIGINT(20) DEFAULT NULL COMMENT '角色编号',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';
创建数据库表对应的实体类User(或者使用MybatisX逆向生成实体类、mapper、service、serviceimpl)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private String pwd;
private Integer rid;
}
创建mapper
package com.lqkj.dong.springboot_shiro.mapper;
import com.lqkj.dong.springboot_shiro.pojo.User;
public class UserMapper extends BaseMapper<User>{
//自定义查询数据库的方法
}
创建service接口及实现类
package com.dong.shirotext.service;
import com.dong.shirotext.pojo.User;
public interface UserService {
//定义根据用户名从数据库中查询用户信息的方法
public User getUSerByName(String name);
}
package com.dong.shirotext.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dong.shirotext.mapper.UserMapper;
import com.dong.shirotext.pojo.User;
import com.dong.shirotext.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceimpl implements UserService {
@Autowired
private UserMapper userMapper;//注入mapper
/**
* 根据用户名,从数据库中查询用户记录
* @param name
* @return
*/
@Override
public User getUSerByName(String name) {
User user = userMapper.selectOne(new QueryWrapper<User>().eq("name", name));
return user;
}
}
项目架构大概是这个样子,大家随意即可
当我们使用shiro框架时,我们需要shiro去查询我的数据库,走我们自己的登录验证的逻辑,这时候就需要我们自定义realm,创建类去继承AuthorizingRealm类,重写里面的两个方法(重载),即可完成自定义的授权和登录认证的方法。详见代码:
package com.dong.shirotext.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class MyRealm extends AuthorizingRealm {
//用于鉴权
/**
* 当前登录用户,我们可以从数据库中查询到该用户的权限,并通过该方法,将我们查询到的
该用户拥有的角色,保存在shiro框架中,后面我们使用shiro框架去核对用户权限时,所比较的
就是我们在这里查询、并且保存到的权限
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 这个方法是用于登录认证
我们要自定义自己的登录逻辑,就需要在该方法中编写自己的登录逻辑
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
接下来,我们自定义自己的登录逻辑,让它从我们的数据库中查询用户的信息进行比对。
package com.dong.shirotext.realm;
import com.dong.shirotext.pojo.User;
import com.dong.shirotext.service.UserService;
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;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//用于鉴权
/**
* 当前登录用户,我们可以从数据库中查询到该用户的权限,并通过该方法,将我们查询到的
* 该用户拥有的角色,保存在shiro框架中,后面我们使用shiro框架去核对用户权限时,所比较的
* 就是我们在这里查询、并且保存到的权限
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 这个方法是用于登录认证
* 我们要自定义自己的登录逻辑,就需要在该方法中编写自己的登录逻辑
*
* @param
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//首先,从token中获取登录时保存的用户信息,注意,该方法是登录时shiro会调用的方法
//也就是说我们在登录时,会将名称和密码封装成token,所以这里不要疑惑为什么token中可以
//获取到用户的信息,后续我们编写登录时,你就会恍然大悟
String name = token.getPrincipal().toString();//得到的就是用户名称
System.out.println("name = " + name);//可以打印出来看看
//从数据库中查询用户信息,根据name查询
User user = userService.getUSerByName(name);
//查到用户信息之后,将登录时的密码,和现在从数据库中查到的密码进行比较
//我们将相关的数据封装到AuthenticationInfo中,知道密码的盐,用户名、密码,
//该对象会帮我们进行校验,并且将校验结果返回给shiro框架
if (user != null) {
//创建AuthenticationInfo对象,在构造器中传递相关参数
AuthenticationInfo info = new SimpleAuthenticationInfo(
token.getPrincipal(),
user.getPwd(),//用户密码
ByteSource.Util.bytes("salt"),//密码使用MD5加密时指定的盐
token.getPrincipal().toString()
);
return info;
}
return null;
}
}
接下来,我们去数据库中准备两条数据,注意密码要使用MD5加密,盐使用“salt”,并且需要迭代三次的密码
(12345)
接下来创建Controller登录接口:
package com.dong.shirotext.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/myController")
public class LoginController {
@GetMapping("/login")//使用get方式,方便参数传递
public String login( String name, String password){
//1.获取subject对象
Subject subject= SecurityUtils.getSubject();
//通过name,password封装为token对象中
//现在能理解我们自定义的登录验证方法中,为什么能够从token中获取用户信息了吧
AuthenticationToken token=new UsernamePasswordToken(name,password);
//然后调用subject.login方法登录
try {
//此时就会去执行我们自定义的登录验证的方法,同时将token作为参数传递过去了
subject.login(token);
return "登录成功";
}catch (AuthenticationException e){
e.printStackTrace();
return "登录失败!";
}
}
}
ok,现在可以试一下我们能否登录成功了,在看一下我目前的目录结构
启动我们的项目(记得在启动上方,添加MapperScan注解,扫描mapper所在的目录)
启动报错
Please create bean of type 'Realm' or add a shiro.ini in the root classpath (src/main/resources/shiro.ini) or in the META-INF folder (src/main/resources/META-INF/shiro.ini).
因为我们目前还没有对shiro进行配置,所以shiro默认去寻找配置文件,发现找不到配置文件,没有配置文件就没有Realm对象,没有Realm对象你让我怎么干活,直接罢工了,所以解下来我们需要对shiro进行配置,既然使用了Spring Boot框架,我们就不需要配置文件了, 直接创建一个配置类即可
package com.dong.shirotext.config;
import com.dong.shirotext.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.management.MXBean;
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm realm; //注入我们自定义的Realm对象
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager=
new DefaultWebSecurityManager();
//创建密码加密对象
HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
//设置加密对象的属性
matcher.setHashAlgorithmName("md5");
matcher.setHashIterations(3);
//将加密对象 存储到Realm对象中
realm.setCredentialsMatcher(matcher);
//将Realm对象 存入 defaultWebSecurityManager对象中
defaultWebSecurityManager.setRealm(realm);
//返回
return defaultWebSecurityManager;
}
//配置shiro的拦截范围,我们的登录接口肯定是需要放行的
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition
=new DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userlogin","anon");
definition.addPathDefinition("/myController/login","anon");
//配置需要拦截的请求范围
definition.addPathDefinition("/**","authc");
return definition;
}
}
ok,对shiro进行完配置,同时放行了我们的登录接口,这次启动应该不会报错了吧,启动项目
启动成功,测试登录接口
http://localhost:8080/myController/login?name=zs&password=12345
可以看到能够登录成功,并且是通过数据库的方式登录。
接下来,我们增加一个前端页面,在前端页面中进行登录操作,在resources文件下创建templates文件,在templates文件下创建login.html和登录成功之后的跳转页面main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Shiro 登录认证</h1>
<br>
<form action="/myController/userlogin">
<div>用户名:<input type="text" name="name" value=""></div>
<div>密码:<input type="password" name="password" value=""></div>
<div><input type="submit" value="登录"></div>
</form>
</body>
</html>
main.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
</body>
</html>
增加一个跳转登录页面的controller方法:
@GetMapping("/login")
public String toLogin(){
return "login";
}
改造登录接口
package com.dong.shirotext.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/myController")
public class LoginController {
@GetMapping("/userlogin")//使用get方式,方便参数传递
public String login(String name, String password, HttpSession session){
//1.获取subject对象
Subject subject= SecurityUtils.getSubject();
//通过name,password封装为token对象中
//现在能理解我们自定义的登录验证方法中,为什么能够从token中获取用户信息了吧
AuthenticationToken token=new UsernamePasswordToken(name,password);
//然后调用subject.login方法登录
try {
//此时就会去执行我们自定义的登录验证的方法,同时将token作为参数传递过去了
subject.login(token);
//将用户信息存到session中
session.setAttribute("user",token.getPrincipal().toString());
//跳转页面
return "main";
}catch (AuthenticationException e){
e.printStackTrace();
return "登录失败!";
}
}
@GetMapping("/login")
public String toLogin(){
return "login";
}
}
再次启动项目,访问http://localhost:8080/myController/login,页面如下:
输入用户名称,密码点击登录,能够登录成功,并且展示存在session中的用户
九、多个Realm校验策略
到这里我们发现了,其实一种Realm就代表了一种认证规则,而Shiro可以同时创建多个Realm,每一个Realm代表不同的认证规则,比用账号密码、手机号+验证码等等,如果存在多个Realm,Shiro又该怎么进行验证?
Shiro 的 ModularRealmAuthenticator 会使用内部的 AuthenticationStrategy 组件判断认证是成功还是失败。
AuthenticationStrategy 是一个无状态的组件,它在身份验证尝试中被询问 4 次(这4 次交互所需的任何必要的状态将被作为方法参数):
- 在所有 Realm 被调用之前
- 在调用 Realm 的 getAuthenticationInfo 方法之前
- 在调用 Realm 的 getAuthenticationInfo 方法之后
- 在所有 Realm 被调用之后
认证策略的另外一项工作就是聚合所有 Realm 的结果信息封装至一个AuthenticationInfo 实例中,并将此信息返回,以此作为 Subject 的身份信息。
Shiro中定义了三种认证策略的实现
AuthenticationStrategy class | 描述 |
---|---|
AtLeastOneSuccessfulStrategy | 只要有一个(或更多)的 Realm 验证成功,那么认证将视为成功 |
FirstSuccessfulStrategy | 第一个Realm验证成功,就认为整体是成功的,后面的Realm会被忽略 |
AllSuccessfulStrategy | 所有的Realm都验证成功,认证才视为成功 |
ModularRealmAuthenticator 默认的认证策略是第一种AtLeastOneSuccessfulStrategy
多个Realm代码实现,主要在配置中配置modularRealmAuthenticator,指定认证策略,然后封装Realm集合
//多个Realm
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
//创建认证对象,并指定认证策略
ModularRealmAuthenticator modularRealmAuthenticator=new ModularRealmAuthenticator();
//这里指定了第三种认证策略,及所有的Realm都要认证成功
modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
//封装Realm集合
List<Realm> list=new ArrayList<>();
list.add(realm);//有几个就添加几个
//将Realm集合存入DefaultWebSecurityManager中返回
manager.setRealms(list);
return manager;
}
}
十、通过Shiro实现记住我
记住我,这个功能相信大家都有使用过,我们直接开始正文,首先,在shiro配置类中进行配置
package com.dong.shirotext.config;
import com.dong.shirotext.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AllSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.management.MXBean;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm realm; //注入我们自定义的Realm对象
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager =
new DefaultWebSecurityManager();
//创建密码加密对象
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//设置加密对象的属性
matcher.setHashAlgorithmName("md5");
matcher.setHashIterations(3);
//将加密对象 存储到Realm对象中
realm.setCredentialsMatcher(matcher);
//将Realm对象 存入 defaultWebSecurityManager对象中
defaultWebSecurityManager.setRealm(realm);
//设置remember Me
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
//返回
return defaultWebSecurityManager;
}
//cokie属性设置
public SimpleCookie remeberMeCookie(){
SimpleCookie cookie=new SimpleCookie("remeberMe");
//设置跨域
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}
//创建Shiro的cookie管理对象
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
cookieRememberMeManager.setCookie(remeberMeCookie());
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}
//配置shiro的拦截范围,我们的登录接口肯定是需要放行的
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition
= new DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userlogin", "anon");
definition.addPathDefinition("/myController/logon", "anon");
//配置需要拦截的请求范围
definition.addPathDefinition("/**", "authc");
//添加存在用户的过滤器(remeberMe)
definition.addPathDefinition("/**","user");
return definition;
}
}
修改controller
@GetMapping("/userlogin")//使用get方式,方便参数传递
public String login(String name, String password,
@RequestParam(defaultValue = "false") boolean rememberMe,
HttpSession session){
//1.获取subject对象
Subject subject= SecurityUtils.getSubject();
//通过name,password封装为token对象中
//现在能理解我们自定义的登录验证方法中,为什么能够从token中获取用户信息了吧
AuthenticationToken token=new UsernamePasswordToken(name,password,rememberMe);
//然后调用subject.login方法登录
try {
//此时就会去执行我们自定义的登录验证的方法,同时将token作为参数传递过去了
subject.login(token);
//将用户信息存到session中
session.setAttribute("user",token.getPrincipal().toString());
//跳转页面
return "main";
}catch (AuthenticationException e){
e.printStackTrace();
return "登录失败!";
}
//登录认证验证 rememberMe
@GetMapping("userLoginRm")
public String userLogin(HttpSession session) {
session.setAttribute("user","rememberMe");
return "main";
}
}
启动项目,直接去访问: http://localhost:8080/myController/userLoginRm 会跳转到登录页面让我们登录,
选择记住我,然后登录,重启浏览器直接访问http://localhost:8080/myController/userLoginRm ,就不会自动跳转到登录页面。,而是直接访问成功。
十一、用户认证登录成功之后退出登录
退出登录很简单,我们直接把退出登录的接口配置在Shiro配置类中即可
//退出登录
definition.addPathDefinition("/logout","logout");
十二、授权,角色验证
用户登录后,需要验证是否具有指定角色指定权限。Shiro也提供了方便的工具进行判断。
这个工具就是Realm的doGetAuthorizationInfo方法进行判断。触发权限判断的有两种方式:
- 在页面中通过shiro:****属性判断
- 在接口服务中通过注解@Requires****进行判断
通过给接口服务方法添加注解可以实现权限校验,可以加在控制器方法上,也可以加在业务方法上,一般加在控制器方法上。常用注解如下:
- @RequiresAuthentication
验证用户是否登录,等同于方法subject.isAuthenticated() - @RequiresUser
验证用户是否被记忆:
登录认证成功subject.isAuthenticated()为true
登录后被记忆subject.isRemembered()为true - @RequiresGuest
验证是否是一个guest的请求,是否是游客的请求
此时subject.getPrincipal()为null - @RequiresRoles
验证subject是否有相应角色,有角色访问方法,没有则会抛出异常AuthorizationException。
例如:@RequiresRoles(“aRoleName”)
void someMethod();
只有subject有aRoleName角色才能访问方法someMethod() - @RequiresPermissions
验证subject是否有相应权限,有权限访问方法,没有则会抛出异常AuthorizationException。
例如:
@RequiresPermissions (“file:read”,”wite:aFile.txt”)
void someMethod();
subject必须同时含有file:read和wite:aFile.txt权限才能访问方法someMethod()
新加接口,该接口添加注解RequiresRoles,表示当前登录的用户需要拥有admin角色才能访问该方法。
@GetMapping("/userLoginRoles")
@ResponseBody
@RequiresRoles("admin")
public String Role(){
return "恭喜你通过认证!";
}
启动项目进行测试,直接报错,报错信息就是当前的用户不具有admin这个角色。
赋予它admin的角色
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//我们直接当前的用户赋予它admin的角色,正常来说角色是需要根据用户名称去数据库中查询的
// String name = principalCollection.getPrimaryPrincipal().toString();
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
authorizationInfo.addRole("admin");
return authorizationInfo;
}
这次就不会再报错
无论是角色还是权限,都是在我们自定义的Realm中的 doGetAuthorizationInfo(PrincipalCollection principalCollection) 方法中进行验证,如果我们的用户存在角色和权限,我们需要再该方法中,将角色和权限从数据库中查询出来,保存在AuthenticationInfo对象中返回。
十三、前端页面授权验证Thymeleaf
添加依赖
<!--配置 Thymeleaf 与 Shrio 的整合依赖-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
配置类中添加新配置
//解析thymeleaf中的shiro:相关属性
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
Thymeleaf 中常用的 shiro:属性
--guest 标签
<shiro:guest>
</shiro:guest>
用户没有身份验证时显示相应信息,即游客访问信息。
--user 标签
<shiro:user>
</shiro:user>
用户已经身份验证/记住我登录后显示相应的信息。
--authenticated 标签
<shiro:authenticated>
</shiro:authenticated>
用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的。
--notAuthenticated 标签
<shiro:notAuthenticated>
</shiro:notAuthenticated>
用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的
也属于未进行身份验证。
--principal 标签
<shiro: principal/>
<shiro:principal property="username"/>
相当于((User)Subject.getPrincipals()).getUsername()。
-lacksPermission 标签
<shiro:lacksPermission name="org:create">
</shiro:lacksPermission>
如果当前 Subject 没有权限将显示 body 体内容。
--hasRole 标签
<shiro:hasRole name="admin">
</shiro:hasRole>
如果当前 Subject 有角色将显示 body 体内容。
--hasAnyRoles 标签
<shiro:hasAnyRoles name="admin,user">
</shiro:hasAnyRoles>
如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容。
--lacksRole 标签
<shiro:lacksRole name="abc">
</shiro:lacksRole>
如果当前 Subject 没有角色将显示 body 体内容。
--hasPermission 标签
<shiro:hasPermission name="user:create">
</shiro:hasPermission>
如果当前 Subject 有权限将显示 body 体内容
十四、EhCache缓存的搭建使用
导入依赖
<!--Shiro 整合 EhCache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!--磁盘的缓存位置-->
<diskStore path="java.io.tmpdir/ehcache"/>
<!--默认缓存-->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>
<!--helloworld 缓存-->
<cache name="HelloWorldCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
<!--
defaultCache:默认缓存策略,当 ehcache 找不到定义的缓存时,则使用这个
缓存策略。只能定义一个。
-->
<!--
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout 将不起作用。
overflowToDisk:是否保存到磁盘,当系统宕机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当
eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间
无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间
介于创建时间和失效时间之间。仅当 eternal=false 对象不是永久有效时使用,默认
是 0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store
persists between restarts of the Virtual Machine. The default value
is false.
diskSpoolBufferSizeMB:这个参数设置 DiskStore(磁盘缓存)的缓存区大
小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是
120 秒。
memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,
Ehcache 将会根据指定的策略去清理内存。默认策略是 LRU(最近最少使用)。你可以
设置为 FIFO(先进先出)或是 LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策
略)、FIFO(先进先出)、LFU(最少访问次数)。
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是
讲一直以来最少被使用的。如上面所讲,缓存的元素有一个 hit 属性,hit 值最小的将
会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当
缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳
离当前时间最远的元素将被清出缓存。
-->
</ehcache>
在在 resources 下添加配置文件 ehcache/ehcache-shiro.xml
配置文件中进行如下配置:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="ehcache" updateCheck="false">
<!--磁盘的缓存位置-->
<diskStore path="java.io.tmpdir"/>
<!--默认缓存-->
<defaultCache
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="false">
</defaultCache>
<!--登录认证信息缓存:缓存用户角色权限-->
<cache name="loginRolePsCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true"/>
</ehcache>
修改shiro配置类
package com.dong.shirotext.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.dong.shirotext.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AllSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.management.MXBean;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm realm; //注入我们自定义的Realm对象
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager =
new DefaultWebSecurityManager();
//创建密码加密对象
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//设置加密对象的属性
matcher.setHashAlgorithmName("md5");
matcher.setHashIterations(3);
//将加密对象 存储到Realm对象中
realm.setCredentialsMatcher(matcher);
//将Realm对象 存入 defaultWebSecurityManager对象中
defaultWebSecurityManager.setRealm(realm);
//设置remember Me
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
//设置缓存管理器
defaultWebSecurityManager.setCacheManager(getCacheManager());
//返回
return defaultWebSecurityManager;
}
//获取缓存管理器
private CacheManager getCacheManager() {
EhCacheManager ehCacheManager=new EhCacheManager();
//读取配置文件
InputStream is=null;
try{
is= ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
}catch (IOException e){
e.printStackTrace();
}
//获取缓存管理器
net.sf.ehcache.CacheManager cacheManager=new net.sf.ehcache.CacheManager(is);
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}
//cokie属性设置
public SimpleCookie remeberMeCookie(){
SimpleCookie cookie=new SimpleCookie("rememberMe");
//设置跨域
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}
//创建Shito的cookie管理对象
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
cookieRememberMeManager.setCookie(remeberMeCookie());
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}
//配置shiro的拦截范围,我们的登录接口肯定是需要放行的
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition
= new DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userlogin", "anon");
definition.addPathDefinition("/myController/logon", "anon");
//配置需要拦截的请求范围
definition.addPathDefinition("/**", "authc");
//添加存在用户的过滤器(remeberMe)
definition.addPathDefinition("/**","user");
//退出登录
definition.addPathDefinition("/logout","logout");
return definition;
}
//解析thymeleaf中的shiro:相关属性
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}
十五、会话管理
会话管理器,负责创建和管理用户的会话(Session)生命周期,它能够在任何环境中在本地管理用户会话,即使没有Web/Servlet/EJB容器,也一样可以保存会话。默认情况下,Shiro会检测当前环境中现有的会话机制(比如Servlet容器)进行适配,如果没有(比如独立应用程序或者非Web环境),它将会使用内置的企业会话管理器来提供相应的会话管理服务,其中还涉及一个名为SessionDAO的对象。SessionDAO负责Session的持久化操作(CRUD),允许Session数据写入到后端持久化数据库。
SessionManager由SecurityManager管理。Shiro提供了三种实现:
- DefaultSessionManager:用于JavaSE环境
- ServletContainerSessionManager:用于web环境,直接使用Servlet容器的会话
- DefaultWebSessionManager:用于web环境,自己维护会话(不使用Servlet容器的会话管理)
获取Session方式
(1)实现
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute(“key”,”value”)
(2)说明
Controller 中的 request,在 shiro 过滤器中的 doFilerInternal 方法,被包装成ShiroHttpServletRequest。
SecurityManager 和 SessionManager 会话管理器决定 session 来源于 ServletRequest还是由 Shiro 管理的会话。
无论是通过 request.getSession 或 subject.getSession 获取到 session,操作session,两者都是等价的。
总结
使用Shiro,导入Shiro依赖,自定义自己的登录逻辑和鉴权,继承AuthorizingRealm类,重写其中两个方法,一个用于鉴权,一个用于登录认证。
然后是shiro配置类的编写,自定义的操作需要再配置类中进行配置,DefaultWebSecurityManager。
其次,在Spring Boot中,可以使用注解@RequiresRole(是否拥有角色)、@RequiresPermissions(是否拥有权限)、@RequiresGuest(是否游客)、@RequiresUser(是否被记忆)、@RequiresAuthentication(是否登录)。
最后是缓存EhCache和Session的使用。demo案例在代码包中,大家可以自己下载来看看。