前后端分离shiro+redis实现分布式会话(session)

在传统的前后端分离模式中,我们通常是在请求头中增加一个请求头Authorization,它的值是一串加密的信息或者密钥,在后台通过对这个请求头值的读取,获取用户的信息。

而在这样的模式中,通常都是开发者自己设计的session或者加密(比如JWT)方式来读取和保存用户信息,而在shiro中,集成了权限控制和用户管理在它的session系统中,这就意味着我们只能通过他所规定的session+cookie来保存用户信息,在这种情况下,该以什么方式在前后端分离的项目中使用shiro?

通过资料的查询,和对shiro设计模式的解读,我发现shiro和servlet一样实在cookie中存储一个session会话的id然后在每次请求中读取该session的id并获取session,这样就可以获取指定session中储存的用户信息。

我们通过重写shiro中获取cookie中的sessionId的方法来获取请求头Authorization中的密钥,而密钥储存的便是登录是返回的sessionId,从而实现在前后端分离的项目中使用shiro框架。

参考代码
https://github.com/gemingyi/shiro_demo

我们来看核心代码

  • 自定义MySessionManager重写DefaultWebSessionManager类
public class MySessionManager extends DefaultWebSessionManager {
    //前端请求头传这个
    private static final String AUTHORIZATION = "Authorization";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    public MySessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中有 Authorization 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}
  • 自定义RedisSessionDAO重写AbstractSessionDAO
public class RedisSessionDAO extends AbstractSessionDAO {
    private static final long DEFAULT_SESSION_LIVE  = 3600L;
    private static final String DEFAULT_SESSION_KEY_PREFIX  = "shiro_redis_session:";
    private long sessionLive = DEFAULT_SESSION_LIVE;
    private String sessionKeyPrefix = DEFAULT_SESSION_KEY_PREFIX;

    //使用的是spring-data-redis
    private JedisConnectionFactory jedisConnectionFactory;

    private byte[] StringToByte(String string) {
        return string == null?null:string.getBytes();
    }

    //使用spring自带的序列工具类
    private byte[] objectToByteArray(Object obj) {
        return SerializationUtils.serialize(obj);
    }

    private Object byteArrayToObject(byte data[]) {
        return SerializationUtils.deserialize(data);
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        this.saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        RedisConnection connection = this.jedisConnectionFactory.getConnection();
        Session session = null;
        try {
            session = (Session) this.byteArrayToObject(connection.get(this.getRedisSessionKey(sessionId)));
        } catch (Exception e) {
            e.printStackTrace();
        }
        connection.close();
        return session;
    }

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

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            return;
        }
        RedisConnection connection = this.jedisConnectionFactory.getConnection();
        try{
            connection.del(this.getRedisSessionKey(session.getId()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        connection.close();
    }


    @Override
    public Collection<Session> getActiveSessions() {
        Set<Session> allSessions = new HashSet<>();
        RedisConnection connection = this.jedisConnectionFactory.getConnection();
        try {
            Set<byte[]> keys = connection.keys(this.objectToByteArray(this.sessionKeyPrefix + "*"));
            if(keys != null && keys.size() > 0) {
                for (byte[] key : keys) {
                    allSessions.add((Session) this.byteArrayToObject(connection.get(key)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        connection.close();
        return allSessions;
    }


    private void saveSession(Session session) {
        RedisConnection connection = this.jedisConnectionFactory.getConnection();
        try {
            byte[] key = this.getRedisSessionKey(session.getId());
            byte[] value = this.objectToByteArray(session);
            connection.setEx(key, sessionLive, value);
        } catch (Exception e){
            e.printStackTrace();
        }
        connection.close();

    }


    private byte[] getRedisSessionKey(Serializable sessionId) {
        return this.StringToByte((this.sessionKeyPrefix + sessionId));
    }



    public void setSessionLive(long sessionLive) {
        this.sessionLive = sessionLive;
    }

    public void setSessionKeyPrefix(String sessionKeyPrefix) {
        this.sessionKeyPrefix = sessionKeyPrefix;
    }

    public void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) {
        this.jedisConnectionFactory = jedisConnectionFactory;
    }
}

自定义MyRealm继承AuthorizingRealm类,这个我就不贴出来了

配置shiro执行我们重写的类,下面是ShiroConfiguration类

@Configuration
public class ShiroConfiguration {
    @Value("${shiro.redis.sessionLive}")
    private long sessionLive;
    @Value("${shiro.redis.sessionPrefix}")
    private String sessionPrefix;
    @Value("${shiro.redis.cacheLive}")
    private long cacheLive;
    @Value("${shiro.redis.cachePrefix}")
    private String cachePrefix;


    /**
     * 凭证匹配器(密码加密)
     * @return
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //加密算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //加密的次数
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }


    /**
     * 定义Session ID生成管理器
     * @return
     */
    @Bean(name = "sessionIdGenerator")
    public JavaUuidSessionIdGenerator sessionIdGenerator() {
        JavaUuidSessionIdGenerator sessionIdGenerator = new JavaUuidSessionIdGenerator();
        return sessionIdGenerator;
    }


    /**
     * 自定义redisSessionDAO(session存放在redis中)
     * @return
     */

    @Bean(name = "redisSessionDAO")
    public RedisSessionDAO redisSessionDAO(JavaUuidSessionIdGenerator sessionIdGenerator, @Qualifier("jedisConnectionFactory")JedisConnectionFactory jedisConnectionFactory) {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator);
        //session过期时间及前缀
        redisSessionDAO.setSessionLive(sessionLive);
        redisSessionDAO.setSessionKeyPrefix(sessionPrefix);
        //注入jedisConnectionFactory
        redisSessionDAO.setJedisConnectionFactory(jedisConnectionFactory);
        return redisSessionDAO;

    }


    /**
     * 自定义sessionManager
     * @return
     */
    @Bean(name = "sessionManager")
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO);
        return mySessionManager;
    }

    @Bean(name = "myRealm")
    public MyRealm myRealm() {
        MyRealm myShiroRealm = new MyRealm();
//        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        myShiroRealm.setCachingEnabled(true);
        return myShiroRealm;
    }


    @Bean(name = "securityManager")
    public SecurityManager securityManager(SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }


    @Bean(name = "shirFilter")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //注意过滤器配置顺序 不能颠倒
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
        //退出
        filterChainDefinitionMap.put("/logout", "logout");
        //匿名访问 跳转页面
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/userLogin", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        //
        filterChainDefinitionMap.put("/**", "authc");
        //未认证 跳转页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 认证成功  跳转页面
//        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授权 跳转页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
}


最后、看看Controller层登录方法

@RequestMapping(value = "/userLogin", method = RequestMethod.POST)
public Map ajaxLogin(User userInfo) {
    Map result = new HashMap<>();
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(userInfo.getUserName(), userInfo.getPassword());
    try {
        subject.login(token);
        result.putAll(ResponseEntity.responseSuccess(subject.getSession().getId()));
    } catch (Exception e) {
        result.put("code", CodeAndMsgEnum.ERROR.getcode());
        result.put("msg", e.getMessage());
    }
    return result;
}

好了,关键代码就这些

下面我们看看,前端测试(我用的是postman)

Redis中session信息

 

参考文章

  1. 在前后端分离的项目中,后台使用shiro框架时,怎样使用它的会话管理系统(session),从而实现权限控制
  2. 在前后端分离的SpringBoot项目中集成Shiro权限框架
  • 0
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值