ShiroConfig联合RedisCacheManager实现shiro频繁访问Redis

WEB采用Shiro联合Redis的示意图

Redis实现shiro缓存,达到分布式共享session和授权信息,把session和授权持久化到redis数据库或者缓存shiro集群为了防止多次插查询数据库,解决web在授权的时候每次都去查询数据库,对于频繁访问的接口,性能和响应速度比较慢的问题

客户登录,发起请求,调用Shiro与redis的过程

所用到的依赖

<!-- shiro-redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
        </dependency>

        <!-- shiro-thymeleaf -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

友情链接:

ShiroConfig.java

package com.AAAAAAAAAAAA.common.shiro;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.AAAAAAAAAAAA.common.config.FiresProperties;
import com.AAAAAAAAAAAA.common.listener.ShiroSessionListener;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro 配置类
 *
 * @author 阿啄debugIT
 */
@Configuration
public class ShiroConfig {
	
	
    /**
     * 缓存和session的管理
     */
    @Autowired
    private FiresProperties firesProperties;

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.timeout}")
    private int timeout;
	
	 /**
     * 用于开启 Thymeleaf 中的 shiro 标签的使用
     * @return ShiroDialect shiro 方言对象
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    /**
     * shiro 中配置 redis 缓存
     * @return RedisManager
     */
    private RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        // 缓存时间,单位为秒
        //redisManager.setExpire(firesProperties.getShiro().getExpireIn()); // removed from shiro-redis v3.1.0 api
        redisManager.setHost(host);
        redisManager.setPort(port);
        if (StringUtils.isNotBlank(password))
            redisManager.setPassword(password);
        redisManager.setTimeout(timeout);
        return redisManager;
    }

    private RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
     /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
     * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager Filter Chain定义说明 
     * 1、一个URL可以配置多个Filter,使用逗号分隔
     * 2、当设置多个过滤器时,全部验证通过,才视为通过 
     * 3、部分过滤器可指定参数,如perms,roles
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();        
        
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();//获取filters
		filters.put("user", new CustomUserFilter());
		
        //必须设置 securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登录的 url
	//需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
        shiroFilterFactoryBean.setLoginUrl(firesProperties.getShiro().getLoginUrl());
        // 登录成功后跳转的 url  
		//登录成功,跳转url,如果前后端分离,则没这个调用
        shiroFilterFactoryBean.setSuccessUrl(firesProperties.getShiro().getSuccessUrl());
        // 未授权 url
		//没有权限,未授权就会调用此方法, 先验证登录-》再验证是否有权限
        shiroFilterFactoryBean.setUnauthorizedUrl(firesProperties.getShiro().getUnauthorizedUrl());
		//拦截器路径,坑一,部分路径无法进行拦截,时有时无;因为同学使用的是hashmap, 无序的,应该改为LinkedHashMap
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 设置免认证 url
        String[] anonUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(firesProperties.getShiro().getAnonUrl(), ",");
        for (String url : anonUrls) {
			//匿名可以访问,也是就游客模式
          //filterChainDefinitionMap.put("/pub/**","anon");
            filterChainDefinitionMap.put(url, "anon");
        }
        // 配置退出过滤器,其中具体的退出代码 Shiro已经替我们实现了
        filterChainDefinitionMap.put(firesProperties.getShiro().getLogoutUrl(), "logout");
        // 除上以外所有 url都必须认证通过才可以访问,未通过认证自动访问 LoginUrl
        filterChainDefinitionMap.put("/**", "user");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
       
        // 配置 rememberMeCookie
        securityManager.setRememberMeManager(rememberMeManager());
        // 配置 缓存管理类 cacheManager
		//自定义缓存实现 使用redis ,生产环境才需要这么设置,开发环境需要清空全选,所以不建议开启这个
        securityManager.setCacheManager(cacheManager());
        securityManager.setSessionManager(sessionManager());
		// 配置 SecurityManager,并注入 shiroRealm
		//设置realm(推荐放到最后,不然某些情况会不生效)
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    @Bean
    public ShiroRealm shiroRealm() {
        // 配置 Realm,需自己实现,见 com.AAAAAAAAAAAA.common.shiro.ShiroRealm
        return new ShiroRealm();
    }

    /**
     * rememberMe cookie 效果是重开浏览器后无需重新登录
     *
     * @return SimpleCookie
     */
    private SimpleCookie rememberMeCookie() {
        // 设置 cookie 名称,对应 login.html 页面的 <input type="checkbox" name="rememberMe"/>
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        // 设置 cookie 的过期时间,单位为秒,这里为一天
        cookie.setMaxAge(firesProperties.getShiro().getCookieTimeout());
        return cookie;
    }

    /**
     * cookie管理对象
     *
     * @return CookieRememberMeManager
     */
    private CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        // rememberMe cookie 加密的密钥
        cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }
   /**
     *@description:
     * 作用:加入注解的使用,不加入这个AOP注解不生效(shiro的注解 例如 @RequiresGuest)
     * 使shiro的注解生效
     *@params:  []
     *@return:  org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
     **/
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

   
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * session 管理对象
     *
     * @return DefaultWebSessionManager
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        Collection<SessionListener> listeners = new ArrayList<>();
        listeners.add(new ShiroSessionListener());
        // 设置session超时时间,单位为毫秒
        sessionManager.setGlobalSessionTimeout(firesProperties.getShiro().getSessionTimeout());
        sessionManager.setSessionListeners(listeners);
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }
	  /**
     * 注册DelegatingFilterProxy(Shiro)
     * 集成Shiro有2种方法:
     * 1. 按这个方法自己组装一个FilterRegistrationBean(这种方法更为灵活,可以自己定义UrlPattern,
     * 在项目使用中你可能会因为一些很但疼的问题最后采用它, 想使用它你可能需要看官网或者已经很了解Shiro的处理原理了)
     * 2. 直接使用ShiroFilterFactoryBean(这种方法比较简单,其内部对ShiroFilter做了组装工作,无法自己定义UrlPattern,* 默认拦截 /*)
     *
     * @param dispatcherServlet
     * @return
     * @create  2016年1月13日
     */
//  @Bean
//  public FilterRegistrationBean filterRegistrationBean() {
//      FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
//      filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
//      //  该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理  
//      filterRegistration.addInitParameter("targetFilterLifecycle", "true");
//      filterRegistration.setEnabled(true);
//      filterRegistration.addUrlPatterns("/*");// 可以自己灵活的定义很多,避免一些根本不需要被Shiro处理的请求被包含进来
//      return filterRegistration;
//  }
}

ShiroRealm .Java

package com.AAAAAAAAAAAA.common.shiro;

import com.AAAAAAAAAAAA.common.util.Constant;
import com.AAAAAAAAAAAA.system.domain.Menu;
import com.AAAAAAAAAAAA.system.domain.Role;
import com.AAAAAAAAAAAA.system.domain.User;
import com.AAAAAAAAAAAA.system.service.MenuService;
import com.AAAAAAAAAAAA.system.service.RoleService;
import com.AAAAAAAAAAAA.system.service.UserService;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import org.apache.shiro.SecurityUtils;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 自定义实现 ShiroRealm,包含认证和授权两大模块
 * @description: 在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的
 * 在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO
 * @author 阿啄debugIT
 */
@Component("shiroRealm")
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private MenuService menuService;

    private String key;
    Integer count = 0;

    /**
     * 用户认证
     *
     * @param token AuthenticationToken 身份认证 token
     * @return AuthenticationInfo 身份认证信息
     * @throws AuthenticationException 认证相关异常
     */
    @Resource
    RedisTemplate<String, Integer> redisTemplate;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        // 获取用户输入的用户名和密码
        String userName = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());
        int MaxCount = 5;
        // 通过用户名到数据库查询用户信息
        User user = this.userService.findByName(userName);
        if (user == null) {
            setMaxLoginCount(userName, MaxCount);
//            throw new UnknownAccountException("账号或密码错误,或账号异常 请联系管理员");
            throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
        }
        // 最大有有效时间不为空的时候
        if (user.getMaxTime() != null && !"".equals(user.getMaxTime())) {
            // 数据库获取到的是date类型
//            String dateStr = "2019-10-19";
            Date dataDate = DateUtil.parse(DateUtil.formatDate(user.getMaxTime()));
            // 当前时间也是date类型
            //当前日期字符串,格式:yyyy-MM-dd
            String today = DateUtil.today();
            Date todayDate = DateUtil.parse(today);
            //dataDate - todayDate >=0 表示可用
            long betweenDay = DateUtil.between(todayDate, dataDate, DateUnit.DAY, false);
            if (betweenDay < 0) {
                user.setStatus(User.STATUS_LOCK);
                userService.updateNotNull(user);
            }
        }
        // 判断用户最后一次修改密码时间
        updatePwdTime(user);
        if (!password.equals(user.getPassword())) {
            setMaxLoginCount(userName, MaxCount);
//            throw new IncorrectCredentialsException("用户名或密码错误!");
            throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
        }
        if (User.STATUS_LOCK.equals(user.getStatus())) {
//            throw new LockedAccountException("账号已被锁定,请联系管理员!");
            throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
        }
        setMaxLoginCount(userName, MaxCount);
        //删除key
        redisTemplate.delete(key);
        user.setUsername(userName);
        return new SimpleAuthenticationInfo(user, password, getName());
    }
	
	/**
     * 授权模块,获取用户角色和权限
     *
     * @param principal principal
     * @return AuthorizationInfo 权限信息
     */
	 /**
     * 授权用户权限
     * 授权的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>标签的时候调用的
     * 它会去检测shiro框架中的权限(这里的permissions)是否包含有该标签的name值,如果有,里面的内容显示
     * 如果没有,里面的内容不予显示(这就完成了对于权限的认证.)
     *
     * shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
     * 当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
     * 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。
     *
     * 在这个方法中主要是使用类:SimpleAuthorizationInfo 进行角色的添加和权限的添加。
     * authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission());
     *
     * 当然也可以添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限
     * authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions);
     *
     * 就是说如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[权限添加]");
     * 就说明访问/add这个链接必须要有“权限添加”这个权限才可以访问
     *
     * 如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]");
     * 就说明访问/add这个链接必须要有 "权限添加" 这个权限和具有 "100002" 这个角色才可以访问
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        String userName = user.getUsername();

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 获取用户角色集
        List<Role> roleList = this.roleService.findUserRole(userName);
        Set<String> roleSet = roleList.stream().map(Role::getRoleName).collect(Collectors.toSet());
        simpleAuthorizationInfo.setRoles(roleSet);

        // 获取用户权限集
        List<Menu> permissionList = this.menuService.findUserPermissions(userName);
        Set<String> permissionSet = permissionList.stream().map(Menu::getPerms).collect(Collectors.toSet());
        simpleAuthorizationInfo.setStringPermissions(permissionSet);
        return simpleAuthorizationInfo;
    }

    private void updatePwdTime(User user) {
        Date updatePwdTime;
        // 判断最后一次修改密码时间
        if (user.getLastUpdatePwdTime() == null) {
            return;
//            updatePwdTime = user.getCrateTime();
        } else {
            updatePwdTime = user.getLastUpdatePwdTime();
        }
        Date todayDate = DateUtil.date();
        long betweenDay = DateUtil.between(updatePwdTime, todayDate, DateUnit.DAY, false);
        // 超过90天
        if (betweenDay > 90) {
            System.out.println("密码时间超过90天");
            throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
        }
    }

    private void setMaxLoginCount(String userName, int maxCount) {
        //当前时间
        Date CurrentDate = DateUtil.date();
        //明天时间
        Date date = DateUtil.tomorrow();
        String formatDate = DateUtil.formatDate(date) + " 00:00:00";
        date = DateUtil.parse(formatDate);
        // key以日期为单位
        key = "user:" + userName + ":" + DateUtil.formatDate(date);
        count = redisTemplate.opsForValue().get(key);
        // 设置间隔多少秒
        long second = DateUtil.between(CurrentDate, date, DateUnit.SECOND);
//        long second = 30;
        if (count == null) {
            redisTemplate.opsForValue().set(key, 1, second, TimeUnit.SECONDS);
        } else {
            count++;
            redisTemplate.opsForValue().set(key, count, second, TimeUnit.SECONDS);
            System.out.println("redis查询的次数: " + count);
            if (count >= maxCount) {
//                throw new UnknownAccountException("今天输入用户名密码错误超过" + maxCount + "次,请明天在来试");
                throw new IncorrectCredentialsException(Constant.LOGIN_ERROR);
            }

        }
    }

    /**
     * 清除权限缓存
     * 使用方法:在需要清除用户权限的地方注入 ShiroRealm,
     * 然后调用其clearCache方法。
     */
    public void clearCache() {
        PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }

    public static void main(String[] args) {
        Date CurrentDate = DateUtil.date();
        Date date = DateUtil.tomorrow();
        String formatDate = DateUtil.formatDate(date) + " 00:00:00";
        date = DateUtil.parse(formatDate);
        System.out.println(DateUtil.formatDateTime(date));
        // 设置间隔多少秒
        long second = DateUtil.between(CurrentDate, date, DateUnit.SECOND);
        System.out.println(second);
    }

}

ShiroConfig.java代码解析

RedisManager.java、RedisSessionDAO和RedisCacheManager采用用shiro-redis中的原始类

使用crazycake开源shiro-redis实现好的工具

  • RedisSessionDAO 可以继承EnterpriseCacheSessionDAO实现session控制
  • RedisCache 继承Cache类实现具体redis操作缓存(remove、get、set、keys
  • RedisCacheManager 实现接口CacheManager的getCache获得RedisCache交给securityManager管理

加载配置类

sessionManager管理session

登录时间cookie的安全管理

同时使用了ConcurrentMap管理数据和缓存,更加高效

private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();

ShiroRealm .Java代码解析

友情链接:https://www.qingtingip.com/h_335109.html

doGetAuthorizationInfo 授权

在调用一下代码的时候,doGetAuthorizationInfo 

Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);


doGetAuthenticationInfo 认证

在调用一下代码的时候,doGetAuthenticationInfo 

  • subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
  • @RequiresRoles("admin") :在方法上加注解的时候;
  • [@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。

credentialsMatcher 配置凭证匹配器,双md5加盐加密

详情实现请见gif

友情链接:https://www.cnblogs.com/chyu/p/5958720.html

发布了108 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/as4589sd/article/details/104215629