一、认证——Authentication
(即登陆),简单分析之前的HelloWorld的认证:
1. 获取当前的 Subject. 调用 SecurityUtils.getSubject();
2. 测试当前的用户是否已经被认证. 即是否已经登录. 调用 Subject 的 isAuthenticated()
3. 若没有被认证, 则把用户名和密码封装为 UsernamePasswordToken 对象
1). 创建一个表单页面
2). 把请求提交到 SpringMVC 的 Handler
3). 获取用户名和密码.
4. 执行登录: 调用 Subject 的 login(AuthenticationToken) 方法.
5. 自定义 Realm 的方法, 从数据库中获取对应的记录, 返回给 Shiro.
1). 实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类
2). 实现 doGetAuthenticationInfo(AuthenticationToken) 方法.
6. 由 shiro 完成对密码的比对.
在 shiro 中,用户需要提供 principals (身份)和 credentials(证 明)给 shiro,从而应用能验证用户身份
接下来我们来实现一下分析的认证流程:
第3步的创建表单页面:login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <h3>Hello login!</h3> <form action="shiroLogin" method="post"> username:<input type="text" name="username"/> <br/> password:<input type="password" name="password"/> <br/> <input type="submit" value="提交"/> </form> </body> </html>
创建相应的handler(controller):
package cn.shiro.handler; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller public class ShrioHandler { /** * 获取登陆的表单数据 * @return */ @RequestMapping("/shiroLogin") public String login(@RequestParam("username") String username, @RequestParam("password") String password){ Subject currentUser = SecurityUtils.getSubject(); if (!currentUser.isAuthenticated()) { // 把用户名和密码封装为 UsernamePasswordToken 对象 UsernamePasswordToken token = new UsernamePasswordToken(username, password); // rememberme token.setRememberMe(true); try { System.out.println("1. " + token.hashCode()); // 执行登录. currentUser.login(token); } // ... catch more exceptions here (maybe custom ones specific to your application? // 所有认证时异常的父类. catch (AuthenticationException ae) { //unexpected condition? error? System.out.println("登录失败: " + ae.getMessage()); } } //执行完成后重定向到list页面 return "redirect:/list.jsp"; } }
将前面创建的Realm改造为继承而不是实现接口的形式来实现自定义Realm:
package cn.shiro.realms; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; public class ShiroRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { // TODO Auto-generated method stub return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException { System.out.println("token:"+arg0.hashCode()); return null; } }
当然,我们得在配置文件中配置controller的匿名访问才可以访问:
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiroLogin = anon
# everything else requires authentication:
/** = authc
</value>
</property>
最后我们访问Login页面,可以看到结果:
//证明我们的reaml拿到了用户名与密码(token)
可以过来以后我们进行认证的完善:
完善ShiroRealm的对比过程:
package cn.shiro.realms; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; 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 ShiroRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { //System.out.println("token:"+arg0.hashCode()); //1. 把 AuthenticationToken 转换为 UsernamePasswordToken UsernamePasswordToken upToken = (UsernamePasswordToken) token; //2. 从 UsernamePasswordToken 中来获取 username String username = upToken.getUsername(); //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录 System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息."); //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常 if("unknown".equals(username)){ throw new UnknownAccountException("用户不存在!"); } //5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常 if("monster".equals(username)){ throw new LockedAccountException("用户被锁定"); } //6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo //1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. Object principal = username; //2). credentials: 密码. Object credentials = "123"; //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String realmName = getName(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName); //返回信息 return info; } }
输入unknown的测试结果(其他暂不列举)
//成功登陆(即认证)后即可访问其他的需要认证的页面
我们来大致捋一下认证的流程:
从表单填写信息——>提交到handler,获取表单的信息——>调用login方法,把用户名密码封装为token对象继续往后传——>在Reaml中调用doGetAuthenticationInfo方法——>由token的用户名去数据库中获取用户信息进行对比,我们把密码封装成SimpleAuthenticationInfo(具体的源码分析:例如何时进行密码比对,如何比对待补充)
接下来进行密码的加密,因为密码不能是明文,并且不能是可逆的,这里介绍常用的MD5,另外的SHA1待补充:
1. 如何把一个字符串加密为 MD5
2. 替换当前 Realm 的 credentialsMatcher 属性. 直接使用 HashedCredentialsMatcher 对象, 并设置加密算法即可.
我们先完成第二步的配置,在配置文件中Realm的配置处配置一个内部的bean:(配置了自定义Realm,当然得让shiro知道我们要使用自定义的Realm)
<!-- 3. 配置 Realm 3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean --> <bean id="jdbcRealm" class="cn.shiro.realms.ShiroRealm"> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5"></property> </bean> </property> </bean>
指定了凭证匹配器后会自动的把前台的明文密码加密为MD5,当然,我们可以指定加密次数:
<property name="hashAlgorithmName" value="MD5"></property> <property name="hashIterations" value="1024"></property>
当然,即使指定了加密次数,如果两个人的密码一样,那么加密后的密码也是一样的,我们还需要改进,于是我们加入 盐值
盐值是一个ByteSource接口,一般我们使用唯一的字符串作为盐,比如不可重复的用户名,示例如下:
@Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { System.out.println("[FirstRealm] doGetAuthenticationInfo"); //1. 把 AuthenticationToken 转换为 UsernamePasswordToken UsernamePasswordToken upToken = (UsernamePasswordToken) token; //2. 从 UsernamePasswordToken 中来获取 username String username = upToken.getUsername(); //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录 System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息."); //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常 if("unknown".equals(username)){ throw new UnknownAccountException("用户不存在!"); } //5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常. if("monster".equals(username)){ throw new LockedAccountException("用户被锁定"); } //6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo //以下信息是从数据库中获取的. //1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. Object principal = username; //2). credentials: 密码. Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4"; if("admin".equals(username)){ credentials = "038bdaf98f2037b31f1e75b5b4c9b26e"; }else if("user".equals(username)){ credentials = "098d2c478e9c11555ce2823231e02ec1"; } //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String realmName = getName(); //4). 盐值. ByteSource credentialsSalt = ByteSource.Util.bytes(username); SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName); info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName); return info; }
盐值加密小结如下:
1. 为什么使用 MD5 盐值加密:
2. 如何做到:
1). 在 doGetAuthenticationInfo 方法返回值创建 SimpleAuthenticationInfo 对象的时候, 需要使用
SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 构造器
2). 使用 ByteSource.Util.bytes() 来计算盐值.
3). 盐值需要唯一: 一般使用随机字符串或 user id
4). 使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 来计算盐值加密后的密码的值.
加密的部分请参见开涛的博客:http://jinnianshilongnian.iteye.com/blog/2021439
接下来我们再介绍多Realm验证
先建立一个SHA1加密的Realm:
package com.atguigu.shiro.realms; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.util.ByteSource; public class SecondRealm extends AuthenticatingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { System.out.println("[SecondReaml] doGetAuthenticationInfo"); //1. 把 AuthenticationToken 转换为 UsernamePasswordToken UsernamePasswordToken upToken = (UsernamePasswordToken) token; //2. 从 UsernamePasswordToken 中来获取 username String username = upToken.getUsername(); //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录 System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息."); //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常 if("unknown".equals(username)){ throw new UnknownAccountException("用户不存在!"); } //5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常. if("monster".equals(username)){ throw new LockedAccountException("用户被锁定"); } //6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo //以下信息是从数据库中获取的. //1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. Object principal = username; //2). credentials: 密码. Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4"; if("admin".equals(username)){ credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06"; }else if("user".equals(username)){ credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718"; } //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String realmName = getName(); //4). 盐值. ByteSource credentialsSalt = ByteSource.Util.bytes(username); SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName); info = new SimpleAuthenticationInfo("secondRealmName", credentials, credentialsSalt, realmName); return info; } public static void main(String[] args) { String hashAlgorithmName = "SHA1"; Object credentials = "123456"; Object salt = ByteSource.Util.bytes("admin");; int hashIterations = 1024; Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); System.out.println(result); } }
在spring配置文件中配置第二个bean:
<bean id="secondRealm" class="com.atguigu.shiro.realms.SecondRealm"> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="SHA1"></property> <property name="hashIterations" value="1024"></property> </bean> </property> </bean>
再把管家的多个Realm进行配置:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> <property name="authenticator" ref="authenticator"></property> <property name="realms"> <list> <ref bean="jdbcRealm"/> <ref bean="secondRealm"/> </list> </property> <property name="rememberMeManager.cookie.maxAge" value="10"></property> </bean>
测试暂不举例(认证策略暂不展开):
AuthenticationStrategy
• AuthenticationStrategy 接口的默认实现:
• FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第
一个 Realm 身份验证成功的认证信息,其他的忽略;
• AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和
FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信
息;
• AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有
Realm身份验证成功的认证信息,如果有一个失败就失败了。
• ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy
策略
退出: 不用我们去实现退出,只要去访问一个退出的url(该 url是可以不存在),由LogoutFilter拦截住,清除session。
在applicationContext-shiro.xml配置LogoutFilter:
<!-- 请求 logout.action地址,shiro去清除session-->
/logout.action = logout
二、授权——Authorization
授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作 等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限 (Permission)、角色(Role)。
授权基础流程:
测试授权的具体流程:
1、在applicationContext-shiro.xml中配置filter规则
<!--商品查询需要商品查询权限 -->
/items/queryItems.action = perms[item:query]
2、用户在认证通过后,请求/items/queryItems.action
3、被PermissionsAuthorizationFilter拦截,发现需要“item:query”权限
4、PermissionsAuthorizationFilter调用realm中的doGetAuthorizationInfo获取数据库中正确的权限
5、PermissionsAuthorizationFilter对item:query 和从realm中获取权限进行对比,如果“item:query”在realm返回的权限列表中,授权通过。
相关的对象解释:
• 主体(Subject):访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权
后才允许访问相应的资源。
• 资源(Resource):在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些
数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
• 权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户
有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用
户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控
制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允
许。
• Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,
即实例级别的)
• 角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有
一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等
都是角色,不同的角色拥有一组不同的权限。
Shiro支持的授权方式:
//当然编码与注解我们还是倾向于使用注解的形式
规则即:“用户名=密码,角色1,角色2”,如果需要在应用中判断用户是否有相应角色,就需要在相应的Realm中返回角色信息,也就是说Shiro不负责维护用户-角色信息,需要应用提供,Shiro只是提供相应的接口方便验证
Shiro相关的拦截器参见day01
1.Shiro 的 Permissions //quickstart的shiro.ini中也有相关的介绍。
规则:资源标识符:操作:对象实例 ID 即对哪个资源的哪个实例可以进行什么操作.
• 实例级访问控制 – 这种情况通常会使用三个部件:域、操作、被付诸实施的实例。如:user:edit:manager,对资源user的manager实例拥有edit操作
– 也可以使用通配符来定义,如:user:edit:*、user:*:*、 user:*:manager
– 部分省略通配符:缺少的部件意味着用户可以访问所 有与之匹配的值,比如:user:edit 等价于 user:edit :*、 user 等价于 user:*:*
– 注意:通配符只能从字符串的结尾处省略部件,也就 是说 user:edit 并不等价于 user:*:edit
2.授权流程简要分析如下:
• 流程如下:
• 1、首先调用 Subject.isPermitted*/hasRole* 接口,其会委托给
SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
• 2、Authorizer是真正的授权者,如果调用如
isPermitted(“user:view”),其首先会通过
• PermissionResolver 把字符串转换成相应的 Permission 实例;
• 3、在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角
色/权限用于匹配传入的角色/权限;
• 4、Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果
有多个Realm,会委托给 ModularRealmAuthorizer 进行循环判断,
如果匹配如 isPermitted*/hasRole* 会返回true,否则返回false表示
授权失败。
如何实现授权:
//授权会被 shiro 回调的方法 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { //1. 从 PrincipalCollection 中来获取登录用户的信息 Object principal = principals.getPrimaryPrincipal(); //2. 利用登录的用户的信息来用户当前用户的角色或权限(可能需要查询数据库) Set<String> roles = new HashSet<>(); roles.add("user"); if("admin".equals(principal)){ roles.add("admin"); } //3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); //4. 返回 SimpleAuthorizationInfo 对象. return info; }
1.编码实现举例:
ini配置文件:(后期应该改造为自定义realm,在数据库存储)
shiro-permission.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.一般情况下,我们都需要通过自定义Realm来进行授权(也就是前面自定义Realm中另外一个未实现的方法):
在之前的customerRealm中完成另外一个授权方法:
// 用于授权 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { //从 principals获取主身份信息 //将getPrimaryPrincipal方法返回值转为真实身份类型(在上边的doGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo中身份类型), ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal(); //根据身份信息获取权限信息 //从数据库获取到权限数据 List<SysPermission> permissionList = null; try { permissionList = sysService.findPermissionListByUserId(activeUser.getUserid()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } //单独定一个集合对象 List<String> permissions = new ArrayList<String>(); if(permissionList!=null){ for(SysPermission sysPermission:permissionList){ //将数据库中的权限标签 符放入集合 permissions.add(sysPermission.getPercode()); } } /* List<String> permissions = new ArrayList<String>(); permissions.add("user:create");//用户的创建 permissions.add("item:query");//商品查询权限 permissions.add("item:add");//商品添加权限 permissions.add("item:edit");//商品修改权限 */ //.... //查到权限数据,返回授权信息(要包括 上边的permissions) SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //将上边查询到授权信息填充到simpleAuthorizationInfo对象中 simpleAuthorizationInfo.addStringPermissions(permissions); return simpleAuthorizationInfo; }
测试方法:
// 自定义realm进行资源授权测试 @Test public void testAuthorizationCustomRealm() { // 创建SecurityManager工厂 Factory<SecurityManager> factory = new IniSecurityManagerFactory( "classpath:shiro-realm.ini"); // 创建SecurityManager SecurityManager securityManager = factory.getInstance(); // 将SecurityManager设置到系统运行环境,和spring后将SecurityManager配置spring容器中,一般单例管理 SecurityUtils.setSecurityManager(securityManager); // 创建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(); } System.out.println("认证状态:" + subject.isAuthenticated()); // 认证通过后执行授权 // 基于资源的授权,调用isPermitted方法会调用CustomRealm从数据库查询正确权限数据 // isPermitted传入权限标识符,判断user:create:1是否在CustomRealm查询到权限数据之内 boolean isPermitted = subject.isPermitted("user:create:1"); System.out.println("单个权限判断" + isPermitted); boolean isPermittedAll = subject.isPermittedAll("user:create:1", "user:create"); System.out.println("多个权限判断" + isPermittedAll); // 使用check方法进行授权,如果授权不通过会抛出异常 subject.checkPermission("items:add:1"); }
方法流程(上文已有一个版本):
1、对subject进行授权,调用方法isPermitted("permission串")
2、SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
3、ModularRealmAuthorizer执行realm(自定义的CustomRealm)从数据库查询权限数据
调用realm的授权方法:doGetAuthorizationInfo
4、realm从数据库查询权限数据,返回ModularRealmAuthorizer
5、ModularRealmAuthorizer调用PermissionResolver进行权限串比对
6、如果比对后,isPermitted中"permission串"在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。
3.Shiro标签:
标签的介绍参见:http://jinnianshilongnian.iteye.com/blog/2026398
4.shiro注解
对系统中类的方法给用户授权,建议在controller层进行方法授权。(下文有进一步原因解释)
在springMVC中配置注解的AOP式支持:
<!-- 开启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>
这样就可以在controller方法上(强烈建议controller方法上,而不是其它层)使用 @RequiresPermissions("order:query") ——执行此方法需要此权限,示例需要此角色类同式注解了:
@RequiresRoles("admin") @RequestMapping("/hello2") public String hello2() { return "success"; }
当调用controller的一个方法,由于该 方法加了@RequiresPermissions("item:query") ,shiro调用realm获取数据库中的权限信息,看"item:query"是否在权限数据中存在,如果不存在就拒绝访问,如果存在就授权通过。
当展示一个jsp页面(真身是servlet)时,页面中如果遇到<shiro:hasPermission name="item:update">,shiro调用realm获取数据库中的权限信息,看item:update是否在权限数据中存在,如果不存在就拒绝访问,如果存在就授权通过。
问题:只要遇到注解或jsp标签的授权,都会调用realm方法查询数据库,需要使用缓存解决此问题。
jeesite中的配置(待补充):
<!-- AOP式方法级权限检查 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
完整配置文件如下:
<?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-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd" default-lazy-init="true"> <description>Shiro Configuration</description> <!-- 加载配置属性文件 --> <context:property-placeholder ignore-unresolvable="true" location="classpath:jeesite.properties" /> <!-- Shiro权限过滤过滤器定义 --> <bean name="shiroFilterChainDefinitions" class="java.lang.String"> <constructor-arg> <value> /static/** = anon /userfiles/** = anon ${adminPath}/cas = cas ${adminPath}/login = authc ${adminPath}/logout = logout ${adminPath}/** = user /act/editor/** = user /ReportServer/** = user </value> </constructor-arg> </bean> <!-- 安全认证过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /><!-- <property name="loginUrl" value="${cas.server.url}?service=${cas.project.url}${adminPath}/cas" /> --> <property name="loginUrl" value="${adminPath}/login" /> <property name="successUrl" value="${adminPath}?login" /> <property name="filters"> <map> <entry key="cas" value-ref="casFilter"/> <entry key="authc" value-ref="formAuthenticationFilter"/> </map> </property> <property name="filterChainDefinitions"> <ref bean="shiroFilterChainDefinitions"/> </property> </bean> <!-- CAS认证过滤器 --> <bean id="casFilter" class="org.apache.shiro.cas.CasFilter"> <property name="failureUrl" value="${adminPath}/login"/> </bean> <!-- 定义Shiro安全管理配置 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="systemAuthorizingRealm" /> <property name="sessionManager" ref="sessionManager" /> <property name="cacheManager" ref="shiroCacheManager" /> </bean> <!-- 自定义会话管理配置 --> <bean id="sessionManager" class="com.thinkgem.jeesite.common.security.shiro.session.SessionManager"> <property name="sessionDAO" ref="sessionDAO"/> <!-- 会话超时时间,单位:毫秒 --> <property name="globalSessionTimeout" value="${session.sessionTimeout}"/> <!-- 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话 --> <property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/> <!-- <property name="sessionValidationSchedulerEnabled" value="false"/> --> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/> <property name="sessionIdCookieEnabled" value="true"/> </bean> <!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: 与SERVLET容器名冲突, 如JETTY, TOMCAT 等默认JSESSIONID, 当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! --> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg name="name" value="jeesite.session.id"/> </bean> <!-- 自定义Session存储容器 --> <!-- <bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.JedisSessionDAO"> --> <!-- <property name="sessionIdGenerator" ref="idGen" /> --> <!-- <property name="sessionKeyPrefix" value="${redis.keyPrefix}_session_" /> --> <!-- </bean> --> <bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.CacheSessionDAO"> <property name="sessionIdGenerator" ref="idGen" /> <property name="activeSessionsCacheName" value="activeSessionsCache" /> <property name="cacheManager" ref="shiroCacheManager" /> </bean> <!-- 自定义系统缓存管理器--> <!-- <bean id="shiroCacheManager" class="com.thinkgem.jeesite.common.security.shiro.cache.JedisCacheManager"> --> <!-- <property name="cacheKeyPrefix" value="${redis.keyPrefix}_cache_" /> --> <!-- </bean> --> <bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManager" ref="cacheManager"/> </bean> <!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- AOP式方法级权限检查 --> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"> <property name="proxyTargetClass" value="true" /> </bean> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> </beans>
注解的实现请参见:http://www.cnblogs.com/lzlblogs/p/5973535.html
【注意】:
在Service方法上使用注解 @Transactional 即在方法开始的时候会有事务,这个时候这个Service已经是一个代理对象,
这个是有把 权限注解加到 Service上是不好用的,会发生类型转换异常。需要加到Controller上,因为不能够让Service是代理的代理。
并且,controller的入口只有一个,而service是可以被其它层公开调用了,就失去了授权的意义。
4.之前我们配置的受保护的资源和对应关系是配置在applicationContext.xml中配置固定的:
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
</value>
</property>
我们可以改进为将数据存入数据库中,然后通过SQL方式进行读取,这样便于管理。
我们把 filterChainDefinitions 配置给ShiroFilter:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
创建相应的Map,存放资源权限数据:
package com.atguigu.shiro.factory; import java.util.LinkedHashMap; public class FilterChainDefinitionMapBuilder { public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){ LinkedHashMap<String, String> map = new LinkedHashMap<>(); map.put("/login.jsp", "anon"); map.put("/shiro/login", "anon"); map.put("/shiro/logout", "logout"); map.put("/user.jsp", "authc,roles[user]"); map.put("/admin.jsp", "authc,roles[admin]"); map.put("/list.jsp", "user"); map.put("/**", "authc"); return map; } }
//实际操作的时候,从数据库取出数据(一定是linkedHashMap,而且注意顺序)
再补全配置文件:
<!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean>
<bean id="filterChainDefinitionMapBuilder"
class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder"></bean>
通过自定义拦截器可以扩展功能,例如:动态url-角色/权 限访问控制的实现、根据 Subject 身份信息获取用户信息 绑定到 Request(即设置通用数据)、验证码验证、在线 用户信息的保存等
三、会话管理
Shiro 提供了完整的企业级会话管理功能,不依赖于底层容 器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境 都可以使用,提供了会话管理、会话事件监听、会话存储/ 持久化、容器无关的集群、失效/过期支持、对Web 的透明 支持、SSO 单点登录的支持等特性。
相关的API这里暂不展开。
1.这里需要提的是Shiro的会话管理可以进入Service,这样就可以在开发时在service层访问session
package com.atguigu.shiro.services; import java.util.Date; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.annotation.RequiresRoles; import org.apache.shiro.session.Session; public class ShiroService { @RequiresRoles({"admin"}) public void testMethod(){ System.out.println("testMethod, time: " + new Date()); Session session = SecurityUtils.getSubject().getSession(); Object val = session.getAttribute("key"); System.out.println("Service SessionVal: " + val); } }
2.SessionDao:
AbstractSessionDAO 提供了 SessionDAO 的基础实现,
如生成会话ID等
• CachingSessionDAO 提供了对开发者透明的会话缓存的
功能,需要设置相应的 CacheManager
• MemorySessionDAO 直接在内存中进行会话维护
• EnterpriseCacheSessionDAO 提供了缓存功能的会话维
护,默认情况下使用 MapCache 实现,内部使用
ConcurrentHashMap 保存缓存的会话。
示例配置:
3.会话验证
• Shiro 提供了会话验证调度器,用于定期的验证会话是否 已过期,如果过期将停止会话 • 出于性能考虑,一般情况下都是获取会话时来验证会话是 否过期并停止会话的;但是如在 web 环境中,如果用户不 主动退出是不知道会话是否过期的,
因此需要定期的检测 会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler • Shiro 也提供了使用Quartz会话验证调度器: QuartzSessionValidationScheduler
【更新】:shiro的缓存
针对上边授权频繁查询数据库,需要使用shiro缓存。
shiro中提供了对认证信息和授权信息的缓存。shiro默认是关闭认证信息缓存的,对于授权信息的缓存shiro默认开启的。主要研究授权信息缓存,因为授权的数据量大。
用户认证通过。
该 用户第一次授权:调用realm查询数据库
该 用户第二次授权:不调用realm查询数据库,直接从缓存中取出授权信息(权限标识符:就是一堆字符串)。
缓存的配置步骤:
导包:
配置缓存管理器(cacheManager):由于缓存管理器也是归安全管理器securityManager这个大管家管理的,所以在其属性处进行配置即可:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <!--diskStore:缓存数据持久化的目录 地址 --> <diskStore path="F:developehcache" /> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>
【更新】:验证码的引入:
思路
shiro使用FormAuthenticationFilter进行表单认证,验证校验的功能应该加在FormAuthenticationFilter中,在认证之前进行验证码校验。
需要写FormAuthenticationFilter的子类,继承FormAuthenticationFilter,改写它的认证方法,在认证之前进行验证码校验。
public class CustomFormAuthenticationFilter extends FormAuthenticationFilter { //原FormAuthenticationFilter的认证方法 @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //在这里进行验证码的校验 //从session获取正确验证码 HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpSession session =httpServletRequest.getSession(); //取出session的验证码(正确的验证码) String validateCode = (String) session.getAttribute("validateCode"); //取出页面的验证码 //输入的验证和session中的验证进行对比 String randomcode = httpServletRequest.getParameter("randomcode"); if(randomcode!=null && validateCode!=null && !randomcode.equals(validateCode)){ //如果校验失败,将验证码错误失败信息,通过shiroLoginFailure设置到request中 httpServletRequest.setAttribute("shiroLoginFailure", "randomCodeError"); //拒绝访问,不再校验账号和密码 return true; } return super.onAccessDenied(request, response); }
<!-- 自定义filter配置 --> <property name="filters"> <map> <!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中--> <entry key="authc" value-ref="formAuthenticationFilter" /> </map> </property>
<!-- 自定义form认证过虑器 --> <!-- 基于Form表单的身份验证过滤器,不配置将也会注册此过虑器,表单中的用户账号、密码及loginurl将采用默认值,建议配置 --> <bean id="formAuthenticationFilter" class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter "> <!-- 表单中账号的input名称 --> <property name="usernameParam" value="username" /> <!-- 表单中密码的input名称 --> <property name="passwordParam" value="password" /> <!-- 记住我input的名称 --> <property name="rememberMeParam" value="rememberMe"/> </bean>
四、认证与记住我
• Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝 等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁, 下次访问时无需再登录即可访问,基本流程如下:
• 1、首先在登录页面选中 RememberMe 然后登录成功;如果是 浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并 保存下来;——用户信息实现序列化借口,方便进行保存与反序列化。
• 2、关闭浏览器再重新打开;会发现浏览器还是记住你的;
• 3、访问一般的网页服务器端还是知道你是谁,且能正常访问;
• 4、但是比如我们访问淘宝时,如果要查看我的订单或进行支付 时,此时还是需要再进行身份认证的,以确保当前用户还是你。
如何配置:
前台页面:
<tr> <TD></TD> <td><input type="checkbox" name="rememberMe" />自动登陆</td> </tr>
//当然,name是可以配置的,详见配置文件applicationContext-shiro.xml:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd "> <!-- web.xml中shiro的filter对应的bean --> <!-- Shiro 的Web过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 --> <property name="loginUrl" value="/login.action" /> <!-- 认证成功统一跳转到first.action,建议不配置,shiro认证成功自动到上一个请求路径 --> <property name="successUrl" value="/first.action"/> <!-- 通过unauthorizedUrl指定没有权限操作时跳转页面--> <property name="unauthorizedUrl" value="/refuse.jsp" /> <!-- 自定义filter配置 --> <property name="filters"> <map> <!-- 将自定义 的FormAuthenticationFilter注入shiroFilter中--> <entry key="authc" value-ref="formAuthenticationFilter" /> </map> </property> <!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 --> <property name="filterChainDefinitions"> <value> <!-- 对静态资源设置匿名访问 --> /images/** = anon /js/** = anon /styles/** = anon <!-- 验证码,可匿名访问 --> /validatecode.jsp = anon <!-- 请求 logout.action地址,shiro去清除session--> /logout.action = logout <!--商品查询需要商品查询权限 ,取消url拦截配置,使用注解授权方式 --> <!-- /items/queryItems.action = perms[item:query] /items/editItems.action = perms[item:edit] --> <!-- 配置记住我或认证通过可以访问的地址 --> /index.jsp = user /first.action = user /welcome.jsp = user <!-- /** = authc 所有url都必须认证通过才可以访问--> /** = authc <!-- /** = anon所有url都可以匿名访问 --> </value> </property> </bean> <!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="customRealm" /> <!-- 注入缓存管理器 --> <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager" /> <!-- 记住我 --> <property name="rememberMeManager" ref="rememberMeManager"/> </bean> <!-- realm --> <bean id="customRealm" class="cn.itcast.ssm.shiro.CustomRealm"> <!-- 将凭证匹配器设置到realm中,realm按照凭证匹配器的要求进行散列 --> <property name="credentialsMatcher" ref="credentialsMatcher"/> </bean> <!-- 凭证匹配器 --> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5" /> <property name="hashIterations" value="1" /> </bean> <!-- 缓存管理器 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/> </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> <!-- 自定义form认证过虑器 --> <!-- 基于Form表单的身份验证过滤器,不配置将也会注册此过虑器,表单中的用户账号、密码及loginurl将采用默认值,建议配置 --> <bean id="formAuthenticationFilter" class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter "> <!-- 表单中账号的input名称 --> <property name="usernameParam" value="username" /> <!-- 表单中密码的input名称 --> <property name="passwordParam" value="password" /> <!-- 记住我input的名称 --> <property name="rememberMeParam" value="rememberMe"/> </bean> <!-- rememberMeManager管理器,写cookie,取出cookie生成用户信息 --> <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"> <!-- rememberMe是cookie的名字 --> <constructor-arg value="rememberMe" /> <!-- 记住我cookie生效时间30天 --> <property name="maxAge" value="2592000" /> </bean> </beans>
配置某些页面可以使用记住我访问:
认证与记住我的区别:
subject.isAuthenticated() 表示用户进行了身份验证登录的, 即使有 Subject.login 进行了登录;
• subject.isRemembered():表示用户是通过记住我登录的, 此时可能并不是真正的你(如你的朋友使用你的电脑,或者 你的cookie 被窃取)在访问的
• 两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样。
// rememberme
token.setRememberMe(true);
//一般为若单选框的进行了勾选则设置记住我,还可以进一步设置记住我的时间。
记住我请参阅:http://jinnianshilongnian.iteye.com/blog/2031823