apache shiro集成redis缓存

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

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot是一个基于Spring框架的快速开发框架,通过提供一系列的开箱即用的功能和优化配置,简化了Java后台应用的开发流程。 Thymeleaf是一个Java模板引擎,用于在服务端渲染HTML页面。它可以和Spring Boot结合使用,通过在HTML页面中使用Thymeleaf的语法,实现页面的动态渲染和数据绑定。 Layui是一个国内比较流行的前端UI框架,提供了大量的CSS样式和JavaScript组件,可以快速构建美观、响应式的前端界面。 Apache Shiro是一个强大的开源安全框架,可以用于认证、授权和加密操作。它提供了对用户身份验证、角色和权限管理的支持,可以帮助开发者快速实现应用的安全控制。 Redis是一个高性能的内存数据库,常用于缓存和存储数据。它支持多种数据结构和操作,可以用于实现分布式锁、消息队列等功能,提高系统的性能和可扩展性。 MyBatis Plus是一个基于MyBatis框架的增强工具,提供了更简单、更便捷的数据库操作方式。它通过代码生成器和一系列的增强功能,简化了数据层的开发工作,提高了开发效率。 综上所述,可以使用Spring Boot作为后台框架,集成Thymeleaf实现页面渲染和数据绑定,使用Layui构建前端界面,使用Apache Shiro进行安全控制,使用Redis进行数据缓存和存储,使用MyBatis Plus进行数据库操作。这样搭建的后台系统可以实现高效、安全、可扩展的功能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值