Shiro安全框架的使用(记录)
框架介绍
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。
使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
Shrio的主要功能:
- Authentication:用户认证(登录)
- Authorization:权限控制 (授权)
- Session Management:会话管理
- Cryptography:数据加密
- Web Support:支持web的API
- Caching:缓存
- Concurrency:支持多线程应用程序
- Testing:测试的支持
- Run As:假设一个用户为另一个用户的身份
- Remember Me:在Session中保存用户身份
基本原理
Shiro的基本架构:
Shiro有三个核心的概念:Subject、SecurityManager和Realms。
Subject: Subject实质上是一个当前执行用户的特定的安全“视图”,开发者所写的应用代码就通过Subject与Shiro框架进行交互。所有Subject实例都必须绑定到一个SecurityManager上,当使用一个Subject实例时,Subject实例会和SecurityManager进行交互,完成相应操作。
SecurityManager: SecurityManager是Shiro的核心部分,作为一种“保护伞”对象来协调内部安全组件共同构成一个对象图。开发人员并不直接操作SecurityManager,而是通过Subject来操作SecurityManager来完成各种安全相关操作。
Realms: Realms担当Shiro和应用程序的安全数据之间的“桥梁”或“连接器”。从本质来讲,Realm是一个特定安全的DAO,Realm中封装了数据操作的模块和用户自定义的认证匹配过程。SecurityManager可能配置多个Realms,但至少要有一个。
Shiro过滤器
只解释了几个常用的,日后补充
在shiroFilter的配置中配置了要过滤的目录,其中一个号表示匹配当前目录后的参数,两个号表示匹配该目录及其子目录及参数。等号后面是Shiro各类过滤器的简称
Filter Name | Class | explanation |
---|---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名过滤器 |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 表单认证过滤器 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | |
authcBearer | org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter | |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出登录过滤器 |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | |
port | org.apache.shiro.web.filter.authz.PortFilter | |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | |
ssl | org.apache.shiro.web.filter.authz.SslFilter | |
user | org.apache.shiro.web.filter.authc.UserFilter | 记住我以后默认访问 |
缓存文件参数解释
FileName | explanation |
---|---|
eternal | 缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期 |
maxElementsInMemory | 缓存中允许创建的最大对象数 |
overflowToDisk | 内存不足时,是否启用磁盘缓存 |
timeToIdleSeconds | 缓存数据的钝化时间,也就是在一个元素消亡之前,两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是 0 就意味着元素可以停顿无穷长的时间 |
timeToLiveSeconds | 缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间 |
memoryStoreEvictionPolicy | 缓存满了之后的淘汰算法 |
diskPersistent: | 定在虚拟机重启时是否进行磁盘存储,默认为false |
diskExpiryThreadIntervalSeconds: | 属性可以设置该线程执行的间隔时间(默认是120秒,不能太小 |
FIFO | 先进先出 |
LFU | 最少被使用,缓存的元素有一个hit属性,hit值最小的将会被清出缓存 |
LRU | 最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存 |
使用方法
1、maven中添加Shiro以及缓存的依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<shiro.version>1.2.3</shiro.version>
</properties>
<!--shiro框架的依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<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>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--缓存依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.0</version>
</dependency>
2、在web.xml添加核心过滤器
<!--配置过滤器,让Spring管理Shiro框架-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!--将Shiro框架过滤器对应的生命周期交还给tomcat-->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、新建spring-shiro.xml配置文件配置Shiro框架(同时让Spring监听器监听此文件)
为显示高亮效果,所有/**均写成了/ **(含空格),写的时候记得改过来
<!--配置spring监听器,默认只加载WEB-INF目录下的spring.xml配置文件-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--设置配置文件路径-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml,classpath:spring-shiro.xml</param-value>
</context-param>
spring-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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置Shiro框架-->
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--注入安全管理器-->
<property name="securityManager" ref="securityManager"></property>
<!--认证成功页面,认证成功以后访问的页面-->
<property name="successUrl" value="/index.jsp"></property>
<!--认证失败页面,没有认证强制访问需要认证的页面自动跳转的页面-->
<property name="loginUrl" value="/admin/login"></property>
<!--自定义的filter-->
<property name="filters">
<map>
<!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中 -->
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<!--配置过滤器链-->
<!-- 过虑器链定义,从上向下顺序执行,一般将/ ** 放在最下边 -->
<property name="filterChainDefinitions">
<value>
<!--静态资源放行(匿名过滤器)anon org.apache.shiro.web.filter.authc.AnonymousFilter-->
/lib/ **=anon
/static/ **=anon
<!--页面-->
/verifyCode.jsp = anon
/login.jsp = anon
/index.jsp = anon
/welcome.jsp = anon
<!--记住我以后访问的默认页面-->
/index.jsp = user
<!--退出登录过滤器 logout org.apache.shiro.web.filter.authc.LogoutFilter-->
/logout = logout
<!--表单认证过滤器 authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter-->
<!-- -/ **=authc 表示所有的url都必须认证通过才可以访问- -->
/ ** = authc
</value>
</property>
</bean>
<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--注入自定义realm-->
<property name="realm" ref="customRealm"></property>
<!--注入缓存管理器(防止用户授权操作一直反复执行)-->
<property name="cacheManager">
<bean class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!--缓存配置文件-->
<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>
</bean>
</property>
<!--注入会话管理器-->
<property name="sessionManager" ref="sessionManager"></property>
<!--记住我cookie-->
<property name="rememberMeManager" ref="rememberMeManager"></property>
</bean>
<!--配置会话管理器(记住我功能)-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!--删除残留的session(退出以后删除session)-->
<property name="deleteInvalidSessions" value="true"/>
<!--设置session时长 单位毫秒 1000*3600表示一小时-->
<property name="globalSessionTimeout" value="#{1000 * 3600}"/>
</bean>
<!--记住我-->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<!--设置cookie时长 单位是秒-->
<property name="maxAge" value="#{3600 * 24 * 3}"/>
</bean>
</property>
</bean>
<!--自定义realm-->
<bean id="customRealm" class="com.shadow.shiro.CustomRealm">
<!--注入凭证匹配器(密码加密)-->
<property name="credentialsMatcher" ref="credentialsMatcher"></property>
</bean>
<!--数据库中存储到的md5的散列值,在realm中需要设置数据库中的散列值它使用散列算法及散列次数,让shiro进行散列对比时和原始数据库中的散列值使用的算法一致-->
<!--配置凭证匹配器(密码加密)-->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--指定加密算法-->
<property name="hashAlgorithmName" value="MD5"></property>
<!--散列次数-->
<property name="hashIterations" value="3"></property>
</bean>
<!--自定义的from认证过滤器-->
<bean id="formAuthenticationFilter" class="com.shadow.shiro.MyFormAuthenticationFilter">
<!-- 表单中账号的input名称 -->
<property name="usernameParam" value="username"/>
<!-- 表单中密码的input名称 -->
<property name="passwordParam" value="password"/>
<property name="rememberMeParam" value="rememberMe"/>
</bean>
<!--完成授权操作*****************************-->
<!--shiro为集成springMVC拦截异常-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!--没有权限异常跳转的页面-->
<prop key="org.apache.shiro.authz.UnauthorizedException">unauthorized</prop>
</props>
</property>
</bean>
<!--开启aop,对代理类-->
<aop:config proxy-target-class="true"/>
<!--开启shiro的注解支持-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<!--注入安全管理器-->
<property name="securityManager" ref="securityManager"></property>
</bean>
</beans>
4、Ehcache的配置文件 ehcache-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">
<!--缓存数据持久化的地址-->
<diskStore path="C:\shiro\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
5、编写自定义reaml
public class CustomRealm extends AuthorizingRealm {
/**
*判断当前的请求中是否包含username和password表单参数
* 1、如果没有
* 说明当前不是登录页面,就判断当前Shiro框架是否认证过
* 如果认证过 放行
* 如果没有认证,跳转认证失败页面 <property name="loginUrl" value="/admin/login"></property>
* 2、如果有
* 进行realm自定义认证
* 认证成功 进认证成功页面 <property name="successUrl" value="index.jsp"></property>
* 认证失败 获取认证失败的错误信息,再shiro框架的FormAuthenticationFilter过滤器中共享admin/login 获取失败errorMsg,显示到login.jsp上
* 自定义认证方法
* 自定义认证思路:
* 1、注入IUserService,service层
* @Autowired
* public IUserService userService;
* 2、从token获取身份(账号信息)
* String username = (String)token.getPrincipal();
* 3、调用IUserService层根据账号查询出用户信息的方法
* User user = userService.findUserByName(username);
* 3.1 users不为null,说明账号存在
* 3.2 users为null,当前方法直接返回null
* 4、创建认证信息对象,将身份(数据查询的那个user对象,和user对象的密码(凭证)作为参数)传递出去
* @param token
* @return
* @throws AuthenticationException
*/
@Autowired
private IUserService userService;
@Autowired
private IPermissionService permissionService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo");
//从token获取身份(账号信息)
String username = (String)token.getPrincipal();
//调用IUserService层根据账号查询出用户信息的方法
User user = userService.findUserByName(username);
if(user == null){
return null;
}
//获取密码
String password = user.getPassword();
//获取盐
String salt = user.getSalt();
//创建认证信息对象 参数:身份 凭证
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(salt), this.getName());
return info;
}
/**
*认证通过以后,因为方法基本都是从后台控制器跳过来的,可以使用权限注解,将需要有权限访问的资源打上@RequiresPermissions
* 注解,在SpringMVC的控制器上面打上注解以后,shiro会为控制器创建代理对象(运用aop),进行权限相关的控制
* 1、<aop:config proxy-target-class="true"/>shiro框架会创建控制器的代理对象
* 2、开启shiro的注解支持,代理对象可以拿到@RequiresPermissions注解
* 3、在底层进行权限判断,走doGetAuthenticationInfo方法,
* 4、程序运行时会根据有权限判断的地方获取权限表达式和设置shiro框架权限表达式数据进行比对
* 若有权限,则可直接访问
* 若无权限,则进入没有权限访问的页面,默认没有捕获异常,可以自己处理
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("doGetAuthorizationInfo");
//获取当前主身份(认证的user对象)
User user = (User) principals.getPrimaryPrincipal();
//获取身份的角色的权限id
String ids = user.getPermission_ids();
//切割权限id字符串获取每一个权限的id值
String[] split = ids.split(",");
//创建List<Integer>.把(字符串数组split)转化成(整型数组idsList)
List<Integer> idsList = new ArrayList<>();
for(String id : split){
idsList.add(Integer.valueOf(id));
}
//根据每一个权限的id值获取对应的权限表达式
List<String> permissionExpressions = permissionService.selectPermissionByIds(idsList);
System.out.println(permissionExpressions);
//创建授权对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//将查询出的权限表达式添加到权限信息对中
info.addStringPermissions(permissionExpressions);
return info;
}
}
6、自定义表单认证过滤器
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
/**
* 记住我功能生效以后,Shiro会将用户身份数据存放到本地cookie,但是Shiro在运行过程中,
* 使用session获取身份进行判断处理的,所以记住我以后会存在一个问题
*
* 当用户关闭浏览器以后,再次打开,除了在过滤器中设置记住我过滤器能够访问的页面以外,/index.jsp = user
* 其他的页面还是无法认证
*
* 因为,Shiro从cookie中获取的信息并没有自动设置到session中,所以我们需要自己把cookie记住我信息存放到session中
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//从请求中获得Shiro的主体
Subject subject = getSubject(request, response);
//从主体中获取Shiro框架的session
Session session = subject.getSession();
//如果主体没有认证并且主体已经设置记住我了
if(!subject.isAuthenticated()&&subject.isRemembered()){
//获取主体的身份,从记住我的cookie中获取
User principal = (User) subject.getPrincipal();
//将身份认证信息共享到session中
session.setAttribute("user",principal);
}
return subject.isAuthenticated() || subject.isRemembered();
}
/**
* 重写FormAuthenticationFilter的onLoginSuccess方法
* 指定的url传递进去,这样就实现了跳转到指定的页面;
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
WebUtils.getAndClearSavedRequest(request);//清理了session中保存的请求信息
WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
return false;
}
/**
* 验证码验证
* 从request请求对象获取验证码表单数据
* 如果有:说明用户在做登录操作 要进行验证码判断
*
* 判断思路:
* 1、获取用户提交的验证码
* 2、从session获取共享的写入图片 随机数
* 3、比对
* 相同:放行,继续调用父类方法
* 不同:跳转到登录页,并且共享错误信息
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//只有HttpServletRequest可以获取session
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//从session获取验证码,正确的验证码
HttpSession session = httpServletRequest.getSession();
String validate = (String) session.getAttribute("sRand");
//获取输入的验证码
String myValidate = httpServletRequest.getParameter("verifyCode");
//验证失败,设置错误信息
if (StringUtils.isNotBlank(validate) && StringUtils.isNotBlank(myValidate)
&& !validate.toLowerCase().equals(myValidate.toLowerCase())) {
httpServletRequest.setAttribute("errorMsg", "验证码错误");
//拒绝访问
return true;
}
//相同:放行,继续调用父类方法
return super.onAccessDenied(request, response);
}
}
7、控制器中方法(示例)
/**
* 管理员页面控制器
*/
@Controller
@RequestMapping("/admin")
public class AdminController {
@Autowired
private IUserService userService;
@Autowired
private IRoleService roleService;
/**
* 认证失败页面,没有认证强制访问需要认证的页面自动跳转的页面
* @return
*/
@RequestMapping("/login")
public String login(HttpServletRequest request,Model model){
//获取认证失败的错误信息,再shiro框架的FormAuthenticationFilter过滤器中共享
//共享的属性名称 shiroLoginFailure
//共享shiro异常的字节码类型
String shiroLoginFailure = (String)request.getAttribute("shiroLoginFailure");
if(shiroLoginFailure != null){
if(UnknownAccountException.class.getName().equals(shiroLoginFailure)){
model.addAttribute("errorMsg","用户名错误");
}else if(IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)){
model.addAttribute("errorMsg","密码错误");
}
}
return "forward:/login.jsp";
}
@RequestMapping("/adminPage")
@RequiresPermissions("admin:adminPage")
public String adminPage(){
return "adminPage";
}
}
8、后台数据库设计(参考)
用户表
角色表
权限表