天天看点

springboot2.0---04、Shiro+redis(一主两从三哨兵)+ehcache+剔除

1.Maven

<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.3.2</version>
		</dependency>
		<dependency>
			<groupId>net.sf.ehcache</groupId>
			<artifactId>ehcache-core</artifactId>
			<version>2.4.8</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-ehcache</artifactId>
			<version>1.4.0</version>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.9.0</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>
           

2.代码

//(1)主配置包含shiro,集成redis,在线剔除

@Configuration
public class ShiroConfig{

    /**
     * 注册踢出用户过滤器
     * @param cacheManager
     * @param sessionManager
     * @return
     */
    @Bean(name = "kickoutFilter")
    @Scope("prototype")
    @DependsOn(value = {"cacheManager","sessionManager"})
    public KickoutFilter kickoutSessionControlFilter(CacheManager cacheManager, SessionManager sessionManager) {
        KickoutFilter kickoutSessionControlFilter = new KickoutFilter();
        //使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
        //这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
        //也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性
        kickoutSessionControlFilter.setCacheManager(cacheManager);
        //用于根据会话ID,获取会话进行踢出操作的;
        kickoutSessionControlFilter.setSessionManager(sessionManager);
        //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。
        kickoutSessionControlFilter.setKickoutAfter(false);
        //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录
        kickoutSessionControlFilter.setMaxSession(1);
        //被踢出后重定向到的地址;
        kickoutSessionControlFilter.setKickoutUrl("/");
        return kickoutSessionControlFilter;
    }

    /**
     * 注册sessionDao
     * @param redisUtil
     * @return
     */
    @Bean
    public SessionDao sessionDao(RedisUtil redisUtil) {
        SessionDao sessionDao = new SessionDao();
        sessionDao.setRedisUtil(redisUtil);
        return sessionDao;
    }

    /**
     * 注册redisUtil
     * @param redisTemplate
     * @return
     */
    @Bean
    public RedisUtil redisUtil(RedisTemplate<String,Object> redisTemplate) {
        RedisUtil redisUtil = new RedisUtil();
        redisUtil.setRedisTemplate(redisTemplate);
        return redisUtil;
    }

    /**
     * Cookie管理
     * @return
     */
    @Bean(name = "rememberMeCookie")
    public SimpleCookie rememberMeCookie() {
        //参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //记住我cookie生效时间10天 ,单位秒;
        simpleCookie.setMaxAge(TimeConstant.LOGIN_COOKIE_TIME);
        return simpleCookie;
    }

    @Bean(name = "rememberMeManager")
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    /**
     * Ehcache管理
     * @return
     */
    @Bean(name = "shiroEhcacheManager")
    public EhCacheManager getEhCacheManager() {
        EhCacheManager em = new EhCacheManager();
        em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
        return em;
    }

    /**
     * session管理器
     * @return
     */
    @Bean(name = "sessionValidationScheduler")
    public ExecutorServiceSessionValidationScheduler getExecutorServiceSessionValidationScheduler() {
        ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler();
        scheduler.setInterval(TimeConstant.LOGIN_SESSION_VALIDATE_TIME);
        return scheduler;
    }

    @Bean(name = "sessionManager")
    public DefaultWebSessionManager defaultWebSessionManager(SessionDao sessionDao) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(TimeConstant.LOGIN_SESSION_TIME);
		//url中是否显示session Id
        sessionManager.setSessionIdUrlRewritingEnabled(false);
		// 删除失效的session
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionValidationInterval(TimeConstant.LOGIN_SESSION_TIME);
        sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());
        //不从新设置新的cookie,从sessionManager获取sessionIdCookie
        sessionManager.getSessionIdCookie().setName("app-session-id");
        sessionManager.getSessionIdCookie().setPath("/");
        sessionManager.getSessionIdCookie().setMaxAge(TimeConstant.LOGIN_COOKIE_TIME);
        //更新redis缓存
        sessionManager.setSessionDAO(sessionDao);
        Collection<SessionListener> c=new ArrayList<>();
        c.add(new ShiroSessionListener());
        sessionManager.setSessionListeners(c);
        return sessionManager;
    }

    /**
     * 初始化shiro权限管理器
     * @return
     */
    @Bean
    @DependsOn(value = "lifecycleBeanPostProcessor")
    public ShiroRealm shiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setCacheManager(getEhCacheManager());
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 散列算法:这里使用MD5算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列的次数,比如散列两次,相当于md5(md5(""))
        hashedCredentialsMatcher.setHashIterations(1);
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * 初始化shiro安全过滤器
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager,KickoutFilter kickoutFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        Map<String,String> map = new HashMap<String, String>();
        //swagger静态资源
        //swagger接口权限 开放
        map.put("/swagger-ui.demo", "anon");
        map.put("/webjars/**", "anon");
        map.put("/v2/**", "anon");
        map.put("/swagger-resources/**", "anon");

        //普通静态资源
        map.put("/common/**","anon");
        map.put("/demo/**","anon");
        //登录请求
        map.put("/login/**","anon");
        map.put("/logout","logout");
        map.put("/**", "kickout,authc");
        //过滤器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("kickout", kickoutFilter);
        filterMap.put("authc",new SuccessUrlFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //登录页
        shiroFilterFactoryBean.setLoginUrl("/login/page");
        //登录成功页
        shiroFilterFactoryBean.setSuccessUrl("/index/page");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;

    }

    /**
     * 注册shiro过滤器注册器
     * @return
     */
    @Bean(name = "filterRegistrationBean1")
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new DelegatingFilterProxy("shiroFilter"));
        filterRegistrationBean.addInitParameter("targetFilterLifecycle", "true");
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.addUrlPatterns("/");
        return filterRegistrationBean;
    }

    /**
     * 修改全局默认shiro安全管理器
     * @param shiroRealm
     * @param defaultWebSessionManager
     * @return
     */
    @Bean(name = "defaultWebSecurityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(ShiroRealm shiroRealm, DefaultWebSessionManager defaultWebSessionManager) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(shiroRealm);
        defaultWebSecurityManager.setSessionManager(defaultWebSessionManager);
        defaultWebSecurityManager.setCacheManager(getEhCacheManager());
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
        return defaultWebSecurityManager;
    }

    /**
     * shiro权限通知
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
        aasa.setSecurityManager(defaultWebSecurityManager);
        return aasa;
    }

    /**
     * shiro生命周期
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor();
        return lifecycleBeanPostProcessor;
    }

}
认证+权限
public class ShiroRealm extends AuthorizingRealm {

    @Lazy
    @Autowired
    private IUserService iUserService;

    /**
     * 角色权限和对应权限添加
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取登录用户名
        String name= (String) principalCollection.getPrimaryPrincipal();
        //查询用户名称
        User user = iUserService.getByUsername(name);
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role:iUserService.getRoles(user.getId())) {
            //添加角色
            simpleAuthorizationInfo.addRole(role.getMark());
            for (Permission permission:iUserService.getRolePermissions(role.getId())) {
                //添加权限
                simpleAuthorizationInfo.addStringPermission(permission.getMark());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * 用户认证
     * @param authenticationToken
     * @return
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken){
        //加这一步的目的是在Post请求的时候会先进认证,然后在到请求
        if (authenticationToken.getPrincipal() == null) {
           return null;
        }
        //获取用户信息
        String username = authenticationToken.getPrincipal().toString();
        User user = iUserService.getByUsername(username);
        if (user == null) {
            //这里返回后会报出对应异常
            return null;
        } else {
            //这里验证authenticationToken和simpleAuthenticationInfo的信息
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
        }
    }
}
           

//(2)在线剔除

public class KickoutFilter extends AccessControlFilter {

    /**
     * 踢出后到的地址
     */
    private String kickoutUrl;

    /**
     * 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
     */
    private boolean kickoutAfter = false;

    /**
     * 同一个帐号最大会话数 默认1
     */
    private int maxSession = 1;

    /**
     * 会话管理器
     */
    private SessionManager sessionManager;

    /**
     * 缓存管理器
     */
    private Cache<String, Deque<Serializable>> cache;

    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }

    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    /**
     * 设置Cache的key的前缀
     * @param cacheManager
     */
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache("deque_session_");
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if (!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果没有登录,直接进行之后的流程
            return true;
        }
        Session session = subject.getSession();
        String username = (String) subject.getPrincipal();
        Serializable sessionId = session.getId();
        //读取缓存 没有就存入
        Deque<Serializable> deque = cache.get(username);
        //初始化
        if(deque == null){
            deque = new LinkedList<>();
        }
        //用户在线数量为0
        if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            //将sessionId存入队列
            deque.push(sessionId);
            //将用户的sessionId队列缓存
            cache.put(username, deque);
        }
        //用户数量>max
        while (deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if (kickoutAfter) {
                //如果踢出后者
                kickoutSessionId = deque.removeFirst();
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
            }
            //踢出后再更新下缓存队列
            cache.put(username, deque);
            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
                System.out.println("===========可忽略异常=============");
            }
        }
        //如果被踢出了,直接退出,重定向到踢出后的地址
        if ((Boolean) session.getAttribute("kickout") != null && (Boolean) session.getAttribute("kickout") == true) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);
            //重定向
            WebUtils.issueRedirect(request, response, kickoutUrl);
            return false;
        }
        return true;
    }
}

           

//(3)持久化部分

public class SessionDao extends EnterpriseCacheSessionDAO {

    private static final Logger log = LoggerFactory.getLogger(SessionDao.class);
    private RedisUtil redisUtil;
    public RedisUtil getRedisUtil() {
        return redisUtil;
    }

    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    /**
     * 创建session,保存到redis(1,1)
     * @param session
     * @return
     */
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.doCreate(session);
        if(null == ThreadContext.getSubject()){
            return sessionId;
        }
        //判断是否登录
        Subject subject = SecurityUtils.getSubject();
        if(subject != null && subject.isAuthenticated()){
            String username = (String) SecurityUtils.getSubject().getPrincipal();
            session.setAttribute("username",username);
            redisUtil.set(CacheConstant.USER_LOGIN_SESSION+sessionId, SeriaUtil.toBytes(session), TimeConstant.LOGIN_SESSION_CACHE_TIME);
        }
        return sessionId;
    }

    /**
     * 获取session(1,0)
     * @param sessionId
     * @return
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        // 先从缓存中获取session,如果没有再去数据库中获取
        Session session = session = super.doReadSession(sessionId);
        if(session == null){
            Object o = redisUtil.get(CacheConstant.USER_LOGIN_SESSION+sessionId);
            if(o != null){
                String str = (String) o;
                session = (Session) SeriaUtil.toObject(str);
            }
        }
        return session;
    }

    /**
     * 更新session的最后一次访问时间
     * @param session
     */
    @Override
    protected void doUpdate(Session session) {
        super.doUpdate(session);
        if(null == ThreadContext.getSubject()){
            return;
        }
        //判断是否存在key
        String key = CacheConstant.USER_LOGIN_SESSION+session.getId();
        //判断是否登录
        Subject subject = SecurityUtils.getSubject();
        if(subject != null && subject.isAuthenticated()){
            if(null == session.getAttribute("username")){
                String username = (String) SecurityUtils.getSubject().getPrincipal();
                session.setAttribute("username",username);
            }
            redisUtil.set(key, SeriaUtil.toBytes(session),TimeConstant.LOGIN_SESSION_CACHE_TIME);
        }
    }

    /**
     * 删除session
     * @param session
     */
    @Override
    protected void doDelete(Session session) {
        super.doDelete(session);
        //判断是否存在key
        String key = CacheConstant.USER_LOGIN_SESSION+session.getId();
        if(redisUtil.exists(key)){
            redisUtil.remove(key.toString());
        }
    }

}


           

//(4)若登录不成功,需要自定义过滤器重定向处理

public class SuccessUrlFilter extends FormAuthenticationFilter {
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        String successUrl = "/index/page";
        WebUtils.issueRedirect(request,response,successUrl);
        return false;
    }
}
           

(5)ehcache-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">

    <!--
    diskStore:为缓存路径,ehcache分为内存和磁盘 2级,此属性定义磁盘的缓存位置
    user.home - 用户主目录
    user.dir - 用户当前工作目录
    java.io.tmpdir - 默认临时文件路径
    -->
    <diskStore path="java.io.tmpdir/Tmp_Ehcache" />
    <!--
      name:缓存名称。
      maxElementsInMemory:缓存最大数目
      maxElementsOnDisk:硬盘最大缓存个数。
      eternal:对象是否永久有效,一但设置了,timeout将不起作用。
      overflowToDisk:是否保存到磁盘,当系统当机时
      timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
      timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
      diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
      diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
      diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
      memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
       clearOnFlush:内存数量最大时是否清除。
        memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
           FIFO,first in first out,这个是大家最熟的,先进先出。
           LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
           LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
   -->
    <defaultCache
            eternal="false"
            maxElementsInMemory="1000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU"
    />
    <cache
            name="demo"
            eternal="false"
            maxElementsInMemory="100"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="300"
            memoryStoreEvictionPolicy="LRU"
    />

</ehcache>
           

(6)springboot.properties

#server
server.port=8080

#jdbc
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_ssh?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

#jpa
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database=mysql
#都加上innodb(事务,行级锁,占用资源比较多)
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

#redis
spring.redis.hostName=127.0.0.1
spring.redis.port=6379
spring.redis.password=123zgf
spring.redis.database=0
spring.redis.timeOut=1000
spring.redis.maxIdle=10
spring.redis.maxWaitMillis=15000
spring.redis.testOnBorrow=true
spring.redis.testWhileIdle=false
#哨兵名称
spring.redis.sentinel.master=mymaster
#哨兵端口号
spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381


           

继续阅读