项目中经常使用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;
@Component
public 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;
@Component
public 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基础才能理解,见谅。