Shiro权限控制
之前写过Shiro的文章,但是当回过头来整理的时候,发现缺了好多东西,今天重新整理一下。
我们都知道Shiro和secitity都是安全的框架,但是相对于Shiro来说,比较入门简单,所需要的功能基本上都能满足,理解起来也会比较容易。
Shiro是一个有许多特性的全面的安全框架,下面一幅图进行介绍。
可以看出Shiro除了基本的认证、授权、会话管理、加密之外还有许多特性。
Shiro架构:
从架构来说,主要包含三个概念,Subject、SecurityManager、Realms(重要),在使用的时候我们都是围绕这三个概念来进行编码和使用
Shiro权限控制2.png
Shiro的应用不依赖任何的容器,可以在javaSE下使用也可以在javaEE上使用,下面是一张用户登录的例图:
Shiro权限控制3.png
首先理解下面的几个类:
SecurityManager:
Shiro的核心是ScurityManager,它是负责安全认证与授权的,Shiro本身已经实现了所有的细节,我们在使用的时候可以完全把它当做黑盒使用。
SecurityUtils:
本质上是一个工厂,类似Spring中的ApplicationContext,
Subject:
Subject是有点难理解的,有些地方理解为user,其实不然,Subject中文翻译是项目,在下面代码中会表现的很清楚
Realm:
在Shiro中,进行的授权和认证就是由它来操作的,我已开始对授权和认证不是很理解,也不是很明白在代码操作的时候命名在认证的时候,可以获取到授权的信息,为什么还要在进行授权的操作,下面对授权和认证做一下解释:所谓的认证,他相当于人的身份证,就是可以证明你身份的证件,在应用中,就是拿着当前登录的用户的名称与数据库中查询,看是当前登录的用户是否在数据库存在,如果存在,好,说明你是你自己。而所谓的授权,就相当于当你购买火车票的时候,如果你买的硬座,你就只能去硬座的车厢,而不能去软卧等其他车厢,这就相当于限制了你的能力,而在应用中也相当于如此,在授权的时候,会从数据库将你的权限信息获取到,交给Shiro,如果此时你要去访问指定的页面的时候,会首先对你的权限进行校验,看你是否有该权限,如果有就可以访问,没有则不可以访问。
代码讲解:
准备ehcache的xml配置 ,为什么要设置ehcache,是因为如果不设置,会每次刷新页面的时候都会去访问授权的方法,加上缓存之后,可以有效的制止
<?xml version="1.0" encoding="UTF-8"?>
<!--add by shanggq 2018/8/31 end-->
<ehcache name="es">
<diskStore path="java.io.tmpdir"/>
<!--
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:
Ehcache的三种清空策略;
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<!-- 登录记录缓存锁定10分钟 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
<!-- add by zhangcf 2018/8/31 start -->
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>springbootshiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springbootshiro</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--<exclusions>-->
<!--<exclusion>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-tomcat</artifactId>-->
<!--</exclusion>-->
<!--</exclusions>-->
</dependency>
<!--!--用于编译jsp–>-->
<!--<!– https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper –>-->
<!--<dependency>-->
<!--<groupId>org.apache.tomcat.embed</groupId>-->
<!--<artifactId>tomcat-embed-jasper</artifactId>-->
<!--</dependency>-->
<!--<!–jsp页面使用jstl标签–>-->
<!--<!– https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl –>-->
<!--<dependency>-->
<!--<groupId>javax.servlet.jsp.jstl</groupId>-->
<!--<artifactId>jstl</artifactId>-->
<!--</dependency>-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-configuration-processor -->
<!-- configuration-processor -->
<!--用来读取配置文件-->
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-configuration-processor</artifactId>-->
<!--<optional>true</optional>-->
<!--</dependency>-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--jpa 对象持久化,利用该jar包,通过bean直接生成数据库表-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- shiro的依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<!--<!– https://mvnrepository.com/artifact/net.sf.ehcache/ehcache –>-->
<!--<dependency>-->
<!--<groupId>net.sf.ehcache</groupId>-->
<!--<artifactId>ehcache</artifactId>-->
<!--<version>2.10.5</version>-->
<!--</dependency>-->
<!-- DRUID是阿里巴巴开源平台上一个数据库连接池实现,它结合了C3P0、DBCP、PROXOOL等DB池的优点,同时加入了日志监控,
可以很好的监控DB池连接和SQL的执行情况,可以说是针对监控而生的DB连接池(据说是目前最好的连接池,不知道速度有没有BoneCP快)。-->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!--spring boot 整合 mybatis 依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- json支持 -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<!-- 包含支持UI模版(Velocity,FreeMarker,JasperReports), 邮件服务, 脚本服务(JRuby), 缓存Cache(EHCache),
任务计划Scheduling(uartz)。 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- 单点登录 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.2.4</version>
</dependency>
</dependencies>
<build>
<!--表示最终的项目名,-->
<finalName>springbootshiro</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--用于告诉maven在打包的时候不需要web.xml,否则会报到不到web.xml de错-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
下面对代码的讲解,我是按照用户一开始登录,到最后的顺序来讲解,如果按照从数据操作到前端访问开始讲的话,有些地方会比较难理解
Controller层的数据 -- 前端页面在访问的时候访问的路径
package com.example.springbootshiro.Controller;
import com.example.springbootshiro.service.ILoginService;
import com.example.springbootshiro.entity.Role;
import com.example.springbootshiro.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @author shanggq
* @date 2018/8/31
*/
// add by shanggq 2018/8/31 start
@RestController
public class LoginResource {
// // 注入业务层
// @Autowired
// private ILoginService iLoginService;
//
@GetMapping("/login")
public String login() {
return "login";
}
//
// POST登录
@PostMapping("/login")
public String login(@RequestBody Map map) {
// 添加用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
map.get("username").toString(),
map.get("password").toString()
);
subject.login(usernamePasswordToken);
return "login";
}
//
@RequestMapping("/index")
public String index(){
return "index";
}
//
//
@PostMapping("/error")
public String error(){
return "error";
}
//
// @RequestMapping("/addUser")
// public String addUser(@RequestBody Map<String,Object> map){
// User user = iLoginService.addUser(map);
// return "addUser id ok \n"+user;
// }
//
//// 角色初始化
//
// @RequestMapping("addRole")
// public String addRole(@RequestBody Map<String ,Object> map ){
// Role role = iLoginService.addRole(map);
// return "addRole is ok ! \n" +role;
// }
//
//// 注解的使用,表示访问该方法需要怎样的权限和角色
//
//
// @RequiresRoles("admin")
// @RequiresPermissions("create")
// @RequestMapping("/create")
// public String create(){
// return "Create success!";
// }
}
// add by shanggq 2018/8/31 endn
上面代码显示,如果当前端按照post的格式进行数据访问login的时候,会请求到login的方法中,接收的参数是一个map,也就是说在页面中进行数据请求的时候,会将登录的用户名和密码或者其他数据进行传递,然后使用@RequetsBody将数据封装到map集合中,然后下面将数据放到了 UsernamePasswordToken(用户名密码认证机制)对象中,然后此时将该对象放到了用 SecurityUtils对象获取到的Subject对象中,SecutityUtils在上面提到过,他相当于Application,本质上是一个工厂类,然后使用该工厂获取到了Subject,在上面的构架图中我们可以看到,Application Code最终是给了Subject,然后Subject却又给了SecutityManager对象,也就是说现在可以理解为页面传递的用户的信息会存储在SecutityManager中。
Realm的编写(最重要的。进行用户的授权和认证)
package com.example.springbootshiro.shiro;
import com.example.springbootshiro.mapper.UserInfoMapper;
import com.example.springbootshiro.service.ILoginService;
import com.example.springbootshiro.entity.Permission;
import com.example.springbootshiro.entity.Role;
import com.example.springbootshiro.entity.User;
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.authz.SimpleAuthorizationInfo;
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;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author shanggq
* @date 2018/8/31
*/
// add by shanggq 2018/8/31 start
@Component
public class MyShiroRealm extends AuthorizingRealm {
/**
* 用於数据库的数据的访问,
*
* @Resource 按照名称进行数据的注入
* @Autowired 按照类型进行数据的注入
*/
@Autowired
private UserInfoMapper tokentokentoken;
/**
* 权限信息(授权)
*
* @param principals
* @return 如果用户正常退出,缓存会自动消除
* 如果用户非正常退出,缓存也会自动消除
* 如果修改了用户的权限,而用户没有退出系统,修改的权限无法立即生效--需手动实现,放在service中
* <p>
* 如果不做缓存,shiro会有自己的时间的间隔机制,时间为2分钟
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
/*
* 当没有使用缓存的时候,不断的刷新页面的话,这个代码会不断的执行,其实没有
* 必要每次都要重新设置权限的信息,所以需要在放到缓存中进行管理,当放到环迅中这样的haunted
* doGetAuthorizationInfo就会只执行一次,缓存在过期之后再次
* */
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
User user = (User) principals.getPrimaryPrincipal();
List<Role> list = new ArrayList<>();
list = tokentokentoken.findRoleListByName(user.getUid());
user.setRoles(list);
// 设置角色,权限,
// info.addRole("admin");
// info.addStringPermission("query");
// 从数据库中设置角色和权限,分别设置到 SimpleAuthorizationInfo 中返回
for (Role role : user.getRoles()) {
info.addRole(role.getRolename());
for (Permission permission : role.getPermissions()) {
info.addStringPermission(permission.getPermission());
}
}
return info;
}
/**
* 身份认证
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户的输入的账号
String userName = (String) token.getPrincipal();
User user = tokentokentoken.findByUserName(userName);
if (user == null) {
return null;
}
// 加密方式,获取密码在存入的时候,加密的盐
// 明文。若存在,将此用户存放到登录认证info中,不需要我们自己进行密码的比较,shiro会自动给我们进行比较
return new SimpleAuthenticationInfo(userName, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
}
}
// add by shanggq 2018/8/31 end
Realm中分为身份认证和授权两个方法,一开始我对这两个方法不理解,因为我的思路是你身份认证完成之后说明你可以进行登录,这样不就完成控制了么,为什么还需要权限认证,当我在继续往下面看的时候,发现并不是这样的。数据全部的操作全部交给SecurityManager对象去操作。
下面配置核心对象 SecurityManager。在这里我们使用的是spring boot 搭建的工程,所以这里也使用java配置,来完成对象注入到spring容器中。
package com.example.springbootshiro.shiro;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.LinkedHashMap;
/**
* 进行相应的bena的初始化
*
* @author shanggq
* @date 2018/8/31
*/
// add by shanggq 2018/8/31 start
@Configuration
public class ShiroConfiguration {
//将自己的验证方式加入容器,因为自己的的容器配置了授权和认证的方法
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
/*
shiro缓存管理器
* 需要注入其他的实体类中
* 1,安全管理器。secuityManager 最核心的管理器,但是配置完成之后基本上不会进行操作
* */
@Bean
public EhCacheManager ehCacheManager() {
System.out.println("ShiroConfiguration.getEhCacheManager()");
EhCacheManager cacheManager = new EhCacheManager();
// 指定配置shiro的文件
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cacheManager;
}
// 权限管理,配置主要是Realm的管理认证
@Bean(name = "securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 设置realm
manager.setRealm(myShiroRealm());
// 注入缓存
manager.setCacheManager(ehCacheManager());
return manager;
}
// Filter 工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须要设置的 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置的拦截器
// HashMap<String, String> hashMap = new HashMap<>();
LinkedHashMap<String, String> hashMap = new LinkedHashMap<>();
// 配置退出的过滤器,其中的具体的退出代码shiro已经替我们实现了
hashMap.put("/logout", "logout");
// 登录页面需要的权限
hashMap.put("/login", "anon");
//对所有的用户进行认证 当所有的认证都通过的时候才可以访问路径,
hashMap.put("/**", "authc");
//登录页
shiroFilterFactoryBean.setLoginUrl("/login");
//登录成功之后跳转到首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面。认证不通过的时候跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(hashMap);
return shiroFilterFactoryBean;
}
// 加入注解的使用,不加入这个注解不生效
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
// add by shanggq 2018/8/31 end
首先创建你继承了 AuthorizingRealm的 对象类,将该对象创建出来使用@Bean放到spring的容器中,然后再创建 EhCacheManager 对象,为什么要创建该对象的呢,我们在权限控制的代码中已经加了说明,是因为现权限在开发中一般很少去发生改变,如果我们每次在刷新页面的时候都要去数据库中获取数据,判断当前用户有没有当前的权限,会加大数据库的访问,所以我们可以将数据放在缓存中,这样在每次刷新页面的时候,不会都去访问该授权的代码,而首先是去缓存中查询数据,当缓存过期之后,才会去访问授权部分的代码。
然后在创建核心的对象SecutityManager对象,将继承身份认证的对象和缓存的对象都放到SecutityManager对象中,然后返回。然后创建下面的 ShiroFilterFactoryBean 对象,该对象在使用传统的配置文件的时候,也需要配置该类,目的是为了进行数据的拦截。将拦截的数据传递给上面创建的对象,然后完成身份和权限的校验,不知道在这里读者有没有在脑海中出现一条思路,也就是说,用户在将数据提交的时候,Subject会接收到数据,然后将数据会传递给SecutityManager对象,这里又有疑问了,数据是怎么传递给SecutityManager对象的呢,所以我就去查看源码,结果找到了下面的代码
控制4.png
也就是说,subject会将前端接收到的数据给SecutityManager,然而Realm返回的数据和 EhCacheManager缓存对象添加到SecutityManager对象中,但是该对象需要被谁给触发呢,所以就到了最后面的拦截器,ShiroFilterFactoryBean,那我们是不是可以理解为,当有请求过来的时候,会触发拦截器,而拦截器回去调用SecutityManager对象,而SecutityManager对象再去分别调用Realm和EhcacheManager对象,分别获取数据,此时用户的身份,权限都已经获取到,就到了下面的权限的设置,我们将全部的权限信息放到了 LinkedHashMap 集合中,而且还可以使用 ShiroFilterFactoryBean 对象设置登录、失败、首页所需要跳转的页面,然后将集合放到 ShiroFilterFactoryBean 对象中。下面对权限的信息进行说明(常用的五种)
Anon:表示可以不用登录直接访问
Authc:表示需要登录之后才可以访问
Perms:表示权限
Roles:表示角色
User:表示用户
AuthorizationAttributeSourceAdvisor对象表示的是开启对方法上的注解的扫描,因为有些方法是在当具有一定的权限的时候才可以访问的,如果不初始化该类的实例,方法上的注解是不起作用的,原因如下
是因为,代理的 方式,应该都知道,java的代理方式,一种是传统的代理方式,当有接口的时候才会使用,一种是cglib代理的方式,传统的代理方式是针对接口而言的,由于此时接口上是没有shiro的注解的,所以此时的注解是不起作用的,所以需要对上面的对象进行实例化,当然也可以改用cglib代理的方式,选择代理的方式是如果有接口就是用传统的代理方式,如果没有接口则使用cglib代理的方式。
文章来源:https://blog.csdn.net/weixin_38297879/article/details/82258119
推荐阅读:https://www.roncoo.com/course/list.html?courseName=Shiro