項目中經常使用shiro做許可權認證與授權功能,當使用者認證成功後,第一次訪問受限的資源時,shiro會去載入使用者能訪問的所有許可權標識。預設情況下,shiro並未緩衝這些許可權標識。當再次訪問受限的資源時,還會去載入使用者能訪問的許可權標識。
當請求多時,這樣處理顯然不適合生產環境,因此需要為shiro加緩衝。shiro本身內建有緩衝功能,需要配置啟用它。shiro為我們提供了兩個緩衝實現,一個是基於本地記憶體(org.apache.shiro.cache.MemoryConstrainedCacheManager),另一個是基於EhCache(org.apache.shiro.cache.ehcache.EhCacheManager)。這兩套實現都只適合單機玩,當在分布式環境下效果就不理想了。於是經過研究,研發了一套基於redis的shiro緩衝實現。
以下不介紹spring與shiro的整合,此類文章網上應有盡有,不是本文關注的重點,因此需要讀者事先已將spring與shiro整合,再進行本文的實踐。同時也不介紹spring與jedis的整合,請配置好spring與jedis架構,並配置好RedisTemplate這個bean。
首先編寫如下兩個類,將這兩個類配置到shiro的設定檔中。
由於是直接從公司的項目中拷貝源碼,所以對包名做了替換,讀者應根據實際情況調整刪減。
//認證與授權類package com.xxx.common.shiro;import java.util.List;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.SimpleAuthenticationInfo;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.cache.Cache;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import com.xxx.common.exception.ReadMessageException;import com.xxx.api.AuthorityApi;import com.xxx.api.RoleApi;import com.xxx.api.UserApi;import com.xxx.domain.Authority;import com.xxx.domain.Role;import com.xxx.domain.User;@Componentpublic class BasicAuthorizingRealm extends AuthorizingRealm { private static final Logger logger=LogManager.getLogger(BasicAuthorizingRealm.class); @Autowired private UserApi userApi; @Autowired private RoleApi roleApi; @Autowired private AuthorityApi authorityApi; private static final String AUTHORIZATION_CACHE_NAME="authorization"; public BasicAuthorizingRealm() { super.setAuthorizationCacheName(AUTHORIZATION_CACHE_NAME); } /*** * 擷取認證資訊 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken at) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) at; try { User user = userApi.getByUsername(token.getUsername()); if(user==null) { throw new AuthenticationException("使用者名稱或密碼錯誤"); } String pwd=new String(token.getPassword()); if(!userApi.checkPassword(user.getId(), pwd)) { throw new AuthenticationException("使用者名稱或密碼錯誤"); } if(user.getStatus()==User.STATUS_DISABLED) { throw new AuthenticationException("使用者已被禁用"); } clearAuthorizationInfoCache(user);//使用者登入後,清除使用者緩衝,以便重新載入使用者權限 return new SimpleAuthenticationInfo(user, token.getPassword(), getName()); } catch (ReadMessageException e) { logger.error(e); throw new AuthenticationException(e); } catch (AuthenticationException e) { logger.error(e); throw e; } } /*** * 擷取授權資訊 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) { User user =(User)pc.fromRealm(getName()).iterator().next(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); try { List<Role> roles=roleApi.queryRoles(user.getId()); for(Role role:roles) { info.addRole(role.getCode()); List<Authority> auths=authorityApi.queryAuths(role.getId()); for(Authority auth:auths) { if(auth.getPermission()==null) break; String[] perms=auth.getPermission().split(","); //支援以逗號隔開的許可權標識 for(String perm:perms) { info.addStringPermission(perm); } } } } catch (ReadMessageException e) { logger.error(e); } return info; } /** * 清除所有使用者的緩衝 */ public void clearAuthorizationInfoCache() { Cache<Object, AuthorizationInfo> cache = getAuthorizationCache(); if(cache!=null) { cache.clear(); } } /** * 清除指定使用者的緩衝 * @param user */ private void clearAuthorizationInfoCache(User user) { Cache<Object, AuthorizationInfo> cache = getAuthorizationCache(); cache.remove(user.getId()); }}
//redis緩衝實作類別package com.xxx.common.shiro;import java.util.Collection;import java.util.Set;import org.apache.shiro.cache.Cache;import org.apache.shiro.cache.CacheException;import org.apache.shiro.cache.CacheManager;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.BoundHashOperations;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import com.xxx.domain.User;@Componentpublic class RedisCacheManager implements CacheManager { private String cacheKeyPrefix = "shiro:"; @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { return new ShiroRedisCache<K,V>(cacheKeyPrefix+name); } /** * 為shiro量身定做的一個redis cache,為Authorization cache做了特別最佳化 */ public class ShiroRedisCache<K, V> implements Cache<K, V> { private String cacheKey; public ShiroRedisCache(String cacheKey) { this.cacheKey=cacheKey; } @Override public V get(K key) throws CacheException { BoundHashOperations<String,K,V> hash = redisTemplate.boundHashOps(cacheKey); Object k=hashKey(key); return hash.get(k); } @Override public V put(K key, V value) throws CacheException { BoundHashOperations<String,K,V> hash = redisTemplate.boundHashOps(cacheKey); Object k=hashKey(key); hash.put((K)k, value); return value; } @Override public V remove(K key) throws CacheException { BoundHashOperations<String,K,V> hash = redisTemplate.boundHashOps(cacheKey); Object k=hashKey(key); V value=hash.get(k); hash.delete(k); return value; } @Override public void clear() throws CacheException { redisTemplate.delete(cacheKey); } @Override public int size() { BoundHashOperations<String,K,V> hash = redisTemplate.boundHashOps(cacheKey); return hash.size().intValue(); } @Override public Set<K> keys() { BoundHashOperations<String,K,V> hash = redisTemplate.boundHashOps(cacheKey); return hash.keys(); } @Override public Collection<V> values() { BoundHashOperations<String,K,V> hash = redisTemplate.boundHashOps(cacheKey); return hash.values(); } protected Object hashKey(K key) { if(key instanceof PrincipalCollection) {//此處很重要,如果key是登入憑證,那麼這是訪問使用者的授權緩衝;將登入憑證轉為user對象,返回user的id屬性做為hash key,否則會以user對象做為hash key,這樣就不好清除指定使用者的緩衝了 PrincipalCollection pc=(PrincipalCollection) key; User user =(User)pc.getPrimaryPrincipal(); return user.getId(); } return key; } }}
將以上兩個類對象配置到shiro設定檔中
<!-- 定義Shiro安全管理配置 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="basicAuthorizingRealm" /> <property name="cacheManager" ref="redisCacheManager" /> </bean>
至此,整合完畢,以下是我的登入介面與登出介面的Controller代碼,以供參考。
package com.xxx.controller.sys;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.subject.Subject;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import com.alibaba.fastjson.JSONObject;import com.xxx.common.JsonMessage;@RestController@RequestMapping("/api/sys")public class LoginController extends BaseController { /** * 登入使用者 * @param json * @return */ @RequestMapping(value = "/login", method = RequestMethod.POST) public JsonMessage login(@RequestBody JSONObject json) { String username = json.getString("username"); String password = json.getString("password"); Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { return respOk(); } UsernamePasswordToken token = new UsernamePasswordToken(username, password); // token.setRememberMe(true); subject.login(token); return respOk(); } /** * 登出使用者 * @return */ @RequestMapping(value = "/logout",method={RequestMethod.POST}) public JsonMessage logout() { Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { subject.logout(); } return respOk(); }}
代碼比較多,需要有shiro,redis,spring mvc基礎才能理解,見諒。