interceptor方式拦截请求进行权限验证和签名验证

外部应用调用我方接口时通常需要做安全校验,这里记录一种验签方式,基于interceptor实现。

先看看接口的定义:

   @ApiOperation(value = "同步第三方商品数据")
    @PostMapping(value = "/sync")
    @ExternalAPI(sourceSystem = SourceSystemEnum.SUNAC)
    public ResponseHeaderVO<ThirdpartResponseSkuMappingVO> syncSpuData(@RequestBody @Validated ThirdpartGoodsDTO dto) {
        try {
            return thirdpartGoodsAppService.syncThirdpartGoods(dto.getData());
        } catch (Exception e) {
            log.error("call syncGoods error...", e);
            return new ResponseHeaderVO<>(RestConstants.SYSTEM_ERROR, e.getMessage(), null);
        }
    }

1 token权限校验

定义拦截器拦截接口请求,做token校验

@Slf4j
@Component
public class RequestInterceptor implements HandlerInterceptor {

    private AuthAppService authService;

    private PermissionsAppService permissionsService;

    private SignProperties signProperties;

    private static final int CREDIT_CODE_LENGTH = 18;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取请求头中的token信息,没有则适配默认值
            String token = fetchToken(request);
            //校验token'
            boolean checked = authService.checkToken(token);
            setUserLanguage(request);
            checked = handleExternalApi(handlerMethod, token, checked);
            // ignoreSecurity请求过滤
            handleIgnoreSecurity(handlerMethod, checked);
            Permissions permissions = handlerMethod.getMethodAnnotation(Permissions.class);
            if (null == permissions) {
                return true;
            }
            // 角色权限校验
            permissionsService.check(permissions.value());
        } else {
            log.error("preHandle:handlerMethod type error:{}", handler.getClass().getName());
        }
        return true;
    }

    private boolean handleExternalApi(HandlerMethod handlerMethod,
                                      String token,
                                      boolean checked) {
        if (checked) {
            return true;
        }
        boolean isExternalApi = handlerMethod.hasMethodAnnotation(ExternalAPI.class);
        if (!isExternalApi) {
            return false;
        }
        ExternalAPI externalAPI = handlerMethod.getMethodAnnotation(ExternalAPI.class);
        if (externalAPI == null) {
            return false;
        }
        SourceSystemEnum sourceSystemEnum = externalAPI.sourceSystem();
        if (SourceSystemEnum.ERP_PCMS.equals(sourceSystemEnum)) {
            throw new BizsException(RestConstants.TOKEN_ERROR, RestConstants.TOKEN_ERROR_MSG);
        } else if (SourceSystemEnum.B2B.equals(sourceSystemEnum)) {
            return handleB2b(token, externalAPI);
        } else {
            return SourceSystemEnum.SUNAC.equals(sourceSystemEnum);
        }
    }

    private boolean handleB2b(String token, ExternalAPI externalAPI) {
        // token验证失败, 验供应商token
        String decryptedSupplierNo = getDecryptedSupplierNo(token);
        if (StringUtils.isNotBlank(decryptedSupplierNo)) {
            return true;
        }
        if (externalAPI.strictMode()) {
            return false;
        }
        // 没有的话必须传18位的统一社会信用代码
        int lenz = StringUtils.length(token);
        if (lenz != CREDIT_CODE_LENGTH) {
            throw new BizsException(RestConstants.TOKEN_ERROR, RestConstants.TOKEN_ERROR_MSG);
        }
        if (!StringUtils.isAlphanumeric(token)) {
            throw new BizsException(RestConstants.TOKEN_ERROR, RestConstants.TOKEN_ERROR_MSG);
        }
        return true;
    }

    private String getDecryptedSupplierNo(String token) {
        String decryptedSupplierNo = null;
        try {
            decryptedSupplierNo = SignUtils.getDecryptContent(token, signProperties.getYgSupplierKey());
        } catch (Exception ex) {
            log.trace(ex.getMessage(), ex);
        }
        return decryptedSupplierNo;
    }

    private void handleIgnoreSecurity(HandlerMethod handlerMethod, boolean checked) {
        boolean ignoreSecurity = handlerMethod.hasMethodAnnotation(IgnoreSecurity.class);
        if (!ignoreSecurity && !checked) {
            throw new BizsException(RestConstants.TOKEN_ERROR, RestConstants.TOKEN_ERROR_MSG);
        }
    }

    private String fetchToken(HttpServletRequest request) {
        String token = request.getHeader(BaseConstants.DEFAULT_TOKEN_NAME);
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter(BaseConstants.DEFAULT_TOKEN_NAME);
        }
        return token;
    }

    private void setUserLanguage(HttpServletRequest request) {
        String language= request.getHeader(BaseConstants.SYSTEM_MESSAGE_LANGUAGE);
        if (StringUtils.isEmpty(language)) {
            language="zh";
        }
        UserLocal.USER.saveLocal(new Locale(language));
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) {
        //清空threadLocal数据
        UserLocal.USER.remove();
    }

    @Autowired
    public void setAuthService(AuthAppService authService) {
        this.authService = authService;
    }

    @Autowired
    public void setPermissionsService(PermissionsAppService permissionsService) {
        this.permissionsService = permissionsService;
    }

    @Autowired
    public void setSignProperties(SignProperties signProperties) {
        this.signProperties = signProperties;
    }
}

关于token权限校验基于redis实现:

@Component
@Slf4j
public class AuthAppService{

    @Autowired
    private RedisManager redisManager;

    public String createToken(UserInfoVO userInfoVO) {
        //创建token
        String token = SHA256Util.sha256(userInfoVO.toString() + DateUtil.getTimeNow(new Date()));
        //token存储到redis中 设置过期时间为30分钟
        String redisKey = RedisConstants.REDIS_KEY_PSI_USERTOEKN.concat(":").concat(token);
        redisManager.set(redisKey, JSON.toJSONString(userInfoVO), RedisConstants.REDIS_EDM_USERTOEKN_EXPTIME);
        return token;
    }

    public boolean checkToken(String token) {
        if (StringHelper.isEmpty(token)) {
            return false;
        }
        String redisKey = RedisConstants.REDIS_KEY_PSI_USERTOEKN.concat(":").concat(token);
        String redisValue = (String) redisManager.get(redisKey);
        if (StringUtils.isEmpty(redisValue)) {
            return false;
        }
        UserInfoVO userInfoVO = JSON.parseObject(redisValue, UserInfoVO.class);
        if (userInfoVO == null) {
            return false;
        }
        UserLocal.USER.saveToken(token);
        UserLocal.USER.saveUserInfo(userInfoVO);
        //刷新token时间
        updateTokenExpireTime(token);
        return true;
    }


    public void deleteToken(String token) {
        String redisKey = RedisConstants.REDIS_KEY_PSI_USERTOEKN.concat(":").concat(token);
        redisManager.del(redisKey);
    }

    public void updateTokenExpireTime(String token) {
        String redisKey = RedisConstants.REDIS_KEY_PSI_USERTOEKN.concat(":").concat(token);
        redisManager.expire(redisKey, RedisConstants.REDIS_EDM_USERTOEKN_EXPTIME);
    }

    public void updateToken(String token, UserInfoVO userInfoVO) {
        String redisKey = RedisConstants.REDIS_KEY_PSI_USERTOEKN.concat(":").concat(token);
        redisManager.set(redisKey, RedisConstants.REDIS_EDM_USERTOEKN_EXPTIME);
    }
}

2 web配置

定义拦截器后需要配置拦截地址等,定义如下:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {


    private static final String WEB_JARS = "/webjars/**";

    private static final String WEB_EXCEL = "/excel/**";

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/"};

    private static final String[] SWAGGER_PATTERN = {"/swagger-resources/**",
            "/v2/api-docs", "/swagger-ui.html", "/swagger-ui.html/**"};

    @Autowired
    private RequestInterceptor requestInterceptor;

    /**
     * request interceptors
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(WEB_JARS)
                .excludePathPatterns(WEB_EXCEL)
                .excludePathPatterns(SWAGGER_PATTERN);
    }

    /**
     * swagger resource
     *
     * @param registry resource handler
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(SWAGGER_PATTERN).addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
        registry.addResourceHandler(WEB_EXCEL)
                .addResourceLocations("classpath:/static/excel/");
        registry.addResourceHandler(WEB_JARS)
                .addResourceLocations(
                        "classpath:/META-INF/resources/webjars/");
    }


}

3 签名验证

关于接口中的注解@ExternalAPI(sourceSystem = SourceSystemEnum.SUNAC)
这里采用自定义注解的方式,根据接口的source来源可以指定不同调用方的签名,从而达到验签的目的

/**
 * 标记为外部调用的api
 * 这些api可以支持token验证,也可以使用签名的方式验证
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExternalAPI {

    SourceSystemEnum sourceSystem() default SourceSystemEnum.ERP_PCMS;

    boolean strictMode() default false;
}

定义注解后,通过对注解方法的代理,可以获取不同source对应的签名,从而对请求参数进行签名验证

@Slf4j
@Aspect
@Component
public class ExternalAPIAspect {

    private SignProperties signProperties;

	/**这里用 @Around(value = "@annotation(externalAPI)"),对注解方法做增强处理*/
    @Around(value = "@annotation(externalAPI)")
    public Object before(ProceedingJoinPoint pjp, ExternalAPI externalAPI) throws Throwable {
        SourceSystemEnum sourceSystem = externalAPI.sourceSystem();
        if (!SourceSystemEnum.SUNAC.equals(sourceSystem)) {
            return pjp.proceed();
        }
        Object[] args = pjp.getArgs();
        if (args == null || args.length != 1) {
            return pjp.proceed();
        }
        Object arg = args[0];
        if (!(arg instanceof DTOForExternalAPI)) {
            return pjp.proceed();
        }
        DTOForExternalAPI dto = (DTOForExternalAPI) arg;
        //校验用户是否一致
        if (!Objects.equals(dto.getUserNo(), signProperties.getSunacAppId())) {
            return new ResponseHeaderVO<String>(ResultEnum.INVALID_SIGN_ERROR.getCode(),
                    ResultEnum.INVALID_SIGN_ERROR.getMsg(), null);
        }
        try {
        	//使用特定的算法校验用户签名是否一致
            SignUtils.verifyExternalApiCall(dto, signProperties.getSunacAppKey());
        } catch (BizsException ex) {
            return new ResponseHeaderVO<String>(ex.getErrorCode(), ex.getErrorMsg(), null);
        }
        return pjp.proceed();
    }

    @Autowired
    public void setSignProperties(SignProperties signProperties) {
        this.signProperties = signProperties;
    }
}

具体的验证方式采用MD5加密:

public static void verifyExternalApiCall(DTOForExternalAPI dto, String apiKey) {
        if (StringUtils.isBlank(dto.getSign())) {
            throw new BizsException(ResultEnum.INVALID_SIGN_FIELD_SIGN_NULL_ERROR);
        }
        String checksum = encodeForExternalAPI(dto.getUserNo(), dto.getTimestamp(), apiKey);
        if (!Objects.equals(dto.getSign(), checksum)) {
            throw new BizsException(ResultEnum.INVALID_SIGN_ERROR);
        }
    }

    public static String encodeForExternalAPI(String userNo, String timestamp, String apiKey) {
        long ts;
        if (StringUtils.isBlank(timestamp)) {
            throw new BizsException(ResultEnum.INVALID_SIGN_FIELD_TIMESTAMP_NULL_ERROR);
        } else {
            try {
                ts = Long.parseLong(timestamp);
            } catch (Exception ex) {
                throw new BizsException(ResultEnum.INVALID_SIGN_FIELD_TIMESTAMP_NULL_ERROR);
            }
        }
        long tsDiff = Math.abs(System.currentTimeMillis() - ts);
        if (tsDiff > DateUtils.MILLIS_PER_HOUR) {
            throw new BizsException(ResultEnum.INVALID_SIGN_TIMESTAMP_EXPIRED_ERROR);
        }
        return MD5.md5Encode(userNo + apiKey + ts, "UTF-8");
    }

ExternalAPIAspect类中的SignProperties是在配置中指定的签名信息,最终要与接口请求数据中的签名信息比较是否一致,实现验签功能。

/**
 * 签名配置
 * 读取配置信息,获取签名信息
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "sign")
public class SignProperties {

    private String sunacAppId;

    private String sunacAppKey;

}

最后我们看看请求数据json:

{
"userNo":"PTNR9019961250",
"timestamp":1634031946432,
"sign":"b4810f2e1b4e053841affbf24bedaa6f",
"data":{
	"source" : "SUNAC",
	"spuList" : 
		[{
			"spuNo": "spu123",
			"spuName": "测试spu123",
			"categoryCode": "123456",
			"spuState": "1",
			"specList": [
				{
					"unifiedSocialCreditCode": "91440101BA59JDD17W",
					"spuImg": "https://test_url/test_image.jpg,https://test_url/test_image2.jpg",
					"spuRemark": "spu描述blabla"
				}
			 ],
			"skuList": [
			...

就是通过校验请求头中的签名从而实现接口的安全验证。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值