背景:xx-user项目引入自定义依赖包xx-base,在xx-base中拦截网关服务传入的请求头x-authorization,在拦截器中进行解析转为认证凭证对象,将controller方法中的Authentication类型参数替换为请求头中解析出来的认证凭证对象。

一、xx-base依赖项目开发:

认证凭证对象

import java.util.Arrays;

/**
 * @Author: BillYu
 * @Description: 认证信息
 * @Date: Created in 16:35 2023/5/8.
 */

public class Authentication {
    private Long uid;

    private String[] scope;

    private String mid;

    private Long enterpriseId;

    private String appId;

    private String[] authorities;

    private String client_id;

    public Authentication() {
    }

    public Authentication(Long uid, String[] scope, String mid, Long enterpriseId, String appId, String[] authorities, String client_id) {
        this.uid = uid;
        this.scope = scope;
        this.mid = mid;
        this.enterpriseId = enterpriseId;
        this.appId = appId;
        this.authorities = authorities;
        this.client_id = client_id;
    }

    @Override
    public String toString() {
        return "Authentication{" +
                "uid=" + uid +
                ", scope=" + Arrays.toString(scope) +
                ", mid='" + mid + '\'' +
                ", enterpriseId=" + enterpriseId +
                ", appId='" + appId + '\'' +
                ", authorities=" + Arrays.toString(authorities) +
                ", client_id='" + client_id + '\'' +
                '}';
    }

    public Long getUid() {
        return uid;
    }

    public void setUid(Long uid) {
        this.uid = uid;
    }

    public String[] getScope() {
        return scope;
    }

    public void setScope(String[] scope) {
        this.scope = scope;
    }

    public void setAuthorities(String[] authorities) {
        this.authorities = authorities;
    }

    public String getMid() {
        return mid;
    }

    public void setMid(String mid) {
        this.mid = mid;
    }

    public Long getEnterpriseId() {
        return enterpriseId;
    }

    public void setEnterpriseId(Long enterpriseId) {
        this.enterpriseId = enterpriseId;
    }

    public String getAppId() {
        return appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getClient_id() {
        return client_id;
    }

    public void setClient_id(String client_id) {
        this.client_id = client_id;
    }

    public String[] getAuthorities() {
        return authorities;
    }
}

参数解析器

import com.alibaba.fastjson.JSON;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;
import javax.servlet.http.HttpServletRequest;

/**
 * @Author: BillYu
 * @Description:将请求头x-authentication转为方法中Authentication参数对象
 * @Date: Created in 14:21 2023/5/9.
 */
@Component
public class AuthenticationHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        //匹配参数类型
        return Authentication.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        //处理参数值
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        String authenticationStr = request.getHeader("x-authentication");
        if (!StringUtils.isEmpty(authenticationStr)) {
            Authentication authentication = getAuthorizationFromRequest(authenticationStr);
            return authentication;
        }
        return null;
    }

    private Authentication getAuthorizationFromRequest(String auth) {
        Authentication authentication = JSON.parseObject(auth, Authentication.class);
        return authentication;
    }
}

权限注解


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

/**
 * @Author: BillYu
 * @Description: 权限限制注解
 * @Date: Created in 16:38 2023/5/6.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresPermission {
    /**
     * 可以访问的scope fs:write/user:read,包含一个即可
     * @return
     */
    String[] scopes() default {};

    /**
     * 可以访问的角色APP/USER,包含一个即可
     * @return
     */
    String[] authorities() default {};

    /**
     * 允许访问的客户端类型
     * @return
     */
    String[] clients() default {};

}

权限拦截处理


import com.alibaba.fastjson.JSON;
import com.xx.note.common.enums.RtCode;
import com.xx.note.common.util.ResponseBuilder;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;

/**
 * @Author: BillYu
 * @Description: 拦截请求头x-authentication中的认证信息,判断scope和authorities是否合法。
 * 使用方法 @RequiresPermission(scopes = {"fs:write"}, authorities = {"USER"})
 * @Date: Created in 17:12 2023/5/6.
 */
@Component
public class PermissionInterceptor implements HandlerInterceptor {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            RequiresPermission annotation = handlerMethod.getMethodAnnotation(RequiresPermission.class);
            if (annotation != null) {
                List<String> scopes = annotation.scopes() == null ? null : Arrays.asList(annotation.scopes());
                List<String> authorities = annotation.authorities() == null ? null : Arrays.asList(annotation.authorities());
                List<String> clients = annotation.clients() == null ? null : Arrays.asList(annotation.clients());
                // 检查用户是否有权限访问该方法
                String authenticationStr = request.getHeader("x-authentication");
                Authentication authentication = getAuthorizationFromRequest(authenticationStr);
                if (StringUtils.isEmpty(authenticationStr)) {
                    directResponse(response, "请求凭证无法访问此接口");
                    return false;
                }
                String[] requestScopes = authentication.getScope();
                String[] requestAuthorities = authentication.getAuthorities();
                String requestClient = authentication.getClient_id();
                if (scopes != null && !scopes.isEmpty()) {
                    boolean containScope = false;
                    for (int i = 0; i < requestScopes.length; i++) {
                        if (scopes.contains(requestScopes[i])) {
                            containScope = true;
                        }
                    }
                    if (!containScope) {
                        directResponse(response, "请求凭证无法访问此接口,scope验证错误");
                        return false;
                    }
                }
                if (authorities != null && !authorities.isEmpty()) {
                    boolean containAuthority = false;
                    for (int i = 0; i < requestAuthorities.length; i++) {
                        if (authorities.contains(requestAuthorities[i])) {
                            containAuthority = true;
                        }
                    }
                    if (!containAuthority) {
                        directResponse(response, "请求凭证无法访问此接口,authorities验证错误");
                        return false;
                    }
                }
                if (clients != null && !clients.isEmpty()) {
                    boolean containClient = false;
                    for (String client : clients) {
                        if (client.equals(requestClient)) {
                            containClient = true;
                        }
                    }
                    if (!containClient) {
                        directResponse(response, "请求凭证无法访问此接口,clients验证错误");
                        return false;
                    }
                }
            }
        }

        return true;
    }

    private Authentication getAuthorizationFromRequest(String auth) {
        Authentication authentication = JSON.parseObject(auth, Authentication.class);
        return authentication;
    }

    private void directResponse(ServletResponse response, String errorMessage) {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(JSON.toJSONString(ResponseBuilder.fail(RtCode.REQUEST_AUTH_ERROR, errorMessage)));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
}


添加注册拦截器和参数解析器


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
import java.util.Optional;

/**
 * @Author: BillYu
 * @Description: 使用注解控制拦截器是否生效
 * @Date: Created in 17:54 2023/5/6.
 */
@ConditionalOnProperty(prefix = "common.base", name = "permission-interceptor", havingValue = "true")
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Value("${common.base.permission-interceptor.path.includes}")
    private Optional<String[]> pathIncludes;

    @Value("${common.base.permission-interceptor.path.excludes}")
    private Optional<String[]> pathExcludes;
    @Autowired
    private PermissionInterceptor permissionInterceptor;
    @Autowired
    private AuthenticationHandlerMethodArgumentResolver authenticationHandlerMethodArgumentResolver;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration interceptorRegistration = registry.addInterceptor(permissionInterceptor);
        if(pathIncludes.isPresent()){
            interceptorRegistration.addPathPatterns(pathIncludes.get());
        }
        if(pathExcludes.isPresent()){
            interceptorRegistration.excludePathPatterns(pathExcludes.get());
        }
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(authenticationHandlerMethodArgumentResolver);
    }
}

发布依赖库

mvn deploy

二、xx-user引入依赖xx-base

添加maven依赖

<dependency>
            <groupId>com.xx</groupId>
            <artifactId>xx-base</artifactId>
            <version>1.0.1.RELEASE</version>
        </dependency>

Application类增加包扫描

@SpringBootApplication(scanBasePackages = {"xx.user","com.xx.security"})

com.xx.security为上面代码类所在的包,需要扫描注入容器。

properties增加拦截配置

#启用权限拦截器
common.base.permission-interceptor=true
common.base.permission-interceptor.path.includes=/openapi/**
common.base.permission-interceptor.path.excludes=

接口增加权限验证注解

    @RequiresPermission(scopes = {"fs:write"})
    @PostMapping("/openapi/doc")
    public RtData<SpaceFileDTO> test(Authentication authentication) {
	log.info(authentication.toString());
        ...
    }

三、排查解决拦截器不生效问题

问题:集成依赖到两个业务项目,其中一个项目不生效

排查思路:

WebMvcConfig是否注入成功,接口地址是否被匹配拦截,请求头是否有传入和被正确解析。

解决办法

最终发现是swagger配置类使用了WebMvcConfigurationSupport实现类,导致覆盖了Spring Boot的一些默认配置和自动配置。

WebMvcConfigurationSupport类对拦截器的影响是提供了一个自定义拦截器的入口点。你可以通过扩展WebMvcConfigurationSupport类并重写addInterceptors()方法来注册自定义的拦截器。

在addInterceptors()方法中,你可以使用InterceptorRegistry对象注册你的自定义拦截器。通过调用registry.addInterceptor()方法并传入你的拦截器实例,可以将拦截器添加到拦截器链中。

@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new YourInterceptor());
    }

    // 其他自定义配置方法...
}


当应用程序启动时,Spring MVC将会自动调用addInterceptors()方法,并将你的自定义拦截器添加到拦截器链中。这样,你的拦截器就会在请求进入控制器之前或之后进行相应的处理。

需要注意的是,当你扩展WebMvcConfigurationSupport类时,可能会覆盖Spring Boot的一些默认配置和自动配置。因此,在扩展时,你需要谨慎处理,并确保在自定义配置类中包含所有必要的配置。

总之,WebMvcConfigurationSupport类允许你通过重写addInterceptors()方法来自定义拦截器的注册。通过将自定义拦截器添加到拦截器链中,你可以实现对请求的预处理和后处理。