Spring boot中Shiro和Redis集成后,Spring的@cacheble注解无效?

Spring boot中Shiro和Redis集成后,Spring的@cacheble注解无效。

情况1:Spring boot和Redis集成,@cacheble可用,会把缓存数据写入Redis。
在情况1的前提下:
情况2:Spring boot和Shiro集成,然后用Spring cache抽象出cachemanager,作为Shiro的缓存。控制台未报错,Shiro的认证信息会存入Redis,但出现@cacheble注解无效,即@Cacheble注解的方法的返回值未存入Redis。
在情况2的前提下:
情况3:将Shiro的@Configuration注解去掉,即不用Shiro,@Cacheble可用。

Redis配置清单:

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;

import javax.annotation.Resource;
import java.lang.reflect.Method;
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {

    /**
     * 通过Spring进行注入,参数在application.properties进行配置{RedisProperties};
     */
    @Resource
    private JedisConnectionFactory factory;

    @Bean
    @Override
    public CacheManager cacheManager() {
        System.out.println("RedisCacheConfig.cacheManager : 实例化");
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
        cacheManager.setDefaultExpiration(60 * 30); // 30min
        return cacheManager;
    }

    @Bean
    public RedisTemplate<Object, Object> redisTemplate() {
        System.out.println("RedisCacheConfig.redisTemplate : 实例化");
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
        redisTemplate.setConnectionFactory(factory);

        redisTemplate.setKeySerializer(new GenericJackson2JsonRedisSerializer());
        // redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        return redisTemplate;
    }

    /**
     * 自定义key.
     * 此方法将会根据完全限定类名+方法名+所有参数的值生成唯一的一个key,即使@Cacheable中的value属性一样,key也会不一样。
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        System.out.println("RedisCacheConfig.keyGenerator : 实例化");
        return new KeyGenerator() {
            @Override
            public Object generate(Object o, Method method, Object... objects) {
                // This will generate a unique key of the class name, the method name
                // and all method parameters appended.
                StringBuilder sb = new StringBuilder();
                sb.append(o.getClass().getName());
                sb.append(method.getName());
                for (Object obj : objects) {
                    sb.append(obj.toString());
                }
                System.out.println("RedisCacheConfig : keyGenerator <== " + sb.toString());
                return sb.toString();
            }
        };
    }

    @Bean
    @Override
    public CacheResolver cacheResolver() {
        return new SimpleCacheResolver(cacheManager());
    }

    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        // 用于捕获从Cache中进行CRUD时的异常的回调处理器。
        return new SimpleCacheErrorHandler();
    }

缓存配置:

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.Set;
@Component
public class SpringCacheManagerWrapper implements CacheManager {

    public static final Logger log = LoggerFactory.getLogger(SpringCacheManagerWrapper.class);

    @Resource
    private org.springframework.cache.CacheManager springCacheManger;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        log.debug("getCache: springCacheManger <== " + springCacheManger);
        org.springframework.cache.Cache springCache = springCacheManger.getCache(name);
        return new SpringCacheWrapper<K, V>(springCache);
    }

    private class SpringCacheWrapper<K, V> implements Cache<K, V> {
        private org.springframework.cache.Cache springCache;
        public SpringCacheWrapper(org.springframework.cache.Cache springCache) {
            this.springCache = springCache;
        }

        @Override
        public V get(K key) throws CacheException {
            Object value = springCache.get(key);
            if (value instanceof SimpleValueWrapper) {
                return (V) ((SimpleValueWrapper)value).get();
            }
            return (V) value;
        }

        @Override
        public V put(K key, V value) throws CacheException {
            springCache.put(key, value);
            return value;
        }

        @Override
        public V remove(K key) throws CacheException {
            Object value = get(key);
            springCache.evict(key);
            return (V) value;
        }

        @Override
        public void clear() throws CacheException {
            springCache.clear();
        }

        @Override
        public int size() {
            throw new UnsupportedOperationException("invoke spring cache abstract size method not supported");

        }

        @Override
        public Set<K> keys() {
            throw new UnsupportedOperationException("invoke spring cache abstract keys method not supported");

        }

        @Override
        public Collection<V> values() {
            throw new UnsupportedOperationException("invoke spring cache abstract values method not supported");
        }
    }
}

Shiro配置:

package me.dengfengdecao.demo.config.shiro;

import me.dengfengdecao.demo.component.cache.SpringCacheManagerWrapper;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.CachingSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfiguration {

    /*
    缓存管理器 使用Redis实现
     */
    @Resource
    private SpringCacheManagerWrapper springCacheManagerWrapper;

    @Bean(name="shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        System.out.println("ShiroConfiguration.shiroFilterFactoryBean : shiro实例化");

        // 拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");

        // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
        filterChainDefinitionMap.put("/**", "authc");
        filterChainDefinitionMap.put("admin/**", "authc, roles[admin]");
        filterChainDefinitionMap.put("docs/**", "authc, perms[document:read]");

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    /**
     * shiro核心安全管理类
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        CachingSecurityManager securityManager = new DefaultWebSecurityManager(myShiroRealm());
        System.out.println("ShiroConfiguration.securityManager : 实例化");

        // 将myShiroRealm注入到securityManager中
        // securityManager.setRealm(myShiroRealm());
        // 将缓存对象注入到SecurityManager中
        // securityManager.setCacheManager(shiroRedisCacheManager);
        securityManager.setCacheManager(springCacheManagerWrapper);
        return securityManager;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        // 注入凭证匹配器
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());

        System.out.println("ShiroConfiguration.myShiroRealm : 实例化");
        return myShiroRealm;
    }

    /**
     * 凭证匹配器
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

        System.out.println("ShiroConfiguration.hashedCredentialsMatcher : 实例化");
        // 加密算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列的次数,比如散列两次,相当于 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);

        return hashedCredentialsMatcher;
    }

    /**
     * 开启shiro aop注解支持.使用代理方式;所以需要开启代码支持;<br/>
     * 拦截@ RequiresPermissions注释的方法
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();

        System.out.println("ShiroConfiguration.authorizaionAttributeSourceAdvisor : 实例化");

        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);

        return authorizationAttributeSourceAdvisor;
    }
}

Realm


import me.dengfengdecao.demo.domain.Permission;
import me.dengfengdecao.demo.domain.Role;
import me.dengfengdecao.demo.domain.User;
import me.dengfengdecao.demo.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.util.Iterator;
import java.util.Set;

/**
 * 身份校验核心类<br/>
 *
 * Created by dengfengdecao on 16/10/1.
 */
public class MyShiroRealm extends AuthorizingRealm {

    public static final Logger log = LoggerFactory.getLogger(MyShiroRealm.class);

    @Resource
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("######## 执行Shiro权限认证 ########");

        String username = (String) principalCollection.getPrimaryPrincipal();

        log.info("doGetAuthorizationInfo: username <== " + username);

        // 获取当前用户信息
        User user = userService.findByUsername(username);

        log.debug("doGetAuthorizationInfo: user <== " + user);
        if (user != null) {
            // 权限信息对象authorizationInfo,用来存放查出的用户的所有的角色(role)及权限(permission)
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

            // 当前用户的角色集合
            Iterator<Role> iterator = user.getRoles().iterator();
            while (iterator.hasNext()) {
                authorizationInfo.addRole(iterator.next().getRole());
            }

            // 当前用户的角色对应的所有权限
            Set<Role> roles = user.getRoles();
            for (Role role : roles) {
                Iterator<Permission> iterator1 = role.getPermissions().iterator();
                while (iterator1.hasNext())
                    authorizationInfo.addStringPermission(iterator1.next().getPermission());
            }

            return authorizationInfo;
        }

        // 返回null的话,就会导致任何用户访问被拦截的请求时,都会自动跳转到unauthorizedUrl指定的地址
        return null;
    }

    /**
     * 用来验证用户身份
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("######## 执行Shiro身份认证 ########");

        // 获取用户登录的用户名
        String username = (String) authenticationToken.getPrincipal();

        log.debug("doGetAuthenticationInfo: 当前用户username <== " + username);

        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User user = userService.findByUsername(username);
        log.debug("doGetAuthenticationInfo: 从数据库查出的用户user <== " + user);
        if (user == null) {
            return null;
        } else {
            //UsernamePasswordToken对象用来存放提交的登录信息
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

            log.info("doGetAuthenticationInfo: 验证当前Subject时获取到token为:" + token);

            // 加密方式
            // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(),
                user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName());

            return authenticationInfo;
        }
    }
}

UserServiceImpl

@Service
@Transactional
@CacheConfig(cacheNames = {"user"})
public class UserServiceImpl implements UserService {

    public static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);

    @Resource
    private UserDao userDao;

    @Cacheable
    @Override
    public User findUserById(Long id) {
        return userDao.findOne(id);
    }

    @Cacheable
    @Override
    public User findByUsername(String username) {
        return userDao.findByUsername(username);
    }

    @CacheEvict
    @Override
    public void delFromCache(Long id) {
        log.info("delFromCache: 移除缓存");
    }

    @Override
    public void save(User user) {
        userDao.save(user);
    }

    @Cacheable
    @Override
    public List<User> getUsers() {
        return (List<User>) userDao.findAll();
    }

UserController

@Controller
public class UserController {

    public static final Logger log = LoggerFactory.getLogger(UserController.class);

    @Resource
    private UserService userService;

    @RequestMapping(value = "user/{userId}", method = RequestMethod.GET)
    @ResponseBody
    public Object findUserById(Model model, @PathVariable("userId")Long userId) {
        log.info("findUserById: userId : " + userId);

        User user = userService.findUserById(userId);

        return user.toString();
    }

    @RequestMapping(value = "user", method = RequestMethod.GET)
    @ResponseBody
    public String findUserByUsername(@RequestParam("username")String username){
        log.info("findUserByUsername: 执行");
        User user = userService.findByUsername(username);

        return user.toString();
    }

    @RequestMapping(value = "user2", method = RequestMethod.GET)
    @ResponseBody
    public String findUserByUsername2(@RequestParam("username")String username){
        log.info("findUserByUsername2: 执行");
        User user = userService.findByUsername(username);

        return user.toString();
    }


    @RequestMapping(value = "user/delete")
    @ResponseBody
    public String delete(Long id) {
        log.info("delete: 执行");
        userService.delFromCache(id);

        return "success";
    }

  
    /**
     * 用户查询<br/>
     * @ RequiresPermissions 进行权限管理
     * @return
     */
    @RequestMapping("user-list")
    @RequiresPermissions("user:list")
    public String userList(ModelMap map){
        List<User> users = userService.getUsers();
        log.info("userList: users <== " + users);
        map.put("users", users);

        return "user/user-list";
    }


    /**
     * 用户添加
     * @return
    */
    @RequestMapping("user-add")
    @RequiresPermissions("user:add")
    public String userAdd(){
        log.info("userAdd: ");
        return "user/user-add";
    }

    /**
     * 用户删除
     * @return
     */
    @RequestMapping("user-del")
    @RequiresPermissions("user:del")
    public String userDelete(){
        log.info("userDelete: ");
        return "user/user-del";
    }

}

application.properties

########################################################
### HTTP encoding (HttpEncodingProperties)
########################################################
# Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly.
spring.http.encoding.charset=UTF-8
# Enable http encoding support.
spring.http.encoding.enabled=true
# Force the encoding to the configured charset on HTTP requests and responses.
spring.http.encoding.force=true
# Force the encoding to the configured charset on HTTP requests. Defaults to true when "force" has not been specified.
spring.http.encoding.force-request=true
# Force the encoding to the configured charset on HTTP responses.
spring.http.encoding.force-response=true


########################################################
### spring
########################################################
spring.mvc.view.prefix=/WEB-INF/content/
spring.mvc.view.suffix=.jsp
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

########################################################
### REDIS (RedisProperties)
########################################################
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.pool.max-active=8
spring.redis.pool.max-idle=8
spring.redis.pool.max-wait=-1
spring.redis.pool.min-idle=0
spring.redis.port=6379

########################################################
### Java Persistence Api
########################################################
# Specify the DBMS
spring.jpa.database=MYSQL
# Show or not log for each sql query
spring.jpa.show-sql=true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto=update
# Naming strategy
# spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the entity manager
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.properties.hibernate.format_sql=true
# Register OpenEntityManagerInViewInterceptor.
# Binds a JPA EntityManager to the thread for the entire processing of the request.
spring.jpa.open-in-view=true

请大家帮帮分析原因。

代码地址

阅读 15.9k
2 个回答

多个Advisor加载导致的问题,在自定义Realm中注入的Service声明中加入@Lazy注解即可解决问题。
stackoverflow有答案
http://stackoverflow.com/ques...

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题