您的位置 首页 java

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

前言:

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

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


正文

技术设计流程

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

我们先看一下实现流程图哈,我们主要使用的技术包括: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的组件执行顺序:

监听器—》过滤器—》拦截器—》 Handler MethodArgumentResolver —》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的实现原理这里就不多做赘述了;

Thread ContextHolder

 /**
 * 线程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();
    }
复制代码  

输出结果:

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


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

文章来源:智云一二三科技

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

文章地址:https://www.zhihuclub.com/189806.shtml

关于作者: 智云科技

热门文章

网站地图