Java实战指南|玩转接口验签-你和高手只差俩个自定义注解

Posted 我是老实人辶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java实战指南|玩转接口验签-你和高手只差俩个自定义注解相关的知识,希望对你有一定的参考价值。

前言:

一些个很朴素的功能【登陆功能+接口验签+登陆用户信息共享】这三个功能想必是大家在日常开发中基本上大都碰到过的吧,如果你还在使用拦截器给接口加白名单来进行过滤那些接口需要验签,如果你还在每次需要拿用户信息的时候都得去查一遍db,那么你就值得看下去,小编教你如何花式玩转接口登陆验签功能👇👇👇


正文

技术设计流程

我们先看一下实现流程图哈,我们主要使用的技术包括:HandlerMethodArgumentResolver(参数解析器),HandlerInterceptor(拦截器),线程的局部变量ThreadLocal;至于生成token的逻辑这里就不给大家写了,小编这里是调的公司用户中心的sso登陆接口,这个实现也很简单:JWT等等或者自己实现都可以,但是要和用户的userId绑定哈;

技术实现:

我们实现验签功能主要围绕着俩个自定义注解来进行实现:

  • @CurrentUser:标注参数实现用户信息获取
  • @UserAuthPassPort:标识需要进行验签的方法

这样做的好处是使用更加的灵活,使我们的代码不在是大段大段的重复性的获取用户信息或者一些你系统里常用到的一些信息,我们使用ThreadLocal也同时保证了线程数据的安全性,也不需要我们在新加入功能时又得新增白名单或者删除白名单等,还有就是够“炫酷”啊,xdm!

@UserAuthPassPort

import com.peppa.userserver.auth.starter.annotation.UserAuthPassport;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;

/**
 * 
 * @author taoze
 * @version 1.0
 * @date 7/5/21 10:39 AM
 */
@Target({METHOD})
@Retention(RetentionPolicy.RUNTIME)
@UserAuthPassport
public @interface UserAuthPassPort {
}

复制代码

@CurrentUser

package com.peppa.core.api.common.auth;
import java.lang.annotation.*;

/**
 * 
 * 示例:@CurrentUser UserInfo userInfo
 * 标注在Controller入参即可,需配合@UserAuthPassPort使用
 *
 * @author taoze
 * @version 1.0
 * @date 7/6/21 10:39 AM
 */
@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CoreCurrentUserId {
}

复制代码

ok!我们接下来先看一下拦截器的代码哈 我们对@UserAuthPassPort这个注解进行拦截,至于为什么使用拦截器呢 使用AOP的环绕通知拦截注解不是更简单吗,大家可以参照一下Java的组件执行顺序:

监听器---》过滤器---》拦截器---》HandlerMethodArgumentResolver ---》AOP

可见AOP的执行顺序是最后的 尤其系统复杂度上去了以后,会依赖各个其他的服务,会有着很多的拦截器什么的,所以当你使用AOP的时候可能都没走到你的AOP就被拦截return掉了;

SsoTokenInterceptor:

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * SsoToken拦截验签
 *
 * @author taoze
 * @version 1.0
 * @date 7/1/21 8:23 PM
 */
@Slf4j
@Component
public class SsoTokenInterceptor implements HandlerInterceptor {

    @Autowired
    private UserAuthFeign authFeign;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        if (method.getAnnotation(UserAuthPassPort.class) != null) {
            //获取对象头中token信息
            log.info("命中拦截器-> 开始验签");
            String userToken = request.getHeader("user-token");
            if (StringUtils.isBlank(userToken)) {
                throw new APIException(“自己实现哈!”);
            }
            try {
                checkoutToken(ssoToken, userToken);
            } catch (RemoteServerException e) {
                log.error("authAspectMethod -> checkoutToken is fail ", e);
                throw new APIException(“自己实现哈!”);
            }
        }
        log.info("未命中拦截器-> 跳过验签");
        return true;

    }

    /**
     * ssoToken 验签
     *
     * @param ssoToken todo 需增加降级策略
     * @return
     */
    private void checkoutToken(String ssoToken, String userToken) throws RemoteServerException {

        UserInfo userInfo = FeignUtil.getResponseData(authFeign.getUserInfoByToken(userToken));
        if (null == userId || userId < 0) {
            throw new APIException(“自己实现哈!”);
        }
        ThreadContextHolder.setUserInfo(userInfo);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        log.info("请求结束-> clear ThreadLocal");
        ThreadContextHolder.destroy();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        // Do nothing because of X and Y.
    }
}
复制代码

简略的解释一下上边的代码,我们通过拦截器preHandle方法进入Controller方法前method.getAnnotation(UserAuthPassPort.class) != null先获取当前请求方法是否标注我们的自定义验签注解,如果标准的话就进入拦截器,进行验签checkoutToken在验签的时候我们需要通过userToken去获取当前userToken对应的user信息保存在ThreadLocal里以便之后的业务中使用;最后生成视图后也就是方法执行结束后postHandle里清除ThreadLocal里的用户信息,否则会有内存泄漏的可能!切记必须清除!必须清除!必须清除! 重要的事情说三遍,为什么必须清除大家可以看一下ThreadLocal的实现原理这里就不多做赘述了;

ThreadContextHolder

/**
 * 线程ThreadLocal
 *
 * @author taoze
 * @version 1.0
 * @date 7/6/21 10:39 AM
 */
public class ThreadContextHolder {

    private ThreadContextHolder() {
    }

    private static ThreadLocal<UserInfo> userInfo = new ThreadLocal<>();

    public static void setUserInfo(UserInfo us) {
        userInfo.set(us);
    }

    public static UserInfo getUserInfo() {
        return userInfo.get();
    }

    public static void destroy() {
        if (userInfo != null) {
            userInfo.remove();
        }
    }
}
复制代码

这个是ThreadLocal的一个公共方法,大家可以根据自己的需求去做一合适自己的更改,可以设置多个ThreadLocal;

CurrentUserResolver

package com.peppa.core.api.common.auth;

import com.peppa.core.api.exception.ServiceException;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

/**
 * SpringMvc参数解析赋值UserId
 *
 * @author taoze
 * @version 1.0
 * @date 7/6/21 3:56 PM
 */
@Component
public class CoreCurrentUserIdResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        CurrentUser currentUserAnnotation = parameter.getParameterAnnotation(CurrentUser.class);
        if (currentUserIdAnnotation == null) {
            throw new ServiceException(-9999, "not found CurrentUser annotation name " + parameter.getParameterName());
        } else {
            UserInfo userInfo = ThreadContextHolder.getUserInfo();
            if (userInfo != null) {
                Class<?> parameterClass = parameter.getParameterType();
                if (parameterClass.equals(UserInfo.class)) {
                    return userInfo;
                }
            }
        }
        return null;
    }
}

复制代码

supportsParameter方法只有返回true的时候才会进入resolveArgument,验证当前注解是否存在,这个方法做了这几个事获取入参的标注的注解,然后获取注解ThreadLocal里的用户信息,判断注解标注参数是不是UserInfo.class,赋值给当前参数;

ok,接下来我们来测试一下:

    @PostMapping("/test")
    @UserAuthPassPort
    public ResponseBody<AuthCodeJson> getAuthCode(@CurrentUser UserInfo userInfo) {
        System.out.println(userInfo.getId());
        System.out.println(ThreadContextHolder.getUserInfo().getUserId);
        return this.success();
    }
复制代码

输出结果:


ok!本期的Java实战就到这了,你掌握了吗,希望可以对大家有帮助,有不对的地方希望大家可以提出来的,共同成长;

整洁成就卓越代码,细节之中只有天地

以上是关于Java实战指南|玩转接口验签-你和高手只差俩个自定义注解的主要内容,如果未能解决你的问题,请参考以下文章

如何从零开始玩转接口测试?

马云入局人工智能寻找新机遇!原来你和大佬比肩只差这一步……

java面向对象你离武林高手只差以下这几点了

java面向对象你离武林高手只差以下这几点了

有了这50+软件,你和CNS只差一个Idear的距离 !(下载时间有限)

性能测试的那点事儿之玩转接口性能测试