文章目录
- 前言
- 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: 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中,供后面的Checker和Resolver获取:
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. 执行流程
通过一个图稍微总结下大概执行流程:
收银台总体来说是比较简单的,没什么复杂场景。
总结
拙技蒙斧正,不胜雀跃。