|
|
package com.yohoufo.common.interceptor;
|
|
|
|
|
|
import com.yoho.core.config.ConfigReader;
|
|
|
import com.yoho.core.redis.cluster.annotation.Redis;
|
|
|
import com.yoho.core.redis.cluster.operations.nosync.YHValueOperations;
|
|
|
import com.yoho.core.redis.cluster.operations.serializer.RedisKeyBuilder;
|
|
|
import com.yoho.core.rest.client.ServiceCaller;
|
|
|
import com.yoho.error.event.LogEvent;
|
|
|
import com.yoho.service.model.request.UserSessionReqBO;
|
|
|
import com.yohoufo.common.annotation.IgnoreSession;
|
|
|
import com.yohoufo.common.exception.GatewayException;
|
|
|
import com.yohoufo.common.exception.SessionExpireException;
|
|
|
import com.yohoufo.common.exception.VersionNotSupportException;
|
|
|
import com.yohoufo.common.utils.ServletUtils;
|
|
|
import org.apache.commons.collections.CollectionUtils;
|
|
|
import org.apache.commons.lang.StringUtils;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.context.ApplicationEventPublisher;
|
|
|
import org.springframework.context.ApplicationEventPublisherAware;
|
|
|
import org.springframework.web.method.HandlerMethod;
|
|
|
import org.springframework.web.servlet.HandlerInterceptor;
|
|
|
import org.springframework.web.servlet.ModelAndView;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
import javax.servlet.http.Cookie;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
import java.lang.reflect.Method;
|
|
|
import java.net.InetAddress;
|
|
|
import java.net.UnknownHostException;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.LinkedList;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
public class SecurityInterceptor implements HandlerInterceptor, ApplicationEventPublisherAware {
|
|
|
|
|
|
private final Logger logger = LoggerFactory.getLogger(SecurityInterceptor.class);
|
|
|
|
|
|
//session缓存key前缀
|
|
|
private static final String SESSION_CACHE_KEY_PRE = "yh:sessionid:";
|
|
|
|
|
|
//是否启用
|
|
|
private boolean isDebugEnable = false;
|
|
|
|
|
|
// 这些url不会进行校验。 例如 "/notify"
|
|
|
private List<String> excludeUrls;
|
|
|
|
|
|
//限制本地IP访问
|
|
|
private List<String> local = new LinkedList<>();
|
|
|
|
|
|
//有货需要检查session接口, 客户端是否已经登录
|
|
|
private List<String> checkSessionMethods = new LinkedList<>();
|
|
|
|
|
|
@Redis("yohoNoSyncRedis")
|
|
|
private YHValueOperations valueOperations;
|
|
|
|
|
|
@Resource
|
|
|
ServiceCaller serviceCaller;
|
|
|
|
|
|
@Resource(name = "core-config-reader")
|
|
|
private ConfigReader configReader;
|
|
|
|
|
|
private ApplicationEventPublisher publisher;
|
|
|
|
|
|
@Override
|
|
|
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
|
|
|
|
|
|
Map<String, String> params = this.getRequestInfo(httpServletRequest);
|
|
|
|
|
|
//(1)校验版本
|
|
|
this.validVersion(params, httpServletRequest);
|
|
|
|
|
|
//(2)不需要校验SESSION的场景. (1)exclude和debug模式,(2)私有网络模式,(3)配置了不需要校验的注解.
|
|
|
if (this.isIgnore(httpServletRequest, params, o)) {
|
|
|
return true;
|
|
|
}
|
|
|
//(3)校验session
|
|
|
this.validateSession(httpServletRequest, params);
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
private void validateSession(HttpServletRequest httpServletRequest, Map<String, String> params) throws SessionExpireException, VersionNotSupportException {
|
|
|
// params为空,说明接口无参数, 无需校验
|
|
|
if (params == null || params.size() == 0) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
String clientType = params.get("client_type");
|
|
|
String sessionType = params.get("session_type");
|
|
|
String method = params.get("method");
|
|
|
String uid = params.get("uid");
|
|
|
String appVersion = params.get("app_version");
|
|
|
//==============以下是完全不校验的场景=========================
|
|
|
|
|
|
//2 是否校验全部接口,开关-true:校验全部接口(除去@IgnoreSession注解接口) 开关-false:只校验核心接口
|
|
|
boolean isVerifyAllMethod = configReader.getBoolean("gateway.session.isVerifyAllMethod", true);
|
|
|
if(!isVerifyAllMethod){
|
|
|
//2.1 当前接口不再校验的范围, 直接返回, 不校验.
|
|
|
if(StringUtils.isEmpty(method) || !checkSessionMethods.contains(method)){
|
|
|
return ;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//============ 以下是必须校验的场景 =====================
|
|
|
|
|
|
//3 如果没有传入UID, 校验不通过
|
|
|
if(StringUtils.isEmpty(uid) || !StringUtils.isNumeric(uid) || Integer.valueOf(uid) < 1){
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
//4 需要校验接口没传appVersion提示升级
|
|
|
if(StringUtils.isEmpty(appVersion)){
|
|
|
logger.warn("need to check session info, appVersion is null, method {} is {} ", method);
|
|
|
throw new VersionNotSupportException();
|
|
|
}
|
|
|
|
|
|
//5 解析客户端传入的COOKIE中的session值
|
|
|
Cookie[] cookies = httpServletRequest.getCookies();
|
|
|
String jSessionID = null;
|
|
|
if (cookies != null) {
|
|
|
for (Cookie cookie : cookies) {
|
|
|
//解析sessionid
|
|
|
if ("JSESSIONID".equals(cookie.getName())) {
|
|
|
jSessionID = cookie.getValue();
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//6 如果cookie中没有jSessionID , 但接口又必须校验会话, 则返回 HTTP 401, 需要重新登录.
|
|
|
if (jSessionID == null) {
|
|
|
logger.warn("check session failed, can not find session id in cookies, check session info failed, method {}, uid {}, appVersion is {}, clientType is {}, sessionType is {}", method, uid, appVersion, clientType, sessionType);
|
|
|
this.verifyFailReport(uid, method, clientType);
|
|
|
throw new SessionExpireException(); //重新登录
|
|
|
}
|
|
|
|
|
|
//7 从REDIS中获取服务端session的值. 如果REDIS中获取不到,可能存在双中心延迟的情况, 回源数据库查询
|
|
|
String sessionInfo;
|
|
|
try {
|
|
|
RedisKeyBuilder cacheKey = getSessionCacheKey(jSessionID, clientType, sessionType);
|
|
|
sessionInfo = valueOperations.get(cacheKey);
|
|
|
if(null == sessionInfo){ //如果REDIS主从延迟, 从主REDIS中获取SESSION
|
|
|
cacheKey = RedisKeyBuilder.newInstance().appendFixed(SESSION_CACHE_KEY_PRE).appendVar(jSessionID);
|
|
|
sessionInfo = valueOperations.get(cacheKey);
|
|
|
}
|
|
|
}catch (Exception redisException){
|
|
|
//如果redis异常,直接放通
|
|
|
logger.warn("redis exception {} when get session", redisException.getMessage());
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
//8 session双云同步延迟时,获取用户session
|
|
|
if(null == sessionInfo){
|
|
|
sessionInfo = this.getUserSesion(uid, jSessionID, clientType, sessionType);
|
|
|
}
|
|
|
|
|
|
//9 校验SESSION, 校验不通过重新登录
|
|
|
if (uid == null || sessionInfo == null || !StringUtils.equals(sessionInfo, uid)) {
|
|
|
logger.warn("check session failed, session unmatched uid, session id {}, uid {} , session info {}, method {}, version is {}, clientType is {}, sessionType is {}", jSessionID, params.get("uid"), sessionInfo, method, appVersion, clientType, sessionType);
|
|
|
this.verifyFailReport(uid, method, clientType);
|
|
|
throw new SessionExpireException(); //重新登录
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
|
|
|
//do nothing
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
|
|
this.publisher = applicationEventPublisher;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* session验证失败上报事件
|
|
|
* @param uid
|
|
|
* @param method
|
|
|
* @param clientType
|
|
|
*/
|
|
|
private void verifyFailReport(String uid, String method, String clientType){
|
|
|
try{
|
|
|
LogEvent logEvent = new LogEvent.Builder("sessionFail").addArg("uid", uid).addArg("method", method).addArg("clientType", clientType).build();
|
|
|
publisher.publishEvent(logEvent);
|
|
|
}catch (Exception e){
|
|
|
logger.warn("verifyFailReport: report session verify event faild, uid is {}, method is {}, error is {}", uid, method, e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
private RedisKeyBuilder getSessionCacheKey(String sessionKey, String clientType, String sessionType){
|
|
|
RedisKeyBuilder keyBuilder = RedisKeyBuilder.newInstance().appendFixed(SESSION_CACHE_KEY_PRE);
|
|
|
//微信商城和h5共用同一session
|
|
|
if ("wechat".equalsIgnoreCase(clientType)){
|
|
|
keyBuilder.appendFixed(SessionTypeEnum.H5.getName()).appendFixed(":");
|
|
|
keyBuilder.appendVar(sessionKey);
|
|
|
return keyBuilder;
|
|
|
}
|
|
|
//h5嵌入其他端如iphone,android的场景
|
|
|
if(StringUtils.isNotEmpty(sessionType)){
|
|
|
if(SessionTypeEnum.contains(sessionType)) {
|
|
|
keyBuilder.appendFixed(sessionType).appendFixed(":");
|
|
|
keyBuilder.appendVar(sessionKey);
|
|
|
return keyBuilder;
|
|
|
}else{
|
|
|
keyBuilder.appendVar(sessionKey);
|
|
|
return keyBuilder;
|
|
|
}
|
|
|
}
|
|
|
if(SessionTypeEnum.contains(clientType)){
|
|
|
keyBuilder.appendFixed(clientType).appendFixed(":");
|
|
|
keyBuilder.appendVar(sessionKey);
|
|
|
return keyBuilder;
|
|
|
}
|
|
|
keyBuilder.appendVar(sessionKey);
|
|
|
return keyBuilder;
|
|
|
}
|
|
|
|
|
|
private boolean isIgnore(HttpServletRequest request, Map<String, String> params, Object o) {
|
|
|
|
|
|
//如果请求url包含在过滤的url,则直接返回. 请求url可能是 "/gateway/xxx”这种包含了context的。
|
|
|
logger.debug("enter isIgnore");
|
|
|
if (excludeUrls != null) {
|
|
|
final String requestUri = request.getRequestURI();
|
|
|
if (this.urlContains(requestUri, excludeUrls)) {
|
|
|
logger.debug("isIgnore check url in excludeUrls");
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//如果请求是本地请求(来自私有网络)
|
|
|
if (this.isLocalRequestMatch(request)) {
|
|
|
logger.debug("isIgnore check ip is local");
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
//配置文件配置为 is_debug_enable 为true,并且请求携带参数debug为XYZ,就放行
|
|
|
if (isDebugEnable && "XYZ".equals(request.getParameter("debug"))) {
|
|
|
logger.debug("isIgnore check debug model");
|
|
|
return true;
|
|
|
}
|
|
|
logger.debug("end to isIgnore check");
|
|
|
|
|
|
//含有IgnoreSession注解的接口放行
|
|
|
if(o.getClass().isAssignableFrom(HandlerMethod.class)){
|
|
|
HandlerMethod handlerMethod = (HandlerMethod)o;
|
|
|
Method bridgedMethod =handlerMethod.getMethod();
|
|
|
if(bridgedMethod.isAnnotationPresent(IgnoreSession.class)){
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//检查zk中设置的忽略验证接口(线上环境可以动态忽略/取消忽略指定接口)
|
|
|
String ignoreMethods = configReader.getString("gateway.session.ignoreMethods", "");
|
|
|
if(StringUtils.isEmpty(ignoreMethods)){
|
|
|
return false;
|
|
|
}
|
|
|
List<String> ignoreMethodList = Arrays.asList(ignoreMethods.split(","));
|
|
|
String method = params.get("method");
|
|
|
String verifyMethod = StringUtils.isEmpty(method) ? request.getRequestURI().substring(8) : method;
|
|
|
if(StringUtils.isNotEmpty(verifyMethod) && ignoreMethodList.contains(verifyMethod)){
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 校验客户端版本
|
|
|
*
|
|
|
* @param params
|
|
|
*/
|
|
|
private void validVersion(Map<String, String> params, HttpServletRequest request) throws GatewayException {
|
|
|
String uid = params.get("uid");
|
|
|
int uidLen = configReader.getInt("gateway.limit.uid.length", 10);
|
|
|
|
|
|
if(StringUtils.isNotEmpty(uid) && (!StringUtils.isNumeric(uid) || uid.length() > uidLen)){
|
|
|
logger.warn("validVersion: uid is illegal: param is {}, ip is {}", params, getIP(request));
|
|
|
throw new SessionExpireException();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取请求信息: requestParam
|
|
|
*
|
|
|
* @param httpServletRequest
|
|
|
* @return
|
|
|
*/
|
|
|
private Map<String, String> getRequestInfo(HttpServletRequest httpServletRequest) {
|
|
|
return ServletUtils.getRequestParams(httpServletRequest);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 本地IP限制
|
|
|
* @param request
|
|
|
* @return
|
|
|
*/
|
|
|
private boolean isLocalRequestMatch(HttpServletRequest request) {
|
|
|
if (CollectionUtils.isEmpty(this.local)) {
|
|
|
return false;
|
|
|
}
|
|
|
final String requestUri = request.getRequestURI();
|
|
|
final String ip = this.getIP(request);
|
|
|
|
|
|
//ip is blank or has multi ip
|
|
|
if(StringUtils.isEmpty(ip) || ip.contains(",")){
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
InetAddress inetAddress = InetAddress.getByName(ip);
|
|
|
if (this.urlContains(requestUri, local) && (inetAddress.isSiteLocalAddress() || inetAddress.isLoopbackAddress())) {
|
|
|
return true;
|
|
|
}
|
|
|
} catch (UnknownHostException e) {
|
|
|
logger.error("unknown ip", e);
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
private boolean urlContains(String requestUri, List<String> excludeUrls) {
|
|
|
|
|
|
for (String excludeUri : excludeUrls) {
|
|
|
if (requestUri.equals(excludeUri) || requestUri.startsWith(excludeUri) || requestUri.startsWith("/gateway" + excludeUri) || requestUri.endsWith(excludeUri)) {
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取用户session信息,解决双云session同步延迟问题
|
|
|
* @param sessionKey
|
|
|
* @return
|
|
|
*/
|
|
|
private String getUserSesion(String uid, String sessionKey, String clientType, String sessionType){
|
|
|
try{
|
|
|
boolean degrade_getSession_enable = configReader.getBoolean("gateway.degrade.users.getUserSesion.enable",false);
|
|
|
if(degrade_getSession_enable){
|
|
|
return null;
|
|
|
}
|
|
|
UserSessionReqBO reqBO = new UserSessionReqBO();
|
|
|
reqBO.setUid(uid == null ? null : Integer.valueOf(uid));
|
|
|
reqBO.setSessionKey(sessionKey);
|
|
|
reqBO.setClientType(clientType);
|
|
|
reqBO.setSessionType(sessionType);
|
|
|
UserSessionReqBO result = serviceCaller.call("uic.selectUserSession", reqBO, UserSessionReqBO.class);
|
|
|
logger.debug("SecurityInterceptor: call uic.selectUserSession, uid is {}, sessionKey is {}");
|
|
|
if(result == null || result.getUid() == null){
|
|
|
return null;
|
|
|
}
|
|
|
return String.valueOf(result.getUid());
|
|
|
}catch(Exception e){
|
|
|
logger.warn("SecurityInterceptor: getUserSession failed ! uid is {}, sessionKey is {}, error is {}", uid, sessionKey, e);
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
public void setIsDebugEnable(boolean isDebugEnable) {
|
|
|
this.isDebugEnable = isDebugEnable;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 设置不校验的url地址
|
|
|
*/
|
|
|
public void setExcludeUrls(List<String> excludeUrls) {
|
|
|
this.excludeUrls = excludeUrls;
|
|
|
}
|
|
|
|
|
|
public void setCheckSessionMethods(Map<String, Object> validateMethods) {
|
|
|
List<String> methods = (List<String>)validateMethods.get("methods");
|
|
|
if(methods != null) {
|
|
|
this.checkSessionMethods.addAll(methods);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private String getIP(HttpServletRequest httpServletRequest) {
|
|
|
String ip = httpServletRequest.getHeader("X-Real-IP");
|
|
|
if (StringUtils.isEmpty(ip)) {
|
|
|
ip = httpServletRequest.getRemoteAddr();
|
|
|
}
|
|
|
return ip;
|
|
|
}
|
|
|
|
|
|
public void setLocal(List<String> local) {
|
|
|
this.local = local;
|
|
|
}
|
|
|
|
|
|
public enum SessionTypeEnum {
|
|
|
|
|
|
IPHONE(1,"iphone"),
|
|
|
ANDROID(2, "android"),
|
|
|
WEB(3, "web"),
|
|
|
H5(4, "h5");
|
|
|
|
|
|
private String name;
|
|
|
|
|
|
private int type;
|
|
|
|
|
|
public String getName() {
|
|
|
return name;
|
|
|
}
|
|
|
|
|
|
public int getType() {
|
|
|
return type;
|
|
|
}
|
|
|
|
|
|
SessionTypeEnum(int type, String name) {
|
|
|
this.type = type;
|
|
|
this.name = name;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 是否包含
|
|
|
* @param name
|
|
|
* @return
|
|
|
*/
|
|
|
public static boolean contains(String name) {
|
|
|
if (StringUtils.isEmpty(name)) {
|
|
|
return false;
|
|
|
}
|
|
|
for (SessionTypeEnum e : values()) {
|
|
|
if (name.equals(e.getName())){
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
} |
...
|
...
|
|