接口鉴权实践-代码

   日期:2020-07-13     浏览:110    评论:0    
核心提示:文章目录目标接口设计认证鉴权服务代码实践声明鉴权注解鉴权注解的具体实现鉴权服务被鉴权注解修饰的公共接口加密解密代码其他目标接口鉴权的代码实践参考:HandlerMethodArgumentResolver用于统一获取当前登录用户springboot自定义参数解析HandlerMethodArgumentResolverWebMvcConfigurer.addArgumentResolvers自定义参数处理器不生效的原理与解决方案百度api-鉴权认证机制鉴权认证机制字节数组与16进制字符串

文章目录

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

目标

  • 接口鉴权的代码实践

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

    springboot自定义参数解析HandlerMethodArgumentResolver

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

    百度api-鉴权认证机制

    鉴权认证机制

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

    spring几种获取 HttpServletRequest 对象的方式

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

接口设计

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

业务细节:

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

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

实现思路:

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

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

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

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

简单流程图:

认证鉴权服务

  • 认证服务

    最常见的就是使用用户名密码进行登录,在传统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


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BusinessAuthentication {
    
    int callNumber() default -1;

// 
// String[] IPLimit() default {};
}

鉴权注解的具体实现

fun.gengzi.codecopy.aop.BusinessAuthenticationAspect

采用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() {

    }

    
    @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;
    }

    
    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;
    }

    
    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


@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 认证字符串

Authorization 的生成策略:

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服