前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >spring整合spring-data-redis和spring-session-data-redis通过shiro实现单点登录

spring整合spring-data-redis和spring-session-data-redis通过shiro实现单点登录

作者头像
全栈程序员站长
发布2022-08-18 20:36:42
9920
发布2022-08-18 20:36:42
举报

大家好,又见面了,我是你们的朋友全栈君。

  • 运行效果图
  • 缓存说明(本项目没有使用shiro的缓存管理器和session管理器) shiro_user_cache:permission:权限缓存,当前只有test用户 shiro_user_cache:role:角色缓存,当前只有test用户 shiro_user_kickout:保存被踢出的用户 shiro_user_online: 保存登录了的用户 sprting:spring-session管理的缓存
  • 上面缓存的创建过程 shiro_user_cache:登录时UserRealm会触发Spring的查询缓存保存用户的角色权限,清除缓存也是利用Spring的注解,如下
代码语言:javascript
复制
<!-- 启用缓存注解功能,在这里起到关键作用 -->
<cache:annotation-driven cache-manager="redisCacheManager" />

代码语言:javascript
复制
package com.shiro;

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.UnknownAccountException;
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 com.entity.User;
import com.service.UserService;

public class UserRealm extends AuthorizingRealm { 
     

    @Autowired
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String)principals.getPrimaryPrincipal();       
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.setRoles(userService.findRolesByUsername(username));
        authorizationInfo.setStringPermissions(userService.findPermissionsByUsername(username));
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String)token.getPrincipal();
        User user = userService.findByUsername(username);
        if(user == null) {
            throw new UnknownAccountException();//没找到帐号
        }

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user.getUsername(),
                user.getPassword(),
                getName()  //realm name
        );
        return authenticationInfo;
    }

    /** * 根据用户名,清除角色和权限缓存 * @param uername */
    public void clearUserCache(String uername) {
        userService.clearUserCache(uername);
    }

    /** * 清除所有用户的角色和权限缓存 */
    public void clearUserCache() {
        userService.clearUserCache();
    }

}

代码语言:javascript
复制
package com.service.impl;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.dao.DictDao;
import com.dao.RoleDao;
import com.dao.UserDao;
import com.entity.RolePermission;
import com.entity.User;
import com.entity.UserRole;
import com.service.UserService;

import core.service.BaseServiceImpl;

@Service
public class UserServiceImpl extends BaseServiceImpl<User, Integer> implements UserService {

    @Autowired
    UserDao userDao;

    @Autowired
    RoleDao roleDao;

    @Autowired
    DictDao daoDao;

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

    @Cacheable(value="shiro_user_cache:role", key="#username")
    @Override
    public Set<String> findRolesByUsername(String username) {
        Set<String> roles = new HashSet<String>();
        User user = this.findByUsername(username);
        if(user==null) {
            return roles;
        }
        List<UserRole> userRoles = user.getUserRoleList();
        for(UserRole userRole:userRoles) {
            roles.add(userRole.getRole().getName());
        }
        return roles;
    }

    @Cacheable(value="shiro_user_cache:permission", key="#username")
    @Override
    public Set<String> findPermissionsByUsername(String username) {
        Set<String> permissions = new HashSet<String>();
        User user = this.findByUsername(username);
        if(user==null) {
            return permissions;
        }
        List<UserRole> userRoles = user.getUserRoleList();
        for(UserRole userRole:userRoles) {
            List<RolePermission> rolePermissions= userRole.getRole().getRolePermissionList();
            for(RolePermission rolePermission:rolePermissions) {
                permissions.add(rolePermission.getPermission().getName());
            }
        }       
        return permissions;
    }

    @CacheEvict(value={
    
    "shiro_user_cache:role","shiro_user_cache:permission"}, key="#username")
    public void clearUserCache(String username) {

    }

    @CacheEvict(value={
    
    "shiro_user_cache:role","shiro_user_cache:permission"}, allEntries=true)
    public void clearUserCache() {

    }

}

shiro_user_kickout和shiro_user_online,跟上面一样通过下面这个缓存管理器创建,通过他们实现单点登录或限定其他登录数.

代码语言:javascript
复制

<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager"
        factory-method="create" c:connection-factory-ref="jedisConnectionFactory" />

代码语言:javascript
复制
package com.shiro;

import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;

import com.contant.SystemContant;

public class KickoutFilter extends AccessControlFilter { 
     

    /** * 踢出前一个登陆或后一个登陆的同一用户 */
    private boolean kickoutBefore = true;
    /** * 同一个用户的最大同时登陆数 */
    private int maxUserCount = 1;

    /** * 保存同一用户登录数<用户名,sessionId队列> */
    private Cache onliceCache;

    /** * 被踢出的登录<用户名,sessionId队列> */
    private Cache kickoutCache;

    @Autowired
    @Qualifier("redisCacheManager")
    public void setCacheManager(CacheManager cacheManager) {
        this.onliceCache = cacheManager.getCache("shiro_user_online");
        this.kickoutCache = cacheManager.getCache("shiro_user_kickout");
    }

    @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);
        Session session = subject.getSession();
        String username = (String) subject.getPrincipal();
        Serializable sessionId = session.getId();
        //如果没有登录,直接进行之后的流程
        if (!subject.isAuthenticated() && !subject.isRemembered()) {            
            return true;
        }

        //先判断当前用户是否被踢出
        Deque<Serializable> kickoutDeque = getKickoutDeque(username);
        for (Serializable id : kickoutDeque) {
            if (sessionId.equals(id)) {             
                subject.logout();
                //踢出后在kickoutDeque中删除当前sessionId
                System.out.println("踢出sessionId:" + id);
                kickoutDeque.remove(id);
                kickoutCache.put(username, kickoutDeque);
                //跳转到登录页
                Map<String, String> params = new HashMap<String, String>();
                params.put(SystemContant.KICKOUT_MSG, "kick out login");
                WebUtils.issueRedirect(request, response, "/login", params);
                return false;
            }
        }

        //如果队列里没有此sessionId,放入队列
        Deque<Serializable> onlineDeque = getOnlineDeque(username);
        if (!onlineDeque.contains(sessionId)) {
            onlineDeque.push(sessionId);
        }
        //判断当前用户在线数目是否超出maxUserCount,然后把超出的用户从onlineDeque移到kickoutDeque
        while (onlineDeque.size() > maxUserCount) {
            Serializable kickoutSessionId = null;
            if (kickoutBefore) {
                kickoutSessionId = onlineDeque.removeLast();
                kickoutDeque.push(kickoutSessionId);
            } else {
                kickoutSessionId = onlineDeque.removeFirst();
                kickoutDeque.push(kickoutSessionId);
            }

        }
        onliceCache.put(username, onlineDeque);
        kickoutCache.put(username, kickoutDeque);

        return true;
    }

    /** * 获取在线用户 * * @param username * @return */
    @SuppressWarnings("unchecked")
    private Deque<Serializable> getOnlineDeque(String username) {
        Deque<Serializable> onlineDeque;
        if (onliceCache.get(username) == null) {
            onlineDeque = new LinkedList<Serializable>();
        } else {
            onlineDeque = (Deque<Serializable>) onliceCache.get(username).get();
        }
        return onlineDeque;
    }

    /** * 获取被踢出的用户 * * @param username * @return */
    @SuppressWarnings("unchecked")
    private Deque<Serializable> getKickoutDeque(String username) {
        Deque<Serializable> kickoutDeque;
        if (kickoutCache.get(username) == null) {
            kickoutDeque = new LinkedList<Serializable>();
        } else {
            kickoutDeque = (Deque<Serializable>) kickoutCache.get(username).get();
        }
        return kickoutDeque;
    }

}

配置文件

代码语言:javascript
复制
applicationContext-redis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <constructor-arg>
            <bean class="org.springframework.data.redis.connection.RedisStandaloneConfiguration" c:host-name="${redis.host}" c:port="${redis.port}" />
        </constructor-arg>      
    </bean>

    <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager" factory-method="create" c:connection-factory-ref="jedisConnectionFactory" />
</beans>
代码语言:javascript
复制
applicationContext-session.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
        <property name="maxInactiveIntervalInSeconds" value="1800" />
    </bean>
</beans>

代码语言:javascript
复制

applicationContext-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm">
            <bean class="com.shiro.UserRealm"/>
        </property>     
    </bean>

    <!-- Shiro Filter -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login" />
        <property name="unauthorizedUrl" value="/login" />
        <property name="filters">
            <util:map>
                <!-- Shiro的单点登录-->
                <entry key="kickout">
                    <bean class="com.shiro.KickoutFilter" />
                </entry>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /login/** = anon
                /common/taglibs.jspf = anon
                /static/** = anon
                /** = kickout,authc
            </value>
        </property>
    </bean>

    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

</beans>
代码语言:javascript
复制
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd" default-autowire="byName">

    <context:annotation-config />
    <context:component-scan base-package="com.service,core" />

    <!-- 配置文件获取 -->
    <bean id="propertyConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location">
            <value>classpath:application.properties</value>
        </property>
        <property name="fileEncoding">
            <value>UTF-8</value>
        </property>
    </bean>

    <!-- 数据源配置 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <!-- 设置连接池初始值 -->
        <property name="initialSize" value="1" />
        <!-- 设置连接池最大值 -->
        <property name="maxActive" value="100" />
        <!-- 设置连接池最小空闲值 -->
        <property name="minIdle" value="10" />
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000" />
        <!-- 配置间隔10分钟,检测空闲了5分钟的连接是否需要关闭,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="600000" />
        <property name="minEvictableIdleTimeMillis" value="300000" />

        <property name="validationQuery" value="SELECT 1 FROM DUAL " />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />

        <property name="filters" value="stat" />
    </bean>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <!-- 适配器 -->
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"></bean>
        </property>
        <property name="packagesToScan" value="com.entity"></property>
        <property name="jpaProperties">
            <props>
                <!-- 生成的数据表的列的映射策略 -->
                <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.use_sql_comments">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
    </bean>

    <!-- 使用JDK代理方式配置AOP,暴露代理到Threadload,解决内部调用存在事务再另起事务失效问题 -->
    <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="false" />
    <!-- 使用注解方式定义事务 -->
    <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="false" />
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"></property>    
    </bean>

     <!-- Spring Data Jpa配置 -->
    <jpa:repositories base-package="com.dao" repository-impl-postfix="Impl" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/>

    <!-- 启用缓存注解功能 -->
    <cache:annotation-driven cache-manager="redisCacheManager" />

    <bean id="redisUtil" class="core.util.RedisUtil" />

</beans>
代码语言:javascript
复制
springServletContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven />
    <mvc:view-controller path="/" view-name="redirect:/login"/>
    <context:component-scan base-package="com.controller">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

    <!-- Enable Shiro Annotations for Spring-configured beans. Only run after -->
    <!-- the lifecycleBeanProcessor has run: -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
        <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

    <bean id="JstlView" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="order" value="1" />
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>

</beans>
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月3,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档