Shiro权限框架使用介绍
权限管理概念
引用:
权限管理是系统的安全范畴,要求必须是合法的用户才可以访问系统(用户认证),且必须具有该 资源的访问权限才可以访问该 资源(授权)。
认证:登录校验。
授权:权限校验。
权限模型:标准权限数据模型包括 :用户、角色、权限(包括资源和权限)、用户角色关系、角色权限关系。
权限分配:通过UI界面方便给用户分配权限,对上边权限模型进行增、删、改、查操作。
权限控制:
基于角色的权限控制:根据角色判断是否有操作权限,因为角色的变化 性较高,如果角色修改需要修改控制代码,系统可扩展性不强。
基于资源的权限控制:根据资源权限判断是否有操作权限,因为资源较为固定,如果角色修改或角色中权限修改不需要修改控制代码,使用此方法系统可维护性很强。建议使用。
权限管理的解决方案:
- 对于粗颗粒权限管理,建议在系统架构层面去解决,写系统架构级别统一代码(基础代码)。
粗颗粒权限:比如对系统的url、菜单、jsp页面、页面上按钮、类方法进行权限管理,即对资源类型进行权限管理。
- 对于细颗粒权限管理:
粗颗粒权限:比如用户id为001的用户信息(资源实例)、类型为t01的商品信息(资源实例),对资源实例进行权限管理,理解对数据级别的权限管理。
细颗粒权限管理是系统的业务逻辑,业务逻辑代码不方便抽取统一代码,建议在系统业务层进行处理。
基于url的权限管理(掌握):
企业开发常用的方法,使用web应用中filter来实现,用户请求url,通过filter拦截,判断用户身份是否合法(用户认证),判断请求的地址是否是用户权限范围内的url(授权)。
shiro概念
权限管理要干的事总体上可以看成2部分,一部分是用户、角色、资源,及它们的关联关系的维护。
二是认证(登录认证:登录时验证用户名、密码;认证校验:访问url时判断校验已经登录)、鉴权(访问url等资源时判断是否有权限),这几个常用操作。
shiro主要功能是对认证、鉴权中的某些步骤进行了封装。
登录认证流程:
1、用户输入用户名、密码,提交登录。
2、后台根据用户名查询用户账号信息。
3、对比用户传入的密码和查询返回的账号信息中的密码是否一致。
认证校验流程:
1、后台校验用户是否登录(shiro中是将已登录的subject放入session中。取出subject进行判断)。
鉴权(校验权限)流程:
1、后台根据用户名查询用户权限。
2、对比用户请求的资源是否在用户所拥有的权限内。
(登录以后立即查询所有权限的过程,这是在准备用户菜单列表,并不是鉴权过程)
shiro对登录认证流程的第2、3步进行了封装;
对认证校验的第1步进行了封装;
对鉴权过程的第1步进行了封装(web环境中对第2步也进行了封装)。
shiro提供了AuthorizingRealm类,用户只需要实现其中的doGetAuthenticationInfo和doGetAuthorizationInfo方法即可。
登录时,将用户输入的用户名、密码封装成AuthenticationToken对象作为参数,调用doGetAuthenticationInfo方法。
查询用户时如果返回null,会自动抛出 UnknownAccountException异常。
返回不为null时,shiro会自动比较传入的密码和查询返回的密码是否一致,如果不一致,会抛出IncorrectCredentialsException异常。
鉴权时,将用户名封装成PrincipalCollection对象作为参数,调用doGetAuthorizationInfo方法,返回一个AuthorizationInfo,不像认证过程的自动比对密码,这里不会自动比对权限,而是交给用户自己判断,因为权限有很多种比对方式,如全部包含、部分包含等各种情况(web环境会自动比对权限)。
shiro配置
1、pom.xml引入依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
2、新建shiro_custom.ini放到classpath下:
内容:
customRealm=com.test.shiroTest.realms.MyRealm
securityManager.realms=$customRealm
这里类似于spring配置,通过$符号引用上面的customRealm。
3、log4j.properties放到classpath下:
内容:
log4j.rootLogger=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
4、新建自定义Realm文件MyRealm.java
自定义realm一般继承AuthorizingRealm
MyRealm.java
import java.util.ArrayList;
import java.util.List;
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;
public class MyRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("--------------doGetAuthenticationInfo--------------");
// 模拟查找用户
if (!",zhangsan,lisi,wangwu,".contains(token.getPrincipal().toString())) {
// 返回null表示没有找到用户
return null;
}
// 模拟数据库查询出的密码
String password = "123456";
// 返回认证信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("zhangsan", password, "MyRealm");
return simpleAuthenticationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("--------------doGetAuthorizationInfo--------------");
// 获取身份信息
String username = (String) principals.getPrimaryPrincipal();
// 根据身份信息从数据库中查询权限数据
// ....这里使用静态数据模拟
List<String> permissions = new ArrayList<String>();
permissions.add("user:create");
permissions.add("user:delete");
// 将权限信息封装装为AuthorizationInfo
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (String permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}
}
5、创建测试程序:
AuthenticationTest.java:
import java.util.Arrays;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;
public class AuthenticationTest {
@Test
public void test2() {
// 从ini文件中创建SecurityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro_custom.ini");
// 创建SecurityManager
SecurityManager securityManager = factory.getInstance();
// 将securityManager设置到运行环境
SecurityUtils.setSecurityManager(securityManager);
// 创建主体对象
Subject subject = SecurityUtils.getSubject();
// 对主体对象进行认证
// 用户登陆
// 设置用户认证的身份(principals)和凭证(credentials)
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
}
// 用户认证状态
Boolean isAuthenticated = subject.isAuthenticated();
System.out.println("用户认证状态:" + isAuthenticated);
// 用户授权检测 基于角色授权
// 是否有某一个角色
System.out.println("用户是否拥有一个角色:" + subject.hasRole("role1"));
// 是否有多个角色
System.out.println("用户是否拥有多个角色:" + subject.hasAllRoles(Arrays.asList("role1", "role2")));
// subject.checkRole("role1");
// subject.checkRoles(Arrays.asList("role1", "role2"));
// 授权检测,失败则抛出异常
// subject.checkRole("role22");
// 基于资源授权
System.out.println("是否拥有某一个权限:" + subject.isPermitted("user:delete"));
System.out.println("是否拥有多个权限:" + subject.isPermittedAll("user:create:1", "user:delete"));
// // 检查权限
// subject.checkPermission("sys:user:delete");
// subject.checkPermissions("user:create:1", "user:delete");
}
}
输出:
使用IniRealm:
当使用IniRealm时,它自己实现了doGetAuthenticationInfo方法和doGetAuthorizationInfo方法,认证时,它会到配置文件中查找用户,返回含有密码的AuthenticationInfo。,鉴权时,它会从配置文件中去查询用户权限返回。因此IniRealm可以看成了自定义Realm的一个实现特例。
使用配置:
不需要新建Realm类了。
1、新建shiro.ini,内容:
[users]
#用户zhang的密码是123,此用户具有role1和role2两个角色
zhang=123,role1,role2
wang=123,role2
[roles]
#角色role1对资源user拥有create、update权限
role1=user:create,user:update
#角色role2对资源user拥有create、delete权限
role2=user:create,user:delete
#角色role3对资源user拥有create权限
role3=user:create
2、认证的测试程序同上
认证的测试程序同上,只用把ini文件改成shiro.ini即可。
如果用户不存在,login会抛异常:
如果密码不正确,会抛异常:
认证流程分析
其中的核心是securityManager。
subject其实是由securityManager创建的。SecurityUtils.getSubject()时会调用securityManager去创建subject。
而subject的login也是调用了securityManager的login
1、调用ModularRealmAuthenticator. doAuthenticate方法。
进入securityManger.login方法,可以看到securityManager有一个Authenticator接口成员,它的实现是ModularRealmAuthenticator。
而login最终调用到了AbstractAuthenticator的authenticate方法上
AbstractAuthenticator的doAuthenticate是个抽象方法,在ModularRealmAuthenticator实现。
2、调用AuthenticatingRealm的getAuthenticationInfo查询用户信息,如果不存在就抛异常。
(realms是ModularRealmAuthenticator的成员变量)
3、AuthenticatingRealm调用子类的doGetAuthenticationInfo方法查询用户,校验密码
进入到realm的查询里面:
它会先根据用户名去查询用户info,如果info不为null,就调用assertCredentialsMatch验证密码,验证失败就抛异常。如果为null或者验证通过,都会返回info。
先看查询用户:
这里由于是从ini文件中读取,所以用的IniRealm。
再看验证密码:
使用散列算法(MD5)
散列算法概念
散列算法也叫Hash算法,用于将一段文本生成摘要信息,不可逆,常用的有MD5、sha等。
散列算法可以通过存储海量明文和密文对应表穷举的方式破解简单密码,因此密码最好不要设置成简单的123456、111111等。
通常会对每一个密码生成一个随机字符串salt,再将明文+salt加密,可以提高破解难度。
shiro已有封装的用salt进行MD5加密的类,如:
@Test
public void test2() {
System.out.println(new Md5Hash("111111", "eteokues", 1).toString());
System.out.println(new SimpleHash("MD5", "111111", "eteokues", 1).toString());
// 都输出cb571f7bd7a6f73ab004a70322b963d5
}
其中111111明文,eteokues是salt,1代表加密1次。
通过源码可以看出,Md5Hash构造函数其实调用了SimpleHash,它会根据传入的不同散列算法名字进行加密。
在realm中使用
shiro.ini配置:
#定义凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列算法
credentialsMatcher.hashAlgorithmName=md5
#散列次数
credentialsMatcher.hashIterations=1
#定义自定义realm
customRealm=com.test.shiroTest.realms.MyRealm
#将凭证匹配器注入到自定义realm上
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm
其中配置了一个凭证匹配器,定义了凭证匹配器的散列算法和散列次数
然后定义一个自定义realm,然后将凭证匹配器注入到realm上。
配置凭证匹配器的原因是告诉shiro加密方式,它会将用户传入的明文password加密后同返回的SimpleAuthenticationInfo中的密码进行比对。
MyRealm.java:
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;
public class MyRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("userCode=" + token.getPrincipal());
// 模拟查找用户
if (!"zhangsan".equals(token.getPrincipal())) {
return null;
}
// 模拟数据库查询出的密码
String password = "cb571f7bd7a6f73ab004a70322b963d5";
String salt = "eteokues"; // 盐
// System.out.println(new Md5Hash("111111", "eteokues", 1).toString()); // 输出cb571f7bd7a6f73ab004a70322b963d5
// 返回认证信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("zhangsan", password, ByteSource.Util.bytes(salt), getName());
return simpleAuthenticationInfo;
}
}
测试程序:
@Test
public void testLoginLogout() {
try {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhangsan", "111111");
subject.login(usernamePasswordToken);
System.out.println(subject.isAuthenticated());
subject.logout();
System.out.println(subject.isAuthenticated());
} catch (Throwable e) {
e.printStackTrace();
}
}
输出
userCode=zhangsan
true
false
web中结合spring使用(以springmvc为例)
基本思路是配置一个filter。拦截请求,然后交给shiro进行认证和鉴权。
web.xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-all.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- shiro过虑器,DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置true由servlet容器控制filter的生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean -->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
由于DelegatingFilterProxy 并不是真正直接使用的filter,真正使用的是bean的id为targetBeanName 配置的shiroFilter的bean,因此配置targetFilterLifecycle为true,表示将真正的filter生命周期也交给servlet管理。
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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
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-3.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- Shiro 的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 -->
<property name="loginUrl" value="/login" />
<property name="successUrl" value="/user/list"/>
<!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 -->
<property name="filterChainDefinitions">
<value>
/logout = logout
/favicon.ico = anon
/user/list = perms[user:query]
/** = authc
</value>
</property>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
</bean>
<!-- 自定义 realm -->
<bean id="userRealm" class="com.f3.realm.CustomRealm1"/>
</beans>
其中配置/favicon.ico = anon的原因是,很多浏览器请求前会先请求这个资源,如果这个资源被shiro拒绝了就会跳到登录页,登录后发现有上一个请求地址且为/favicon.ico,就会跳转到/favicon.ico。配置了以后它就不会被shiro拦,而只有用户真实请求的地址才会拦住,登录后上一个地址就是用户真实的请求地址。
CustomRealm1.java:
public class CustomRealm1 extends AuthorizingRealm {
@Override
public String getName() {
return "customRealm";
}
// 支持什么类型的token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从token中 获取用户身份信息
String username = (String) token.getPrincipal();
// 拿username从数据库中查询
// ....
// 如果查询不到则返回null
if (!username.equals("zhang")) {// 这里模拟查询不到
return null;
}
// 获取从数据库查询出来的用户密码
String password = "123";// 这里使用静态数据模拟。。
// 将登录以后需要展示的信息进行封装,这里可以注入service进行查询,因为realm对象已经交给ioc容器管理。
Map<String, Object> loginInfo = new HashMap<String, Object>();
loginInfo.put("username", "zhang");
loginInfo.put("displayName", "张三");
loginInfo.put("address", "测试的地址");
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(loginInfo, password, getName());
return simpleAuthenticationInfo;
}
// 获取权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 从principals取出username
Map<String, Object> loginInfo = (Map)principals.getPrimaryPrincipal();
System.out.println("username=" + loginInfo.get("username"));
// 模拟根据username到数据库查询用户权限
List<String> permissions = new ArrayList<String>();
permissions.add("user:query");
permissions.add("user:update");
// 将权限信息封装为AuthorizationInfo
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (String permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}
}
IndexController.java:
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
// 用户登陆提交
@RequestMapping("/login")
public String loginsubmit(Model model, HttpServletRequest request) throws Exception {
// shiro在认证过程中出现错误后将异常类路径通过request返回
String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
if (exceptionClassName != null) {
if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
throw new RuntimeException("账号不存在");
} else if (IncorrectCredentialsException.class.getName().equals(exceptionClassName)) {
throw new RuntimeException("用户名/密码错误");
} else if ("randomCodeError".equals(exceptionClassName)) {
throw new RuntimeException("验证码错误");
} else {
throw new Exception();// 最终在异常处理器生成未知错误
}
}
return "login";
}
}
说明:
配置说明
web.xml中配置的targetBeanName是shiroFilter,与spring-shiro.xml中的bean的id相对应,ShiroFilterFactoryBean是一个FactoryBean,并不是真正使用的filter,它里面有多个filter。
总体配置是配置一个自定义Realm,将它注入到SecurityManager,再将SecurityManager注入到ShiroFilterFactoryBean。
认证校验和鉴权过程:
请求时,会自动把请求通过配置的filter链依次匹配,交给最先匹配上的filter处理.
每个filter有不同的处理逻辑,
a:比如AnonymousFilter,直接通过。
b:如果是formAuthenticationFilter,就会验证登录,判断当前session中是否有subject且为已登录状态,如果不是,就会跳转到配置好的登录地址。
c:又如LogoutFilter,会清除当前session,然后跳转到登录页(可自定义配置)
登录过程:
登录提交以后,shiro会从表单中取出name为username和password的输入域的值(可配置成其他name),封装成token,调用Realm中的doGetAuthenticationInfo得到密码,然后比对,如果返回null,或者比对密码不成功,会分别抛出UnknownAccountException和IncorrectCredentialsException异常,不过异常不会抛出去,而是会把异常类的名字存到request里,key是shiroLoginFailure,controller中通过request.getAttribute("shiroLoginFailure")取出名字如果不为空,即为登录失败,可以通过它的名字,走不同的处理逻辑。
loginUrl
<property name="loginUrl" value="/login" />
filter拦截到用户请求,发现没有认证,就跳转到该地址。
successUrl
<property name="successUrl" value="/user/list"/>
表示登录前没有上一个地址时,使用该地址,如请求的地址就直接是/login,如果有上一个地址优先取上一个地址。
unauthorizedUrl
<property name="unauthorizedUrl" value="/unauthorizedUrl.jsp"/>
表示鉴权失败时的跳转地址
filterChainDefinitions
<property name="filterChainDefinitions">
<value>
/logout = logout
/favicon.ico = anon
/user/list = perms[user:query]
/** = authc
</value>
</property>
匹配url和处理器的映射规则,左边是url,右边是filter的简称
最先匹配上的就使用。权限filter虽然被先找到,但它内部会先去找有没有认证filter,如果有,就让认证filter先执行。而如果先找到认证filter,则它不会去找其他filter,因此权限filter要配置到认证filter前面。
其中/user/list = perms[user:query]表示有user:query权限才能访问这个链接。
filter简称对应
其中filterChainDefinitions配置filter,各种简称对应filter如下:
过滤器简称 | 对应的java类 |
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
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 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
DelegatingFilterProxy和ShiroFilterFactoryBean源码分析
web.xml中配置的DelegatingFilterProxy是一个代理filter,当调用它的doFilter方法时,它会去调用目标filter的方法:
首先从spring容器中通过targetBeanName取出目标filter的bean,然后调用它的doFilter方法。
取filter的代码:
调用目标filter的doFilter方法的代码:
而spring-shiro.xml中配置的filter如下:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
这个ShiroFilterFactoryBean并不是Filter的子类,也没有doFilter方法,而DelegatingFilterProxy却能调用它的doFilter方法,是因为ShiroFilterFactoryBean是一个FactoryBean。容器在通过getBean方法获取ShiroFilterFactoryBean实例的时候,发现它是一个FactoryBean,则会调用它的getObject方法得到bean(调用getObjectType方法得到bean的类类型),而不是直接反射创建ShiroFilterFactoryBean的实例。
先看getObjectType得到bean的类类型
getObjectType方法返回了一个Filter接口的子类类型。
再看getObject得到的bean。
getObject方法返回了一个实现Filter接口的子类实例。
由上可以看出,DelegatingFilterProxy能调用到目标filter的doFilter方法
shiro的Filter源码分析
AnonymousFilter
基本上就啥也没干,直接通过了。
使用注解授权:
可以使用注解代替在spring中配置url需要的权限,这样在开发功能的时候写权限,比在一个地方统一配置更容易维护。
配置:
spring中增加配置:
<!-- 开启aop,对类代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 开启shiro注解支持 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
方法上增加注解:
@RequiresPermissions("user:query")
如:
表示执行此方法需要user.query权限。
shiro的jsp标签:
shiro提供了jsp标签方便控制页面元素。
标签名称 | 标签条件(均是显示标签内容) |
<shiro:authenticated> | 登录之后 |
<shiro:notAuthenticated> | 不在登录状态时 |
<shiro:guest> | 用户在没有RememberMe时 |
<shiro:user> | 用户在RememberMe时 |
<shiro:hasAnyRoles name="abc,123" > | 在有abc或者123角色时 |
<shiro:hasRole name="abc"> | 拥有角色abc |
<shiro:lacksRole name="abc"> | 没有角色abc |
<shiro:hasPermission name="abc"> | 拥有权限资源abc |
<shiro:lacksPermission name="abc"> | 没有abc权限资源 |
<shiro:principal> |
<shiro:principal property="username"/> 显示用户身份中的属性值
使用时先加上标签声明:
<%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro" %>
页面使用如:
显示:
使用缓存(cacheManager)
鉴权时每次都会调用realm的doGetAuthorizationInfo获取权限。
可使用缓存减小开销,不用每次都调用而从缓存中取权限, echcache是一个java进程内缓存框架,很少用于集群,用于集群时多台java服务器的ehcache需要做同步,hibernate默认使用的就是ehcache。
shiro配置ehcache缓存:
pom.xml引入:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.4.0</version>
</dependency>
spring中对安全管理器注入缓存管理器:
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
使用缓存后,只有第一次才查询,后面都从缓存中取
某个用户的权限发生修改等变化时,需要清空他的权限缓存。
清空缓存的方法在Realm里,是protected的,因此需要在自定义Realm执行super.clearCache
自定义Realm增加方法:
public void clearCache() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
其中SecurityUtils.getSubject().getPrincipals()会从当前session中取用户认证信息。
由于Realm交给了spring管理,因此需要清除缓存的地方注入它即可:
如:
// 清空缓存
@RequestMapping("/clearCache")
public String clearCache(Model model, HttpServletRequest request) throws Exception {
customRealm1.clearCache();
return null;
}
一般是在用户权限发生改变后调用。
配置session失效时间(sessionManager)
可在shiro中配置session失效时间,类似于tomcat的session失效时间配置,它们是独立的配置,互不影响。session失效后,就需要重新登录,如果浏览器关闭再打开,由于jsessionid发生了变化,因此也需要重新登录。
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
<property name="sessionManager" ref="sessionManager" />
</bean>
<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session的失效时长,单位毫秒 -->
<property name="globalSessionTimeout" value="600000"/>
<!-- 删除失效的session -->
<property name="deleteInvalidSessions" value="true"/>
</bean>
验证码
验证码一般访问页面时生成(或点生成验证码时生成),放到session中,用户提交请求时,将session中的验证码和用户提交的验证码进行比较。
在用户登录的验证码校验时,需要先校验验证码正确,才进行用户名密码校验。而shiro对登录校验进行了封装,登录校验用的是FormAuthenticationFilter,调用的onAccessDenied方法,因此需要继承该filter,覆盖该方法:
1、自定义Filter
CustomFilter.java:
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
public class CustomFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 比较session中的验证码和用户传入的验证码
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
String requestCode = httpServletRequest.getParameter("validateCode");
Object sessionCode = httpServletRequest.getSession().getAttribute("validateCode");
if (!sessionCode.equals(requestCode) && !sessionCode.toString().equals(requestCode)) {
// randomCodeError表示验证码错误
request.setAttribute("shiroLoginFailure", "randomCodeError");
//拒绝访问,不再校验账号和密码
return true;
}
return super.onAccessDenied(request, response);
}
}
2、spring的shiroFilter中配置filters
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="filters">
<map>
<!-- FormAuthenticationFilter是基于表单认证的过虑器 -->
<entry key="authc" value-ref="formAuthenticationFilter" />
</map>
</property>
...
<!-- 自定义的AuthenticationFilter -->
<bean id="formAuthenticationFilter" class="com.f3.CustomFilter">
<!-- 表单中账号的input名称 -->
<property name="usernameParam" value="username" />
<!-- 表单中密码的input名称 -->
<property name="passwordParam" value="password" />
</bean>
3、登录controller增加对验证码错误的提示
记住我
就是登录的"自动登录"复选框
勾选该复选框以后,登录后就会将上次登录成功时Realm封装的principal信息序列化以后写入cookie,如:
因此它需要实现序列化接口,本例中的HashMap已实现Serializable接口。
配置:
spring配置:
<!-- rememberMeManager管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie" />
</bean>
<!-- 记住我cookie -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe" />
<!-- 记住我cookie生效时间30天 -->
<property name="maxAge" value="2592000" />
</bean>
<bean id="formAuthenticationFilter" class="com.f3.CustomFilter">
<!-- 表单中账号的input名称 -->
<property name="usernameParam" value="username" />
<!-- 表单中密码的input名称 -->
<property name="passwordParam" value="password" />
<!-- 复选框的name -->
<property name="rememberMeParam" value="rememberMe"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="filters">
<map>
FormAuthenticationFilter是基于表单认证的过虑器
<entry key="authc" value-ref="formAuthenticationFilter" />
</map>
</property>
…
…
…
<property name="filterChainDefinitions">
<value>
/logout = logout
/favicon.ico = anon
/user/list = user
/** = authc
</value>
</property>
其中/user/list = user表示该链接是记住我后,下次访问时无需登录的连接。
login.jsp:
<BODY>
<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>
</BODY>
浙公网安备 33010602011771号