系统中用了shiro做权限控制和身份认证(其实身份认证可以用jwt的,这在我以后的博客中会写到)。本来是单一系统。但是现在要做成分布式的。所以就只能用到session共享。其实不用spring-session也能实现session共享,只需要将session存入redis即可。但是spring-session作为现成的框架,把许多底层的东西都已经封装了,不用白不用啊。spring-session通过适配器模式、责任链模式、装饰者模式等对httpsession做了封装。

我是将shiro和spring-session/redis分别建成了两个子项目,然后通过springcloud集成的如下:

Shiro配置redis session管理 shiro redis session共享_session

对于shiro的项目,首先你应该引入shiro的jar包,并配置好shiro。

maven:

<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>${shiro-spring-boot-web-starter.version}</version>
        </dependency>

shiro的配置类,核心的就两个,分别是继承AuthorizingRealm的ShiroRealm和ShiroConfig

package com.lenovoedu.config;

import com.lenovoedu.constants.ShiroConstants;
import com.lenovoedu.model.sys.model.SysUser;
import com.lenovoedu.shiro.service.IPermitService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 项目名称:
 * 类名称:ShiroRealm
 * 类描述:shiro AuthorizingRealm继承实现
 * 创建人:YuanGL
 * 创建时间:2019年4月1日16:17:46
 * version 2.0
 */
@Component("authorizer")
public class ShiroRealm extends AuthorizingRealm {

    private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);

    @Autowired
    private IPermitService permitService;

    private static HashedCredentialsMatcher hc;

    static {
        hc = new HashedCredentialsMatcher();
        hc.setHashIterations(3);
        hc.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
    }

    public ShiroRealm(){
        super(new MemoryConstrainedCacheManager(),hc);
    }

    /**
     * 授权
     * @param principals 用户信息
     * @return AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        String username = (String)getAvailablePrincipal(principals);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(permitService.getSysUserRolesByEmail(username));
        info.setStringPermissions(permitService.getSysUserAuthCodesByEmail(username));
        return info;
    }

    /**
     * 验证
     * @param token 用户权限信息
     * @return AuthenticationInfo
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        SysUser sysUser = permitService.getUserByEmail(usernamePasswordToken.getUsername());
        if(sysUser==null){
            throw new UnknownAccountException();
        }
        // 检查用户状态
        checkUserStatus(sysUser.getStatus());
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(sysUser.getEmail(),sysUser.getPassword(), super.getName() );
        info.setCredentialsSalt(ByteSource.Util.bytes(sysUser.getSalt().toCharArray()));
        return info;
    }

    /**
     * 检查用户状态
     * 如果用户状态异常,则抛出对应的错误
     * @param status 用户状态
     * @throws AuthenticationException 用户状态错误信息
     */
    private void checkUserStatus(int status)throws AuthenticationException{
        switch (status){
            case ShiroConstants.ADMIN_STATUS_DISABLED:
                throw new DisabledAccountException();
            case ShiroConstants.ADMIN_STATUS_LOCKED:
                throw new LockedAccountException();
            case ShiroConstants.ADMIN_STATUS_DELETE:
                throw new UnknownAccountException();
        }
    }
}
package com.lenovoedu.config;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 项目名称:
 * 类名称:ShiroConfig
 * 类描述:shiro配置类
 * 创建人:YuanGL
 * 创建时间:2019年4月1日14:12:36
 * version 2.0
 */
@Configuration
public class ShiroConfig {

    /**
     * ServletContainerSessionManager 类中有一个方法是isServletContainerSessions(),返回的是true.
     * DefaultWebSessionManager类中有一个方法是isServletContainerSessions(),返回是false。
     * 因为实现了Spring Session,代理了所有Servlet里的session,所以这里的session一定是Servlet能控制的,否则无法实现Spring session共享。
     *
     * @return
     */
    @Bean
    public SessionManager sessionManager(){
        ServletContainerSessionManager sessionManager = new         
        ServletContainerSessionManager();
        return sessionManager;
    }
    @Bean("authenticator")
    public SecurityManager securityManager(ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new 
        DefaultShiroFilterChainDefinition();
        chainDefinition.addPathDefinition("/css/**", "anon");
        chainDefinition.addPathDefinition("/fonts/**", "anon");
        chainDefinition.addPathDefinition("/images/**", "anon");
        chainDefinition.addPathDefinition("/js/**", "anon");
        chainDefinition.addPathDefinition("/themes/**", "anon");
        chainDefinition.addPathDefinition("/vendor/**", "anon");
        chainDefinition.addPathDefinition("/favicon.ico", "anon");
        chainDefinition.addPathDefinition("/login.html", "anon");
        chainDefinition.addPathDefinition("/swagger-ui.html", "anon");
        chainDefinition.addPathDefinition("/webjars/**", "anon");
        chainDefinition.addPathDefinition("/v2/**", "anon");
        chainDefinition.addPathDefinition("/swagger-resources/**", "anon");
        chainDefinition.addPathDefinition("/v2.0/lls/sys/login", "anon");
        chainDefinition.addPathDefinition("/v2.0/lls/sys/logout", "anon");
        chainDefinition.addPathDefinition("/v2.0/lls/sys/isLogin", "anon");
        chainDefinition.addPathDefinition("/verifyCode", "anon");
//        chainDefinition.addPathDefinition("/**", "anon");
        chainDefinition.addPathDefinition("/**", "authc");
        return chainDefinition;
    }

    /**
     * LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部
     * 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的。
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor 
    authorizationAttributeSourceAdvisor(ShiroRealm shiroRealm) {
        AuthorizationAttributeSourceAdvisor advisor = new 
        AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager(shiroRealm));
        return advisor;
    }
}

此时,shiro的就已经配置完毕了!

接下来针对session-redis工程,要配置redis和spring-session

maven如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--spring boot 与redis应用基本环境配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
            <version>1.4.7.RELEASE</version>
        </dependency>
        <!--spring session 与redis应用基本环境配置,需要开启redis后才可以使用,不然启动Spring boot会报错 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
    </dependencies>

配置好redis,配置类代码如下:

package com.lenovoedu.base.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 项目名称:
 * 类名称:RedisCache
 * 类描述:redis缓存实现
 * 创建人:YuanGL
 * 创建时间:2019年3月25日11:02:21
 * version 2.0
 */
@Component
public class RedisCache extends AbstractCache {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private void init() {
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(stringSerializer);
    }

    public void setMap(String key, Map obj, final long timeout, final TimeUnit unit) {
        init();
        redisTemplate.opsForHash().putAll(this.getPrefixKey(key), obj);
        redisTemplate.expire(this.getPrefixKey(key), timeout, unit);
    }

    public Map getMap(String key) {
        init();
        return redisTemplate.opsForHash().entries(this.getPrefixKey(key));
    }

    public void updateMap(String key, Map obj, final long timeout, final TimeUnit unit) {
        setMap(key, obj, timeout, unit);
    }

    @Override
    public void set(String key, String value) {
        stringRedisTemplate.opsForValue().set(this.getPrefixKey(key), value);
    }

    @Override
    public void set(String key, String value, long timeout) {
        stringRedisTemplate.opsForValue().set(this.getPrefixKey(key), value, timeout);
    }

    @Override
    public void set(String key, String value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(this.getPrefixKey(key), value, timeout, unit);
    }

    @Override
    public boolean setIfAbsent(String key, String value) {
        return stringRedisTemplate.opsForValue().setIfAbsent(this.getPrefixKey(key), value);
    }

    @Override
    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(this.getPrefixKey(key));
    }

    @Override
    public String getSub(String key, long start, long end) {
        return stringRedisTemplate.opsForValue().get(this.getPrefixKey(key), start, end);
    }

    @Override
    public String getAndSet(String key, String value) {
        return stringRedisTemplate.opsForValue().getAndSet(this.getPrefixKey(key), value);
    }

    @Override
    public boolean exists(String key) {
        return stringRedisTemplate.hasKey(this.getPrefixKey(key));
    }

    @Override
    public void delete(String key) {
        stringRedisTemplate.delete(this.getPrefixKey(key));
    }

    @Override
    public void delete(List<String> keys) {
        if(CollectionUtils.isEmpty(keys)){
            return;
        }
        for (String key:
             keys) {
            stringRedisTemplate.delete(this.getPrefixKey(key));
        }
    }

    @Override
    public Long incrementForInteger(String key) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), 1L);
    }

    @Override
    public Double incrementForDouble(String key) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), 1.0);
    }

    @Override
    public Long increment(String key, Long value) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), value);
    }

    @Override
    public Double increment(String key, Double value) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), value);
    }

    @Override
    public Boolean expire(String key, Date endTime) {
        return stringRedisTemplate.expireAt(this.getPrefixKey(key), endTime);
    }

    @Override
    public Boolean expire(String key, Long timeout, TimeUnit unit) {
        return stringRedisTemplate.expire(this.getPrefixKey(key), timeout, unit);
    }

    @Override
    public Set<String> keys(String keyPrefix) {
        return stringRedisTemplate.keys(this.getPrefixKey(keyPrefix))
                .parallelStream()
                .map(key->key.replace(getPrefix()+":", ""))
                .collect(Collectors.toSet());
    }

    @Override
    public Long decrement(String key) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -1L);
    }

    @Override
    public Long decrement(String key, Long value) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -value);
    }

    @Override
    public Double decrementForDouble(String key) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -1.0);
    }

    @Override
    public Double decrementForDouble(String key, Double value) {
        return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -value);
    }

    public Long leftPushAll(String key, Set<String> values){
        return stringRedisTemplate.opsForList().leftPushAll(this.getPrefixKey(key), values);
    }

    public Long leftPush(String key, String value){
        return stringRedisTemplate.opsForList().leftPush(this.getPrefixKey(key), value);
    }

    public String leftPop(String key){
        return stringRedisTemplate.opsForList().leftPop(this.getPrefixKey(key));
    }

    public Long rightPushAll(String key, Set<String> values){
        return stringRedisTemplate.opsForList().rightPushAll(this.getPrefixKey(key), values);
    }

    public Long rightPush(String key, String value){
        return stringRedisTemplate.opsForList().rightPush(this.getPrefixKey(key), value);
    }

    public String rightPop(String key){
        return stringRedisTemplate.opsForList().rightPop(this.getPrefixKey(key));
    }

}




package com.lenovoedu.base.redis;

import org.springframework.beans.factory.annotation.Value;

/**
 * 项目名称:lenovolls2.0
 * 类名称:AbstractCache
 * 类描述:
 * 创建人:YuanGL
 * 创建时间:2019年3月25日11:03:00
 * version 2.0
 */
public abstract class AbstractCache implements Cache {

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

    public String getPrefixKey(String key){
        return prefix + ":" + key;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }
}






package com.lenovoedu.base.redis;

import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 项目名称:lenovolls2.0
 * 类名称:Cache
 * 类描述:缓存接口类
 * 创建人:YuanGL
 * 创建时间:2019-3-25 11:03:16
 * version 2.0
 */
public interface Cache {

    /**
     * 设置cache
     * @param key 缓存key
     * @param value 值
     */
    void set(String key, String value);
    /**
     * 设置cache
     * 指定时间后过期
     * @param key 缓存key
     * @param value 值
     * @param timeout 过期时间,单位毫秒
     */
    void set(String key, String value, long timeout);
    /**
     * 设置cache
     * @param key 缓存key
     * @param value 值
     * @param timeout 过期时间
     * @param unit 时间单位
     */
    void set(String key, String value, long timeout, TimeUnit unit);
    /**
     * 设置cache,如果该cache不存在的话
     * @param key 缓存key
     * @param value 值
     */
    boolean setIfAbsent(String key, String value);
    /**
     * 获取cache
     * @param key 缓存key
     * @return String
     */
    String get(String key);

    /**
     * 获取cache并截取
     * key: a value: abcd start: 0 end: 2 result: abc
     * @param key 缓存key
     * @param start 开始
     * @param end 结束
     * @return 被截取后的字符串
     */
    String getSub(String key, long start, long end);
    /**
     * 获取cache并设置新的值
     * @param key 缓存key
     * @param value 设置的值
     * @return 设置前的值
     */
    String getAndSet(String key, String value);

    /**
     * 判断cache是否存在
     * @param key 缓存key
     * @return true:存在 false:不存在
     */
    boolean exists(String key);
    /**
     * 删除cache
     * @param key 缓存key
     */
    void delete(String key);

    /**
     * 删除cache集合
     * @param keys 缓存key
     */
    void delete(List<String> keys);

    /**
     * 递增
     * 默认步长 1
     * @param key 缓存key
     * @return 递增后的值
     */
    Long incrementForInteger(String key);

    /**
     * 递增
     * 默认步长 1
     * @param key 缓存key
     * @return 递增后的值
     */
    Double incrementForDouble(String key);

    /**
     * 递增
     * @param key 缓存key
     * @param value 步长
     * @return 递增后的值
     */
    Long increment(String key, Long value);

    /**
     * 递增
     * @param key 缓存key
     * @param value 步长
     * @return 递增后的值
     */
    Double increment(String key, Double value);

    /**
     * 设置失效时间
     * @param key 缓存key
     * @param endTime 失效时间点
     * @return
     */
    Boolean expire(String key, Date endTime);

    /**
     * 设置失效时间
     * @param key 缓存key
     * @param timeout 时间长度
     * @param unit 时间单位
     * @return
     */
    Boolean expire(String key, Long timeout, TimeUnit unit);

    /**
     * 获得类似的key集合
     * @param keyPrefix *key*,*:通配符
     * @return 所有的类似的key
     */
    Set<String> keys(String keyPrefix);

    /**
     * 递减
     * 默认步长 -1
     * @param key 缓存key
     * @return 递减后的值
     */
    Long decrement(String key);

    /**
     * 递减
     * @param key 缓存key
     * @param value 递减步长
     * @return 递减后的值
     */
    Long decrement(String key, Long value);

    /**
     * 递减
     * 默认步长 -1.0
     * @param key 缓存key
     * @return 递减后的值
     */
    Double decrementForDouble(String key);

    /**
     * 递减
     * @param key 缓存key
     * @param value 递减步长
     * @return 递减后的值
     */
    Double decrementForDouble(String key, Double value);

}

针对spring-session只需一下两个类:

配置比较简单,主要是添加@EnableRedisHttpSession注解即可,该注解会创建一个名字叫springSessionRepositoryFilter的Spring Bean,其实就是一个Filter,这个Filter负责用Spring Session来替换原先的默认HttpSession实现,同时实现是采用redis管理session的方式。在这个例子中,Spring Session是用Redis来实现的。

package com.lenovoedu.base.session;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

/**
 * 启用RedisHttpSession功能,并向Spring容器中注册一个RedisConnectionFactory
 *
 * 对象10分钟(maxInactiveIntervalInSeconds=600)后失效。
 */
@EnableRedisHttpSession(maxInactiveIntervalInSeconds=600)
public class SessionConfig {

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

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

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

    @Bean
    public JedisConnectionFactory connectionFactory() {
        JedisConnectionFactory connection = new JedisConnectionFactory();
        connection.setHostName(host);
        connection.setPassword(password);
        connection.setPort(port);
        return connection;
    }
}
package com.lenovoedu.base.session;

import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;

public class SessionInitializer extends AbstractHttpSessionApplicationInitializer {

    public SessionInitializer () {
        super(SessionConfig.class);
    }
}

然后在application.yml中配置好redis的信息,就OK了

spring:
  redis:
    prefix: lls
    open: true  # 是否开启redis缓存  true开启   false关闭
    database: 0
    host: 127.0.0.1
    port: 6379
    password:       # 密码(默认为空)
    timeout: 6000             # 连接超时时长(毫秒)
    pool:
      max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
      max-wait: -1      # 连接池最大阻塞等待时间(使用负值表示没有限制)
      max-idle: 10      # 连接池中的最大空闲连接
      min-idle: 5       # 连接池中的最小空闲连接

至此,spring-session和redis、shiro、springboot就已经完全集成完毕。快去测试一下吧!


因为项目中也使用了zuul作为网关:

在实际的使用中,我们发现每次请求经过zuul的时候,session就会发生改变,导致后面的请求获取不到session中的内容,该问题疑似是cookie丢失导致的,目前的解决方案是每次请求的时候,前端都将cookie信息带过来,从而获取session里面的信息,如有更好的解决方案,还请不吝赐教。