kisso 常用拦截器源码解析记录,基于3.7.0版本

Kisso 拦截器源码解析

什么是kisso

kisso = cookie sso 基于 Cookie 的 SSO 中间件,它是一把快速开发 java Web 登录系统(SSO)的瑞士军刀。(源自官网介绍)kisso的认证就是基于jwt实现的。

jwt的概念

它是json web tokens 的缩写,翻译过来,就是json web令牌

作用:
授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。它的开销很小并且可以在不同的域中使用。如:单点登录。

信息交换:在各方之间安全地传输信息。JWT可进行签名(如使用公钥/私钥对),因此可确保发件人。由于签名是使用标头和有效负载计算的,因此还可验证内容是否被篡改。

基于kisso的3.7版本拜读了一下大佬的代码,kisso的源码很精简,其中有个人感觉比较关键的拦截器,下面一个一个看过去,特此记录,不写下来感觉等于白看~

KissoAbstractInterceptor

public abstract class KissoAbstractInterceptor {

    /* SSO 拦截控制器 */
    private SSOHandlerInterceptor handlerInterceptor;

    public SSOHandlerInterceptor getHandlerInterceptor() {
        if (handlerInterceptor == null) {
            return KissoDefaultHandler.getInstance();
        }
        return handlerInterceptor;
    }

    public void setHandlerInterceptor(SSOHandlerInterceptor handlerInterceptor) {
        this.handlerInterceptor = handlerInterceptor;
    }
}

KissoAbstractInterceptor ,虽然命名成了拦截器,其实是一个抽象类,它只提供一个创建 SSOHandlerInterceptor 的方法,它有一个成员变量 SSOHandlerInterceptor 并且是个接口可以通过getHandlerInterceptor 对其进行初始化,项目中并没有涉及使用,如果想自定义 SSOHandlerInterceptor 可以实现这个类。

该接口定义了两个抽象方法,前者用于判断token 为空时进行拦截到ajax 方法,如何处理响应,后者用于处理其他请求未登录时的处理逻辑,通过实现这个抽象类,可以自定义这两类请求的处理逻辑,否则就用了kisso中默认的处理逻辑。

public interface SSOHandlerInterceptor {

    /**
     * token 为空未登录, 拦截到 AJAX 方法时
     *
     * @param request
     * @param response
     * @return
     */
    boolean preTokenIsNullAjax(HttpServletRequest request, HttpServletResponse response);

    /**
     * token 为空未登录, 自定义处理逻辑
     * <p>
     * 返回 true 继续执行(清理登录状态,重定向至登录界面),false 停止执行
     * </p>
     *
     * @param request
     * @param response
     * @return true 继续执行,false 停止执行
     */
    boolean preTokenIsNull(HttpServletRequest request, HttpServletResponse response);

kisso的默认实现如下:

/**
 * <p>
 * SSO 默认拦截处理器,自定义 Handler 可继承该类。
 * </p>
 *
 * @author hubin
 * @since 2015-12-19
 */
public class KissoDefaultHandler implements SSOHandlerInterceptor {

    private static KissoDefaultHandler handler;

    /**
     * new 当前对象
     */
    public static KissoDefaultHandler getInstance() {
        if (handler == null) {
            handler = new KissoDefaultHandler();
        }
        return handler;
    }

    /**
     * 未登录时,处理 AJAX 请求。
     * <p>
     * 返回 HTTP 状态码 401(未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
     * </p>
     */
    public boolean preTokenIsNullAjax(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.getWriter().write("{code:\"logout\", msg:\"Have logout\"}");
        } catch (IOException e) {
            // to do nothing
        }
        return false;
    }

    public boolean preTokenIsNull(HttpServletRequest request, HttpServletResponse response) {
        /* 预留子类处理 */
        return true;
    }

}

SSOSpringInterceptor

第二个拦截器是 SSOSpringInterceptor,这个拦截器实现了 HandlerInterceptorAdapter接口, 是一个典型的拦截器,只需要关注 override的 preHandle 以及postHandle 方法即可。

preHandle 方法中,首先判断是否为HandleMethod 的实例

如果是的话,则判断方法上有没有@Login 注释,有且 注释内的值为SKIP 则直接放行,没有正常执行认证

先获取当前request中的token,如果为null ,按照ajax 请求与非ajax 请求分类返回处理。具体实现在 KissoDefaultHandler 里面,这个类实现了 SSOHandlerInterceptor 接口,用实现上面提及的分类处理返回。

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
    /**
     * 处理 Controller 方法
     * <p>
     * 判断 handler 是否为 HandlerMethod 实例
     * </p>
     */
    if (handler instanceof HandlerMethod) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Login login = method.getAnnotation(Login.class);
        if (login != null) {
            if (login.action() == Action.Skip) {
                /**
                 * 忽略拦截
                 */
                return true;
            }
        }

        /**
         * 正常执行
         */
        SSOToken ssoToken = SSOHelper.getSSOToken(request);
        if (ssoToken == null) {
            if (HttpUtil.isAjax(request)) {
                /*
                 * Handler 处理 AJAX 请求
                 */
                this.getHandlerInterceptor().preTokenIsNullAjax(request, response);
                return false;
            } else {
                /*
                 * token 为空,调用 Handler 处理
                 * 返回 true 继续执行,清理登录状态并重定向至登录界面
                 */
                if (this.getHandlerInterceptor().preTokenIsNull(request, response)) {
                    logger.fine("logout. request url:" + request.getRequestURL());
                    SSOHelper.clearRedirectLogin(request, response);
                }
                return false;
            }
        } else {
            /*
             * 正常请求,request 设置 token 减少二次解密
             */
            request.setAttribute(SSOConstants.SSO_TOKEN_ATTR, ssoToken);
        }
    }

    /**
     * 通过拦截
     */
    return true;
}

如果不为null,则将request 中新增一个属性,用于减少后续解密的次数,最后通过拦截。

获取token的具体实现在这行代码中

SSOToken ssoToken = SSOHelper.getSSOToken(request);

可以看到调用了SSOHelper 中的这个方法,该方法会先获取 kissoservice 没有会new 一个 ConfigurableAbstractKissoService 使用,有的话直接进行调用。

// SSOHelper 类
public static <T extends SSOToken> T getSSOToken(HttpServletRequest request) {
    return (T) getKissoService().getSSOToken(request);
}
// SSOHelper 类
/**
 * Kisso 服务初始化
 */
public static ConfigurableAbstractKissoService getKissoService() {
    if (kissoService == null) {
        kissoService = new ConfigurableAbstractKissoService();
    }
    return kissoService;
}

然后就是调用ConfigurableAbstractKissoService 中的 getSSOToken 方法:

/**
 * 获取当前请求 SSOToken
 * <p>
 * 从 Cookie 解密 SSOToken 使用场景,拦截器,非拦截器建议使用 attrSSOToken 减少二次解密
 * </p>
 *
 * @param request
 * @return SSOToken {@link SSOToken}
 */
@Override
public SSOToken getSSOToken(HttpServletRequest request) {
    SSOToken tk = checkIpBrowser(request, cacheSSOToken(request, config.getCache()));
    /**
     * 执行插件逻辑
     */
    List<SSOPlugin> pluginList = config.getPluginList();
    if (pluginList != null) {
        for (SSOPlugin plugin : pluginList) {
            boolean valid = plugin.validateToken(tk);
            if (!valid) {
                return null;
            }
        }
    }
    return tk;
}

具体取获取token的逻辑则调用了cacheSSOToken方法:

具体逻辑:

  1. cache 是否为null 不为null 则尝试从传入请求获取ssoToken, 获取的到,判断是否过期,过期则清除返回null, 未过期则判断缓存是否宕机,没宕机cookie的登录时间是否和cache 中的登录时间相同,不相同则返回null。
  2. cache 为null 则尝试从header中解析token,如果有解析过的jwtToken在请求头中,则直接拿来解析并返回,没有的话,再尝试从cookie 中解析并返回,cookie中还找不到,则返回null。
  3. 执行完上述逻辑后再执行可能配置的插件处理逻辑,至此sso拦截器就处理完了,如果通过,请求里面的header 会加入对应的token,没有则会被重定向到登录页
/**
 * <p>
 * SSOToken 是否缓存处理逻辑
 * </p>
 * <p>
 * 判断 SSOToken 是否缓存 , 如果缓存不存退出登录
 * </p>
 *
 * @param request
 * @return SSOToken {@link SSOToken}
 */
protected SSOToken cacheSSOToken(HttpServletRequest request, SSOCache cache) {
    /**
     * 如果缓存不存退出登录
     */
    if (cache != null) {
        SSOToken cookieSSOToken = getSSOTokenFromCookie(request);
        if (cookieSSOToken == null) {
            /* 未登录 */
            return null;
        }

        SSOToken cacheSSOToken = cache.get(cookieSSOToken.toCacheKey(), config.getCacheExpires());
        if (cacheSSOToken == null) {
            /* 开启缓存且失效,返回 null 清除 Cookie 退出 */
            logger.debug("cacheSSOToken SSOToken is null.");
            return null;
        } else {
            /*
             * 开启缓存,判断是否宕机:
 * 1、缓存正常,返回 tk
 * 2、缓存宕机,执行读取 Cookie 逻辑
 */
            if (cacheSSOToken.getFlag() != TokenFlag.CACHE_SHUT) {
                /*
                 * 验证 cookie 与 cache 中 SSOToken 登录时间是否<br>
     * 不一致返回 null
     */
                if (cookieSSOToken.getTime() == cacheSSOToken.getTime()) {
                    return cacheSSOToken;
                } else {
                    logger.debug("Login time is not consistent or kicked out.");
                    request.setAttribute(SSOConstants.SSO_KICK_FLAG, SSOConstants.SSO_KICK_USER);
                    return null;
                }
            }
        }
    }

    /**
     * SSOToken 为 null 执行以下逻辑
     */
    return getSSOToken(request, config.getCookieName());
}

SSOPermissionInterceptor

这个基本逻辑与上面看过的 SSOSpringInterceptor 类似,校验请求携带的token 是否他有权限访问当前url,或者请求的方法带有@Permission 注释,值为skip,只不过缺少授权的时候的返回处理不一致。如果没有权限,则返回 403 Forbidden页面以及配置的非法请求提示 的url。

难以理解的是为什么请求里面获取不到token, 也放行请求了,后续再研究研究吧。

**
 * <p>
 * kisso 权限拦截器(必须在 kisso 拦截器之后执行)
 * </p>
 *
 * @author hubin
 * @since 2016-04-03
 */
public class SSOPermissionInterceptor extends HandlerInterceptorAdapter {

    private static final Logger logger = Logger.getLogger("SSOPermissionInterceptor");

    /*
     * 系统权限授权接口
     */
    private SSOAuthorization authorization;

    /*
     * 非法请求提示 URL
     */
    private String illegalUrl;

    /**
     * 无注解情况下,设置为true,不进行权限验证
     */
    private boolean nothingAnnotationPass = false;

    /**
     * <p>
     * 用户权限验证
     * </p>
     * <p>
     * 方法拦截 Controller 处理之前进行调用。
     * </p>
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        if (handler instanceof HandlerMethod) {
            SSOToken token = SSOHelper.attrToken(request);
            if (token == null) {
                return true;
            }

            /*
             * 权限验证合法
             */
            if (isVerification(request, handler, token)) {
                return true;
            }

            /*
             * 无权限访问
             */
            return unauthorizedAccess(request, response);
        }

        return true;
    }


    /**
     * <p>
     * 判断权限是否合法,支持 1、请求地址 2、注解编码
     * </p>
     *
     * @param request
     * @param handler
     * @param token
     * @return
     */
    protected boolean isVerification(HttpServletRequest request, Object handler, SSOToken token) {
        /*
         * URL 权限认证
         */
        if (SSOConfig.getInstance().isPermissionUri()) {
            String uri = request.getRequestURI();
            if (uri == null || this.getAuthorization().isPermitted(token, uri)) {
                return true;
            }
        }
        /*
         * 注解权限认证
         */
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Permission pm = method.getAnnotation(Permission.class);
        if (pm != null) {
            if (pm.action() == Action.Skip) {
                /**
                 * 忽略拦截
                 */
                return true;
            } else if (!"".equals(pm.value()) && this.getAuthorization().isPermitted(token, pm.value())) {
                /**
                 * 权限合法
                 */
                return true;
            }
        } else if (this.isNothingAnnotationPass()) {
            /**
             * 无注解情况下,设置为true,不进行期限验证
             */
            return true;
        }
        /*
         * 非法访问
         */
        return false;
    }


    /**
     * <p>
     * 无权限访问处理,默认返回 403  ,illegalUrl 非空重定向至该地址
     * </p>
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    protected boolean unauthorizedAccess(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logger.fine(" request 403 url: " + request.getRequestURI());
        if (HttpUtil.isAjax(request)) {
            /* AJAX 请求 403 未授权访问提示 */
            HttpUtil.ajaxStatus(response, 403, "ajax Unauthorized access.");
        } else {
            /* 正常 HTTP 请求 */
            if (this.getIllegalUrl() == null || "".equals(this.getIllegalUrl())) {
                response.sendError(403, "Forbidden");
            } else {
                response.sendRedirect(this.getIllegalUrl());
            }
        }
        return false;
    }


    public SSOAuthorization getAuthorization() {
        return authorization;
    }


    public void setAuthorization(SSOAuthorization authorization) {
        this.authorization = authorization;
    }


    public String getIllegalUrl() {
        return illegalUrl;
    }


    public void setIllegalUrl(String illegalUrl) {
        this.illegalUrl = illegalUrl;
    }


    public boolean isNothingAnnotationPass() {
        return nothingAnnotationPass;
    }


    public void setNothingAnnotationPass(boolean nothingAnnotationPass) {
        this.nothingAnnotationPass = nothingAnnotationPass;
    }

}

简单总结一下,代码注释充分,逻辑清晰,读懂不难,后续有时间再写一个demo作为实例吧,最后感谢开源大佬们的贡献~

参考地址:
https://github.com/baomidou/kisso

posted @ 2024-06-20 17:06  charler。  阅读(37)  评论(0编辑  收藏  举报