赞
踩
1、对外提供接口,为保护参数不被修改,保护数据的安全性,需要在客户端调用时接口添加签名、服务端对接口进行验签;
2、本实例中authKey是不参与通信,整个过程中authKey是不参与通信的,所以只要保证authKey不泄露,即使请求参数被其他原因泄露,请求也不会被伪造。
1.己方系统提供
clientId: 由于己方系统可能会对外不同角色方提供接口(第三方系统app、第三方系统PC、定时任务调用等),所以针对不同角色类型的外部系统做了配置化处理,
每个clientId对应某一外部角色类型方,并在后台库中存储了该clientId对应的authKey、authorization及其具有的权限信息,配置信息写入redis中,后期从redis中获取这些信息;
authKey: 某一clientId对应的authKey,专门进行加签、验签操作,为接口安全考虑,不参与通信;
authorization: 接口header中携带的token,因为系统中接口权限验证需要校验token,所以为了匹配整个系统中的访问权限验证需要这个参数;
2.url参数如下:
signTimestamp 签名失效时间,时间戳,单位毫秒,13位【必填】
sign 签名
3.签名的计算规则如下:
a、对所有入参【注意:参数值非空的才会参与签名的计算】clientId、authKey、authorization、signTimestamp 按照字段名的 ASCII 码从小到大排序(字典序)后,
使用 URL 键值对的格式(即 key1=value1&key2=value2…)拼接成字符串 str;
b、对 str 进行 md5 运算,再将得到的字符串所有字符转换为大写,得到 sign 值
4.下面定义了一段生成 sign 字符串的示范过程:
1)、POST请求
URL:http://localhost:7777/xxxxx-service/testResource/test?param3=456
header: key :Authorization value :201295823105949696
body:
{
“param1”:“参数1”,
“param2”:“参数2”,
“param5”:[“www”,“wfawefwe”,“jagjergre”]
}
2)、获取当前时间戳
signTimestamp:1615458960605
3)、按照a规则,排序后的字符串 str:
authKey=303e6bd7-472d-11ea-a802-fa163ecd8c7a&authorization=201295823105949696¶m1=参数1¶m2=参数2¶m3=456¶m5=[“哈哈哈”,“呜呜呜”,“急急急”]&signTimestamp=1615458960605
4)、按照b规则得到签名 sign:
DigestUtils.md5DigestAsHex(“authKey=303e6bd7-472d-11ea-a802-fa163ecd8c7a&authorization=201295823105949696¶m1=参数1¶m2=参数2¶m3=456¶m5=[“哈哈哈”,“呜呜呜”,“急急急”]&signTimestamp=1615458960605”).toUpperCase()
输出:“EBD4B596A4DDDFB6ACBCFAF3E5C6BE6A”
5)、最终请求如下:
URL:http://localhost:7777/xxxxx-service/testResource/test?param3=456&signTimestamp=1615458960605&sign=EBD4B596A4DDDFB6ACBCFAF3E5C6BE6A
header: key :Authorization value :201295823105949696
body:
{
“param1”:“参数1”,
“param2”:“参数2”,
“param5”:[“www”,“wfawefwe”,“jagjergre”]
}
1、生成签名
String signTimestamp = SignUtil.getSignTimestamp();
String getTestDataUrlStr = "/xxxxx-service/roomTest/getTestData?signTimestamp="+signTimestamp+"&sign="+SignUtil.getSign(token,authKey,signTimestamp);
/** * @author * @Description 生成签名 * @Date */ public class SignUtil { private static final Logger LOGGER = LoggerFactory.getLogger(SignUtil.class); //生成签名; public static String getSign(String token,String authKey,String signTimestamp){ //获取签名; SortedMap<String, String> allParams = new TreeMap<>(); allParams.put("authorization",token); allParams.put("authKey",authKey); //String signTimestampStr = signTimestamp; allParams.put("signTimestamp",signTimestamp); StringBuilder stringBuilder = new StringBuilder(150); for (Map.Entry<String, String> entry : allParams.entrySet()) { if (!StringUtils.isEmpty(entry.getValue())) { stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } } String paramsStr = stringBuilder.toString(); if (StringUtils.isNotBlank(paramsStr)) { paramsStr = paramsStr.substring(0, paramsStr.length() - 1); } return DigestUtils.md5DigestAsHex(paramsStr.getBytes()).toUpperCase(); } public static String getSignTimestamp(){ //取得指定时区的时间(东八区) TimeZone zone = TimeZone.getTimeZone("GMT-8:00"); Calendar cal = Calendar.getInstance(zone); long currentSecond = cal.getTime().getTime(); String currentTimestamp = String.valueOf(currentSecond); return currentTimestamp; }
2、网关服务 过滤器校验token是否存在及有效性
/** * 请求认证过滤 */ @Component @Slf4j public class AuthFilter implements GlobalFilter, Ordered { /** * 基于LRU暂存token对应的用户信息和权限 */ public static final ConcurrentLinkedHashMap<String, TokenValueInMap> cacheMap = new ConcurrentLinkedHashMap.Builder<String, TokenValueInMap>() .maximumWeightedCapacity(20000) .weigher(Weighers.singleton()) .build(); /** * 基于记录无效token,防止无效token多次,以提升系统性能 */ private static final ConcurrentLinkedHashMap<String, Long> cacheInValidToken = new ConcurrentLinkedHashMap.Builder<String, Long>() .maximumWeightedCapacity(2000) .weigher(Weighers.singleton()) .build(); @Autowired RedisService<HashMap, String> redisService; @Autowired RedisProperties redisProperties; private static ThreadPoolExecutor threadPoolExecutorForSendToRedis; static { threadPoolExecutorForSendToRedis = new ThreadPoolExecutor( //线程池维护线程的最少数量 100, //线程池维护线程的最大数量 10000, //线程池维护线程所允许的空闲时间 120, TimeUnit.SECONDS, //线程池所使用的缓冲队列 new ArrayBlockingQueue<Runnable>(500), //加入失败,则在调用的主线程上执行 new ThreadPoolExecutor.CallerRunsPolicy()); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); HttpResult httpResult = HttpStatus.Unauthorized; //备注:针对白名单请求,如果具有有效的token,则将有效的token(即:自身能够正常解析 + redis中未失效)也一并解析 try { //从请求头部获取token String token = getTokenFromRequest(request); //是否是匿名请求,即:请求路径中是否含有unAuth boolean isHasUnAuth = false; //对于未携带token的请求,需要判断是否能够直接放行,通过PassConfig中的白名单进行控制 AntPathMatcher antPathMatcher = new AntPathMatcher(); long count = PassConfig.WITELIST.stream().filter(pattern -> antPathMatcher.match(pattern, request.getPath().value())).count(); if (count > 0) { isHasUnAuth = true; if (StringUtils.isBlank(token)) { //放行前校验请求中的签名 this.preChainFilterProcess(request, ""); //放行 未携带token的unAuth请求 return chain.filter(exchange); } } //访问需要授权的接口时未携带token if (!isHasUnAuth && StringUtils.isBlank(token)) { throw new Exception("请先登录系统"); } String authorities = null; Map<String, Object> userMap = null; //获取token对应的用户信息 // tokenValueInMap为null,表示token无效,需要重新登录 TokenValueInMap tokenValueInMap = getTokenValue(token); if (null != tokenValueInMap) { authorities = tokenValueInMap.tokenValueInRedis.getAuthority(); userMap = tokenValueInMap.tokenValueInRedis.getClaims(); } //非白名单内的请求,且携带自身解析有效的token,但是token在redis中不存在,直接抛出异常,需要用户重新登录 if (!isHasUnAuth && null == tokenValueInMap) { throw new Exception("登录已超时, 请重新登录"); } //白名单内的请求,且携带自身解析有效的token,但是token在redis中不存在,可以放行 if (isHasUnAuth && null == tokenValueInMap) { this.preChainFilterProcess(request, ""); return chain.filter(exchange); } //token自动续期 final Map<String, Object> userMapTmp = userMap; threadPoolExecutorForSendToRedis.execute(() -> { this.resetTokenExpireTime(token, userMapTmp); }); ServerHttpRequest httpRequest = request.mutate() .header(User.CONTEXT_USER_ID, userMap.get(JwtTokenUtils.USERID).toString()) .header(User.CONTEXT_USER_NAME, userMap.get(JwtTokenUtils.USERNAME).toString()) .header(User.CONTEXT_USER_LOGIN_TYPE, userMap.get(JwtTokenUtils.USERLOGINTYPE).toString()) .header(User.CONTEXT_CLIENT_ID, userMap.get(JwtTokenUtils.CLIENTID).toString()) .header(Company.CONTEXT_COMPANY_ID, null == userMap.get(JwtTokenUtils.COMPANYID) ? "" : userMap.get(JwtTokenUtils.COMPANYID).toString()) .header(Company.CONTEXT_COMPANY_NAME, null == userMap.get(JwtTokenUtils.COMPANYNAME) ? "" : URLEncoder.encode(userMap.get(JwtTokenUtils.COMPANYNAME).toString(), "UTF-8")) .header(Dept.CONTEXT_DEPT_ID, null == userMap.get(JwtTokenUtils.DEPTID) ? "" : userMap.get(JwtTokenUtils.DEPTID).toString()) .header(Dept.CONTEXT_DEPT_NAME, null == userMap.get(JwtTokenUtils.DEPTNAME) ? "" : URLEncoder.encode(userMap.get(JwtTokenUtils.DEPTNAME).toString(), "UTF-8")) .header(User.CONTEXT_USER_AUTHORITIES, authorities) .header(User.CONTEXT_INVITATION_CODE, null == userMap.get(JwtTokenUtils.INVITATIONCODE) ? "" : userMap.get(JwtTokenUtils.INVITATIONCODE).toString()) //邀请码 .header(User.CONTEXT_USER_TOKEN, token).build(); this.preChainFilterProcess(request, userMap.get(JwtTokenUtils.USERLOGINTYPE).toString()); return chain.filter(exchange.mutate().request(httpRequest).build()); } catch (BaseException baseException) { if (!BaseException.DEFAULT_CODE.equals(baseException.getCode())) { httpResult.setStatus(baseException.getCode()); } return processException(httpResult, baseException, exchange); } catch (Exception ex) { return processException(httpResult, ex, exchange); } } /** * 认证时的异常处理 * * @param httpResult * @param ex * @param response * @return */ private Mono<Void> processException(HttpResult httpResult, Exception ex, ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); httpResult.setMessage(ex.getMessage()); String result = JSONObject.toJSON(httpResult).toString(); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); response.setStatusCode(org.springframework.http.HttpStatus.OK); return exchange.getResponse() .writeWith(Flux.just(exchange.getResponse().bufferFactory().wrap(result.getBytes()))); } /** * 过滤器执行的优先级,值越小优先级越高 * * @return */ @Override public int getOrder() { return -5; } /** * 从请求总获取token * * @param request * @return */ private String getTokenFromRequest(ServerHttpRequest request) { String token = request.getHeaders().getFirst(JwtTokenUtils.HEADER_AUTH); //由于websocket比较特殊,涉及http upgrade的过程 //无法在header中携带token,只能在请求路径中携带token //如果请求以"/websocket-server/chat/"开头,就认为是websocket类型的请求,token放在请求的最后,通过"/"截取,以获取token if (request.getURI().getPath().startsWith("/websocket-server/chat/")) { token = request.getURI().getPath().substring(request.getURI().getPath().lastIndexOf("/") + 1); } return token; } /** * 自动续期 * 重新设置token的过期时间 * * @param token * @param userMap * @return */ private void resetTokenExpireTime(String token, Map<String, Object> userMap) { String userId = userMap.get(JwtTokenUtils.USERID).toString(); String userLoginType = userMap.get(JwtTokenUtils.USERLOGINTYPE).toString(); //为了系统安全,通过员工超级密码生成的token不会自动续期,默认12小时后过期 if (UserLoginTypeConstants.EmployeeLoginPCBySuperPassword.equals(userLoginType)) { return; } if (LoginDeviceTypeConstants.APP.equals(LoginRelatedHelper.getLoginDeviceType(userLoginType))) { //登录方式为app,自动续期12天 boolean isOK = redisService.expire(token, redisProperties.getAppTokenExpireMilliSeconds()); if (isOK) { //从性能上考虑,只有上一步执行成功,才会执行下一步 redisService.expire(userLoginType + userId, redisProperties.getAppTokenExpireMilliSeconds()); } } else if (LoginDeviceTypeConstants.PC.equals(LoginRelatedHelper.getLoginDeviceType(userLoginType))) { //登录方式为pc或第三方模拟登录,自动续期8小时ø boolean isOK = redisService.expire(token, redisProperties.getPcTokenExpireMilliSeconds()); if (isOK) { //从性能上考虑,只有上一步执行成功,才会执行下一步 redisService.expire(userLoginType + userId, redisProperties.getPcTokenExpireMilliSeconds()); } } else { //针对虚拟用户的调用,由于token是不失效的,所以不用续期 } } /** * 执行chain.filter之前进行的预处理 * * @param request * @param userLoginType */ private void preChainFilterProcess(ServerHttpRequest request, String userLoginType) { this.veritySign(request, userLoginType); } /** * 校验请求中的签名 * * @param request * @param userLoginType */ private void veritySign(ServerHttpRequest request, String userLoginType) { userLoginType = null == userLoginType ? "" : userLoginType; //暂时认定通过第三方系统 虚拟用户登录 的请求中必须要携带sign和signTimestamp //【待开发】等前端改造完后再开放对所有用户请求的签名校验 if (userLoginType.startsWith(UserTypeConstants.VirtualUser)) { String sign = request.getQueryParams().getFirst("sign"); String signTimestamp = request.getQueryParams().getFirst("signTimestamp"); if (StringUtils.isBlank(sign)) { throw new BaseException(HttpStatus.SignError.getStatus(), "请求中的签名 sign 不能为空"); } if (StringUtils.isBlank(signTimestamp)) { throw new BaseException(HttpStatus.SignError.getStatus(), "请求中签名的时间戳 signTimestamp 不能为空"); } } } /** * 先从缓存中获取token对应的用户和权限,如果存在 且 没有过期,则直接返回,否则需要到redis中去获取,再写入map * 由于这里对token进行了缓存,所以在分布式场景中,客户退出登录后,token还在缓存中,这样就无法做到立即实现单点控制,但是考虑到服务器的tps,目前暂时采取这种折中的方式 * * @param token * @return */ private TokenValueInMap getTokenValue(String token) { // 先从缓存中获取token对应的权限,如果存在 且 没有过期,则直接返回,否则需要到redis中去获取,再写入map TokenValueInMap authorityInMapValue = cacheMap.get(token); // 60*60*8 * 1000 = 28800 * 1000 = 28800000 (map中的值8小时后过期) if (null == authorityInMapValue || (authorityInMapValue.timeMillis + 28800000) < System.currentTimeMillis()) { //如果判定是无效token,则直接返回 if (cacheInValidToken.containsKey(token)) { Long timeMillis = cacheInValidToken.get(token); // 60*60*24 * 1000 = 86400 * 1000 = 86400000 (24小时) if (timeMillis + 86400000 > System.currentTimeMillis()) { return null; } } String tokenValueInRedis = redisService.getValue(token); if (StringUtils.isNotBlank(tokenValueInRedis)) { TokenValueInRedis tokenValue = JSON.parseObject(tokenValueInRedis, new TypeReference<TokenValueInRedis>() { }); authorityInMapValue = new TokenValueInMap(tokenValue, System.currentTimeMillis()); cacheMap.put(token, authorityInMapValue); } else { cacheInValidToken.put(token, System.currentTimeMillis()); } } return authorityInMapValue; } }
3.authclient服务拦截器进行验签
/** * 校验签名拦截器 * 放在所有拦截器的最前面 */ @Slf4j @Component @Setter public class VerifySignatureInterceptor extends HandlerInterceptorAdapter { /** * 由外部实例化VerifySignatureInterceptor对象后传入 */ private RedisService redisService; public static final String Authorization = "Authorization"; public static final String AuthKey = "authKey"; /** * 签名校验 * * @param request * @param response * @param handler * @return * @throws IOException */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { try { //如果是访问内部资源文件,则直接放行 if (!(handler instanceof HandlerMethod)) { return true; } if (!(request instanceof RequestWrapper)) { return true; } RequestWrapper requestWrapper = (RequestWrapper) request; //请求中包含签名参数 sign 和 signTimestamp 时,才会校验签名的正确性 SortedMap<String, String> urlParams = HttpUtils.getUrlParams(requestWrapper); String sign = SignUtils.getSign(urlParams); String signTimestamp = SignUtils.getSignTimestamp(urlParams); if (!StringUtils.isEmpty(sign) && !StringUtils.isEmpty(signTimestamp)) { //从请求中获取所有参数,并组装成验证签名所需要的map SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper); this.verifyRequestBySign(requestWrapper, allParams); } return true; } catch (Exception ex) { ex.printStackTrace(); // this.returnJson(response, ex); CommonExceptionHandler.sendErrorByResponse(response, ex); return false; } } /** * 根据签名校验请求的有效性,根据所有入参进行签名计算 * * @param allParams */ private void verifyRequestBySign(HttpServletRequest request, SortedMap<String, String> allParams) { String sign = SignUtils.getSign(allParams); String signTimestamp = SignUtils.getSignTimestamp(allParams); if (!StringUtils.isEmpty(sign) && !StringUtils.isEmpty(signTimestamp)) { // 写入token String token = request.getHeader(Authorization); allParams.put(Authorization.toLowerCase(), null == token ? "" : token); //从redis中获取签名用的key,并放入SortedMap中 boolean isHasClientID = false; String clientId = request.getHeader(User.CONTEXT_CLIENT_ID); if (StringUtils.isEmpty(clientId)) { clientId = allParams.get("clientId"); } if (!StringUtils.isEmpty(clientId)) { Object authClientObj = this.redisService.hashGet(AuthConstants.PREFIX_AUTH_CLIENT, clientId); if (!StringUtils.isEmpty(authClientObj)) { JSONObject jsonObject = JSON.parseObject(authClientObj.toString()); allParams.put(AuthKey, jsonObject.getString(AuthKey)); isHasClientID = true; } } if (!isHasClientID) { throw new BaseException(HttpStatus.SignError.getStatus(), "请求参数中或token中缺少ClientID的值"); } log.info("==allParams====allParams={}"+JSON.toJSONString(allParams)); // 对参数进行签名验证 boolean isSigned = SignUtils.verifySign(allParams, 30); if (!isSigned) { throw new BaseException(HttpStatus.SignError.getStatus(), "请求的签名有误,可能是由于请求过期或参数被篡改"); } } } } /** * 签名工具类 */ @Slf4j public class SignUtils { /** * 根据入参校验 * * @param params * @return */ public static boolean verifySign(SortedMap<String, String> params, int timeoutSecond) { String urlSign = SignUtils.getSign(params); String signTimestamp = SignUtils.getSignTimestamp(params); //如果请求中没有签名和时间戳,则直接放行,用于开发内部调测,或各环境中服务间的RPC调用 if (StringUtils.isBlank(urlSign) || StringUtils.isBlank(signTimestamp)) { return true; } log.info("请求中的签名 : {}", urlSign); // 把参数加密 String paramsSign = getParamsSign(params); log.info("==paramsSign====paramsSign={}" + JSON.toJSONString(paramsSign)); log.info("计算后的签名 : {}", paramsSign); return !StringUtils.isBlank(paramsSign) && urlSign.equals(paramsSign) && checkRequestUrlIsValid(params, timeoutSecond); } /** * 获取签名数据 * * @param params * @return */ public static String getSign(SortedMap<String, String> params) { return params.get("sign"); } /** * 获取签名的时间戳 * * @param params * @return */ public static String getSignTimestamp(SortedMap<String, String> params) { return params.get("signTimestamp"); } /** * 将请求的参数进行MD5加密 * * @param params * @return */ public static String getParamsSign(SortedMap<String, String> params) { // 要先去掉 Url 里的 Sign params.remove("sign"); StringBuilder stringBuilder = new StringBuilder(150); for (Map.Entry<String, String> entry : params.entrySet()) { //value非空,才会参与签名计算 if (!org.springframework.util.StringUtils.isEmpty(params.get(entry.getKey()))) { stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } } //清除最后一位的符号:& String paramsStr = stringBuilder.toString(); if (StringUtils.isNotBlank(paramsStr)) { paramsStr = paramsStr.substring(0, paramsStr.length() - 1); } return DigestUtils.md5DigestAsHex(paramsStr.getBytes()).toUpperCase(); } /** * 校验请求中的时间戳是否过期 * * @param params * @return */ public static boolean checkRequestUrlIsValid(SortedMap<String, String> params, int timeoutSecond) { String signTimestamp = SignUtils.getSignTimestamp(params); if (StringUtils.isBlank(signTimestamp)) { return false; } Long signTimestampLong = 0L; try { signTimestampLong = Long.valueOf(signTimestamp); } catch (Exception ex) { return false; } //取得指定时区的时间(东八区) TimeZone zone = TimeZone.getTimeZone("GMT-8:00"); Calendar cal = Calendar.getInstance(zone); long currentTimestamp = cal.getTime().getTime(); long internalSecond = ((currentTimestamp - signTimestampLong) / 1000); log.info("internalSecond:" + internalSecond); //超出时间间隔,则认定请求过期 if (internalSecond > timeoutSecond) { return false; } return true; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。