背景: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()方法来自定义拦截器的注册。通过将自定义拦截器添加到拦截器链中,你可以实现对请求的预处理和后处理。