分布式无状态服务下的业务与数据权限治理

2020-02-29  本文已影响0人  geweixinerr

久未写博客,文字略有生疏。这篇主要是关于分布式集群架构下,无状态应用服务器基于Shiro与Jwt的集成解决业务与数据权限治理的技术方案。本文不涉及任何权限数据模型。

在叙述之前,首先先来了解下何为无状态应用服务器,一般来说Web应用或多或少都会涉及到状态这个概念,何为状态?通俗点说,即对服务器的访问产生了留存数据。客户端的访问行为依赖于这些数据,且这些数据会随着请求的动作而产生变化,如Java当中的HttpSession,此即为状态。
我们先看下常规的应用服务器集群架构,如下图:


架构图.png

假设我们的应用服务器是存在状态的,使用HttpSession或者其他。此刻由于访问量激增需要弹性扩容,那么我们是否可以保证客户端的访问不受影响呢?譬如:保持登录状。客观的说这不难,一些硬件级别的设备如F5或软负载Nginx都提供了会话保持的粘滞策略,但都有其局限性。就举一个场景来表述下这个局限性:路由的目标服务器扩容期间崩溃了,那所有的状态数据就都丢失了,显然无法满足高可用特性。那我们再看看无状态服务器,其不存储任何状态信息。所有的集群节点下任意一台服务器都是一样的,任何一台宕机都不影响服务的可用性,完全满足高可用性的设计指标。

于此,一些基础知识的普及已经完成,有想更深入了解的同学推荐一本书<大型网站技术架构>。再回过头看我们需要解决的问题:1.无状态下的业务与数据权限治理,安全这块侧重于解决token防窃持。 我们采用的技术栈是SpringBoot + Shiro + Jwt, 这个技术栈当中Shrio的权限治理默认是基于用户会话的与HttpSession类似。显然这个默认行为违背了我们设计无状态应用服务器的初衷。我们需要加以改造,核心代码如下:

    @Bean(value = "securityManager")
    public SecurityManager securityManager(@Qualifier(value = "userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        // 无状态应用服务器禁止session创建
        DefaultSubjectDAO subjectDao = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator webSubjectDao = (DefaultSessionStorageEvaluator) subjectDao
                .getSessionStorageEvaluator();
        webSubjectDao.setSessionStorageEnabled(false);
        securityManager.setSubjectDAO(subjectDao);
        securityManager.setSubjectFactory(new StatelessDefaultSubjectFactory());
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setSessionValidationSchedulerEnabled(false);
        securityManager.setSessionManager(defaultWebSessionManager);

        return securityManager;
    }

完成了这块的改造后,Shiro也就完成了无状态的改造。

那如何与Jwt集成呢?首先我们需要了解什么是Jwt,根据百度百科描述:Jwt是轻量级的客户端与服务器认证通信解决方案。Jwt数值存储于客户端,介质可以是cookie/localStorage或者其它,每次请求时需要将Jwt签名数值传递至服务器, 服务器主要完成对Jwt的验签即可完成对访问请求的准确性认证。部分集成代码如下:

    /**
     * jwt 拦截具体动作
     * 
     * @author gewx
     **/
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        // 从请求头或者URL当中获取token
        String token = ObjectUtils.defaultIfNull(req.getHeader(AUTH_TOKEN), req.getParameter(AUTH_TOKEN));
        if (StringUtils.isBlank(token)) {
            String responseJson = JSONObject
                    .toJSONString(Response.FAIL.newBuilder().addGateWayCode(GateWayCode.E0001).toResult());
            outFail(resp, responseJson);
            return false;
        }

        try {
            try {
                               //本地鉴权/远程鉴权
                boolean bool = JwtUtils.verifyToken(token);
                if (!bool) {
                    String responseJson = JSONObject.toJSONString(
                            Response.FAIL.newBuilder().addGateWayCode(GateWayCode.E0002).out("鉴权失败~").toResult());
                    outFail(resp, responseJson);
                    return false;
                }
            } finally {
                /**
                 * create new token 无论认证通过与否,token必须具备一次消费属性
                 **/
                Jwt.JwtBean bean = JwtUtils.parseToken(token);
                JwtToken jwtToken = new JwtToken(
                        Jwt.create().setUserName(bean.getUserName()).setExpires(30).build().sign());
                getSubject(request, response).login(jwtToken);
                resp.setHeader(AUTH_TOKEN, jwtToken.getToken());
            }
        } catch (Exception ex) {
            String responseJson = JSONObject.toJSONString(
                    Response.FAIL.newBuilder().addGateWayCode(GateWayCode.E9999).out("token 认证失败~").toResult());
            outFail(resp, responseJson);
            return false;
        }
        return true;
    }

基于此,Shrio与Jwt核心集成部分就算是结束了。那我们接着聊聊Session服务器的作用之一:解决token窃持。先上图:


鉴权.jpg

何为token窃持? 即发送给客户端的token令牌被第三方中间人获取,中间人通过此token模拟合法身份进行恶意请求。那如上架构是如何解决的呢?核心的思路是令牌必须只能消费一次,基于Session服务器利用缓存或其他存储介质完成token认证留痕。凡是存在消费记录的token都会被记录下来,即可判断出是否是重复使用。当然如果请求量大,对于缓存的存储压力也是比较大的。个人设计的Jwt内部结构如下:

//userName为持有人,expiresDate为过期时间
{"expires":30,"expiresDate":1582945397426,"userName":"userName"}

从上图可以看出鉴权分为本地鉴权与远程鉴权:本地鉴权根据expiresDate做判断,集群环境下需要保持时钟一致性。 本地鉴权通过-->远程鉴权,采用Redis作为存储介质可以设置过期时间 > expiresDate即可。如token有效期30分钟,缓存可以设置为35分钟即可,有效解决了token大量堆积问题。

灵魂一问:假设窃持发生在第一次请求时,该如何处理呢?即token未被消费时即被窃持了。好吧,有时间再写下一篇吧。核心思路是:数据防篡改。

PS:题外话1:窃持问题不仅仅是token设计独有的,HttpSession也有。它是同种问题的变种延伸。题外话2:有条件建议全站Https,真的可以省去很多问题。

上一篇 下一篇

猜你喜欢

热点阅读