用Shiro做登录权限控制时,密码加密是自定义的。
数据库的密码通过散列获取,如下,算法为:md5,盐为一个随机数字,散列迭代次数为3次,最终将salt与散列后的密码保存到数据库内,第二次登录时将登录的令牌再进行同样的运算后再与数据库的做对比。
String algorithmName = "md5";
String userName = "rose";
String password = "rose123";
int hashIterations = 3; //散列迭代次数
String salt = new SecureRandomNumberGenerator().nextBytes().toHex();
// 将用户的密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐
SimpleHash simpleHash = new SimpleHash(algorithmName,password,userName+salt,hashIterations);
String encodedPassword = simpleHash.toHex();
System.out.println("salt is "+salt);
System.out.println("encodedPassword is "+encodedPassword);
创建RetryLimitHashedCredentialsMatcher类,此类有登录失败次数的判断,多于5次后再等待10分钟后才能重试。
缓存机制用到了Ehcache,Ehcache是很多Java项目中使用的缓存框架,Hibernate就是其中之一。它的本质就是将原本只能存储在内存中的数据通过算法保存到硬盘上,再根据需求依次取出。你可以把Ehcache理解为一个Map<String,Object>对象,通过put保存对象,再通过get取回对象。
package com.ken.shiro; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import java.util.concurrent.atomic.AtomicInteger; public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private Cache<String,AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager){ passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String userName = token.getPrincipal().toString(); System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); System.out.println(userName); System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); AtomicInteger retryCount = passwordRetryCache.get(userName); if (null == retryCount) { retryCount = new AtomicInteger(0); passwordRetryCache.put(userName,retryCount); } if (retryCount.incrementAndGet() > 5) { throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if (matches){ passwordRetryCache.remove(userName); } return matches; } }
spring-shiro.xml内加入配置
<!-- 数据库保存的密码是使用MD5算法加密的,所以这里需要配置一个密码匹配对象 --> <!--<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.Md5CredentialsMatcher"></bean>--> <bean id="credentialsMatcher" class="com.ken.shiro.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"></constructor-arg> <property name="hashAlgorithmName" value="md5"></property><!--加密算法为md5--> <property name="hashIterations" value="3"></property><!--3次md5迭代--> <!--是否存储散列后的密码为16进制,需要和生成密码时的一样,默认是base64--> <property name="storedCredentialsHexEncoded" value="true"></property> </bean>
<!--缓存管理-->
<!--<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.MemoryConstrainedCacheManager"></bean>-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager" >
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
</bean>
ehcache.xml的配置如下:
<?xml version="1.0" encoding="UTF-8"?> <ehcache name="shirocache"> <diskStore path="java.io.tmpdir" /> <!-- name:缓存名称。 maxElementsInMemory:缓存最大个数。 eternal:对象是否永久有效,一但设置了,timeout将不起作用。 timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。 overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。 diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。 maxElementsOnDisk:硬盘最大缓存个数。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false. diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。 clearOnFlush:内存数量最大时是否清除。 --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU" /> <!-- 登录记录缓存 锁定10分钟 --> <cache name="passwordRetryCache" eternal="false" maxElementsInMemory="0" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authorizationCache" eternal="false" maxElementsInMemory="0" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authenticationCache" eternal="false" maxElementsInMemory="0" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro-activeSessionCache" eternal="false" maxElementsInMemory="0" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> </ehcache>
实现Realm类MyShiro继承自AuthorizingRealm,AuthorizingRealm实现它的抽象方法doGetAuthorizationInfo权限角色进行配置,AuthorizingRealm又继承自AuthenticatingRealm,AuthenticatingRealm也有一个抽象方法doGetAuthenticationInfo,实现doGetAuthenticationInfo方法对登录的令牌等信息进行验证。
myShiro的职则是对登录进行授权,对角色、权限进行验证等。
package com.ken.service.impl; import com.ken.entity.TRole; import com.ken.entity.TUser; import com.ken.service.IUserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class MyShiro extends AuthorizingRealm { @Autowired IUserService userService; @Autowired //注入父类的属性,注入加密算法匹配密码时使用 public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher){ super.setCredentialsMatcher(credentialsMatcher); } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("######################"); System.out.println("realm==="+this.getName()); System.out.println("######################"); //获取登录时输入的用户名 String loginName = (String)principalCollection.fromRealm(getName()).iterator().next(); //获取当前的用户名,跟上面的一样 String currentUsername = (String)super.getAvailablePrincipal(principalCollection); System.out.println(currentUsername); TUser user = userService.findUserByName(loginName); if (user != null) { //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //用户的角色集合 simpleAuthorizationInfo.setRoles(user.getRolesName()); //对应角色的权限 List<TRole> roles = user.getRoles(); for (TRole role:roles){ simpleAuthorizationInfo.addStringPermissions(role.getPermissionName()); } return simpleAuthorizationInfo; } return null; } //如果验证成功,将返回AuthenticationInfo验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的AuthenticationException实现。 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken; System.out.println("AuthenticationInfo####################"); System.out.println(((UsernamePasswordToken) authenticationToken).getUsername()); String pwd = new String(((UsernamePasswordToken) authenticationToken).getPassword()); System.out.println("getPassword="+pwd); System.out.println(authenticationToken.getPrincipal().toString()); String password = new String((char[])authenticationToken.getCredentials()); //得到密码 System.out.println(password); this.setSession("currentUser",authenticationToken.getPrincipal().toString()); TUser user = userService.findUserByName(token.getUsername()); if (user != null) { System.out.println("user salt is "+user.getCredentialsSalt()); //这里获取到数据库的用户名密码,然后验证用户名密码,如果不对则执出异常 return new SimpleAuthenticationInfo(user.getUserName(),user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()) //获取盐 ,getName()); } return null; } /** * 将一些数据放到ShiroSession中,以便于其它地方使用 * 比如Controller,使用时直接用HttpSession.getAttribute(key)就可以取到 */ private void setSession(Object key,Object value){ Subject subject = SecurityUtils.getSubject(); if (null != subject) { Session session = subject.getSession(); if (null != session) { session.setAttribute(key,value); } } } }
完整的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:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置权限管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- ref对应我们写的realm MyShiro --> <property name="realm" ref="myShiro"></property> <property name="cacheManager" ref="cacheManager"></property> </bean> <!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- 调用我们配置的权限管理器 --> <property name="securityManager" ref="securityManager"></property> <property name="loginUrl" value="/login"></property> <!-- 配置我们在登录页登录成功后的跳转地址,如果你访问的是非/login地址,则跳到您访问的地址 --> <property name="successUrl" value="/user"></property> <!-- 如果您请求的资源不再您的权限范围,则跳转到/403请求地址 --> <property name="unauthorizedUrl" value="/403"></property> <!-- 权限配置 --> <property name="filterChainDefinitions"> <value> <!-- anon表示此地址不需要任何权限即可访问 refer to:http://blog.csdn.net/jadyer/article/details/12172839 --> /static/**=anon /verifyImage=anon <!-- perms[user:query]表示访问此连接需要权限为user:query的用户 --> /user=perms[query] <!-- roles[manager]表示访问此连接需要用户的角色为manager --> /user/add=roles[manager] /user/del/**=roles[admin] /user/edit/**=roles[manager] <!--所有的请求(除去配置的静态资源请求或请求地址为anon的请求)都要通过登录验证,如果未登录则跳到/login--> /** = authc </value> </property> </bean> <!-- 数据库保存的密码是使用MD5算法加密的,所以这里需要配置一个密码匹配对象 --> <!--<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.Md5CredentialsMatcher"></bean>--> <bean id="credentialsMatcher" class="com.ken.shiro.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"></constructor-arg> <property name="hashAlgorithmName" value="md5"></property><!--加密算法为md5--> <property name="hashIterations" value="3"></property><!--3次md5迭代--> <!--是否存储散列后的密码为16进制,需要和生成密码时的一样,默认是base64--> <property name="storedCredentialsHexEncoded" value="true"></property> </bean> <!-- 配置 Bean 后置处理器: 会自动的调用和 Spring 整合后各个组件的生命周期方法. --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean> <!--缓存管理--> <!--<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.MemoryConstrainedCacheManager"></bean>--> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager" > <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property> </bean> </beans>