用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>
