> 文章列表 > 支付系统设计:收银台设计二

支付系统设计:收银台设计二

支付系统设计:收银台设计二

文章目录

  • 前言
  • 1. 接口校验
    • 1.1 Chains
    • 1.2 Checker
      • 1.2.1 AbstractChecker
      • 1.2.2 TokenChecker
      • 1.2.3 OrderChecker
      • 1.2.4 UserInfoChecker
      • 1.2.5 BaseInfoChecker
      • 1.2.6 SignChecker
    • 1.3 ApiFilter
  • 2. 下单
  • 3. 收银台首页
    • 2.1 OrderInfoResolver
    • 2.2 UserBaseInfoResolver
  • 4. 执行流程
  • 总结

前言

支付系统设计:收银台设计二
本篇将讲解下单以及拉起收银台加载收银台页面时的代码实现(1/4/5/7/8/9步骤),本篇偏向代码实现。


1. 接口校验

1.1 Chains

api:security:enabled: truetimestampMilliseconds: 600000securityChains:- /syt/user/auth=none- /syt/user/auth/new=none- /syt/payment/mode=token- /syt/=token- /cashier/v2/home=order,user,base,sign- /cashier/v2/payment/=order,user,base,sign- /cashier/v2/coupon/list=order,user,base,sign- /withdraw/home=withdraw,user,sign- /withdraw/confirm=withdraw,user,sign... ...

cashierapi系统为业务系统和收银台前端系统提供了很多接口,这些接口所要检验的内容也有所不同,所以,将接口校验规则组成不同的Chains,配置到YML中。

如上:/cashier/v2/home=order,user,base,sign标识加载收银台首页需要进行订单校验、用户信息校验、终端校验、签名校验。

1.2 Checker

接口,主要定义了一些常量以及构建CheckerChain 的方法:

/* @author Kkk* @Describe: Checker接口*/
public interface Checker{String USERDETAILS_KEY = "USER_BASE_INFO";String APPLICATION_INFO_KEY = "BASE_PARAM_INFO";String SECRET_KEY = "SECRET_KEY";String ORDER_INFO_KEY = "ORDER_INFO";String UNIQUE_ID = "UNIQUE_ID";CheckResult PASSED = new CheckResult(200,null,null);CheckResult UNAUTHORIZED = new CheckResult(401,null,null);CheckResult FORBIDDEN = new CheckResult(403,null,null);CheckResult check(HttpServletRequest request, HttpServletResponse response);class CheckerChain implements Checker {private Checker checker;private CheckerChain chain;public CheckerChain(Iterator<Checker> iterator) {this.checker = iterator.next();if (iterator.hasNext()) {this.chain = new CheckerChain(iterator);}}public CheckResult check(HttpServletRequest request, HttpServletResponse response) {CheckResult result = checker.check(request, response);if (result.isSucess() && chain != null) {return chain.check(request, response);} else {response.setStatus(result.getStatus());return result;}}}/* 校验结果*/class CheckResult {private int status;private String code;private String message;public CheckResult(int status, String code, String message) {this.status = status;this.code = code;this.message = message;}... ...}
}

1.2.1 AbstractChecker

抽象层,主要定义一些从HttpServletRequest 头中获取指定参数的方法:

/* @author Kkk* @Describe: Checker抽象层*/
public abstract class AbstractChecker implements Checker{static CheckResult E_SYS = new CheckResult(700, ErrorEnum.ERR_SYSTEM);protected SecurityProperties securityProperties;public AbstractChecker(SecurityProperties securityProperties) {this.securityProperties = securityProperties;}protected String getFromHeaderOrParameter(HttpServletRequest request, String key) {if (request.getParameterMap().containsKey(key)) {return request.getParameter(key);} else {return request.getHeader(key);}}protected String getFromHeader(HttpServletRequest request, String key) {return request.getHeader(key);}protected String getRequestPath(HttpServletRequest request) {String url = request.getServletPath();if (request.getPathInfo() != null) {url += request.getPathInfo();}return url;}public String getToken(HttpServletRequest request){return getFromHeader(request, securityProperties.getToken());}public String getSign(HttpServletRequest request){return getFromHeaderOrParameter(request,securityProperties.getSign());}public String getRequestId(HttpServletRequest request){return getFromHeader(request, securityProperties.getRequestId());}public String getSourceInfo(HttpServletRequest request){return getFromHeader(request,securityProperties.getSourceInfo());}... ...
}

1.2.2 TokenChecker

进行Token校验:

/* @author Kkk* @Describe: TokenChecker*/
public class TokenChecker extends AbstractChecker {private static Checker.CheckResult E_TOKEN = new CheckResult(701, ErrorEnum.API_TOKEN);private IUserService userService;private ITokenService tokenService;@Overridepublic CheckResult check(HttpServletRequest request, HttpServletResponse response) {String token = getToken(request);String sourceInfo = getSourceInfo(request);if(StringUtils.isNotBlank(sourceInfo)) {try {sourceInfo = URLDecoder.decode(sourceInfo, "UTF-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}}log.info("==> The request {},appKey:{},sourceInfo:{}",getRequestPath(request),getApplication(request),sourceInfo);if (StringUtils.isBlank(token)) {log.warn("==> The request {} from IP {}, token is null.", getRequestPath(request), IPUtil.getClientIP(request));return E_TOKEN;}//验证token是否有效String secretKey;try {secretKey  = tokenService.tokenCheck(token);}catch (Exception e){return E_TOKEN;}log.debug("token获取的secretKey为:{}",secretKey);if(StringUtils.isBlank(secretKey)){return E_TOKEN;}UserBaseInfo userBaseInfo;try {userBaseInfo = userService.getUserBaseInfoByToken(token);log.debug("用户基本信息:{}", MaskUtils.toJson(userBaseInfo));}catch (Exception e) {return E_TOKEN;}if (userBaseInfo == null) {log.warn("==> The request {} from {}, token {} not found.", getRequestPath(request), IPUtil.getClientIP(request), token);return E_TOKEN;}log.info("==> The request {}{}", getRequestPath(request),MaskUtils.maskMobile(userBaseInfo.getMobile()));request.setAttribute(USERDETAILS_KEY, userBaseInfo);request.setAttribute(SECRET_KEY,secretKey);return PASSED;}
}

1.2.3 OrderChecker

校验根据HttpServletRequest 头中获取到的Token(下单时返回的accessToken)能否获取到订单信息:

/* @author Kkk* @Describe: OrderChecker*/
public class OrderChecker extends AbstractChecker {private static Checker.CheckResult E_TOKEN = new CheckResult(701, ErrorEnum.API_TOKEN);private ITokenService tokenService;@Overridepublic CheckResult check(HttpServletRequest request, HttpServletResponse response) {String sourceInfo = getSourceInfo(request);String accessToken = this.getToken(request);if (StringUtils.isBlank(accessToken)) {return E_TOKEN;}logger.info("==> The request {},appKey:{},token:{},sourceInfo:{},ip:{},requestId:{}",getRequestPath(request), getApplication(request), accessToken, sourceInfo, IPUtil.getClientIP(request), getRequestId(request));OrderInfo orderInfo;try {orderInfo = tokenService.getOrderInfo(accessToken);} catch (Exception e) {logger.error("request:{},来源IP:{},查询orderInfo异常:{}", getRequestPath(request), IPUtil.getClientIP(request), e.getMessage());return E_TOKEN;}request.setAttribute(ORDER_INFO_KEY, orderInfo);request.setAttribute(SECRET_KEY, orderInfo.getSecretKey());request.setAttribute(UNIQUE_ID, orderInfo.getUniqueId());return PASSED;}
}

并将orderInfo以及其中的自动SecretKey、UniqueId都存入到request中,供后面的CheckerResolver获取:

   request.setAttribute(ORDER_INFO_KEY, orderInfo);request.setAttribute(SECRET_KEY, orderInfo.getSecretKey());request.setAttribute(UNIQUE_ID, orderInfo.getUniqueId());

1.2.4 UserInfoChecker

校验根据OrderChecker获取到的UniqueId校验用户信息:

OrderChecker根据Token获取到订单信息并去除其中的UniqueId信息存入到HttpServletRequest,此处就可以从Request中获取到UniqueId信息了,然后调用客户管理系统获取并校验用户信息了。

request.setAttribute(UNIQUE_ID, orderInfo.getUniqueId());

/* @author Kkk* @Describe: UserInfoChecker*/
public class UserInfoChecker extends AbstractChecker {private IUserService userService;@Overridepublic CheckResult check(HttpServletRequest request, HttpServletResponse response) {String uniqueId = request.getAttribute(UNIQUE_ID).toString();UserBaseInfo userBaseInfo;try {userBaseInfo = userService.getUserBaseInfoByUniqueId(uniqueId);}catch (Exception e){e.printStackTrace();logger.info("调用cmc系统异常,{}",e.getMessage());return E_SYS;}logger.info("==> The request {}{},orderInfo:{}", getRequestPath(request), MaskUtils.maskMobile(userBaseInfo.getMobile()), JsonUtil.toJson(request.getAttribute(ORDER_INFO_KEY)));request.setAttribute(USERDETAILS_KEY,userBaseInfo);return PASSED;}
}

OrderChecker 一样的套路将userBaseInfo存入到request中:

 request.setAttribute(USERDETAILS_KEY,userBaseInfo);

1.2.5 BaseInfoChecker

校验客户端信息,同样是从HttpServletRequest中取出OrderInfo ,从OrderInfo中取出applicationKey ,查询对应系统进行校验:

/* @author Kkk* @Describe: BaseInfoChecker*/
public class BaseInfoChecker extends AbstractChecker {private IApiApplicationService apiApplicationService;@Overridepublic CheckResult check(HttpServletRequest request, HttpServletResponse response) {OrderInfo orderInfo = (OrderInfo) request.getAttribute(ORDER_INFO_KEY);String applicationKey = orderInfo.getAppKey();if (StringUtils.isBlank(applicationKey)) {logger.warn("==> The request {} from {}, appKey is null.", getRequestPath(request), IPUtil.getClientIP(request));return E_SYS;}ApiApplication apiApplication;try {apiApplication = apiApplicationService.getApiApplication(applicationKey);if (apiApplication == null) {logger.error("==> The request {} from {}, appKey: {} is not found.", getRequestPath(request), IPUtil.getClientIP(request), applicationKey);return E_SYS;}} catch (Exception e) {String message = String.format("==> The request %s from %s, get appKey: %s faild.", getRequestPath(request), IPUtil.getClientIP(request), applicationKey);logger.error(message, e);return E_SYS;}BaseRequest baseRequest = convertToBaseRequest(apiApplication);baseRequest.setIp(IPUtil.getClientIP(request));request.setAttribute(APPLICATION_INFO_KEY,baseRequest);return PASSED;}
}

OrderChecker 一样的套路将baseRequest存入到request中:

  request.setAttribute(APPLICATION_INFO_KEY,baseRequest);

1.2.6 SignChecker

首先从HttpServletRequest头部取出需要参加验签的字段,然后拼接,取出在OrderChecker根据Token获取到的订单信息中的存入到request中的SecretKey,进行验签:

/* @author Kkk* @Describe: SignChecker*/
public class SignChecker extends AbstractChecker {private static CheckResult E_SIGN = new CheckResult(701, ErrorEnum.API_SIGN);private static CheckResult E_REATTACK = new CheckResult(703, ErrorEnum.ERR_REATTACK);private JedisCluster jedisCluster;private String[] headerKeys = new String[2];@Overridepublic CheckResult check(HttpServletRequest request, HttpServletResponse response) {..... .....return PASSED;}
}

1.3 ApiFilter

构建Filter,在过滤器中执行CheckerChain

 @Bean@Order(2)public Filter apiFilter() {SecurityChecker securityChecker = new SecurityChecker(securityProperties.isEnabled(), securityProperties.getSecurityChains());List<TerminalConfig> configs = JsonUtil.jsonToGenericObject(terminalConfig, new TypeReference<List<TerminalConfig>>() {});securityChecker.addChecker("sign", new SignChecker(securityProperties,jedisCluster));securityChecker.addChecker("token",	new TokenChecker(securityProperties, userService, tokenService));securityChecker.addChecker("order",	new OrderChecker(securityProperties, tokenService));securityChecker.addChecker("user",	new UserInfoChecker(securityProperties, userService));securityChecker.addChecker("base",	new BaseInfoChecker(securityProperties, apiApplicationService));securityChecker.addChecker("caller",new CallerChecker(securityProperties,configs));securityChecker.addChecker("withdraw",new WithdrawOrderChecker(tokenService,securityProperties));... ...return new ApiSecurityFilter(securityChecker);}

SecurityChecker中构建MatchChecker,以达到根据请求路径执行对应的Checker:

/* @author Kkk* @Describe: SecurityChecker*/
public class SecurityChecker implements Checker {private static final String NONE = "none";private static Checker none = new Checker() {@Overridepublic CheckResult check(HttpServletRequest request, HttpServletResponse response) {return PASSED;}};private boolean enabled;private List<String> securityChains = new ArrayList<>();private Map<String, Checker> checkers = new HashMap<>();private List<MatchChecker> matchCheckers = new ArrayList<>();public SecurityChecker(boolean enabled, List<String> securityChains) {this.enabled = enabled;this.securityChains = securityChains;}public SecurityChecker addChecker(String name, Checker checker) {checkers.put(name, checker);return this;}... ...public void init() {logger.info("===> Init security chains {}", securityChains);for (String securityPattern : securityChains) {String[] sp = StringUtils.splitPreserveAllTokens(securityPattern, "=");matchCheckers.add(new MatchChecker(PathMatcher.create(sp[0]), parse(sp[1])));}}@Overridepublic CheckResult check(HttpServletRequest request, HttpServletResponse response) {String path = getRequestPath(request);for (MatchChecker mc : matchCheckers) {if (mc.matcher.matches(path)) {return mc.checker.check(request, response);}}logger.info("===> Security checker none for path: {}", path);return PASSED;}private String getRequestPath(HttpServletRequest request) {String url = request.getServletPath();if (request.getPathInfo() != null) {url += request.getPathInfo();}return url;}private Checker parse(String strCheckers) {if (StringUtils.isBlank(strCheckers)) {return none;}List<Checker> cs = new ArrayList<Checker>();for (String name : strCheckers.trim().split(",")) {if (NONE.equals(name)) {cs.add(none);} else if (!checkers.containsKey(name)) {throw new CashierException(ErrorEnum.ERR_PARAM, String.format("Security checker name:%s not support.", name));} else {cs.add(this.checkers.get(name));}}return cs.isEmpty() ? none : new CheckerChain(cs.iterator());}private static class MatchChecker {public PathMatcher.Matcher matcher;public Checker checker;public MatchChecker(PathMatcher.Matcher matcher, Checker checker) {this.matcher = matcher;this.checker = checker;}}
}

到此我们就完成了下单以及加载收银台首页需要进行的校验了,

#下单需要校验的Checker
- /syt/=token
#拉起收银台需要校验的Checker
- /cashier/v2/home=order,user,base,sign

2. 下单

业务系统首先调用cashierapi系统进行下单:

    @RequestMapping(value = "/auth/payment",method = RequestMethod.POST)public AuthenResultVO payment(@RequestBody PaymentRequestVO requestVO){return cashierService.payment(requestVO);}

PaymentRequestVO 即为下单对象,传入到系统后创建订单并存入Redis,同时生成token、secretKey。

   ... ...String token = SytUtil.getUUID();String secretKey = SytUtil.getSecretKey(16);... ...OrderInfo orderInfo = BeanUtil.copyProperties(request, OrderInfo.class);orderInfo.setSecretKey(secretKey);orderInfo.setToken(token);redisService.setex(key, RedisSettings.EXPIRE_ACCESS_TOKEN_SALT, token);redisService.setex(token, RedisSettings.EXPIRE_ACCESS_TOKEN_SALT, JsonUtil.toJson(orderInfo));return new AuthenResultVO(request.getUniqueId(), token, secretKey, RedisSettings.EXPIRE_ACCESS_TOKEN_SALT);

AuthenResultVO 响应对象:

/* @author  Kkk* @Describe: 主动支付下单响应VO*/
public class AuthenResultVO {/* 用户唯一标识*/private String uniqueId;/* 准入令牌*/private String accessToken;/* 秘钥*/private String secretKey;/* 令牌剩余时间*/private Integer expireSeconds;... ...  
}

待业务系统发起支付拉起收银台时加载收银台页面只需要传入Token就能获取到下单时的原订单信息了。

3. 收银台首页

/* @author Kkk* @Describe: 首页模块*/
@Api(value = "首页模块",description = "首页模块",produces="application/json")
@RequestMapping("/cashier")
@RestController
public class HomeController {@Autowiredprivate IHomeService homeService;@ApiOperation("获取标准收银台首页数据")@RequestMapping(value = "/v2/home", method = RequestMethod.GET)public HomeInfoVO getHomeInfo(UserBaseInfo userBaseInfo, OrderInfo orderInfo, BaseRequest baseRequest) {return homeService.getHomeInfo(userBaseInfo, orderInfo, baseRequest);}@ApiOperation("获取免登陆收银台首页数据")@RequestMapping(value = "/v3/home", method = RequestMethod.GET)public HomeInfoVO getHomeInfoNoLanding(UserBaseInfo userBaseInfo, OrderInfo orderInfo, BaseRequest baseRequest) {return homeService.getHomeInfo(userBaseInfo, orderInfo, baseRequest);}
}

当看到接口中入参如下,是不是感觉好像不对,但是又说不出来什么?

getHomeInfo(UserBaseInfo userBaseInfo, OrderInfo orderInfo, BaseRequest baseRequest)

缺少@RequestBody,那么我们就为其加上,但是不是加注解,而是使用HandlerMethodArgumentResolver方式:

2.1 OrderInfoResolver

将SecurityChecker中添加到HttpServletRequest的参数取出来对应到getHomeInfo中对应的参数中:

/* @author Kkk* @Describe: OrderInfoResolver*/
public class OrderInfoResolver implements HandlerMethodArgumentResolver {@Overridepublic boolean supportsParameter(MethodParameter parameter) {return OrderInfo.class.equals(parameter.getParameterType());}@Overridepublic Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {return webRequest.getNativeRequest(HttpServletRequest.class).getAttribute(SecurityChecker.ORDER_INFO_KEY);}
}

2.2 UserBaseInfoResolver

一样的套路:略

4. 执行流程

通过一个图稍微总结下大概执行流程:
支付系统设计:收银台设计二
收银台总体来说是比较简单的,没什么复杂场景。


总结

拙技蒙斧正,不胜雀跃。