Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。
既然shiro将安全认证相关的功能抽取出来组成一个框架,使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户开始使用shiro。
java领域中spring security(原名Acegi)也是一个开源的权限管理框架,但是spring security依赖spring运行,而shiro就相对独立,最主要是因为shiro使用简单、灵活,所以现在越来越多的用户选择shiro。
Subject 主体
Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。
Subject在shiro中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权。
SecurityManager 安全管理器
SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
Authenticator 认证器
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer 授权器
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
sessionManager 会话管理
sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
realm 领域
Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库,那么realm就需要从数据库获取用户身份信息。
注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。在realm中存储授权和认证的逻辑。
SessionDAO 会话dao
SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
CacheManager 缓存管理
CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
Cryptography 密码管理
Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
本章节以权限管理展开:
什么是权限管理?
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
subject:主体,理解为用户,可能是程序,都要去访问系统的资源,系统需要对subject进行身份认证。
principal:身份信息,通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(primary principal)
credential:凭证信息,是只有主体自己知道的安全信息。可以是密码 、证书、指纹。
授权的过程理解为:who对what(which)进行how操作。
who:主体即subject,subject在认证通过后系统进行访问控制。
what(which):资源(Resource),subject必须具备资源的访问权限才可访问该 资源。资源比如:系统用户列表页面、商品修改菜单、商品id为001的商品信息。
资源分为资源类型和资源实例:
系统的用户信息就是资源类型,相当于java类。
系统中id为001的用户就是资源实例,相当于new的java对象。
how:权限/许可(permission) ,针对资源的权限或许可,subject具有permission访问资源,如何访问/操作需要定义permission,权限比如:用户添加、用户修改、商品删除。
用户拥有了权限即可操作权限范围内的资源,系统不知道主体是否具有访问权限需要对用户的访问进行控制。
上图中的判断逻辑代码可以理解为:
if(主体.hasRole("总经理角色id")){
查询工资
}
缺点:以角色进行访问控制粒度较粗,如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断主体的角色是否是总经理或部门经理”,系统可扩展性差。
资源在系统中是不变的,比如资源有:类中的方法,页面中的按钮。
对资源的访问需要具有permission权限,代码可以写为:
if(主体.hasPermission("查询工资权限标识")){
查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也只需要将“查询工资信息权限”添加到“部门经理角色”的权限列表中,判断逻辑不用修改,系统可扩展性强。
建议使用基于资源的访问控制实现权限管理。
什么是粗颗粒度和细颗粒度
对资源类型的管理称为粗颗粒度权限管理,即只控制到菜单、按钮、方法,粗粒度的例子比如:用户具有用户管理的权限,具有导出订单明细的权限。
对资源实例的控制称为细颗粒度权限管理,即控制到数据级别的权限,比如:用户只允许修改本部门的员工信息,用户只允许导出自己创建的订单明细。
如何实现粗颗粒度和细颗粒度
对于粗颗粒度的权限管理可以很容易做系统架构级别的功能,即系统功能操作使用统一的粗颗粒度的权限管理。
对于细颗粒度的权限管理不建议做成系统架构级别的功能,因为对数据级别的控制是系统的业务需求,随着业务需求的变更业务功能变化的可能性很大,建议对数据级别的权限控制在业务层个性化开发,比如:用户只允许修改自己创建的商品信息可以在service接口添加校验实现,service接口需要传入当前操作人的标识,与商品信息创建人标识对比,不一致则不允许修改商品信息。
比如:部门经理只查询本部门员工信息,在service接口提供一个部门id的参数,controller中根据当前用户的信息得到该 用户属于哪个部门,调用service时将部门id传入service,实现该用户只查询本部门的员工。
基于url拦截是企业中常用的权限管理方法,实现思路是:将系统操作的每个url配置在权限表中,将权限对应到角色,将角色分配给用户,用户访问系统功能通过Filter进行过虑,过虑器获取到用户访问的url,只要访问的url是用户分配角色中的url则放行继续访问。
环境准备
jdk:1.7.0_72 web容器:tomcat7 系统框架:springmvc3.2.0+mybatis3.2.7(详细参考springmvc教案) 前台UI:jquery easyUI1.2.2 |
数据库
创建mysql5.1数据库 创建用户表、角色表、权限表、角色权限关系表、用户角色关系表。 导入脚本,先导入shiro_sql_talbe.sql再导入shiro-sql_table_data.sql |
activeUser用户身份类
用户登陆成功记录activeUser信息并将activeUser存入session。
public class ActiveUser implements java.io.Serializable {
private String userid;//用户id
private String usercode;// 用户账号
private String username;// 用户名称
private List menus;// 菜单
private List permissions;// 权限
anonymousURL.properties
anonymousURL.properties公开访问地址,无需身份认证即可访问。
commonURL.properties
commonURL.properties公共访问地址,身份认证通过无需分配权限即可访问。
用户身份认证拦截器
使用springmvc拦截器对用户身份认证进行拦截,如果用户没有登陆则跳转到登陆页面,本功能也可以使用filter实现 。
public class LoginInterceptor implements HandlerInterceptor {
// 在进入controller方法之前执行
// 使用场景:比如身份认证校验拦截,用户权限拦截,如果拦截不放行,controller方法不再执行
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 校验用户访问是否是公开资源地址(无需认证即可访问)
List open_urls = ResourcesUtil.gekeyList("anonymousURL");
// 用户访问的url
String url = request.getRequestURI();
for (String open_url : open_urls) {
if (url.indexOf(open_url) >= 0) {
// 如果访问的是公开 地址则放行
return true;
}
}
// 校验用户身份是否认证通过
HttpSession session = request.getSession();
ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
if (activeUser != null) {
// 用户已经登陆认证,放行
return true;
}
// 跳转到登陆页面
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request,
response);
return false;
}
配置拦截器 在springmvc.xml中配置拦截器
用户授权拦截器
使用springmvc拦截器对用户访问url进行拦截,如果用户访问的url没有分配权限则跳转到无权操作提示页面(refuse.jsp),本功能也可以使用filter实现。
public class PermissionInterceptor implements HandlerInterceptor {
// 在进入controller方法之前执行
// 使用场景:比如身份认证校验拦截,用户权限拦截,如果拦截不放行,controller方法不再执行
// 进入action方法前要执行
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// TODO Auto-generated method stub
// 用户访问地址:
String url = request.getRequestURI();
// 校验用户访问是否是公开资源地址(无需认证即可访问)
List open_urls = ResourcesUtil.gekeyList("anonymousURL");
// 用户访问的url
for (String open_url : open_urls) {
if (url.indexOf(open_url) >= 0) {
// 如果访问的是公开 地址则放行
return true;
}
}
//从 session获取用户公共访问地址(认证通过无需分配权限即可访问)
List common_urls = ResourcesUtil.gekeyList("commonURL");
// 用户访问的url
for (String common_url : common_urls) {
if (url.indexOf(common_url) >= 0) {
// 如果访问的是公共地址则放行
return true;
}
}
// 从session获取用户权限信息
HttpSession session = request.getSession();
ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
// 取出session中权限url
// 获取用户操作权限
List permission_list = activeUser.getPermissions();
// 校验用户访问地址是否在用户权限范围内
for (SysPermission sysPermission : permission_list) {
String permission_url = sysPermission.getUrl();
if (url.contains(permission_url)) {
return true;
}
}
// 跳转到页面
request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(
request, response);
return false;
}
mapper接口:根据用户id查询用户权限的菜单
用户登陆
用户输入用户账号和密码登陆,登陆成功将用户的身份信息(用户账号、密码、权限菜单、权限url等)记入activeUser类,并写入session。
service(1,根据账号查询用户,进行用户名和密码校验;2,根据用户id查询用户权限;3,根据用户id获取菜单)
/**
*
*
* Title: authenticat
*
*
* Description:用户认证
*
*
* @param usercode
* 用户账号
* @param password
* 用户密码
* @return ActiveUser 用户身份信息
* @throws Exception
*/
public ActiveUser authenticat(String usercode, String password)
throws Exception;
// 根据账号查询用户
public SysUser findSysuserByUsercode(String usercode) throws Exception;
// 根据用户id获取权限
public List findSysPermissionList(String userid)
throws Exception;
// 根据用户id获取菜单
public List findMenuList(String userid) throws Exception;
controller(用户身份认证、记录session)
//用户登陆提交
@RequestMapping("/loginsubmit")
public String loginsubmit(HttpSession session,String usercode,String password,String randomcode) throws Exception{
//校验验证码
//从session获取正确的验证码
String validateCode = (String)session.getAttribute("validateCode");
if(!randomcode.equals(validateCode)){
//抛出异常:验证码错误
throw new CustomException("验证码 错误 !");
}
//用户身份认证
ActiveUser activeUser = sysService.authenticat(usercode, password);
//记录session
session.setAttribute("activeUser", activeUser);
return "redirect:first.action";
}
配置授权拦截器 注意:将授权拦截器配置在用户认证拦截的下边。
小结
使用基于url拦截的权限管理方式,实现起来比较简单,不依赖框架,使用web提供filter就可以实现。
问题:需要将所有的url全部配置起来,有些繁琐,不易维护,url(资源)和权限表示方式不规范。
shiro的jar包
与其它java开源框架类似,将shiro的jar包加入项目就可以使用shiro提供的功能了。shiro-core是核心包必须选用,还提供了与web整合的shiro-web、与spring整合的shiro-spring、与任务调度quartz整合的shiro-quartz等。
认证流程
.ini 文件是Initialization File的缩写,即初始化文件。
INI配置文件是一种key/value的键值对配置,提供了分类的概念,每一个类中的key不可重复,#号代表注释,shiro.ini文件默认在/WEB-INF/ 或classpath下,shiro会自动查找,INI配置文件一般适用于用户少且不需要在运行时动态创建的情景下使用。
shiro框架里新造了一个Ini类,当我们传入资源时,Ini里使用流一行一行的读资源,
当遇到”#”或”;”开头的则直接跳过;
遇到“[*]”则将中括号里的字符串看过Section(区块)的key,后面一行一行都视做该区域的内容,直到遇到新的中括号。随后再解读区域下面多行字符串(至少一行);
如果遇到“:”或“=”或“”,则前面当做key,后面的则是为value(同时会过滤掉value里前后空格以及“=”前后空格),存到一个Section里,最后把所有行解析完后放到名为sections的HashMap里。
IniSecurityManagerFacotry继承自IniFactorySupport,而IniFactorySupport有个setIni()方法将解析出来的Ini结构数据保存到该类里,其它什么都不做。
源码解析:
当配置文件里出现[users]或[roles]时,IniSecurityManagerFacotry会初始化一个IniRealm做为数据源,把ini传入到IniRealm里,IniRealm的name是“iniRealm”。并把realm存到securityManager的realms属性集合里。
当出现[main]时,说明是主配置。当key里不出现“.”或者以“.class”结尾,说明是需要实例化的类,value值即为类的全限名,这些实例最张会被反射注入到DefaultSecurityManager的实例securityManager里。否则视为属性,用反射去设置上次实例化的对象属性值。其中objects是包含着key是“securityManager”,value为DefaultSecurityManager对象的Map对象。所有被main标记的都会被注入到securityManager”。
当不出现“[]”时,“”空即为sections的key。只要第一行没出现”[]”则一定会出现key为空的map的键值对。(注意一点,如果没有[main],则取sections里””即空为的key的数据做为主配置)
当我们调用IniSecurityManagerFacotry里getInstance()方法时,会根据是否有ini数据来调用不同的方法创建不同的SecurityManager.当有ini时调用
protected SecurityManager createInstance(Ini ini) {
if (CollectionUtils.isEmpty(ini)) {
throw new NullPointerException("Ini argument cannot be null or empty.");
}
//createSecurityManager()才是重点
SecurityManager securityManager = createSecurityManager(ini);
if (securityManager == null) {
String msg = SecurityManager.class + " instance cannot be null.";
throw new ConfigurationException(msg);
}
return securityManager;
}
当没有时调用:
protected SecurityManager createDefaultInstance() {
return new DefaultSecurityManager();
}
一般将realm标记为[main],那么会生成Realm的实例,保存到DefaultSecurityManager的realms集合里,这样securityManager就有数据源了。
常用的用shiro的API的例子:
Factory factory =
new IniSecurityManagerFactory("auth.ini");
// Setting up the SecurityManager...
org.apache.shiro.mgt.SecurityManager securityManager
= factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject user = SecurityUtils.getSubject();
把配置文件里配置的参数放到Ini里,并把ini传给Realm实例,同时造一个DefaultSecurityManager实例。将securityManager放到SecurityUtils里,同时从SecurityUtils里取Subject。Subject相当于当前线程里的相当“用户”,体现在程序里即是保存用户相关身份和凭证等的信息,以及操作方法。
如果我们跳出Ini配置的束缚,我们应该能得到结论,我们应该给SecurityManager提供一个或多个Realm对象在到realms里,比如在spring框架的xml里配,SecurityUtils作用把Subject和SecurityManager关联起来了,只要能把当前用户信息(Subject)和SecurityManager(包含验证的数据来源信息等配置)搭上话,那么要么我们自己用API做事,要么Spring帮我们管理都很方便。
1.[users]部分
#提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2
username=password,role1,role2
例如:
配置用户名/密码及其角色,格式:“用户名=密码,角色1,角色2”,角色部分可省略。如:
[users]
zhang=123,role1,role2
wang=123
2. [roles]
#提供了角色及权限之间关系的配置,角色=权限1,权限2
role1=permission1,permission2
例如:
配置角色及权限之间的关系,格式:“角色=权限1,权限2”;如:
[roles]
role1=user:create,user:update
role2=*
如果只有角色没有对应的权限,可以不配roles
3. [main]部分
提供了对根对象securityManager及其依赖对象的配置。
创建对象
securityManager=org.apache.shiro.mgt.DefaultSecurityManager
其构造器必须是public空参构造器,通过反射创建相应的实例。
1、对象名=全限定类名 相对于调用public无参构造器创建对象
2、对象名.属性名=值 相当于调用setter方法设置常量值
3、对象名.属性名=$对象引用 相当于调用setter方法设置对象引用
4.[urls]
#用于web,提供了对web url拦截相关的配置,url=拦截器[参数],拦截器
/index.html = anon
/admin/** = authc, roles[admin],perms["permission1"]
shiro-first.ini 通过此配置文件创建securityManager工厂。
需要修改eclipse的ini的编辑器:
// 用户登陆和退出
@Test
public void testLoginAndLogout() {
// 创建securityManager工厂,通过ini配置文件创建securityManager工厂
Factory factory = new IniSecurityManagerFactory(
"classpath:shiro-first.ini");
//创建SecurityManager
SecurityManager securityManager = factory.getInstance();
//将securityManager设置当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);
//从SecurityUtils里边创建一个subject
Subject subject = SecurityUtils.getSubject();
//在认证提交前准备token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "111111");
try {
//执行认证提交
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//是否认证通过
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("是否认证通过:" + isAuthenticated);
//退出操作
subject.logout();
//是否认证通过
isAuthenticated = subject.isAuthenticated();
System.out.println("是否认证通过:" + isAuthenticated);
}
1、通过ini配置文件创建securityManager
2、调用subject.login方法主体提交认证,提交的token
3、securityManager进行认证,securityManager最终由ModularRealmAuthenticator进行认证。
4、ModularRealmAuthenticator调用IniRealm(给realm传入token) 去ini配置文件中查询用户信息
5、IniRealm根据输入的token(UsernamePasswordToken)从 shiro-first.ini查询用户信息,根据账号查询用户信息(账号和密码)
如果查询到用户信息,就给ModularRealmAuthenticator返回用户信息(账号和密码)
如果查询不到,就给ModularRealmAuthenticator返回null
6、ModularRealmAuthenticator接收IniRealm返回Authentication认证信息
如果返回的认证信息是null,ModularRealmAuthenticator抛出异常(org.apache.shiro.authc.UnknownAccountException)
如果返回的认证信息不是null(说明inirealm找到了用户),对IniRealm返回用户密码 (在ini文件中存在)和 token中的密码 进行对比,如果不一致抛出异常(org.apache.shiro.authc.IncorrectCredentialsException)
ModularRealmAuthenticator作用进行认证,需要调用realm查询用户信息(在数据库中存在用户信息)
ModularRealmAuthenticator进行密码对比(认证过程)。
realm:需要根据token中的身份信息去查询数据库(入门程序使用ini配置文件),如果查到用户返回认证信息,如果查询不到返回null。
账号不存在异常如下:
org.apache.shiro.authc.UnknownAccountException: No account found for user。。。。
当输入密码错误会抛此异常,如下:
org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - zhangsan, rememberMe=false] did not match the expected credentials.
更多如下:
DisabledAccountException(帐号被禁用)
LockedAccountException(帐号被锁定)
ExcessiveAttemptsException(登录失败次数过多)
ExpiredCredentialsException(凭证过期)等
上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。
最基础的是Realm接口,CachingRealm负责缓存处理,AuthenticationRealm负责认证,AuthorizingRealm负责授权,通常自定义的realm继承AuthorizingRealm。
自定义Realm
public class CustomRealm1 extends AuthorizingRealm {
@Override
public String getName() {
return "customRealm1";
}
//支持UsernamePasswordToken
@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";//这里使用静态数据模拟。。
//返回认证信息由父类AuthenticatingRealm进行认证
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
username, password, getName());
return simpleAuthenticationInfo;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
[main]
#自定义 realm
customRealm=cn.itcast.shiro.authentication.realm.CustomRealm1
#将realm设置到securityManager
securityManager.realms=$customRealm
测试
同上边的入门程序,需要更改ini配置文件路径:
Factory
"classpath:shiro-realm.ini");
散列算法一般用于生成一段文本的摘要信息,散列算法不可逆,将内容可以生成摘要,无法将摘要转成原始内容。散列算法常用于对密码进行散列,常用的散列算法有MD5、SHA。
一般散列算法需要提供一个salt(盐)与原始内容生成摘要信息,这样做的目的是为了安全性,比如:111111的md5值是:96e79218965eb72c92a549dd5a330112,拿着“96e79218965eb72c92a549dd5a330112”去md5破解网站很容易进行破解,如果要是对111111和salt(盐,一个随机数)进行散列,这样虽然密码都是111111加不同的盐会生成不同的散列值。
例子
//md5加密,不加盐
String password_md5 = new Md5Hash("111111").toString();
System.out.println("md5加密,不加盐="+password_md5);
//md5加密,加盐,一次散列
String password_md5_sale_1 = new Md5Hash("111111", "eteokues", 1).toString();
System.out.println("password_md5_sale_1="+password_md5_sale_1);
String password_md5_sale_2 = new Md5Hash("111111", "uiwueylm", 1).toString();
System.out.println("password_md5_sale_2="+password_md5_sale_2);
//两次散列相当于md5(md5())
//使用SimpleHash
String simpleHash = new SimpleHash("MD5", "111111", "eteokues",1).toString();
System.out.println(simpleHash);
在realm中使用
实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。
自定义realm
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
//用户账号
String username = (String) token.getPrincipal();
//根据用户账号从数据库取出盐和加密后的值
//..这里使用静态数据
//如果根据账号没有找到用户信息则返回null,shiro抛出异常“账号不存在”
//按照固定规则加密码结果 ,此密码 要在数据库存储,原始密码 是111111,盐是eteokues
String password = "cb571f7bd7a6f73ab004a70322b963d5";
//盐,随机数,此随机数也在数据库存储
String salt = "eteokues";
//返回认证信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
username, password, ByteSource.Util.bytes(salt),getName());
return simpleAuthenticationInfo;
}
配置shiro-cryptography.ini
[main]
#定义凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列算法
credentialsMatcher.hashAlgorithmName=md5
#散列次数
credentialsMatcher.hashIterations=1
#将凭证匹配器设置到realm
customRealm=cn.itcast.shiro.authentication.realm.CustomRealm2
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm
Shiro 支持三种方式的授权:
编程式:通过写if/else 授权代码块完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
注解式:通过在执行的Java方法上放置相应的注解完成:
@RequiresRoles("admin")
public void hello() {
//有权限
}
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
在此授权测试使用第一种编程方式,实际与web系统集成使用后两种方式。
在认证章节写的自定义realm类中完善doGetAuthorizationInfo方法,此方法需要完成:根据用户身份信息从数据库查询权限字符串,由shiro进行授权。
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// 获取身份信息
String username = (String) principals.getPrimaryPrincipal();
// 根据身份信息从数据库中查询权限数据
//....这里使用静态数据模拟
List permissions = new ArrayList();
permissions.add("user:create");
permissions.add("user:delete");
//将权限信息封闭为AuthorizationInfo
//查到授权数据,返回授权信息(要包括上边的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//将上边查询到的授权信息填充到simpleAuthorInfo对象中
for(String permission:permissions){
simpleAuthorizationInfo.addStringPermission(permission);
}
return simpleAuthorizationInfo;
}
在shiro-realm.ini中配置自定义的realm,将realm设置到securityManager中。
ini配置文件还使用认证阶段使用的,不用改变。
shiro与springweb项目整合在“基于url拦截实现的工程”基础上整合,基于url拦截实现的工程的技术架构是springmvc+mybatis,整合注意两点:
1、shiro与spring整合
2、加入shiro对web应用的支持
去掉springmvc.xml中配置的LoginInterceptor(登录拦截器)和PermissionInterceptor拦截器。
在web系统中,shiro也通过filter进行拦截。filter拦截后将操作权交给spring中配置的filterChain(过虑链儿)
shiro提供很多filter。在web.xml中配置filter
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
targetBeanName
shiroFilter
shiroFilter
/*
/logout.action = logout
/refuse.jsp = anon
/item/list.action = roles[item],authc
/js/** anon
/images/** anon
/styles/** anon
/validatecode.jsp anon
/item/* authc
/** = authc
securityManager:这个属性是必须的。
loginUrl:没有登录认证的用户请求将跳转到此地址进行认证,不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
unauthorizedUrl:没有权限默认跳转的页面
使用FormAuthenticationFilter过虑器实现 ,原理如下:
将用户没有认证时,请求loginurl进行认证,用户身份和用户密码提交数据到loginurl。FormAuthenticationFilter拦截住取出request中的username和password(两个参数名称是可以配置的)
FormAuthenticationFilter调用realm传入一个token(username和password)
realm认证时根据username查询用户信息(在Activeuser中存储,包括 userid、usercode、username、menus)。
如果查询不到,realm返回null,FormAuthenticationFilter向request域中填充一个参数(记录了异常信息)
登陆代码实现
// 用户登陆提交
// 登录提交地址和applicationContext-shiro.xml中配置的loginurl一致
@RequestMapping("/login")
public String loginsubmit(Model model, HttpServletRequest request)
throws Exception {
// shiro在认证过程中出现错误后将异常类路径通过request返回
// shiroLoginFailure就是shiro异常类的权限名
String exceptionClassName = (String) request
.getAttribute("shiroLoginFailure");
// 根据shiro返回的异常类路径判断,抛出指定异常信息
if(exceptionClassName!=null){
if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
// 最终会抛给异常处理类
throw new CustomException("账号不存在");
} else if (IncorrectCredentialsException.class.getName().equals(
exceptionClassName)) {
throw new CustomException("用户名/密码错误");
} else if("randomCodeError".equals(exceptionClassName)){
throw new CustomException("验证码错误");
} else{
throw new Exception();//最终在异常处理器生成未知错误
}
}
// 此方法不处理登陆成功(认证成功),shiro认证成功会自动跳转到上一个请求路径
// 登录失败还到login页面
return "login";
由于使用shiro的sessionManager,不用开发退出功能,使用shiro的logout拦截器即可。使用LogoutFilter,不用我们去实现退出,只要去访问一个退出的url(该url是可以不存在),由LogoutFilter拦截住,清除session。
在applicationContext-shiro.xml配置LogoutFilter:
/logout.action = logout
可以删除原来的logout的controller方法代码。
1、认证后用户菜单在首页显示
2、认证后用户的信息在页头显示
由于session由shiro管理,需要修改首页的controller方法,将session中的数据通过model传到页面。
//系统首页
@RequestMapping("/first")
public String first(Model model)throws Exception{
//主体,从shiro的session中获取activeUser
Subject subject = SecurityUtils.getSubject();
//身份
ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
// 通过model传到页面
model.addAttribute("activeUser", activeUser);
return "/first";
}
如果授权失败,跳转到refuse.jsp,需要在spring容器中配置:
当用户无操作权限,shiro将跳转到refuse.jsp页面。
1、在applicationContext-shiro.xml中配置过虑器链接,需要将全部的url和权限对应起来进行配置,比较发麻不方便使用。
2、每次授权都需要调用realm查询数据库,对于系统性能有很大影响,可以通过shiro缓存来解决。
修改realm的doGetAuthenticationInfo,从数据库查询用户信息,realm返回的用户信息中包括 (md5加密后的串和salt),实现让shiro进行散列串的校验。
数据库中存储到的md5的散列值,在realm中需要设置数据库中的散列值它使用散列算法 及散列次数,让shiro进行散列对比时和原始数据库中的散列值使用的算法 一致。
添加凭证匹配器实现md5加密校验。
修改applicationContext-shiro.xml:
修改realm代码从数据库中查询用户身份信息,将sysService注入realm。
public class CustomRealm1 extends AuthorizingRealm {
// 注入sevice
@Autowired
private SysService sysService;
@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 usercode = (String) token.getPrincipal();
SysUser sysUser = null;
try {
sysUser = sysService.findSysuserByUsercode(usercode);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 如果账号不存在
if (sysUser == null) {
return null;
}
// 根据用户id取出菜单
List menus = null;
try {
menus = sysService.findMenuList(sysUser.getId());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 用户密码
String password = sysUser.getPassword();
//盐
String salt = sysUser.getSalt();
// 构建用户身体份信息
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(sysUser.getId());
activeUser.setUsername(sysUser.getUsername());
activeUser.setUsercode(sysUser.getUsercode());
activeUser.setMenus(menus);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
activeUser, password, ByteSource.Util.bytes(salt),getName());
return simpleAuthenticationInfo;
}
}
修改realm的doGetAuthorizationInfo,从数据库查询权限信息。
使用注解式授权方法。
使用jsp标签授权方法。
public class CustomRealm1 extends AuthorizingRealm {
@Autowired
private SysService sysService;
@Override
public String getName() {
return "customRealm";
}
// 支持什么类型的token
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//身份信息
//从 principals获取主身份信息
//将getPrimaryPrincipal方法返回值转为真实身份类型(在上边的doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中身份类型),
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
//用户id
String userid = activeUser.getUserid();
//获取用户权限
//从数据库获取到权限数据
List permissions = null;
try {
permissions = sysService.findSysPermissionList(userid);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//构建shiro授权信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for(SysPermission sysPermission:permissions){
simpleAuthorizationInfo.addStringPermission(sysPermission.getPercode());
}
return simpleAuthorizationInfo;
}
}
对系统中类的方法给用户授权,建议在controller层进行方法授权。
在springmvc.xml中配置shiro注解支持,可在controller方法中使用shiro注解配置权限:
在springmvc.xml中配置:
商品查询controller方法添加权限(item:query)
// 查询商品列表
@RequestMapping("/queryItem")
@RequiresPermissions("item:query")
public ModelAndView queryItem() throws Exception {
上边代码@RequiresPermissions("item:query")表示必须拥有“item:query”权限方可执行。
同理,商品修改controller方法添加权限(item:update):
@RequestMapping(value = "/editItem")
@RequiresPermissions("item:update")
public String editItem(@RequestParam(value = "id", required = true) Integer id, Model model) throws Exception
// 商品修改提交
@RequestMapping("/editItemSubmit")
@RequiresPermissions("item:update")
public String editItemSubmit(@ModelAttribute("item") Items items,BindingResult result,
MultipartFile pictureFile,Model model,HttpServletRequest request)
throws Exception
标签介绍:Jsp页面添加:
<%@ tagliburi="http://shiro.apache.org/tags" prefix="shiro" %>
标签名称 标签条件(均是显示标签内容)
登录之后
不在登录状态时
用户在没有RememberMe时
用户在RememberMe时
在有abc或者123角色时
拥有角色abc
没有角色abc
拥有权限资源abc
没有abc权限资源
显示用户身份名称
显示用户身份中的属性值
如果有商品修改权限页面显示“修改”链接。
修改
当调用controller的一个方法,由于该方法加了@RequiresPermissions("item:query"),shiro调用realm获取数据库中的权限信息,看"item:query"是否在权限数据中存在,如果不存在就拒绝访问,如果存在就授权通过。
当展示一个jsp页面时,页面中如果遇到
问题:只要遇到注解或jsp标签的授权,都会调用realm方法查询数据库,需要使用缓存解决此问题。
过滤器简称 |
对应的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 |
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数
user:例如/admins/user/**=user没有参数表示必须存在用户, 身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查
authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
roles:例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms:例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
anon,authcBasic,auchc,user是认证过滤器,
perms,roles,ssl,rest,port是授权过滤器
shiro每次授权都会通过realm获取权限信息,为了提高访问速度需要添加缓存,第一次从realm中读取权限数据,之后不再读取,这里Shiro和Ehcache整合(EhCache是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。)。
shiro中提供了对认证信息和授权信息的缓存。shiro默认是关闭认证信息缓存的,对于授权信息的缓存shiro默认开启的。主要研究授权信息缓存,因为授权的数据量大。
用户认证通过。
该 用户第一次授权:调用realm查询数据库
该 用户第二次授权:不调用realm查询数据库,直接从缓存中取出授权信息(权限标识符)。
在applicationContext-shiro.xml中配置缓存管理器。
如果用户正常退出,缓存自动清空。
如果用户非正常退出,缓存自动清空。
如果修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。需要手动进行编程实现:
当用户权限修改后,用户再次登陆shiro会自动调用realm从数据库获取权限数据,如果在修改权限后想立即清除缓存则可以调用realm的clearCache方法清除缓存。
realm中定义clearCached方法:
下边的代码正常开发时要放在service中调用。在service中,权限修改后调用realm的方法。在realm中定义clearCached方法:
//清除缓存
public void clearCached() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,调用clearCached方法。
和shiro整合后,使用shiro的session管理,shiro提供sessionDao操作 会话数据。
在applicationContext-shiro.xml中配置sessionManager:
思路:
shiro使用FormAuthenticationFilter进行表单认证,验证校验的功能应该加在FormAuthenticationFilter中,在认证之前进行验证码校验。
需要写FormAuthenticationFilter的子类,继承FormAuthenticationFilter,改写它的认证方法,在认证之前进行验证码校验。
需要在验证账号和名称之前校验验证码。自定义FormAuthenticationFilter
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response, Object mappedValue) throws Exception {
// 校验验证码
// 从session获取正确的验证码
HttpSession session = ((HttpServletRequest)request).getSession();
//页面输入的验证码
String randomcode = request.getParameter("randomcode");
//从session中取出验证码
String validateCode = (String) session.getAttribute("validateCode");
if (randomcode!=null && validateCode!=null) {
if (!randomcode.equals(validateCode)) {
// randomCodeError表示验证码错误
request.setAttribute("shiroLoginFailure", "randomCodeError");
//拒绝访问,不再校验账号和密码
return true;
}
}
return super.onAccessDenied(request, response, mappedValue);
}
}
修改applicationContext-shiro.xml中对FormAuthenticationFilter的配置。在shiroFilter中添加filters:
formAuthenticationFilter定义:
添加验证码:
验证码:
刷新
修改applicationContext-shiro.xml:
用户登陆选择“自动登陆”本次登陆成功会向cookie写身份信息,下次登陆从cookie中取出身份信息实现自动登陆。
向cookie记录身份信息需要用户身份信息对象实现序列化接口,如下
修改formAuthenticationFitler添加页面中“记住我checkbox”的input名称:
在login.jsp中添加“记住我”checkbox。
自动登陆
如果设置记住我,下次访问某些url时可以不用登陆。将记住我即可访问的地址配置让UserFilter拦截。