接口鉴权实践-代码

文章目录

    • 目标
    • 接口设计
    • 认证鉴权服务
    • 代码实践
      • 声明鉴权注解
      • 鉴权注解的具体实现
      • 鉴权服务
      • 被鉴权注解修饰的公共接口
    • 加密解密
      • 代码
    • 其他

目标

  • 接口鉴权的代码实践

    参考:HandlerMethodArgumentResolver用于统一获取当前登录用户

    springboot自定义参数解析HandlerMethodArgumentResolver

    WebMvcConfigurer.addArgumentResolvers自定义参数处理器不生效的原理与解决方案

    百度api-鉴权认证机制

    鉴权认证机制

    字节数组与16进制字符串的相互转换

    spring几种获取 HttpServletRequest 对象的方式

    AES自动生成base64密钥加密解密

接口设计

业务场景:模拟普通用户调用公共接口

业务细节:

此公共接口要求只允许普通用户调用10次。

此接口要求验证请求者的身份保护传输中的数据,防止非法篡改防止重放攻击

实现思路:

用户需要携带认证token,认证字符串(签名),请求参数,请求公共接口。

token :用户通过登录,手机验证码等方法调用系统,颁发的认证标识。(验证请求者身份)

签名(sign):将请求参数和随机码(reqnum)和有效时间(timespace)拼接,根据密钥(signkey )加密得到。(保护传输中的数据,防止非法篡改,防止重复攻击)

鉴权服务检查token的合法性有效性,根据签名检查请求参数没有被非法篡改,根据签名中的有效时间来防止在有效时间外来重放请求。

简单流程图:

接口鉴权实践-代码_第1张图片

认证鉴权服务

  • 认证服务

    最常见的就是使用用户名密码进行登录,在传统web项目中使用session来保证是同一会话,在微服务的架构中,常用redis来保存用户信息,来模拟session的作用。redis的单点登陆,实践参考:https://github.com/gengzi/GsjBlog 关注interceptor 和UserController 包下的代码。

  • 鉴权服务

    鉴权需要在执行真正controller接口方法之前执行,之前的web项目一般使用拦截器(Interceptor)或者过滤器(Filter)来实现请求的拦截,校验session是否存在,不存在即用户登陆失效,让用户重新登陆。基于springboot工程,可以使用注解(annotation),webmvcconfig的形式来针对请求拦截的处理,来实现鉴权服务。

代码实践

项目源码地址:https://github.com/gengzi/codecopy

版本环境 :jdk1.8,Spring boot 2.2.7,mysql5.7,reids

上述已经说明了思路和一般常见的实现方式。

下面通过注解的方式,来实现。使得被注解修饰的controller接口,进行鉴权操作。

如果希望与所有的接口或者很多接口都执行鉴权操作,可以使用webmvcconfig配置对方法的处理。在参考的一些文章中,有具体说明。

声明鉴权注解

fun.gengzi.codecopy.aop.BusinessAuthentication

/**
 * 

接口鉴权注解

* 用于标识那些controller 接口需要鉴权才能调用 * * @author gengzi * @date 2020年6月4日16:43:30 *

* 对于接口鉴权,也可以使用 拦截器,过滤器,指定那些路径进行拦截。 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface BusinessAuthentication { /** * 调用次数限定 默认 -1 不限制次数 * @return */ int callNumber() default -1; // /** // * ip 限定,限定指定范围的ip地址,访问该接口 // * // * @return // */ // String[] IPLimit() default {}; }

鉴权注解的具体实现

fun.gengzi.codecopy.aop.BusinessAuthenticationAspect

采用aop的思想,为添加了鉴权注解的方法,增强功能

/**
 * 

接口鉴权校验 aop

*/
@Aspect @Configuration public class BusinessAuthenticationAspect { private Logger logger = LoggerFactory.getLogger(BusinessAuthenticationAspect.class); // 鉴权服务接口url地址 @Value("${token.url.validToken}") private String validToken; // AES 密钥 @Value("${token.aeskey}") private String aeskey; // RSA 的密钥 @Value("${token.publickey}") private String publickey; //切入点 @Pointcut("@annotation(fun.gengzi.codecopy.aop.BusinessAuthentication)") public void BusinessAuthenticationAspect() { } /** *

环绕通知

* // 根据controller 配置的注解,执行该方法 * // 获取请求信息中的 token 校验,存在,执行 token 校验,不存在,阻断 * // 校验 token ,失败,阻断 * // 获取该注解配置的字段信息 * // 校验该用户是否还有调用次数 , 无 ,阻断 * // 校验该用户是否在允许的ip 范围, 无,阻断 * // 放行 * * @param joinPoint * @return */
@Around("BusinessAuthenticationAspect()") public Object around(ProceedingJoinPoint joinPoint) { logger.info("鉴权 - BusinessAuthenticationAspect start"); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); BusinessAuthentication businessAuthentication = method.getAnnotation(BusinessAuthentication.class); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 校验token 是否有效 final String token = request.getHeader(HttpHeaders.AUTHORIZATION); // 获取controller 方法名称 final String name = method.getName(); logger.info("被鉴权的方法 - BusinessAuthenticationAspect method name : {}", name); // 如果没有 token,进行记录并抛出异常,响应前台 if (StringUtils.isBlank(token)) { logger.info("无token结束 - BusinessAuthenticationAspect no token, end !"); throw new RrException("无权限", RspCodeEnum.NOTOKEN.getCode()); } // 调用鉴权服务接口,鉴权 Boolean flag = reqValidToken(businessAuthentication, token); Object obj; try { if (flag) { // 鉴权成功,允许调用指定接口 logger.info("鉴权成功 - BusinessAuthenticationAspect success !"); obj = joinPoint.proceed(); } else { logger.info("鉴权失败- BusinessAuthenticationAspect failure !"); throw new RrException("无权限"); } } catch (Throwable e) { logger.error("鉴权失败,出现异常- BusinessAuthenticationAspect failure ! , exception : {} ", e.getMessage()); throw new RrException("无权限"); } return obj; } /** *

调用接口鉴权服务

*

* // 调用鉴权服务,进行 token 校验,并返回该用户信息 * // 先定义 aes 秘钥,定义 rsa 的公钥和秘钥 * // 使用 aes 秘钥对 组装后的参数加密,再base64 转码 生成一个签名 * // 调用服务端, token 设置在请求头,请求体是 签名 , 其他固定的几个参数 *

* // 服务端校验 token, 解析请求体参数,将签名 使用 base64 解开,然后 aes 解密 * // 解密完成,比较 参数是否一致,一致说明,参数没有在传递时发生更改。 * // 服务端鉴权完毕,响应数据,将数据使用 rsa 私钥加密,使用base64 转码 *

* // 客户端获取响应数据,对应字段使用base64 转码,使用 rsa 公钥解密,解密完成,正式成功 * * @param businessAuthentication 注解 * @param token 认证token * @return 鉴权成功 true ,鉴权失败 false */ private Boolean reqValidToken(BusinessAuthentication businessAuthentication, String token) { ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>(); StringBuilder signBuilder = new StringBuilder(); // 获取注解中的字段 final int callNumber = businessAuthentication.callNumber(); // 随机码 String reqNum = IdUtil.randomUUID(); // 组拼参数,例如: reqNum=uuid值&callNumber=33& if (callNumber > 0) { concurrentHashMap.put(AuthenticationConstans.CALLNUMBER, String.valueOf(callNumber)); } concurrentHashMap.put(AuthenticationConstans.REQNUM, reqNum); concurrentHashMap.forEach((key, value) -> signBuilder.append(key).append("=").append(value).append("&")); // 将参数加密 String signStr = AESUtils.encrypt(signBuilder.toString(), aeskey) .orElseThrow(() -> new RrException("error", RspCodeEnum.FAILURE.getCode())); // 调用鉴权服务接口 logger.info("调用鉴权接口参数- BusinessAuthenticationAspect qryParams token :{},reqNum :{} ,signStr :{}", token, reqNum, signStr); ReturnData returnData = getReturnData(token, reqNum, signStr); logger.info("调用鉴权接口结果- BusinessAuthenticationAspect result", returnData.toString()); Boolean flag = false; if (RspCodeEnum.SUCCESS.getCode() == returnData.getStatus()) { Object info = returnData.getInfo(); if(info instanceof JSONObject){ TokenUserInfoResp.UserinfoData userinfoData = JSONUtil.toBean((JSONObject) info, TokenUserInfoResp.UserinfoData.class); String certificateNo = userinfoData.getCertificateNo(); // RSA 解密 String certificateNoDecrpt = RSAUtils.decrypt(certificateNo, publickey).orElseThrow(() -> new RrException("error")); if (callNumber == -1 || callNumber >= Integer.valueOf(certificateNoDecrpt) ){ flag = true; } } } return flag; } /** * 调用鉴权服务接口 * * @param token token * @param reqNum 随机码 * @param signStr 签名 * @return {@link ReturnData} */ private ReturnData getReturnData(String token, String reqNum, String signStr) { // 封装请求参数 RequestParamEntity requestParamEntity = new RequestParamEntity(); requestParamEntity.setReqNum(reqNum); requestParamEntity.setSign(signStr); String jsonBody = JSONUtil.parseObj(requestParamEntity, false).toStringPretty(); String body = HttpRequest.post(validToken) .header(Header.AUTHORIZATION, token) .body(jsonBody).execute().body(); return JSONUtil.toBean(body, ReturnData.class); } }

鉴权服务

fun.gengzi.codecopy.business.authentication.controller.AuthenticationController

/**
 * 

接口鉴权controller

* * @author gengzi * @date 2020年6月5日10:42:08 */
@Api(value = "接口鉴权", tags = {"接口鉴权"}) @Controller @RequestMapping("/api/v1") public class AuthenticationController { private Logger logger = LoggerFactory.getLogger(AuthenticationController.class); // RSA 的密钥 @Value("${token.secretkey}") private String secretkey; private final AuthenticationService authenticationService; public AuthenticationController(AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @ApiOperation(value = "校验token", notes = "校验token") @ApiImplicitParams({ @ApiImplicitParam(name = "RequestParamEntity", value = "请求参数实体", required = true)}) @ApiResponses({@ApiResponse(code = 200, message = "\t{\n" + "\t \"status\": 200,\n" + "\t \"info\": {\n" + "\t }\n" + "\t \"message\": \"success\",\n" + "\t}\n")}) @PostMapping("/validToken") @ResponseBody public ReturnData validToken(@RequestBody RequestParamEntity requestParamEntity, HttpServletRequest request) { ReturnData ret = ReturnData.newInstance(); final String token = request.getHeader(HttpHeaders.AUTHORIZATION); // 校验token 是否有效 Boolean validToken = authenticationService.isValidToken(token); if(validToken){ // 校验签名 Boolean validSign = authenticationService.isValidSign(requestParamEntity); if(validSign){ // 根据token 获取用户信息,响应 // TODO 默认响应一个,该用户调用的次数,写死 10 TokenUserInfoResp.UserinfoData userinfoData = new TokenUserInfoResp.UserinfoData(); // 将返回字段都进行 rsa 加密 String numNo = RSAUtils.encrypt("1", secretkey).orElse(""); userinfoData.setCertificateNo(numNo); ret.setSuccess(); ret.setInfo(userinfoData); ret.setMessage("success"); return ret; } } ret.setFailure("failure"); return ret; } }

被鉴权注解修饰的公共接口

fun.gengzi.codecopy.business.shorturl.controller.ShortUrlGeneratorController

当调用此接口时,先执行鉴权注解的方法,如果鉴权成功,再执行真正的目标接口。

    @PostMapping("/getShortUrlByTest")
    @ResponseBody
    @BusinessAuthentication(callNumber = 10)
    public ReturnData testgeneratorShortUrl(@RequestParam("longurl") String longurl) {
        logger.info("getShortUrl start {} ", System.currentTimeMillis());
        String shortUrl = shortUrlGeneratorService.generatorShortUrl(longurl);
        ReturnData ret = ReturnData.newInstance();
        ret.setSuccess();
        ret.setMessage(shortUrl);
        return ret;
    }

加密解密

上述在鉴权注解aop和鉴权服务中,分别使用两种加密方式,RSA 非对称加密,AES 对称加密。

对称加密

非对称加密

在响应给前端的一些数据中,也可以使用DSA加密后,响应,考虑脱敏后再响应。

其中在加密解密中,还使用base64 转码,为什么要使用base64 ,因为AES 和 RSA 加密后的数据都是字节数组,base64可以将字节数组转为字符串,方便于传输数据。

代码

使用了 httool 工具类,参考github

fun.gengzi.codecopy.utils.AESUtils

fun.gengzi.codecopy.utils.RSAUtils

其他

对于完善的接口设计,上述内容还是太薄弱。可以参考大厂官方api接口的设计,更加的复杂也会更加的安全,也可以作为设计的参考。

下面是百度api的一些设计,可以参考。

百度api-鉴权认证机制

这里关注一下请求头里面的 Authorization 认证字符串

接口鉴权实践-代码_第2张图片

Authorization 的生成策略:

接口鉴权实践-代码_第3张图片

接口鉴权实践-代码_第4张图片

你可能感兴趣的:(业务场景实现,接口,鉴权,安全)