什么是 Apache Shiro?
Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
Shiro 能做什么事情?
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。
Shiro 架构
-
Subject(用户): 访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;Subject 一词是一个专业术语,其基本意思是“当前的操作用户”。它是一个抽象的概念,可以是人,也可以是第三方进程或其他类似事物,如爬虫,机器人等。 在程序任意位置:Subject currentUser = SecurityUtils.getSubject(); 类似 Employee user = UserContext.getUser()一旦获得 Subject,你就可以立即获得你希望用 Shiro 为当前用户做的 90%的事情,如登录、登出、访问会话、执行授权检查等
-
SecurityManager(安全管理器) 安全管理器,它是 shiro 功能实现的核心,负责与后边介绍的其他组件(认证器/授权器/缓存控制器)进行交互,实现 subject 委托的各种功能。有点类似于SpringMVC 中的 DispatcherServlet 前端控制器。
-
Realms(数据源) Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。;可以把Realm 看成 DataSource,即安全数据源。执行认证(登录)和授权(访问控制)时,Shiro 会从应用配置的 Realm 中查找相关的比对数据。以确认用户是否合法,操作是否合理
-
Authenticator Authenticator 用于认证,协调一个或者多个 Realm,从 Realm 指定的数据源取得数据之后进行执行具体的认证。
-
Authorizer Authorizer 用户访问控制授权,决定用户是否拥有执行指定操作的权限。
-
SessionManager Shiro 与生俱来就支持会话管理,这在安全类框架中都是独一无二的功能。即便不存在 web容器环境,shiro 都可以使用自己的会话管理机制,提供相同的会话 API。
-
CacheManager 缓存组件,用于缓存认证信息等。
-
Cryptography Shiro 提供了一个加解密的命令行工具 jar 包,需要单独下载使用。
Shiro 架构的核心(Subject,SecurityManager,Realms)
subject 主体 ,当前登录用户 (里面有个状态可以区分是否有登录)
securityManager 安全管理器,管理很多组件,分发调度
realm数据源 , 提供数据给shiro进行逻辑处理
Shiro 在应用中最常使用的两个功能:用户登录认证和访问授权
Shiro 认证概述
认证的过程即为用户的身份确认过程,所实现的功能就是我们所熟悉的登录验证,用户输入账号和密码提交到后台,后台通过访问数据库执行账号密码的正确性校验。
基于 ini 的 Shiro 认证
-
导入基本的 jar 包
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency>
-
编写 ini 配置文件:shiro.ini
#用户的身份、凭据 [users] zhangsan=555 lisi=666
-
使用 Shiro 相关的 API 完成身份认证
@Test public void testShiro() throws Exception { //加载 shiro.ini 配置文件,得到配置中的用户信息(账号+密码) IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //创建 Shiro 的安全管理器 SecurityManager manager = factory.getInstance(); //将创建的安全管理器添加到运行环境中 SecurityUtils.setSecurityManager(manager); //获取登录的用户主体对象 Subject subject = SecurityUtils.getSubject(); System.out.println("登录前的认证状态: " + subject.isAuthenticated());//false //创建登录用户的身份凭证 UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "555"); try {//登录认证 subject.login(token); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("密码错误"); } System.out.println("登录后的认证状态:" + subject.isAuthenticated());//true }
登录失败存在两种情况
-
账号错误
org.apache.shiro.authc.UnknownAccountException
-
密码错误
org.apache.shiro.authc.IncorrectCredentialsException
自定义 Realm
Reaml 的继承体系
在继承体系中的每个类所能够实现的功能不一样,在后面的开发中,我们通常需要使用到缓存,认证和授权所有的功能,所以选择继承 AuthorizingRealm。
public class CrmRealm extends AuthorizingRealm {
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
通过配置修改 SecurityManager 中的默认 Realm 的使用 shiro.ini
#自定义的 Realm 信息
crmRealm=cn.wolfcode.crm.shiro.CRMRealm
#将 crmRealm 设置到当前的环境中
securityManager.realms=$crmRealm
集成 Shiro 认证
添加依赖(Shiro 常用的依赖都在这里)
<properties>
<shiro.version>1.5.2</shiro.version>
</properties>
<!--shiro 核心-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 的 Web 模块-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 和 Spring 集成-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 底层使用的 ehcache 缓存-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 依赖的日志包-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!--shiro 核心-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 的 Web 模块-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 和 Spring 集成-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 底层使用的 ehcache 缓存-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro 依赖的日志包-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!--shiro 依赖的工具包-->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<!--Freemarker 的 shiro 标签库-->
<dependency>
<groupId>net.mingsoft</groupId>
<artifactId>shiro-freemarker-tags</artifactId>
<version>1.0.1</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
</exclusion>
</exclusions>
</dependency>
shiroFilter过虑器
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
shiro 过虑器,DelegatingFilterProx 会从 spring 容器中找 shiroFilter,所以过滤器的生命周期还是交给 Spring 进行管理的。Shiro 中定义了多个过滤器来完成不同的预处理操作。
anon: 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon”
authc: 表示需要认证(登录)才能使用;示例“/**=authc” 主要属性:
usernameParam:表单提交的用户名参数名( username);
passwordParam:表单提交的密码参数名(password);
rememberMeParam:表单提交的密码参数名(rememberMe);
loginUrl:登录页面地址(/login.jsp);
successUrl:登录成功后的默认重定向地址;
failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure);
authcBasic: Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);
roles:角色授权拦截器,验证用户是否拥有资源角色;示例“/admin/=roles[admin]”
**perms:**权限授权拦截器,验证用户是否拥有资源权限;示例“/employee/input=perms[“user:update”]” **user:**用户拦截器,用户已经身份验证/记住我登录的都可;示例“/index=user”
**logout:**注销拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout”
**port:**端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径/参数等都一样
rest: rest 风格拦截器,自动根据请求方法构建权限字符(GET=read,POST=create,PUT=update,DELETE=delete, HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串; 示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹isPermittedAll);
**ssl:**SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样;
shiro.xml 配置文件
为了单独对 Shiro 相关的配置进行管理,我们分离出一个 shiro.xml 配置文件,最后在 mvc.xml 中引入
mvc.xml
<import resource="classpath:shiro.xml"/>
shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="cn.wolfcode.shiro"/>
<!--shiro过滤器 spring管理 指定系统资源需要使用的具体过滤器-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--引用指定的安全管理器-->
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.html"/>
<property name="filterChainDefinitions">
<value>
/login.html=anon
/login.do=anon
/js/**=anon
/images/**=anon
/css/**=anon
/logout.do=logout
/**=authc
</value>
</property>
</bean>
<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="crmRealm"/>
<!--注册缓存器-->
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!--凭证匹配器-->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--指定加密算法-->
<property name="hashAlgorithmName" value="MD5"/>
</bean>
<!--开启shiro注解扫描器-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!-- 设置配置文件 -->
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
</beans>
Shiro 授权概述
系统中的授权功能就是为用户分配相关的权限,只有当用户拥有相应的权限后,才能访问对应的资源。如果系统中无法管理用户的权限,那么将会出现非常客户信息泄露,数据没恶意篡改等问题,所以在绝大多数的应用中,我们都会有权限管理模块。
Shiro 授权方式分为三种:
-
编程式 通过写 if/else 授权代码块完成
Subject subject = SecurityUtils.getSubject(); if(subject.hasRole("admin")) { //有权限 } else { //无权限 }
-
注解式 通过在执行的 Java 方法上放置相应的注解完成
@RequiresRoles("admin") @RequiresPermissions("user:create") public void hello() { //有权限 }
-
JSP 标签(shiro 自带)或 freemarker 的标签(第三方) 在 JSP 页面通过相应的标签完成
<shiro:hasRole name="admin"> <!-- 有权限 --> </shiro:hasRole>
Shiro 标签
在前端页面上,我们通常可以根据用户拥有的权限来显示具体的页面,如:用户拥有删除员工的权限,页面上就把删除按钮显示出来,否则就不显示删除按钮,通过这种方式来细化权限控制。
要能够实现上面的控制,需要使用 Shiro 中提供的相关标签,标签的使用步骤如下:
-
引入相关的 jar 包
<dependency> <groupId>net.mingsoft</groupId> <artifactId>shiro-freemarker-tags</artifactId> <version>1.0.0</version> </dependency>
-
前端页面我们选择的是 freemarker,而默认 freemarker 是不支持 shiro 标签的,所以需要对其功能做拓展
可以理解为注册 shiro 的标签,达到在 freemarker 页面中使用的目的。
public class MyFreeMarkerConfig extends FreeMarkerConfigurer{ @Override public void afterPropertiesSet() throws IOException, TemplateException{ //继承之前的属性配置,这不不能省 super.afterPropertiesSet(); Configuration cfg = this.getConfiguration(); cfg.setSharedVariable("shiro", new ShiroTags());//shiro 标签 } }
-
在 mvc.xml 中将 MyFreeMarkerCon_g 设置成当前环境中使用的配置对象
<!-- 注册 FreeMarker 配置类 (配置自定义的配置类,带有shiro标签功能)--> <bean class="cn.wolfcode.shiro.MyFreeMarkerConfig"> <!-- 配置 FreeMarker 的文件编码 --> <property name="defaultEncoding" value="UTF-8" /> <!-- 配置 FreeMarker 寻找模板的路径 --> <property name="templateLoaderPath" value="/WEB-INF/views/" /> </bean>
有了上面的准备工作后,我们就可以在 freemarker 页面中使用 shiro 相关的标签来对页面显示做控制了。
shiro 的 freemarker 常用标签:
-
authenticated 标签:已认证通过的用户。
<@shiro.authenticated></@shiro.authenticated>
-
notAuthenticated 标签:未认证通过的用户。与 authenticated 标签相对。
<@shiro.notAuthenticated></@shiro.notAuthenticated>
-
principal 标签:输出当前用户信息,通常为登录帐号信息
<@shiro.principal property="name" />
-
hasRole 标签:验证当前用户是否属于该角色 ,
<@shiro.hasRole name=”admin”>Hello admin!</@shiro.hasRole>
-
hasAnyRoles 标签:验证当前用户是否属于这些角色中的任何一个,角色之间逗号分隔 ,
<@shiro.hasAnyRoles name="admin,user,operator">Hello admin! </@shiro.hasAnyRoles>
-
hasPermission 标签:验证当前用户是否拥有该权限 ,
<@shiro.hasPermission name="/order:*">订单/@shiro.hasPermission>
示例
<@shiro.hasPermission name="employee:delete">
<a href="#" class="btn btn-danger btn-xs btn-delete"data-href="/employee/delete.do?id=${entity.id}">
<span class="glyphicon glyphicon-trash"></span> 删除
</a>
</@shiro.hasPermission>
MD5 加密
加密的目的是从系统数据的安全考虑,如,用户的密码,如果我们不对其加密,那么所有用户的密码在数据库中都是明文,只要有权限查看数据库的都能够得知用户的密码,这是非常不安全的。所以,对用用户来说非常机密的数据,我们都应该想到使用加密技术,这里我们采用的是 MD5 加密。
在 Shiro 中继承了 MD5 的算法,所以可以直接使用它来实现对密码进行加密。
@Test
public void testMD5() throws Exception{
Md5Hash hash = new Md5Hash("1");
System.out.println(hash);//c4ca4238a0b923820dcc509a6f75849b
}
使用 Md5Hash 直接对数据进行加密
MD5 加密的数据如果一样,那么无论在什么时候加密的结果都是一样的,所以,相对来说还是不够安全,但是别怕,我们可以对数据加“盐”。同样的数据加不同的“盐”之后就是千变万化的,因为我们不同的人加的“盐”都不一样。这样得到的结果相同率也就变低了。
@Test
public void testMD5() throws Exception{
Md5Hash hash = new Md5Hash("1","admin");
System.out.println(hash);//e00cf25ad42683b3df678c61f42c6bda
}
Md5Hash()构造方法中的第二个参数就是对加密数据添加的“盐”,加密之后的结果也和之前不一样了。
如果还觉得不够安全,我们还可以通过加密次数来增加 MD5 加密的安全性。
@Test
public void testMD5() throws Exception{
Md5Hash hash = new Md5Hash("1","admin",3);
System.out.println(hash);//f3559efea469bd6de83d27d4284b4a7a
}
上面指定对密码进行 3 次 MD5 加密,在开发中可以根据实际情况来指定加密次数。
EhCache缓存机制
在请求中一旦进行权限的控制
,如:
@RequiresPermissions(“employee:view”)
<shiro:hasPermission name=“employee:input”>
都去到 Realm 中的 doGetAuthorizationInfo 方法进行授权,我们授权信息应该要从数据库中查询的。 如果每次授权都要去查询数据库就太频繁了,性能不好. 而且用户登陆后,授权信息一般很少变动,所有我们可以在第一次授权后就把这些授权信息存到缓存中,下一次就直接从缓存中获取,避免频繁访问数据库。
Shiro 中没有实现自己的缓存机制,只提供了一个可以支持具体缓存实现(如:Hazelcast, Ehcache, OSCache, Terracotta, Coherence, GigaSpaces, JBossCache 等)的抽象 API 接口,这样就允许 Shiro用户根据自己的需求灵活地选择具体的 CacheManager。这里我们选择使用 EhCache。
实现步骤:
-
添加依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency>
-
配置缓存管理器并引用缓存管理器
<!-- 缓存管理器 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <!-- 设置配置文件 --> <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/> </bean>
-
添加 ehcache 配置文件:shiro-ehcache.xml
<ehcache> <defaultCache> maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>
Ehcache 配置属性说明:
name: 缓存名称。
maxElementsInMemory: 缓存最大个数。
eternal:对象是否永久有效,一但设置了,timeout 将不起作用。
timeToIdleSeconds: 设置对象在失效前的允许闲置时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于
创建时间和失效时间之间。仅当 eternal=false 对象不是永久有效时使用,默认是 0.,也就是对
象存活时间无穷大。
over owToDisk:当内存中对象数量达到 maxElementsInMemory 时,Ehcache 将会对象写到磁盘中。 diskSpoolBu erSizeMB:这个参数设置 DiskStore(磁盘缓存)的缓存区大小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区。
maxElementsOnDisk:硬盘最大缓存个数。
diskPersistent:是否缓存虚拟机重启期数据Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是 120 秒。
memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会
根据指定的策略去清理内存。默认策略是 LRU(最近最少使用)。你可以设置为 FIFO(先进
先出)或是 LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。