整合SSM-授权
增加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.21.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.21.RELEASE</version>
</dependency>
springMVC配置文件中开启shiro注解
<!-- 开启Shiro注解 -->
<aop:config proxy-target-class="true"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
注解方式:权限验证
@Controller
public class UserController {
/**
* @RequiresRoles 角色
* @RequiresAuthentication 必须登陆
* @RequiresGuest 匿名访问
* @RequiresPermissions 权限
* @RequiresUser 必须是那个用户才能访问
*/
//@RequiresRoles("roel1")
@RequiresRoles(value={"role1","role3"},logical=Logical.OR)
@RequestMapping("/query")
public String query(){
System.out.println("query...");
return "/success";
}
@RequiresRoles(value={"role1","role3"},logical=Logical.AND)
@RequestMapping("/add")
public String add(){
System.out.println("add...");
return "/success";
}
@RequiresPermissions("user:update")
@RequestMapping("/delete")
public String delete(){
System.out.println("delete...");
return "/success";
}
}
指定没有权限访问的跳转地址
注意虽然配置了此设置,但是在使用注解的形式下,未授权的情况下还是不会跳转,
此时我们需要在SpringMVC中添加对应的拦截器处理
<!-- 配置异常处理bean -->
<bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!-- 根据servlet报的错,作为键名来匹配,找上了就跳对应的资源 -->
<prop key="org.apache.shiro.authz.UnauthorizedException">redirect:/login.jsp</prop>
<prop key="org.apache.shiro.authz.UnauthenticatedException">redirect:/login.jsp</prop>
</props>
</property>
</bean>
jsp标签方式:验证
<%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro" %>
标签验证
<h1>系统首页:</h1>
<h2>当前状态:
<shiro:authenticated>
登陆状态
<!-- 如果认证的时候 返回的 对象此处就是对象,需要通过property来获取,如果是 String 就不用在处理-->
<shiro:principal property="username"/>
</shiro:authenticated>
<shiro:guest>
未登陆状态
</shiro:guest>
</h2>
<ul>
<shiro:hasRole name="role1">
<li><a href="#">用户管理</a></li>
</shiro:hasRole>
<shiro:hasRole name="role3">
<li><a href="#">角色管理</a></li>
</shiro:hasRole>
<shiro:hasPermission name="user:create">
<li><a href="#">菜单管理</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete">
<li><a href="#">部门管理</a></li>
</shiro:hasPermission>
</ul>
shiro标签说明
标签 | 说明 |
---|---|
shiro:authenticated | 表示已认证通过 |
shiro:guest | 只有是没有登录过,以游客的身份浏览才会看到标签内的内容 |
shiro:hashRole | 表示拥有某一角色 |
shiro:hasPermission | 表示拥有某一权限 |
shiro:lacksRole | 表示不拥有某一角色 |
shiro:lacksPermission | 表示不拥有某一权限 |
shiro:notAuthenticated | 表示没有通过验证 |
shiro:user | 表示已登录 |
shiro:principal | 表示用户的身份 |
shiro:authenticated
表示已认证通过
说明: 只有已通过用户认证,但不是通过记住我(remember me)浏览才会看到标签内的内容
<shiro:authenticated>
<label>用户身份验证已通过</label>
</shiro:authenticated>
shiro:guest
说明: 只有是没有登录过,以游客的身份浏览才会看到标签内的内容
<shiro:guest>
<label>您当前是游客,</label><a href="/login.jsp" >请登录</a>
</shiro:guest>
shiro:hashRole
表示拥有某一角色
说明: 只有成功登录后,且具有admin角色的用户才可以看到标签内的容,name属性中只能填写一个角色的名称
<shiro:hasRole name="admin">
<label>这个用户拥有的角色是admin</label>
</shiro:hasRole>
shiro:hasAnyRoles
表示拥有多个角色中的一个即可
说明: 只有成功登录后,且具有admin或者user角色的用户才会看到标签内的内容;name属性中可以填写多个角色名称,以逗号(,)分隔
<shiro:hasAnyRoles name="admin,user">
<label>这是拥有admin或者是user角色的用户</label>
</shiro:hasAnyRoles>
shiro:hasPermission
表示拥有某一权限
说明: 只有成功登录后,且具有admin:add权限的用户才可以看到标签内的内容,name属性中只能填写一个权限的名称
<shiro:hasPermission name="admin:add">
<label>这个用户拥有admin:add的权限</label>
</shiro:hasPermission>
shiro:lacksRole
表示不拥有某一角色
说明: 只有成功登录后,且不具有admin角色的用户才可以看到标签内的内容,name属性中只能填写一个角色的名称
<shiro:lacksRole name="admin">
<label>这个用户不拥有admin的角色</label>
</shiro:lacksRole>
shiro:lacksPermission
表示不拥有某一权限
说明: 只有成功登录后,且不具有admin:delete权限的用户才可以看到标签内的内容,name属性中只能填写一个权限的名称
<shiro:lacksPermission name="admin:delete">
<label>这个用户不拥有admin:delete的权限</label>
</shiro:lacksPermission>
shiro:notAuthenticated
表示没有通过验证
说明: 只有没有通过验证的才可以看到标签内的内容,包括通过记住我(remember me)登录的
<shiro:notAuthenticated>
<label>用户身份验证没有通过(包括通过记住我(remember me)登录的) </label>
</shiro:notAuthenticated>
shiro:user
表示已登录
<shiro:user>
<label>欢迎[<shiro:principal/>],</label><a href="/logout.jsp">退出</a>
</shiro:user>
shiro:principal
表示用户的身份
取值取的是你登录的时候,在Realm 实现类中的new SimpleAuthenticationInfo(第一个参数,…) 放的第一个参数
如果第一个放的是username或者是一个值 ,那么就可以直接用。
<!--取到username-->
<shiro: principal/>
如果第一个参数放的是对象,比如放User 对象。那么如果要取其中某一个值,可以通过property属性来指定。
<!--需要指定property-->
<shiro:principal property="username"/>
多realm认证
为什么要使用多realm认证?
实际开发中存在这样一种场景,同一个密码可能在MqSQL中存储,也可能在Oracle中存储,有可能MqSQL中使用的是MD5加密算法,而Oracle使用SHA1加密算法。
这就需要有多个Realm以及认证策略的问题。
MD5和SHA1加密简单实现
public static void main(String[] args) {
// 算法引入的是 org.apache.shiro.crypto.hash.Sha1Hash
// shiro中提供的jar包
// SHA1算法 原始密码 盐值 加密次数
Sha1Hash sha1= new Sha1Hash("1234", "iii", 1024);
System.out.println(sha1);
//md5
md5 = new Md5Hash("1234","iii",1024);
System.out.println(md5);
// 输出结果是:4de1f44e9cc52ab105336e67a9c9943f5a8c43bf
// 输出结果是:b3f8aa64ef17d41dff1e5bbd7e6d5766
}
往数据库中模拟数据
sys_user表中存储的数据是MD5加密的数据
sys_user_sha1表中存储的数据是Sha1加密的数据
mapper接口
public interface UserMapper {
List<User> query(String username);
List<User> querySha1(String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.i.mapper.UserMapper">
<select id="query" resultType="user">
SELECT * FROM sys_user WHERE username = #{username}
</select>
<select id="querySha1" resultType="user">
SELECT * FROM sys_user_sha1 WHERE username = #{username}
</select>
</mapper>
service
public interface UserService {
List<User> login(String username);
List<User> loginSha1(String username);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper mapper;
@Override
public List<User> login(String username) {
return mapper.query(username);
}
@Override
public List<User> loginSha1(String username) {
return mapper.querySha1(username);
}
}
增加一个自定义Realm文件
添加多Realm配置 applicationContext-shiro中
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 注册自定义Realm -->
<bean class="com.i.realm.MyRealm" id="myRealm">
<!-- 配置凭证匹配器 -->
<property name="credentialsMatcher">
<!-- 定义凭证匹配器 -->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 设置加密的算法和迭代次数 -->
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="1024"/>
</bean>
</property>
</bean>
<!-- 注册第二个Realm -->
<bean class="com.i.realm.MyRealmSha1" id="myRealmSha1">
<!-- 配置凭证匹配器 -->
<property name="credentialsMatcher">
<!-- 定义凭证匹配器 -->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 设置加密的算法和迭代次数 -->
<property name="hashAlgorithmName" value="sha1"/>
<property name="hashIterations" value="1024"/>
</bean>
</property>
</bean>
<!-- 注册SecurityManager -->
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
<property name="authenticator">
<!-- 配置多Realm的匹配策略 -->
<bean class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<!-- 至少有一个Realm认证通过 -->
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
</property>
</bean>
</property>
<property name="realms">
<list>
<ref bean="myRealm"/>
<ref bean="myRealmSha1"/>
</list>
</property>
</bean>
<!-- 注册ShiroFilterFactoryBean 注意id必须和web.xml中注册的targetBeanName的值一致 -->
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiro">
<!-- 注册SecurityManager -->
<property name="securityManager" ref="securityManager"/>
<!-- 登陆地址 如果用户请求的地址是 login 那么会对该地址认证-->
<property name="loginUrl" value="/login"/>
<!-- 登陆成功的跳转地址 -->
<property name="successUrl" value="/success.jsp"/>
<!-- 访问未授权的页面跳转的地址 -->
<property name="unauthorizedUrl" value="/refuse.jsp"/>
<!-- 设置过滤器链 -->
<property name="filterChainDefinitions">
<value>
<!-- 加载顺序从上往下
authc:需要认证
anon:可以匿名访问的资源
-->
/login=authc
/login.jsp=anon
/**=authc
</value>
</property>
</bean>
</beans>
验证策略 | 说明 |
---|---|
AllSuccessfulStrategy | 所有的Realm验证都得通过才通过 |
AtLeastOneSuccessfulStrategy | 最少有一个Realm验证通过 【默认】 |
FirstSuccessfulStrategy | 第一个Realm验证通过 |
缓存
为什么要使用缓存
在没有使用缓存的情况下,我们每次发送请求都会调用一次doGetAuthorizationInfo方法来进行用户的授权操作,但是我们知道,一个用户具有的权限一般不会频繁的修改,也就是每次授权的内容都是一样的,所以我们希望在用户登录成功的第一次授权成功后将用户的权限保存在缓存中,下一次请求授权的话就直接从缓存中获取,这样效率会更高一些。
使用Ehcache来实现缓存
引入jar包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.5.0</version>
</dependency>
添加ehcache的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!--diskStore:缓存数据持久化的目录 地址 -->
<diskStore path="D:\tools\ehcache" />
<!-- eternal:缓存中对象是否位永久的,如果是,超时设置将被忽略,对象从不过期
maxElementsInMemory:缓存中允许创建的最大对象数
maxElementsOnDisk:在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
overflowToDisk:内存不足时,是否启用磁盘缓存
diskPersistent:设定在虚拟机重启时是否进行磁盘存储,默认为false
diskExpiryThreadIntervalSecounds:属性可以设置该线程执行的间隔时间(默认时120秒,不能太小)
timeToIdleSeconds:缓存数据的钝化时间,也就是在一个元素消亡之前,两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值为0就意味这元素可以停顿无穷长的时间
timeToLiveSeconds:缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值为0就意味着元素可以停顿无穷长的时间
memoryStoreEvictionPolicy:缓存满了之后的淘汰算法
1 FIFO:先进先出
2 LFU:最少被使用,缓存的元素有一个hit属性,hit值最小的将会被清出缓存
3 LRU:最近最少被使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素时,那么现有缓存元素中时间戳离当前时间最远的元素将被请出缓存 -->
<defaultCache
eternal="false"
maxElementsInMemort="1000"
maxElementsOnDisk="10000000"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
在shiro的配置文件中配置缓存
<!-- 注册SecurityManager -->
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
<!-- 配置自定义Realm -->
<property name="realm" ref="myRealm"/>
<!-- 配置缓存管理器 -->
<property name="cacheManager">
<bean class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!-- 关联配置文件 -->
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
</property>
</bean>
清空缓存
在自定义realm中添加清空方法
//清空缓存
public void clearCache(){
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
添加业务处理方法
//清空缓存
@RequestMapping("/clearCache")
@ResponseBody
public void clearCache(){
System.out.println("清空缓存...");
myRealm.clearCache();
}
测试
第一次正常请求会授权,之后访问从缓存中获取。当调用清空缓存方法后,再次请求的时候因为缓存已经空了,所以会再次授权,
场景: 权限修改生效后,立即刷新清空缓存,则可以实现用户不退出生效新的权限
session
shiro提供的session不依赖web容器,可以直接使用,如果是在web环境下,session中的数据和httpsession中的数据是通的。Shiro中的session可以出现在任何地方,例如service、dao等,不需要从controller中传递session参数,用户保存在session中的数据可以在HTTP session中获取,保存在httpsession中的数据也可以从session中获取。
session常用方法
api | api |
---|---|
startTimestamp | session的创建时间 |
stopTimestamp | session的失效时间 |
lastAccessTime | session的最近一次访问时间,初始值是startTimestamp |
timeout | timeout |
attributes | session的属性容器 |
touch | 刷新 |
stop | 销毁会话,Subject.logout()时会自动调用stop方法来销毁会话 |
实现登录成功后保存登录信息到session中
创建FormAuthenticationFilter的子类重写onLoginSuccess方法
//自定义的表单认证过滤器
public class MyFormAuthenticationFilter extends FormAuthenticationFilter{
//认证成功会执行的方法
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,ServletResponse response) throws Exception {
//添加在认证成功后,我们自己的逻辑
Session session = subject.getSession();
session.setAttribute("msg", "登陆成功了...");
//执行原有认证成功的逻辑
return super.onLoginSuccess(token, subject, request, response);
}
}
shiro
shiro配置文件中配置
<!-- 配置自定义的表单过滤器 -->
<bean id="myFormAuthenticationFilter" class="com.i.filter.MyFormAuthenticationFilter">
<!-- 可以修改表单接收的参数名称 自定义 -->
<property name="usernameParam" value="username"/>
<property name="passwordParam" value="password"/>
</bean>
<!-- 注册ShiroFilterFactoryBean 注意id必须和web.xml中注册的targetBeanName的值一致 -->
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiro">
<!-- 注册SecurityManager -->
<property name="securityManager" ref="securityManager"/>
<!-- 登陆地址 如果用户请求的地址是 login 那么会对该地址认证-->
<property name="loginUrl" value="/login"/>
<!-- 登陆成功的跳转地址 -->
<property name="successUrl" value="/success.jsp"/>
<!-- 访问未授权的页面跳转的地址 -->
<property name="unauthorizedUrl" value="/refuse.jsp"/>
<!-- 配置自定义过滤器 -->
<property name="filters">
<map>
<entry key="authc" value-ref="myFormAuthenticationFilter"/>
</map>
</property>
<!-- 设置过滤器链 -->
<property name="filterChainDefinitions">
<value>
<!-- 加载顺序从上往下
authc:需要认证
anon:可以匿名访问的资源
-->
/login=authc
/login.jsp=anon
/**=authc
</value>
</property>
</bean>
remember me
Shiro提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
- 首先在登录页面选中RememberMe然后登录成功;
如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来 - 关闭浏览器再重新打开;会发现浏览器还是记住你的
- 访问一般的网页服务器端还是知道你是谁,且能正常访问
登录表单中添加记住我复选框
<form action="login" method="post">
账号:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="checkbox" name="rememberMe"/>记住密码<br>
<input type="submit" value="提交">
</form>
配置文件中配置
<!-- RememberMe配置 -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="httpOnly" value="true"/>
<!-- 默认记住7天(单位:秒) -->
<property name="maxAge" value="604800"/>
</bean>
<!-- RememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- 保存 的规则 Base64提供的编码-->
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}" />
<property name="cookie" ref="rememberMeCookie"/>
</bean>
<!-- 注册SecurityManager -->
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
<!-- 配置自定义Realm -->
<property name="realm" ref="myRealm"/>
<!-- 注册RememberMe -->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
到此就可以测试了。登录的时候勾选记住密码关闭浏览器,再访问user级别的请求就直接可以访问了。
但是因为此时并没有真正的认证,所以此时的session并不能使用,这时我们可以实现一个过滤器。来拦截rememberMe功能的请求即可
创建RememberMe的过滤器
//自定义rememberMe的过滤器
public class MyRememberMeFilter extends FormAuthenticationFilter{
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
//记住密码,没有登陆isAuthenticated()肯定为false
if(!subject.isAuthenticated() && subject.isRemembered() && session.getAttribute("msg") == null){
System.out.println("记住的用户是: "+subject.getPrincipal());
session.setAttribute("msg", "RememberMe中保存的数据");
}
return subject.isAuthenticated()||subject.isRemembered();
}
}
shiro配置文件中修改
<!-- 注册RememberMe的过滤器 -->
<bean id="myRememberMeFilter" class="com.i.filter.MyRememberMeFilter"/>
<!-- 注册ShiroFilterFactoryBean 注意id必须和web.xml中注册的targetBeanName的值一致 -->
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiro">
<!-- 注册SecurityManager -->
<property name="securityManager" ref="securityManager"/>
<!-- 登陆地址 如果用户请求的地址是 login 那么会对该地址认证-->
<property name="loginUrl" value="/login"/>
<!-- 登陆成功的跳转地址 -->
<property name="successUrl" value="/success.jsp"/>
<!-- 访问未授权的页面跳转的地址 -->
<property name="unauthorizedUrl" value="/refuse.jsp"/>
<!-- 配置自定义过滤器 -->
<property name="filters">
<map>
<entry key="authc" value-ref="myFormAuthenticationFilter"/>
<entry key="remember" value="myRememberMeFilter"/>
</map>
</property>
<!-- 设置过滤器链 -->
<property name="filterChainDefinitions">
<value>
<!-- 加载顺序从上往下
authc:需要认证
anon:可以匿名访问的资源
-->
/login=authc
/login.jsp=anon
/success.jsp=user,remember <!-- 必须配置为user级别,authc级别的rememberMe没有效果 -->
/**=authc
</value>
</property>
注意:如果我们在认证的AuthenticationInfo info = new SimpleAuthenticationInfo(user, pwd, credentialsSalt, “myrealm”);
保存的是自定义的对象,那么该对象必须实现Serializable接口,因为该对象要被持久化到cookie中!!!