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方法:
具体逻辑:
- cache 是否为null 不为null 则尝试从传入请求获取ssoToken, 获取的到,判断是否过期,过期则清除返回null, 未过期则判断缓存是否宕机,没宕机cookie的登录时间是否和cache 中的登录时间相同,不相同则返回null。
- cache 为null 则尝试从header中解析token,如果有解析过的jwtToken在请求头中,则直接拿来解析并返回,没有的话,再尝试从cookie 中解析并返回,cookie中还找不到,则返回null。
- 执行完上述逻辑后再执行可能配置的插件处理逻辑,至此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作为实例吧,最后感谢开源大佬们的贡献~