shiro+cas+spring-data-redis实现多系统单点登录和分布式项目的session同步

CSDN开通很久了,但是一直没写东西,2018年了,这是我CSDN的第一篇文章,欢迎各位评论探讨和指点。  

 

一、背景:

现在公司的业务系统要做多台分布式集群,由于是web项目,要做session同步,想到的方案是用目前火热的redis数据库存储session,还有业务系统已经是使用shiro+cas做了单点登录的。

   参考了一些行家的文章,自己加工写了一个sharesession的项目,抽取成了一个jar包,可导入需要同步session的业务系统。

 

 

二、项目简介:

    代码结构图

shiro+cas+spring-data-redis实现多系统单点登录和分布式项目的session同步_第1张图片

使用gradle构建的项目

三、源码分析

1.gradle的构建文件build.gradle如下

group 'com.gaojccn'
version '1.0.0-SNAPSHOT'

apply plugin: 'idea'
apply plugin: 'java'
apply plugin: "maven"
apply plugin: 'groovy'

sourceCompatibility = 1.7
compileJava.options.encoding = 'UTF-8'
compileJava.options.compilerArgs = ["-Xlint:unchecked", "-Xlint:deprecation"]

compileTestJava.options.encoding = 'UTF-8'
compileTestJava.options.compilerArgs = ["-Xlint:unchecked", "-Xlint:deprecation"]

buildscript {
    repositories {
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
}

repositories {
    mavenCentral()
    /*本地中央仓库*/
    maven {
        url "http://192.168.35.26:8081/nexus/content/repositories/central/"
    }
    maven {
        url "http://192.168.35.26:8081/nexus/content/repositories/thirdparty/"
    }
    maven {
        url "http://192.168.35.26:8081/nexus/content/repositories/releases/"
    }
    maven {
        url "http://192.168.35.26:8081/nexus/content/repositories/snapshots/"
    }
}


dependencies {
    compile 'org.apache.shiro:shiro-core:1.2.4'
    compile 'org.apache.shiro:shiro-cas:1.2.4'
    compile 'org.apache.shiro:shiro-web:1.2.4'
    compile 'org.apache.shiro:shiro-all:1.2.4'
    compile 'org.apache.shiro:shiro-ehcache:1.2.4'
    compile("redis.clients:jedis:2.1.0")
    compile 'org.springframework.data:spring-data-redis:1.0.2.RELEASE'
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.4'

    //testCompile 'org.springframework:spring-test:3.1.2.RELEASE'
    //testCompile 'com.github.springtestdbunit:spring-test-dbunit:1.0.1'
    //testCompile 'org.unitils:unitils-dbunit:3.3'
   // testCompile group: 'junit', name: 'junit', version: '4.12'
}

project.ext {
    versionFile = file('version.properties') //版本属性
}

class ProjectVersion {
    Integer major
    Integer minor
    Integer bugfix
    Boolean release

    ProjectVersion(Integer major, Integer minor, Integer bugfix) {
        this.major = major
        this.minor = minor
        this.bugfix = bugfix
        this.release = Boolean.FALSE
    }

    ProjectVersion(Integer major, Integer minor, Integer bugfix, Boolean release) {
        this.major = major
        this.minor = minor
        this.bugfix = bugfix
        this.release = release
    }

    @Override
    String toString() {
        "$major.$minor.$bugfix${release ? '' : '-SNAPSHOT'}"
    }
}

task printVersion {
    doLast {
        logger.quiet("version:$version")
    }
}
task loadVersion {
    project.version = readVersion()
}

def isRelease() {
    ProjectVersion projectVersion = readVersion()
    projectVersion.release
}

ProjectVersion readVersion() {
    logger.quiet('Reading the version file.')
    if (!versionFile.exists()) {
        throw new GradleException("Required version file does not exists:$versionFile.canonicalPath")
    }
    Properties versionProps = new Properties()
    versionFile.withInputStream { stream ->
        versionProps.load(stream)
    }
    new ProjectVersion(versionProps.major.toInteger(), versionProps.minor.toInteger(), versionProps.bugfix.toInteger(), versionProps.release.toBoolean())
}

task writeVersionFile << {
    def versionFilePath = 'version.json'
    File file = new File(versionFilePath)
    if (!file.exists()) file.createNewFile()

    def versionFile = new File(versionFilePath)
    versionFile.text = '{"version":"' + version + '"}'
}

uploadArchives {
    dependsOn build
    configuration = configurations.archives
    repositories.mavenDeployer {
        repository(url: 'http://192.168.35.26:8081/nexus/content/repositories/releases/') {
            authentication(userName: "admin", password: "admin123")
        }

        snapshotRepository(url: 'http://192.168.35.26:8081/nexus/content/repositories/snapshots/') {
            authentication(userName: "admin", password: "admin123")
        }

        pom.project {
            name 'gaojccn'
            packaging 'jar'
            description 'none'
//            url 'http://192.168.35.26:8081/nexus/content/repositories/releases/'
            url 'http://192.168.35.26:8081/nexus/content/repositories/snapshots/'
            groupId "com.gaojccn"
            artifactId "sharesession"
            version version
        }
    }
}

processResources {
    dependsOn writeVersionFile
}

jar {
    baseName = 'sharesession'
    version version
    manifest {
        attributes 'Implementation-Title': 'session',
                'Implementation-Version': version,
                'Created-By': 'gaojc'
    }
}

task makeReleaseVersion(group: 'versioning', description: 'Makes project a release version.') << {
    ant.propertyfile(file: versionFile) {
        entry(key: 'release', type: 'string', operation: '=', value: 'true')
    }
}

2.主配置文件share_session.xml




    
    
        
        
        
        
    

    
        
        
        
        
        
    

    
        
    

    

    
    

    
    
        
        
        
        
            
                
            
        
    

    
    
        
        
        
        
        
        
        
        
        
        
            
                
            
        
    

    

    
    
        
        
        
        
        
        
        
    

    
    

    
    
        
        
        
    

    
    
        
        
    

    
    
        
        
        
        
        
            
                
                
                
                
                
            
        
        
    

    
    
        
    

    
    

    
    

    
    
        
    

    
    
        
    


    
    
        
        
        
        
    

    
        
        
        
        
        
    

    
        
    

    

    
    

    
    
        
        
        
        
            
                
            
        
    

    
    
        
        
        
        
        
        
        
        
        
        
            
                
            
        
    

    

    
    
        
        
        
        
        
        
        
    

    
    

    
    
        
        
        
    

    
    
        
        
    

    
    
        
        
        
        
        
            
                
                
                
                
                
            
        
        
    

    
    
        
    

    
    

    
    

    
    
        
    

    
    
        
    

3.src目录下的java文件

 3.1 RedisManager

package com.gaojccn.sharesession;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.dao.DataAccessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

@Service
public class RedisManager {
    private static Logger logger = LoggerFactory.getLogger(RedisManager.class);
    @Autowired
    private RedisTemplate redisTemplate;

    private RedisSerializer serializer = new StringRedisSerializer();

    /**
     * 添加缓存数据(给定key已存在,进行覆盖)
     *
     * @param key
     * @param obj
     * @throws DataAccessException
     */
    public  void set(String key, T obj) throws DataAccessException {
        final byte[] bkey = serializer.serialize(key);
        final byte[] bvalue = serializer.serialize(obj.toString());

        logger.info("set key {} value {}", key, obj);
        redisTemplate.execute(new RedisCallback() {
            @Override
            public Void doInRedis(RedisConnection connection) throws DataAccessException {
                connection.set(bkey, bvalue);
                return null;
            }
        });
    }

    /**
     * 添加缓存数据(给定key已存在,不进行覆盖,直接返回false)
     *
     * @param key
     * @param obj
     * @return 操作成功返回true,否则返回false
     * @throws DataAccessException
     */
    public  boolean setNX(String key, T obj) throws DataAccessException {
        final byte[] bkey = serializer.serialize(key);
        final byte[] bvalue = serializer.serialize(obj.toString());
        logger.info("setNX key {} value {}", key, obj);
        boolean result = redisTemplate.execute(new RedisCallback() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.setNX(bkey, bvalue);
            }
        });

        return result;
    }

    /**
     * 添加缓存数据,设定缓存失效时间
     *
     * @param key
     * @param obj
     * @param expireSeconds 过期时间,单位 秒
     * @throws DataAccessException
     */
    public  void setEx(String key, T obj, final long expireSeconds) throws DataAccessException {
        final byte[] bkey = serializer.serialize(key);
        final byte[] bvalue = serializer.serialize(obj.toString());
        logger.info("setEx key {} value {}", key, obj);
        redisTemplate.execute(new RedisCallback() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                connection.setEx(bkey, expireSeconds/1000, bvalue);
                return true;
            }
        });
    }

    /**
     * 获取key对应value
     *
     * @param key
     * @return
     * @throws DataAccessException
     */
    public  T get(final String key) throws DataAccessException {
        final byte[] keyStr = serializer.serialize(key);
        return get(keyStr);
    }

    /**
     * 根据 key字节数组 获取value
     *
     * @param keyStr
     * @param 
     * @return
     */
    public  T get(final byte[] keyStr) {
        T result = redisTemplate.execute(new RedisCallback() {
            public T doInRedis(RedisConnection connection)
                    throws DataAccessException {
                byte[] value = connection.get(keyStr);
                return deseriaValueByte(value);
            }
        });
        return result;
    }

    /**
     * 反序列化value字节数组
     *
     * @param value
     * @param 
     * @return
     */
    private  T deseriaValueByte(byte[] value) {
        if (value == null) {
            return null;
        }
        String valueStr = serializer.deserialize(value);
        T retStr;
        try {
            retStr = SerializableUtils.deserialize(valueStr);
        } catch (Exception e) {
            logger.error("deseriaValueByte {} happen RuntimeException {}", value, e.getMessage());
            return null;
        }
        return retStr;
    }

    /**
     * 删除指定key数据
     *
     * @param key
     * @return 返回操作影响记录数
     */
    public Long delete(final String key) throws DataAccessException {
        logger.info("delete key {} from redis", key);
        if (StringUtils.isEmpty(key)) {
            return 0l;
        }
        Long delNum = redisTemplate.execute(new RedisCallback() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                byte[] keys = serializer.serialize(key);
                return connection.del(keys);
            }
        });
        return delNum;
    }

    /**
     * 根据key模糊查询value set集合
     *
     * @param key
     * @param 
     * @return
     * @throws DataAccessException
     */
    public  Set keys(final String key) throws DataAccessException {
        if (StringUtils.isEmpty(key)) {
            return null;
        }
        Set res = redisTemplate.execute(new RedisCallback>() {
            public Set doInRedis(RedisConnection connection)
                    throws DataAccessException {
                Set tSet = new HashSet<>();
                byte[] keys = serializer.serialize(key);
                Set keysByteSet = connection.keys(keys);
                if (keysByteSet != null && keysByteSet.size() > 0)
                    for (byte[] key : keysByteSet) {
                        byte[] valueByte = connection.get(key);
                        T value = deseriaValueByte(valueByte);
                        tSet.add(value);
                    }
                return tSet;
            }
        });
        return res;
    }

    /**
     * 清空缓存
     *
     * @return
     */
    public boolean flushDB() throws DataAccessException {
        logger.info("flushDB in redis...");
        boolean result = redisTemplate.execute(new RedisCallback() {
            public Boolean doInRedis(RedisConnection connection)
                    throws DataAccessException {
                connection.flushDb();
                return true;
            }
        });
        return result;
    }

}


3.2  RedisSessionDao

package com.gaojccn.sharesession;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.Serializable;
import java.util.Collection;
import java.util.Set;

/**
 * RedisSessionDao
 */
@Service("sessionDao")
public class RedisSessionDao extends AbstractSessionDAO {
    private static Logger logger = LoggerFactory.getLogger(RedisSessionDao.class);

    @Autowired
    private RedisManager redisManager;

    //设置过期时间
    @Value("${session.expireTime}")
    private long expireTime;

    // The Redis key prefix for the sessions
    @Value("${session.keyPrefix}")
    private String keyPrefix;

    @Override
    public Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, formatSessionId(sessionId));
        logger.info("session id is" + session.getId());
        this.saveSession(session);
        return session.getId();
    }

    private String formatSessionId(Serializable sid) {
        try {
            String sessionId = String.valueOf(sid).replace("-", "").toUpperCase();
            return sessionId;
        } catch (Exception e) {
            logger.error("formatSessionId happen exception {}", e.getMessage());
            return null;
        }
    }

    @Override
    public Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            logger.error("session id is null");
            return null;
        }
        Session s = redisManager.get(keyPrefix + sessionId);
        return s;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        this.saveSession(session);
    }

    private void saveSession(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return;
        }
        session.setTimeout(expireTime);
        redisManager.setEx(keyPrefix + session.getId(), SerializableUtils.serialize(session), expireTime);
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return;
        }
        redisManager.delete(keyPrefix + session.getId());
    }

    @Override
    public Collection getActiveSessions() {
        Set sessions = redisManager.keys(this.keyPrefix + "*");
        return sessions;
    }

}


3.3  RedisSessionListener

package com.gaojccn.sharesession;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * redisSession事件监听器
 * author:gaojc
 */
@Service
public class RedisSessionListener implements SessionListener {
    private static final Logger logger = LoggerFactory.getLogger(RedisSessionListener.class);

    @Autowired
    private RedisSessionDao sessionDao;

    @Override
    public void onStart(Session session) {//会话创建时触发
        logger.debug("会话创建:" + session.getId());
    }

    @Override
    public void onExpiration(Session session) {//会话过期时触发
        logger.debug("会话过期:" + session.getId());
        sessionDao.delete(session);
    }

    @Override
    public void onStop(Session session) {//退出时触发
        logger.info("会话停止:" + session.getId());
        sessionDao.delete(session);
    }
}


3.4 SerializableUtils

package com.gaojccn.sharesession;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.Session;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializableUtils {
    public static String serialize(Session session) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(session);
            return Base64.encodeToString(bos.toByteArray());
        } catch (Exception e) {
            throw new RuntimeException("serialize session error", e);
        }
    }

    public static  T deserialize(String sessionStr) {
        try {
            ByteArrayInputStream bis = new ByteArrayInputStream(Base64.decode(sessionStr));
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (T) ois.readObject();
        } catch (Exception e) {
            throw new RuntimeException("deserialize session error", e);
        }
    }
}

4. web项目中的使用

 4.1 导入sharesession.jar    

 4.2 web.xml里面加入shiro过滤器


        shiroFilter
        org.springframework.web.filter.DelegatingFilterProxy
        
            targetFilterLifecycle
            true
        
    
    
        shiroFilter
        /*
        REQUEST
        FORWARD
        INCLUDE
        ERROR
    

 

而且用contextLoaderListener加载spring配置文件

 

 
        contextConfigLocation
        classpath:spring-context.xml
    
    
        
            org.springframework.web.context.ContextLoaderListener
        
    

4.3 spring-context.xml里面加入相关文件


    
    
    

4.4 share_session.properties

##redis连接参数
redis.host=192.168.1.109
#这里用的是简单的单节点
redis.port=6379
redis.database=0
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true
#当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
redis.timeout=10000

#session有效时间30分钟 单位毫秒
session.expireTime=1800000
#sessionId前缀
session.keyPrefix=redis_session:
#redis session alias (session cookieName)(session key名称,用和web容器默认一样的名称jsessionid防止出错)
session.cookieName=jsessionid
#定时清理失效会话间隔时间 20分钟
session.sessionValidationInterval=1200000


#shiro url配置
loginUrl=https://cas-ad.share.gaojccn.com:8443/cas/login?service=http://netpay-web.gaojccn.com:8080/netpay/shiro-cas
casFilter.failureUrl=/error.jsp
logout.redirectUrl=https://cas-ad.share.gaojccn.com:8443/cas/logout

#shiroFilter.webDir=netpayweb

#casRealm
casRealm.roles=ROLE_USER
casRealm.casServerUrlPrefix=https://cas-ad.share.gaojccn.com:8443/cas
casRealm.casService=http://netpay-web.gaojccn.com:8080/netpay/shiro-cas

#filterChainDefinitions 用\n换行来分割多行value
filterChainDefinitions=/shiro-cas = cas\n/rest/version = anon\n/rest/** = authc\n/netpayweb/version.json = anon\n/netpayweb/report/** = anon\n/netpayweb/** = authc\n/logout = logout

ok,到此结束,测试使用吧。

附上 代码地址: https://github.com/gaojccn/sharesession

你可能感兴趣的:(java技术)