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

文章目录

  • 前言
  • 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张图片
本篇将讲解下单以及拉起收银台加载收银台页面时的代码实现(1/4/5/7/8/9步骤),本篇偏向代码实现。


1. 接口校验

1.1 Chains

api:
  security:
    enabled: true
    timestampMilliseconds: 600000
    securityChains:
      - /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;

    @Override
    public 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;

    @Override
    public 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;

    @Override
    public 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;

    @Override
    public 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];

    @Override
    public 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() {
        @Override
        public 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])));
        }
    }

    @Override
    public 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 {

    @Autowired
    private 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 {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return OrderInfo.class.equals(parameter.getParameterType());
    }

    @Override
    public 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. 执行流程

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


总结

拙技蒙斧正,不胜雀跃。

你可能感兴趣的:(支付系统设计,java,servlet,spring)