一起来学SpringBoot(十六)优雅的整合Shiro

Apache Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。借助Shiro易于理解的API,您可以快速轻松地保护任何应用程序 - 从最小的移动应用程序到最大的Web和企业应用程序。网上找到大部分文章都是以前SpringMVC下的整合方式,很多人都不知道shiro提供了官方的starter可以方便地跟SpringBoot整合。

请看shiro官网关于springboot整合shiro的链接:Integrating Apache Shiro into Spring-Boot Applications

整合准备

这篇文档的介绍也相当简单。我们只需要按照文档说明,然后在spring容器中注入一个我们自定义的Realm,shiro通过这个realm就可以知道如何获取用户信息来处理鉴权(Authentication),如何获取用户角色、权限信息来处理授权(Authorization)。如果是web应用程序的话需要引入shiro-spring-boot-web-starter,单独的应用程序的话则引入shiro-spring-boot-starter

依赖

<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-spring-boot-web-starterartifactId>
    <version>1.4.0-RC2version>
dependency>

用户实体

首先创建一个用户的实体,用来做认证

package com.maoxs.pojo;

import lombok.Data;

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

@Data
public class User  implements Serializable {
    private Long uid;       // 用户id
    private String uname;   // 登录名,不可改
    private String nick;    // 用户昵称,可改
    private String pwd;     // 已加密的登录密码
    private String salt;    // 加密盐值
    private Date created;   // 创建时间
    private Date updated;   // 修改时间
    private Set<String> roles = new HashSet<>();    //用户所有角色值,用于shiro做角色权限的判断
    private Set<String> perms = new HashSet<>();    //用户所有权限值,用于shiro做资源权限的判断
}

这里了为了方便,就不去数据库读取了,方便测试我们把,权限信息,角色信息,认证信息都静态模拟下。

Resources

package com.maoxs.service;

import org.springframework.stereotype.Service;

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

@Service
public class ResourcesService {
    /**
     * 模拟根据用户id查询返回用户的所有权限
     *
     * @param uid
     * @return
     */
    public Set<String> getResourcesByUserId(Long uid) {
        Set<String> perms = new HashSet<>();
        //三种编程语言代表三种角色:js程序员、java程序员、c++程序员
        //docker的权限
        perms.add("docker:run");
        perms.add("docker:ps");
        //maven的权限
        perms.add("mvn:debug");
        perms.add("mvn:test");
        perms.add("mvn:install");
        //node的权限
        perms.add("npm:clean");
        perms.add("npm:run");
        perms.add("npm:test");
        return perms;
    }

}

Role

package com.maoxs.service;

import org.springframework.stereotype.Service;

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

@Service
public class RoleService {

    /**
     * 模拟根据用户id查询返回用户的所有角色
     *
     * @param uid
     * @return
     */
    public Set<String> getRolesByUserId(Long uid) {
        Set<String> roles = new HashSet<>();
        //这里用三个工具代表角色
        roles.add("docker");
        roles.add("maven");
        roles.add("node");
        return roles;
    }

}

User

package com.maoxs.service;

import com.maoxs.pojo.User;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Random;

@Service
public class UserService {

    /**
     * 模拟查询返回用户信息
     *
     * @param uname
     * @return
     */
    public User findUserByName(String uname) {
        User user = new User();
        user.setUname(uname);
        user.setNick(uname + "NICK");
        user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密码明文是123456
        user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密码的盐值
        user.setUid(new Random().nextLong());//随机分配一个id
        user.setCreated(new Date());
        return user;
    }
}

认证

Shiro 从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource , 即安全数据源。

Realm

package com.maoxs.realm;

import com.maoxs.cache.MySimpleByteSource;
import com.maoxs.pojo.User;
import com.maoxs.service.ResourcesService;
import com.maoxs.service.RoleService;
import com.maoxs.service.UserService;
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.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;

/**
 * 这个类是参照JDBCRealm写的,主要是自定义了如何查询用户信息,如何查询用户的角色和权限,如何校验密码等逻辑
 */
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private ResourcesService resourcesService;

    //告诉shiro如何根据获取到的用户信息中的密码和盐值来校验密码
    {
        //设置用于匹配密码的CredentialsMatcher
        HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
        hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        hashMatcher.setStoredCredentialsHexEncoded(false);
        hashMatcher.setHashIterations(1024);
        this.setCredentialsMatcher(hashMatcher);
    }


    //定义如何获取用户的角色和权限的逻辑,给shiro做权限判断
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        User user = (User) getAvailablePrincipal(principals);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        System.out.println("获取角色信息:" + user.getRoles());
        System.out.println("获取权限信息:" + user.getPerms());
        info.setRoles(user.getRoles());
        info.setStringPermissions(user.getPerms());
        return info;
    }

    //定义如何获取用户信息的业务逻辑,给shiro做登录
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        // Null username is invalid
        if (username == null) {
            throw new AccountException("请输入用户名");
        }
        User userDB = userService.findUserByName(username);
        if (userDB == null) {
            throw new UnknownAccountException("用户不存在");
        }
        //查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方
        //SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
        Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
        Set<String> perms = resourcesService.getResourcesByUserId(userDB.getUid());
        userDB.getRoles().addAll(roles);
        userDB.getPerms().addAll(perms);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
        if (userDB.getSalt() != null) {
            info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt()));
        }
        return info;
    }

}

相关配置

然后呢在只需要吧这个Realm注册到Spring容器中就可以啦

@Bean
public CustomRealm customRealm() {
   CustomRealm realm = new CustomRealm();
   return realm;  
}

为了保证实现了Shiro内部lifecycle函数的bean执行 也是shiro的生命周期,注入LifecycleBeanPostProcessor

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
	return new LifecycleBeanPostProcessor();
}

紧接着配置安全管理器,SecurityManager是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

@Bean
public DefaultWebSecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm());
    return securityManager;
}

除此之外Shiro是一堆一堆的过滤链,所以要对shiro 的过滤进行设置,

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    chainDefinition.addPathDefinition("favicon.ico", "anon");
    chainDefinition.addPathDefinition("/login", "anon");
    chainDefinition.addPathDefinition("/**", "user");
    return chainDefinition;
}

如果想要自定义过滤链那么 ShiroFilterChainDefinition 就不ok了 我们就要换个写法 定义一个 ShiroFilterFactoryBean

 /**
  * 不需要在此处配置权限页面,因为上面的ShiroFilterFactoryBean已经配置过,
  * 但是此处必须存在,因为shiro-spring-boot-web-starter或查找此Bean,没有会报错
  *
  * @return
  */
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    return new DefaultShiroFilterChainDefinition();
}

/* *********************************************shiro过滤连**********************************************/
@Bean
 public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
     ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
     shiroFilterFactoryBean.setSecurityManager(securityManager);
     //拦截器
     Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
     // 添加自己的过滤器并且取名
     Map<String, Filter> filterMap = new HashMap<>(16);
     filterMap.put("my", new MyFilter());
     shiroFilterFactoryBean.setFilters(filterMap);
     filterChainDefinitionMap.put("login", "anon");
     //
    simpleCookie.setMaxAge(259200);
    return simpleCookie;
}

然后呢配置rememberMeManager

@Bean
public CookieRememberMeManager rememberMeManager() {
    //System.out.println("ShiroConfiguration.rememberMeManager()");
    CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    cookieRememberMeManager.setCookie(rememberMeCookie());
    //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
    cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
    return cookieRememberMeManager;
}

rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中。

@Bean
public DefaultWebSecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm(redisCacheManager));
    securityManager.setRememberMeManager(rememberMeManager());
    return securityManager;
}

好了记住我功能就到这里了,不过要记住一点,如果使用了authc的过滤的url的是不能使用记住我功能的,切记,至于什么原因,很好理解。有一些操作你是不需要别人在记住我功能下完成的,这样很不安全,所以shiro规定记住我功能最多得user级别的,不能到authc级别

启用缓存

Shiro提供了类似Spring的Cache抽象,即Shiro本身不实现Cache,但是对Cache进行了又抽象,方便更换不同的底层Cache实现。对应前端的一个页面访问请求会同时出现很多的权限查询操作,这对于权限信息变化不是很频繁的场景,每次前端页面访问都进行大量的权限数据库查询是非常不经济的。因此,非常有必要对权限数据使用缓存方案。

由于Spring和Shiro都各自维护了自己的Cache抽象,为防止Realm注入的service里缓存注解和事务注解失效,所以定义自己的CacheManager处理缓存。

整合Redis

CacheManager代码如下。

package com.maoxs.cache;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.util.Destroyable;
import org.springframework.data.redis.cache.RedisCacheManager;

import java.util.Collection;
import java.util.Set;

public class ShiroRedisCacheManager implements CacheManager, Destroyable {
    private RedisCacheManager cacheManager;

    public RedisCacheManager getCacheManager() {
        return cacheManager;
    }

    public void setCacheManager(RedisCacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    //为了个性化配置redis存储时的key,我们选择了加前缀的方式,所以写了一个带名字及redis操作的构造函数的Cache类
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        if (name == null) {
            return null;
        }
        return new ShiroRedisCache<K, V>(name, getCacheManager());
    }

    @Override
    public void destroy() throws Exception {
        cacheManager = null;
    }

    /**
    * 

自定义缓存 将数据存入到redis中

* * @param * @param * @author xxx * @date 2018年2月1日 * @time 22:32:11 */
@Slf4j class ShiroRedisCache<K, V> implements org.apache.shiro.cache.Cache<K, V> { private RedisCacheManager cacheManager; private org.springframework.cache.Cache cache; // private RedisCache cache2; public ShiroRedisCache(String name, RedisCacheManager cacheManager) { if (name == null || cacheManager == null) { throw new IllegalArgumentException("cacheManager or CacheName cannot be null."); } this.cacheManager = cacheManager; //这里首先是从父类中获取这个cache,如果没有会创建一个redisCache,初始化这个redisCache的时候 //会设置它的过期时间如果没有配置过这个缓存的,那么默认的缓存时间是为0的,如果配置了,就会把配置的时间赋予给这个RedisCache //如果从缓存的过期时间为0,就表示这个RedisCache不存在了,这个redisCache实现了spring中的cache this.cache = cacheManager.getCache(name); } @Override public V get(K key) throws CacheException { log.info("从缓存中获取key为{}的缓存信息", key); if (key == null) { return null; } org.springframework.cache.Cache.ValueWrapper valueWrapper = cache.get(key); if (valueWrapper == null) { return null; } return (V) valueWrapper.get(); } @Override public V put(K key, V value) throws CacheException { log.info("创建新的缓存,信息为:{}={}", key, value); cache.put(key, value); return get(key); } @Override public V remove(K key) throws CacheException { log.info("干掉key为{}的缓存", key); V v = get(key); cache.evict(key);//干掉这个名字为key的缓存 return v; } @Override public void clear() throws CacheException { log.info("清空所有的缓存"); cache.clear(); } @Override public int size() { return cacheManager.getCacheNames().size(); } /** * 获取缓存中所的key值 */ @Override public Set<K> keys() { return (Set<K>) cacheManager.getCacheNames(); } /** * 获取缓存中所有的values值 */ @Override public Collection<V> values() { return (Collection<V>) cache.get(cacheManager.getCacheNames()).get(); } @Override public String toString() { return "ShiroSpringCache [cache=" + cache + "]"; } } }

然后呢就是把这个CacheManager注入到securityManager中

@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
    Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper mapper = new ObjectMapper();
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    serializer.setObjectMapper(mapper);
    template.setValueSerializer(serializer);
    //使用StringRedisSerializer来序列化和反序列化redis的key值
    template.setKeySerializer(new StringRedisSerializer());
    template.afterPropertiesSet();
    return template;
}
/**
* Spring缓存管理器配置
*
* @param redisTemplate
* @return
*/
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
    CollectionSerializer<Serializable> collectionSerializer = CollectionSerializer.getInstance();
    RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
    RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofHours(1))
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(collectionSerializer));
    return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
/**
* shiro缓存管理器的配置
*
* @param redisCacheManager
* @return
*/
@Bean
public ShiroRedisCacheManager shiroRedisCacheManager(RedisCacheManager redisCacheManager) {
    ShiroRedisCacheManager cacheManager = new ShiroRedisCacheManager();
    cacheManager.setCacheManager(redisCacheManager);
    //name是key的前缀,可以设置任何值,无影响,可以设置带项目特色的值
    return cacheManager;
}

相对应的Realm和securityManager也要稍做更改

@Bean
public CustomRealm customRealm(RedisCacheManager redisCacheManager) {
    CustomRealm realm = new CustomRealm();
    realm.setCachingEnabled(true);
    //设置认证密码算法及迭代复杂度
    //realm.setCredentialsMatcher(credentialsMatcher());
    //认证
    realm.setCacheManager(shiroRedisCacheManager(redisCacheManager));
    realm.setAuthenticationCachingEnabled(true);
    //授权
    realm.setAuthorizationCachingEnabled(true);
    //这里主要是缓存key的名字
    realm.setAuthenticationCacheName("fulinauthen");
    realm.setAuthenticationCacheName("fulinauthor");
    return realm;
}
@Bean
public DefaultWebSecurityManager securityManager(RedisCacheManager redisCacheManager) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm(redisCacheManager));
    securityManager.setCacheManager(shiroRedisCacheManager(redisCacheManager));
    securityManager.setRememberMeManager(rememberMeManager());
    return securityManager;
}

这样的话每次认证的时候就会把权限信息放入redis中,就不用反复的去查询数据库了。

注意

Realm里注入的UserService等service,需要延迟注入,所以都要添加@Lazy注解(如果不加需要自己延迟注入),否则会导致该service里的@Cacheable缓存注解、@Transactional事务注解等失效

整合的时候应该会有人遇到不能序列化的问题吧,原因是因为用了Shiro的SimpleAuthenticationInfo中的setCredentialsSalt注入的属性ByteSource没有实现序列化接口,此时呢只用把源码一贴,实现下序列化接口即可

package com.maoxs.cache;

import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;

/**
 * 解决ByteSource 序列化问题
 */
public class MySimpleByteSource implements ByteSource, Serializable {
    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MySimpleByteSource() {
    }

    public MySimpleByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MySimpleByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MySimpleByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MySimpleByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MySimpleByteSource(File file) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
    }

    public MySimpleByteSource(InputStream stream) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public byte[] getBytes() {
        return this.bytes;
    }

    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    public String toString() {
        return this.toBase64();
    }

    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource) o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}

然后在realm中改变使用

if (userDB.getSalt() != null) {
	info.setCredentialsSalt(new MySimpleByteSource(userDB.getSalt()));
}

整合Ehcache

整合ehcache就更简单,套路都是一样的只不过2.x和3.x 需要注入不同的CacheManager即可。这里需要注入下3.x的Ehcache是实现了Jcache,不过整合起来都是一样的,详情可以去看我之前的整合Spring抽象缓存的帖子。

官方提供了shiro-ehcache的整合包,不过这个整合包是针对Ehcache2.x的。

Redis存储Session

关于共享session的问题大家都应该知道了,传统的部署项目,两个相同的项目部署到不同的服务器上,Nginx负载均衡后会导致用户在A上登陆了,经过负载均衡后,在B上要重新登录,因为A上有相关session信息,而B没有。这种情况也称为“有状态”服务。而“无状态”服务则是:在一个公共的地方存储session,每次访问都会统一到这个地方来拿。思路呢就是实现Shiro的Session接口,然后呢自己控制,这里我们实现AbstractSessionDAO。

package com.maoxs.cache;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;


@Slf4j
public class ShiroRedisSessionDao extends AbstractSessionDAO {

    private RedisTemplate redisTemplate;

    public ShiroRedisSessionDao(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        log.info("更新seesion,id=[{}]", session.getId().toString());
        try {
            redisTemplate.opsForValue().set(session.getId().toString(), session, 30, TimeUnit.MINUTES);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    @Override
    public void delete(Session session) {
        log.info("删除seesion,id=[{}]", session.getId().toString());
        try {
            String key = session.getId().toString();
            redisTemplate.delete(key);
        } catch (Exception e) {
            log.info(e.getMessage(), e);
        }

    }

    @Override
    public Collection<Session> getActiveSessions() {
        log.info("获取存活的session");
        return Collections.emptySet();
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        log.info("创建seesion,id=[{}]", session.getId().toString());
        try {
            redisTemplate.opsForValue().set(session.getId().toString(), session, 30, TimeUnit.MINUTES);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        log.info("获取seesion,id=[{}]", sessionId.toString());
        Session readSession = null;
        try {
            readSession = (Session) redisTemplate.opsForValue().get(sessionId.toString());
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return readSession;
    }

}

最后吧你写好的SessionDao注入到shiro的securityManager中即可

/**
* 配置sessionmanager,由redis存储数据
*/
@Bean(name = "sessionManager")
public DefaultWebSessionManager sessionManager(RedisTemplate redisTemplate) {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    CollectionSerializer<Serializable> collectionSerializer = CollectionSerializer.getInstance();
    redisTemplate.setDefaultSerializer(collectionSerializer);
    //redisTemplate默认采用的其实是valueSerializer,就算是采用其他ops也一样,这是一个坑。
    redisTemplate.setValueSerializer(collectionSerializer);
    ShiroRedisSessionDao redisSessionDao = new ShiroRedisSessionDao(redisTemplate);
    //这个name的作用也不大,只是有特色的cookie的名称。
    sessionManager.setSessionDAO(redisSessionDao);
    sessionManager.setDeleteInvalidSessions(true);
    SimpleCookie cookie = new SimpleCookie();
    cookie.setName("starrkCookie");
    sessionManager.setSessionIdCookie(cookie);
    sessionManager.setSessionIdCookieEnabled(true);
    return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(RedisTemplate redisTemplate, RedisCacheManager redisCacheManager) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm(redisCacheManager));
    securityManager.setCacheManager(shiroRedisCacheManager(redisCacheManager));
    securityManager.setRememberMeManager(rememberMeManager());
    securityManager.setSessionManager(sessionManager(redisTemplate));
    return securityManager;
}

这样每次读取Session就会从Redis中取读取了,当然还有谢谢开源的插件解决方案,比如crazycake ,有机会在补充这个。

本博文是基于springboot2.x 如果有什么不对的请在下方留言。

你可能感兴趣的:(一起来学SpringBoot)