Authored by qinchao

Merge branch 'master' of http://git.yoho.cn/ufo/yohoufo-fore

/*
* Copyright (C), 2016-2017, yoho
* FileName: NetworkControl.java
* Author: Maelk_liu
* Date: 2017年4月10日 上午11:53:33
* Description: //模块目的、功能描述
* History: //修改记录
* <author> <time> <version> <description>
* 修改人姓名 修改时间 版本号 描述
*/
package com.yohoufo.common.annotation;
import java.lang.annotation.*;
/**
* 忽略session验证
*
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface IgnoreSession {
}
... ...
/*
* Copyright (C), 2016-2017, yoho
* FileName: NetworkControl.java
* Author: Maelk_liu
* Date: 2017年4月10日 上午11:53:33
* Description: //模块目的、功能描述
* History: //修改记录
* <author> <time> <version> <description>
* 修改人姓名 修改时间 版本号 描述
*/
package com.yohoufo.common.annotation;
import java.lang.annotation.*;
/**
* 忽略消息签名验证
*
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface IgnoreSignature {
String clientType() default ""; //指定忽略验签的客户端,不指定时忽略验证所有客户端,配置多个时用逗号分隔
String version() default ""; //忽略验签的版本, 低于该版本不校验
}
... ...
/*
* Copyright (C), 2016-2017, yoho
* FileName: NetworkControl.java
* Author: Maelk_liu
* Date: 2017年4月10日 上午11:53:33
* Description: //模块目的、功能描述
* History: //修改记录
* <author> <time> <version> <description>
* 修改人姓名 修改时间 版本号 描述
*/
package com.yohoufo.common.annotation;
import java.lang.annotation.*;
/**
* 控制内网访问接口权限
*
* @author Maelk_liu
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface InnerApi {
}
... ...
package com.yohoufo.common.exception;
/**
*
* API Gateway异常父类
*
* Created by chang@yoho.cn on 2015/11/3.
*/
public class GatewayException extends Exception{
private int code;
private String desc;
/**
*/
public GatewayException(int code, String desc){
this.code = code;
this.desc=desc;
}
@Override
public String getMessage() {
return "[" + this.code + ":" + this.desc + "]";
}
public String getDesc() {
return desc;
}
public int getErrorCode() {
return code;
}
}
... ...
package com.yohoufo.common.exception;
/**
* Created by xjipeng on 16/2/15.
*/
public class SessionExpireException extends GatewayException {
public SessionExpireException() {
super(401, "登录会话超时,请退出重新登录.");
}
}
... ...
package com.yohoufo.common.exception;
/**
* 参数验签不正确
*
*/
public class SignatureNotMatchException extends GatewayException {
/**
* 异常
*
*/
public SignatureNotMatchException() {
super(508, "");
//客户端会透传,暂时不作提示
//super(508, "数据签名验证错误.");
}
}
... ...
package com.yohoufo.common.exception;
/**
* Created by lenovo on 2017/4/9.
*/
public class VersionNotSupportException extends GatewayException {
/**
* 版本不支持异常
*/
public VersionNotSupportException() {
super(500, "请升级至最新版本");
}
}
... ...
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;
}
}
}
... ...
package com.yohoufo.common.interceptor;
import java.lang.reflect.Method;
import java.util.*;
import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.yoho.error.event.LogEvent;
import com.yohoufo.common.annotation.IgnoreSignature;
import com.yohoufo.common.exception.SignatureNotMatchException;
import com.yohoufo.common.utils.MD5Util;
import com.yohoufo.common.utils.ServletUtils;
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 com.google.common.collect.ImmutableList;
import com.netflix.config.DynamicPropertyFactory;
import com.yoho.core.common.utils.MD5;
import com.yoho.core.config.ConfigReader;
public class SignatureVerifyInterceptor implements HandlerInterceptor, ApplicationEventPublisherAware {
private final Logger logger = LoggerFactory.getLogger(SignatureVerifyInterceptor.class);
//验签算法
private final static String VERIFY_ARITHMETIC = "HmacSHA256";
//客户端加签请求头
private final static String HTTP_HEADER_VERIFY_DATA = "x-yoho-verify";
//是否调试模式
private boolean isDebugEnable = false;
//这些方法需要校验
private List<String> checkMethods = new LinkedList<>();
@Resource(name = "core-config-reader")
private ConfigReader configReader;
private ApplicationEventPublisher publisher;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//(1) 验签开关, 控制是否需要验签. 默认需要验证
boolean isSignatureVerifyEnable = configReader.getBoolean("gateway.security.isSignatureVerifyEnable", true);
if(!isSignatureVerifyEnable){
logger.debug("gateway.security.isSignatureVerifyEnable is false");
return true;
}
//(2) 排除掉debug模式
Map<String, String> params = this.getRequestInfo(httpServletRequest);
String method = params.get("method");
String clientType = params.get("client_type");
String appVersion = params.get("app_version");
String udid = params.get("udid");
if (this.isIgnore(httpServletRequest, o, method, clientType, appVersion)) {
return true;
}
//(3) 排除不需要校验的场景
if(this.validateReqParams(httpServletRequest, params)){
return true;
}
//(4) 验证消息签名.
String message = this.getPramsString(params);
String verifyMessage = httpServletRequest.getHeader(HTTP_HEADER_VERIFY_DATA);
logger.debug("verifySignature: message is {}, verifyMessage is {}", message, verifyMessage);
boolean verifyRes = this.verifySignatureNew(udid, message, verifyMessage);
logger.debug("verifySignature: message is {}, verifyMessage is {}, res is {}", message, verifyMessage, verifyRes);
if(!verifyRes){
logger.warn("verifySignature: signature failed, uri is {}, message is {}, verifyMessage is {}, verifyType is {}", httpServletRequest.getRequestURI(), message, verifyMessage, "");
this.verifyFailReport(message, "", verifyMessage, "");
throw new SignatureNotMatchException(); //验证不通过
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
//do nothing
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
private boolean isIgnore(HttpServletRequest request, Object o, String method, String clientType, String appVersion) {
logger.debug("enter isIgnore");
//(1) 配置文件配置为 is_debug_enable 为true,并且请求携带参数debug为XYZ,就放行
if (isDebugEnable && "XYZ".equals(request.getParameter("debug"))) {
logger.debug("isIgnore check debug model");
return true;
}
//(2) 是否含有IgnoreSignature、IgnoreClientSecret和innerApi注解
if(o.getClass().isAssignableFrom(HandlerMethod.class)){
HandlerMethod handlerMethod = (HandlerMethod)o;
//忽略验签接口放行
if(isVerifySignature(handlerMethod, clientType, appVersion)) {
return true;
}
}
//(3) 检查zk中设置的忽略验签接口(线上环境可以动态忽略/取消忽略指定接口)
String ignoreMethods = configReader.getString("gateway.signature.ignoreMethods", "");
if(StringUtils.isEmpty(ignoreMethods)){
return false;
}
List<String> ignoreMethodList = Arrays.asList(ignoreMethods.split(","));
String verifyMethod = StringUtils.isEmpty(method) ? request.getRequestURI().substring(8) : method;
if(StringUtils.isNotEmpty(verifyMethod) && ignoreMethodList.contains(verifyMethod)){
return true;
}
return false;
}
/**
* 验证请求参数
* @param params 请求参数
* @return
*/
private boolean validateReqParams(HttpServletRequest request, Map<String, String> params){
//是否校验全部接口,开关-true:校验全部接口(除含@IgnoreSignature注解接口) 开关-false:只校验核心接口
boolean isVerifyAllMethod = configReader.getBoolean("gateway.signature.isVerifyAllMethod", false);
if(!isVerifyAllMethod){
String method = params.get("method");
//如果请求method在校验包内,则校验
if (StringUtils.isNotEmpty(method) && checkMethods.contains(method)) {
logger.debug("method {} to verify signature", method);
return false;
}
return true;
}
return false;
}
/**
* 获取请求信息: requestParam
*
* @param httpServletRequest
* @return
*/
private Map<String, String> getRequestInfo(HttpServletRequest httpServletRequest) {
return ServletUtils.getRequestParams(httpServletRequest);
}
/**
* 拼接请求参数,k1=v1&k2=v2
* @param reqParams
* @return
*/
private String getPramsString(Map<String, String> reqParams) {
ImmutableList list = ImmutableList.of();
SortedMap<String, String> filtedMap = new TreeMap<>();
for (Map.Entry<String, String> entry : reqParams.entrySet()) {
String k = entry.getKey();
if (!list.contains(k)) {
filtedMap.put(k, entry.getValue());
}
}
//string: k1=v1&k2=v2
List<String> array = new LinkedList<>();
for (Map.Entry<String, String> entry : filtedMap.entrySet()) {
String pair = entry.getKey() + "=" + entry.getValue();
array.add(pair.trim());
}
String paramStr = String.join("&", array);
logger.debug("getRequestInfo: param str is: {}", paramStr);
return paramStr;
}
/**
* 验证签名
* @param udid
* @param message
* @param verifyMessage
* @return
*/
private boolean verifySignatureNew(String udid, String message, String verifyMessage){
//(1) 验证信息为空或者udid为空,直接返回
if(StringUtils.isEmpty(verifyMessage) || StringUtils.isEmpty(udid)){
return false;
}
//(2) 获取秘钥
String md5Salt = DynamicPropertyFactory.getInstance().getStringProperty("gateway.signature.key.salt", "mQyMTMwZjlmZTZmYjY4UjkNmYwZGM0OTk0Y").get();
String encryptStr = udid + md5Salt;
//对udid加盐后二次md5
String signatureKey = MD5.md5(MD5.md5(encryptStr));
//(3) 对消息加签
String signatureRes = this.HmacSHA256(message, signatureKey);
if(verifyMessage.equals(signatureRes)){
return true;
}
logger.warn("verifySignatureNew: signature verify failed: message is {}, verifyMessage is {}, udid is {}, key is {}", message, verifyMessage, udid, signatureKey);
return false;
}
/**
* 签名失败上报事件
* @param message
* @param secretkey
* @param verifyMsg
* @param verifyType
*/
private void verifyFailReport(String message, String secretkey, String verifyMsg, String verifyType){
try{
LogEvent logEvent = new LogEvent.Builder("signatureFail").addArg("message", message).addArg("key", secretkey).addArg("verifyMsg", verifyMsg).addArg("verifyType", verifyType).build();
publisher.publishEvent(logEvent);
}catch (Exception e){
logger.warn("verifyFailReport: report signature verify event faild, message is {}, error is {}",message, e);
}
}
/**
* 使用hmac加密
* @param data
* @param secret
* @return
*/
private String HmacSHA256(String data, String secret){
String res = null;
try{
Mac sha256_HMAC = Mac.getInstance(VERIFY_ARITHMETIC);
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), VERIFY_ARITHMETIC);
sha256_HMAC.init(secret_key);
byte[] encryptByte = sha256_HMAC.doFinal(data.getBytes());
res = MD5Util.byteArrayToHexString(encryptByte);
logger.debug("HmacSHA256: data is {}, secret is {}, res is {}", data, secret, res);
}catch (Exception e){
logger.warn("HmacSHA256: hmac failed, data is {}, error is {}", data, e);
}
return res;
}
//含忽略签名校验注解的接口,不验证
private boolean isVerifySignature(HandlerMethod handlerMethod, String clientType, String appVersion){
Method bridgedMethod =handlerMethod.getMethod();
if(bridgedMethod.isAnnotationPresent(IgnoreSignature.class)){
IgnoreSignature is = bridgedMethod.getAnnotation(IgnoreSignature.class);
String ignoreClient = is.clientType();
String ignoreVersion = is.version();
//(1) 忽略验证客户端和版本都没配置,默认全部忽略
if(StringUtils.isEmpty(ignoreClient) && StringUtils.isEmpty(ignoreVersion)){
return true;
}
//(2) 指定忽略验证客户端包含传入客户端,则不验证
if(StringUtils.isNotEmpty(ignoreClient) && StringUtils.isNotEmpty(clientType)
&& Arrays.asList(ignoreClient.split(",")).contains(clientType)){
return true;
}
//(3) 传入客户端版本低于指定忽略验签的版本,则不验签
if(StringUtils.isNotEmpty(ignoreVersion)){
if(StringUtils.isEmpty(appVersion)){
return true;
}
if(ignoreVersion.compareTo(appVersion) > 0){
return true;
}
}
}
return false;
}
public void setIsDebugEnable(boolean isDebugEnable) {
this.isDebugEnable = isDebugEnable;
}
public void setCheckMethods(Map<String, Object> checkMethods) {
List<String> methods = (List<String>)checkMethods.get("methods");
if(methods != null) {
this.checkMethods.addAll(methods);
}
}
}
... ...
package com.yohoufo.common.utils;
import java.security.MessageDigest;
public class MD5Util {
public static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++)
resultSb.append(byteToHexString(b[i]));
return resultSb.toString();
}
public static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n += 256;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname))
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
else
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
} catch (Exception exception) {
}
return resultString;
}
private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
}
... ...
package com.yohoufo.common.utils;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* utils
* Created by chunhua.zhang@yoho.cn on 2016/3/24.
*/
public class ServletUtils {
public final static String getServiceName(HttpServletRequest request) {
//如果直接请求/gateway 或者/,则取method请求参数作为method
final List<String> CONTEXTS = Lists.newArrayList("/", "/gateway", "/gateway/");
String requestUrl = request.getRequestURI();
String service = requestUrl;
if (CONTEXTS.contains(requestUrl)) {
service = request.getParameter("method");
}
return service;
}
public final static Map<String, String> getRequestParams(HttpServletRequest httpServletRequest) {
Map<String, String> map = new HashMap<>();
Enumeration paramNames = httpServletRequest.getParameterNames();
while (paramNames.hasMoreElements()) {
String key = (String) paramNames.nextElement();
String value = httpServletRequest.getParameter(key);
map.put(key, value);
}
return map;
}
public static String getRemoteIP( final HttpServletRequest httpServletRequest ) {
String ip = httpServletRequest.getHeader( "X-Forwarded-For" );
if ( StringUtils.isEmpty( ip ) ) {
ip = httpServletRequest.getRemoteAddr();
}
return ip;
}
}
... ...
... ... @@ -23,7 +23,7 @@
<!-- ******************** interceptors ******************** -->
<bean id="securityInterceptor" class="com.yoho.gateway.common.interceptor.SecurityInterceptor">
<bean id="securityInterceptor" class="com.yohoufo.common.interceptor.SecurityInterceptor">
<property name="isDebugEnable" value="${is_debug_enable:false}" />
<property name="excludeUrls">
<list>
... ... @@ -37,35 +37,9 @@
<property name="local">
<value>/service_control</value>
</property>
<property name="checkSessionShopsMethods">
<list>
<value>app.shopInbox.getList</value>
<value>app.sellerShop.sellType</value>
<value>app.shops.shopbrandrank</value>
<value>app.shops.shopbusinessoverview</value>
<value>app.shopInbox.getShopInboxTotal</value>
<value>app.shops.sales</value>
<value>app.shops.refund</value>
<value>app.shops.storagestatistics</value>
<value>app.shops.storagein</value>
<value>app.shops.storageout</value>
<value>app.purchase.delivery</value>
<value>app.express.getExpressList</value>
<value>app.purchase.queryBySupplierId</value>
<value>app.shops.accountbalance</value>
<value>app.shopInbox.batchSetIsRead</value>
<value>app.purchase.stockOut</value>
<value>app.purchase.list</value>
<value>app.shops.checkAppVersion</value>
<value>app.followPrice.list</value>
<value>app.followPrice.getFrontPageStatisticsData</value>
<value>app.followPrice.changeVipStatus</value>
<value>app.followPrice.follow</value>
</list>
</property>
<property name="checkSessionMethods" ref="validateMethodMap" />
</bean>
<bean id="signatureVerifyInterceptor" class="com.yoho.gateway.common.interceptor.SignatureVerifyInterceptor">
<bean id="signatureVerifyInterceptor" class="com.yohoufo.common.interceptor.SignatureVerifyInterceptor">
<property name="isDebugEnable" value="${is_debug_enable:false}" />
<property name="checkMethods" ref="validateMethodMap"/>
</bean>
... ...