阅读 196

关于微服务之间认证问题

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

关于微服务之间相互认证问题的记录.因微服务常使用token作为鉴权,且将token存放到请求头中,但是微服务之间没有传递头文件,所以记录一下微服务之间的认证问题.

1 关于微服务架构认证

image-20210826212159211

以常用的购物为例:

用户登录后, 后台会生成token,传给前台,前台直接放到请求头中;或者后台保存数据到redis中,将唯一标识写到cookie中,由网关根据cookie获取token,增加请求头.

上面描述的是用户访问单一的微服务应用. 如果涉及到微服务之间的相互调用,由于不经过网关,所以无法由网关进行请求头增强,但是被调用的另一个微服务又需要鉴权.所以就产生了一个feign接口调用的鉴权问题?

对于上述问题,我们可以采用feign拦截器,在本服务调用其他微服务时,拦截请求,将token数据添加到请求头中.这样处理后,再调用另一个微服务时,可正常通过权限校验.

2 feign拦截器的使用

方法一: 定义一个拦截器,在启动类中添加拦截器.

/**
 * feign拦截器,只要请求就会拦截
 */
@Component
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            if (request != null) {
                Enumeration<String> headerNames = request.getHeaderNames();
                if (headerNames != null) {
                    while (headerNames.hasMoreElements()) {
                        String headerName = headerNames.nextElement();
                        if (headerName.equals("authorization")) {
                            String headerValue = request.getHeader(headerName);
                            template.header(headerName, headerValue);
                        }
                    }
                }
            }
        }
    }
}

复制代码
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }

    @Bean
    public FeignInterceptor feignInterceptor(){
        return new FeignInterceptor();
    }
}
复制代码

方法二: 定义一个拦截器,从后台上下文中取出数据,直接放入.

@Slf4j
public class UserFeignInterceptor  implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("Authorization",BaseContextHandler.getToken());
    }
}
复制代码
// 当feign接口调用异常时会进入这个类中处理
@Component
public class UserServiceFallback implements UserServiceApi {

     @Override
     public String getUserById(String userId){
         ... // 自定义逻辑
     }
    
}
复制代码
@FeignClient(value = "ali-order", configuration = UserFeignInterceptor.class, fallback  = UserServiceFallback.class)
public interface UserServiceClient extends UserServiceApi {

}
复制代码

3 feign拦截器的说明

自定义feign拦截器能起到作用,正是因为实现了feign提供的RequestInterceptor接口.

RequestInterceptor接口定义了apply方法,其参数为RequestTemplate;它有一个抽象类为BaseRequestInterceptor,还有几个实现类分别为BasicAuthRequestInterceptor、FeignAcceptGzipEncodingInterceptor、FeignContentGzipEncodingInterceptor

image-20210826220339727

其中BasicAuthRequestInterceptor实现了RequestInterceptor接口,其apply方法往RequestTemplate添加名为Authorization的header(见下图源码)

public class BasicAuthRequestInterceptor implements RequestInterceptor {

  private final String headerValue;

  /**
   * Creates an interceptor that authenticates all requests with the specified username and password
   * encoded using ISO-8859-1.
   *
   * @param username the username to use for authentication
   * @param password the password to use for authentication
   */
  public BasicAuthRequestInterceptor(String username, String password) {
    this(username, password, ISO_8859_1);
  }

  /**
   * Creates an interceptor that authenticates all requests with the specified username and password
   * encoded using the specified charset.
   *
   * @param username the username to use for authentication
   * @param password the password to use for authentication
   * @param charset the charset to use when encoding the credentials
   */
  public BasicAuthRequestInterceptor(String username, String password, Charset charset) {
    checkNotNull(username, "username");
    checkNotNull(password, "password");
    this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset));
  }

  /*
   * This uses a Sun internal method; if we ever encounter a case where this method is not
   * available, the appropriate response would be to pull the necessary portions of Guava's
   * BaseEncoding class into Util.
   */
  private static String base64Encode(byte[] bytes) {
    return Base64.encode(bytes);
  }

  @Override
  public void apply(RequestTemplate template) {
    template.header("Authorization", headerValue);
  }
}
复制代码

其中抽象类BaseRequestInterceptor定义的addHeader方法,向requestTemplate添加非重名的header

public abstract class BaseRequestInterceptor implements RequestInterceptor {

	/**
	 * The encoding properties.
	 */
	private final FeignClientEncodingProperties properties;

	/**
	 * Creates new instance of {@link BaseRequestInterceptor}.
	 * @param properties the encoding properties
	 */
	protected BaseRequestInterceptor(FeignClientEncodingProperties properties) {
		Assert.notNull(properties, "Properties can not be null");
		this.properties = properties;
	}

	/**
	 * Adds the header if it wasn't yet specified.
	 * @param requestTemplate the request
	 * @param name the header name
	 * @param values the header values
	 */
	protected void addHeader(RequestTemplate requestTemplate, String name,
			String... values) {

		if (!requestTemplate.headers().containsKey(name)) {
			requestTemplate.header(name, values);
		}
	}

	protected FeignClientEncodingProperties getProperties() {
		return this.properties;
	}

}
复制代码

其中FeignAcceptGzipEncodingInterceptor继承抽象类BaseRequestInterceptor,apply方法中添加了键为Accept-Encoding,值为gzip,deflate的header

public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {

	/**
	 * Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
	 * @param properties the encoding properties
	 */
	protected FeignAcceptGzipEncodingInterceptor(
			FeignClientEncodingProperties properties) {
		super(properties);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void apply(RequestTemplate template) {

		addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER,
				HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
	}

}
复制代码

其中FeignContentGzipEncodingInterceptor继承抽象类BaseRequestInterceptor,apply方法中,判断了是否需要compression,即mimeType是否符合要求以及content大小是否超出阈值,需要compress的话则添加名为Content-Encoding,值为gzip,deflate的header

public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor {

	/**
	 * Creates new instance of {@link FeignContentGzipEncodingInterceptor}.
	 * @param properties the encoding properties
	 */
	protected FeignContentGzipEncodingInterceptor(
			FeignClientEncodingProperties properties) {
		super(properties);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void apply(RequestTemplate template) {

		if (requiresCompression(template)) {
			addHeader(template, HttpEncoding.CONTENT_ENCODING_HEADER,
					HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
		}
	}

	/**
	 * Returns whether the request requires GZIP compression.
	 * @param template the request template
	 * @return true if request requires compression, false otherwise
	 */
	private boolean requiresCompression(RequestTemplate template) {

		final Map<String, Collection<String>> headers = template.headers();
		return matchesMimeType(headers.get(HttpEncoding.CONTENT_TYPE))
				&& contentLengthExceedThreshold(headers.get(HttpEncoding.CONTENT_LENGTH));
	}

	/**
	 * Returns whether the request content length exceed configured minimum size.
	 * @param contentLength the content length header value
	 * @return true if length is grater than minimum size, false otherwise
	 */
	private boolean contentLengthExceedThreshold(Collection<String> contentLength) {

		try {
			if (contentLength == null || contentLength.size() != 1) {
				return false;
			}

			final String strLen = contentLength.iterator().next();
			final long length = Long.parseLong(strLen);
			return length > getProperties().getMinRequestSize();
		}
		catch (NumberFormatException ex) {
			return false;
		}
	}

	/**
	 * Returns whether the content mime types matches the configures mime types.
	 * @param contentTypes the content types
	 * @return true if any specified content type matches the request content types
	 */
	private boolean matchesMimeType(Collection<String> contentTypes) {
		if (contentTypes == null || contentTypes.size() == 0) {
			return false;
		}

		if (getProperties().getMimeTypes() == null
				|| getProperties().getMimeTypes().length == 0) {
			// no specific mime types has been set - matching everything
			return true;
		}

		for (String mimeType : getProperties().getMimeTypes()) {
			if (contentTypes.contains(mimeType)) {
				return true;
			}
		}

		return false;
	}

}

复制代码

4 总结

随着微服务架构的流行,微服务之间的调用也越来越变得频繁,所以关于微服务之间的鉴权也是值得关注的.相信将来一定会有更好的方法.

文章分类
后端
文章标签