以前对shiro都是一知半解,最近系统学了一遍shiro并集成到了在做的项目中。
下面就详细向大家描述一下shiro的用法。
首先是对spring的配置文件,如下:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 6 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> 7 8 <!-- 配置自定调用 Shiro 对象 init 和 destroy 方法的 BeanPostProcessor --> 9 <!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> 10 <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> 11 12 <!-- 配置使 Shiro 注解起作用的两个 bean, 前提是必须配置 LifecycleBeanPostProcessor bean --> 13 <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"> 14 <property name="proxyTargetClass" value="true" /> 15 </bean> 16 <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> 17 <property name="securityManager" ref="securityManager"/> 18 </bean> 19 20 <!-- 配置 Shiro 的 CacheManager --> 21 <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> 22 <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> 23 </bean> 24 25 <!-- 26 配置 Realm. 实际进行认证和授权的对象 27 通过 init-method 初始化 credentialsMatcher 属性 28 --> 29 <bean id="myRealm" 30 class="com.activiti.shiro.realms.MyRealm" 31 init-method="initCredentialsMatcher"></bean> 32 33 <!-- 配置 Shiro 的 SecurityManager 实例 --> 34 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> 35 <property name="cacheManager" ref="cacheManager"/> 36 <property name="realm" ref="myRealm"/> 37 </bean> 38 39 40 41 <bean id="filterChainDefinitionMapBuilder" 42 class="com.activiti.shiro.FilterChainDefinitionMapBuilder"></bean> 43 44 <!-- 配置 Shiro Filter. 该 bean 的 id 必须和 web.xml 文件中配置的 filter 的 filter-name 一致 --> 45 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> 46 <!-- 添加各种验证过滤器 --> 47 <property name="filters"> 48 <map> 49 <entry key="roleOrFilter" value-ref="roleOrFilter"/> 50 </map> 51 </property> 52 <property name="securityManager" ref="securityManager"/> 53 <property name="loginUrl" value="/index.jsp"/> 54 <property name="successUrl" value="/list.jsp"/> 55 <property name="unauthorizedUrl" value="/unauthorized.jsp"/> 56 <!-- 配置 filterChainDefinitons 属性. 具体需要拦截哪些资源, 以及访问这些资源需要有哪些权限 --> 57 <!-- 若把资源极其对应的权限放入到数据表中, 则需要注入其 filterChainDefinitionMap, 传入的是一个 Map 类型. --> 58 <property name="filterChainDefinitionMap"> 59 <bean factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"/> 60 </property> 61 </bean> 62 63 <!-- 自定义的过滤器,用来判断当前用户是否是roleOrFilter["comm,test"]中的某个角色 --> 64 <bean id="roleOrFilter" class="com.activiti.shiro.RolesAuthorizationFilter" /> 65 66 </beans>
其中12行到18行代码,如果不使用shiro注解的话,可以去掉。
还有关于cache的配置信息也可以去掉。
接下来我们来梳理配置文件中定义的bean,对于上面那些固定搭配我就不再阐述了,只来看一下需要我们自己配置的。
首先是我们自己定义的myRealm,代码如下:
1 /** 2 * 处理shiro登录授权 3 * @author zhangjiahui 4 * 5 */ 6 @Component 7 public class MyRealm extends AuthorizingRealm{ 8 9 @Resource 10 private UserService userService; 11 @Resource 12 private RoleService roleService; 13 14 /** 15 * 授权方法 16 */ 17 @Override 18 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 19 //获取登录名 20 Object primaryPrincipal = principals.getPrimaryPrincipal(); 21 //根据登录名获取角色放入roles中 22 User user = userService.getUserByLoginName(primaryPrincipal.toString()); 23 Set<String> roles = user.getRoles(); 24 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); 25 return info; 26 } 27 28 /** 29 * 验证登录 30 */ 31 @Override 32 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 33 34 System.out.println("======开始验证登录======"); 35 //从token中获取到登录信息 36 UsernamePasswordToken usernamePassword = (UsernamePasswordToken) token; 37 //获取用户名 38 Object principal = usernamePassword.getPrincipal(); 39 //获取当前realm的名称 40 String realmName = getName(); 41 //从数据库中取出的密码 42 User user = userService.getUserByLoginName(principal.toString()); 43 if(user==null){ 44 throw new UnknownAccountException(); 45 } 46 String passWord = user.getPassword(); 47 //将得到的密码盐值加密 48 Object hashedCredentials = this.getSaltPassWord(passWord); 49 //定义的盐值 50 String salt = "www.yiwukong.com"; 51 ByteSource credentialsSalt = ByteSource.Util.bytes(salt.getBytes()); 52 //生成对象,交给shiro去验证 53 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, realmName); 54 return info; 55 } 56 57 /** 58 * 初始化方法 59 */ 60 public void initCredentialsMatcher(){ 61 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); 62 credentialsMatcher.setHashAlgorithmName("MD5"); 63 credentialsMatcher.setHashIterations(1024); 64 setCredentialsMatcher(credentialsMatcher); 65 } 66 67 private Object getSaltPassWord(String passWord){ 68 String hashAlgorithmName = "MD5"; 69 String credentials = passWord; 70 Object salt = ByteSource.Util.bytes("www.yiwukong.com".getBytes()); 71 int hashIterations = 1024; 72 Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 73 return result; 74 } 75 76 }
该类需要继承AuthorizingRealm类,然后实现其中的doGetAuthorizationInfo和doGetAuthenticationInfo两个方法,来实现登录及授权。
doGetAuthenticationInfo,登录用的方法登录时候拿到登录名,然后到数据库里面去取出密码,加盐之后交给shiro去比对,当然这里也可以不加盐,具体的操作步骤注释中已经写的很清楚,就不再描述了。
doGetAuthorizationInfo,授权方法,获取到登录名,从数据库中查出他所拥有的角色,封装为Set交给shiro去处理,具体步骤也已在代码中体现。
接下来是我们定义的filterChainDefinitionMapBuilder,代码如下:
1 package com.activiti.shiro; 2 3 import java.util.LinkedHashMap; 4 5 import javax.annotation.Resource; 6 7 import com.activiti.service.FunctionService; 8 9 public class FilterChainDefinitionMapBuilder { 10 11 @Resource 12 private FunctionService functionService; 13 14 /** 15 * 为shiro返回权限信息 16 * @return 17 */ 18 public LinkedHashMap<String, Object> buildFilterChainDefinitionMap(){ 19 20 LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(); 21 22 result = functionService.getFunctionGroupByRoleList(); 23 24 // result.put("/**", "roles[admin]"); 25 result.put("/user/list", "roleOrFilter[people,process]"); 26 result.put("/shiro/login", "anon"); 27 result.put("/shiro/logout", "logout"); 28 System.out.println(result); 29 30 return result; 31 } 32 33 }
在该类中,定义方法,从数据库中查处所有的需要过滤权限的路径以及所对应的拥有该权限的角色名,以map的形式返回,然后用户登录之后需要访问某个路径时候shiro就回去验证是否拥有权限。
之后,在shiroFilter中我们自定义了自己的filter,因为shiro默认的roles这个filter当在其中填入多个角色的时候,默认为用户拥有这些全部的角色才能访问该路径;而我们所要实现的需求肯定是用户拥有其中一个权限就可以访问该路径,配置文件上面已经描述的很清楚,下面就来看filter的具体代码:
1 package com.activiti.shiro; 2 3 import javax.servlet.ServletRequest; 4 import javax.servlet.ServletResponse; 5 6 import org.apache.shiro.subject.Subject; 7 import org.apache.shiro.web.filter.authz.AuthorizationFilter; 8 9 public class RolesAuthorizationFilter extends AuthorizationFilter{ 10 11 @Override 12 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { 13 Subject subject = getSubject(request, response); 14 String[] rolesArray = (String[]) mappedValue; 15 16 if (rolesArray == null || rolesArray.length == 0) { //没有角色限制,有权限访问 17 return false; 18 } 19 for (int i = 0; i < rolesArray.length; i++) { 20 if (subject.hasRole(rolesArray[i])) { //若当前用户是rolesArray中的任何一个,则有权限访问 21 return true; 22 } 23 } 24 25 return false; 26 } 27 28 }
我们所写的filter需要继承授权的AuthorizationFilter,重写isAccessAllowed方法,具体实现注释中已写明。
至此shiro的基本实现,已完成。
下面在说一下,密码输错次数太多,短时间锁定用户的做法。这里就用到了上面配置的ehcache,需要在spring配置文件里面加入如下代码:
1 <!-- 配置 Shiro 的 CacheManager --> 2 <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> 3 <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> 4 </bean> 5 6 <bean id="credentialsMatcher" class="com.sshhyy.security.CustomCredentialsMatcher"> 7 <constructor-arg ref="cacheManager" /> 8 <property name="hashAlgorithmName" value="md5" /> 9 <property name="hashIterations" value="3" /> 10 <property name="storedCredentialsHexEncoded" value="true" /> 11 </bean>
其中配置cache的步骤在上面已经提到,而配置credentialsMatcher是为了自定义密码验证方法。
其中echache配置代码如下:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <ehcache name="shirocache"> 3 4 <diskStore path="java.io.tmpdir" /> 5 6 <!-- 登录记录缓存 锁定10分钟 --> 7 <cache name="passwordRetryCache" eternal="false" 8 timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" 9 statistics="true"> 10 </cache> 11 12 <cache name="authorizationCache" eternal="false" 13 timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" 14 statistics="true"> 15 </cache> 16 17 <cache name="authenticationCache" eternal="false" 18 timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" 19 statistics="true"> 20 </cache> 21 22 <cache name="shiro-activeSessionCache" eternal="false" 23 timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" 24 statistics="true"> 25 </cache> 26 27 </ehcache>
自定义的credentialsMatcher代码如下:
1 import java.util.concurrent.atomic.AtomicInteger; 2 3 import org.apache.shiro.authc.AuthenticationInfo; 4 import org.apache.shiro.authc.AuthenticationToken; 5 import org.apache.shiro.authc.ExcessiveAttemptsException; 6 import org.apache.shiro.authc.credential.HashedCredentialsMatcher; 7 import org.apache.shiro.cache.Cache; 8 import org.apache.shiro.cache.CacheManager; 9 10 /** 11 * @author zhangjiahui 12 */ 13 public class CustomCredentialsMatcher extends HashedCredentialsMatcher { 14 15 private Cache<String,AtomicInteger> passwordRetryCache; 16 17 public CustomCredentialsMatcher(CacheManager cacheManager) { 18 passwordRetryCache = cacheManager.getCache("passwordRetryCache"); 19 } 20 21 @Override 22 public boolean doCredentialsMatch(AuthenticationToken token, 23 AuthenticationInfo info) { 24 String loginName = (String) token.getPrincipal(); 25 AtomicInteger retryCount = passwordRetryCache.get(loginName); 26 if(retryCount==null){ 27 retryCount = new AtomicInteger(); 28 passwordRetryCache.put(loginName, retryCount); 29 } 30 if(retryCount.incrementAndGet()>5){ 31 throw new ExcessiveAttemptsException(); 32 } 33 boolean matchs = super.doCredentialsMatch(token, info); 34 if(matchs){ 35 passwordRetryCache.remove(loginName); 36 } 37 return super.doCredentialsMatch(token, info); 38 } 39 40 public Cache<String, AtomicInteger> getPasswordRetryCache() { 41 return passwordRetryCache; 42 } 43 44 public void setPasswordRetryCache(Cache<String, AtomicInteger> passwordRetryCache) { 45 this.passwordRetryCache = passwordRetryCache; 46 } 47 48 }
这样当密码输错五次时,就会抛出账号密码输入次数过多异常,十分钟后cache中记录会清空。
controller层的登录代码:
1 public Map<String, String> login(HttpServletRequest request, Model model) { 2 Map<String, String> map = new HashMap<String, String>(); 3 map.put("returnCode", ERR_CODE); 4 String msg = ""; 5 String nameLogin = request.getParameter(SessionEnum.CURRENT_LOGIN_NAME); 6 if (StringUtil.isBlank(nameLogin)) { 7 msg = "请输入登录名!"; 8 model.addAttribute("message", msg); 9 } 10 String password = request.getParameter(SessionEnum.CURRENT_LOGIN_PASSWORD); 11 if (StringUtil.isBlank(password)) { 12 msg = "密码不能为空!"; 13 model.addAttribute("message", msg); 14 } 15 boolean flag = false; 16 String isRememberMe = request.getParameter(SessionEnum.IS_REMEMBER_ME); 17 if (null == isRememberMe) 18 flag = true; 19 20 UsernamePasswordToken token = new UsernamePasswordToken(nameLogin, password); 21 token.setRememberMe(flag); 22 try { 23 Subject subject = SecurityUtils.getSubject(); 24 subject.login(token); 25 } catch (IncorrectCredentialsException e) { 26 e.printStackTrace(); 27 msg = "登录名或密码错误,请重新登录!"; 28 model.addAttribute("message", msg); 29 } catch (ExcessiveAttemptsException e) { 30 e.printStackTrace(); 31 msg = "登录失败次数过多"; 32 model.addAttribute("message", msg); 33 } catch (LockedAccountException e) { 34 e.printStackTrace(); 35 msg = "帐号("+token.getPrincipal()+")已被锁定!"; 36 model.addAttribute("message", msg); 37 } catch (DisabledAccountException e) { 38 e.printStackTrace(); 39 msg = "帐号("+token.getPrincipal()+")已被禁用!"; 40 model.addAttribute("message", msg); 41 } catch (ExpiredCredentialsException e) { 42 e.printStackTrace(); 43 msg = "帐号("+token.getPrincipal()+")已过期!"; 44 model.addAttribute("message", msg); 45 } catch (UnknownAccountException e) { 46 e.printStackTrace(); 47 msg = "登录名或密码错误,请重新登录!"; 48 model.addAttribute("message", msg); 49 } catch (UnauthorizedException e) { 50 e.printStackTrace(); 51 msg = "您没有得到相应的授权!" + e.getMessage(); 52 model.addAttribute("message", msg); 53 } catch (NullPointerException e) { 54 e.printStackTrace(); 55 msg = "输入框不能为空!"; 56 model.addAttribute("message", msg); 57 } 58 map.put("message", msg); 59 return map; 60 }
至此shiro配置成功,对于一般的项目来说应该是够用了,至少目前来说对于我够用了,哈哈。