相关文章推荐

对项目重构时有这样一个需求,1)要把代码库某个目录下的所有代码作为一个新代码库的根目录,2)并且之前所有的代码提交记录要一并迁移到这个新的git repo。

当你尝试用 git filter-branch --subdirectory-filter YOUR_SUB_DIR -- --all 来解决问题时,会看到一个警告推荐我们使用 git filter-repo 。它是一个用于重写git history的多功能小工具,用法参考 filter-repo使用手册 。我们的需求在这里只是它的一个小case。

下面直接列出操作步骤:

1)安装 git-filter-repo

brew install git-filter-repo

2)Clone 原来的Repo

mkdir codebase
cd codebase
git clone YOUR_GIT_REPO_URL/myProject
cd myProject

3) 拉取所有信息到本地

git fetch --all
git pull --all

4)执行 filter-repo 命令,让某个子目录成为新repo的根目录。

git filter-repo --subdirectory-filter The_SubDir_in_myProject

5) 在github/gitlab创建一个新repo,把这个repo设为这个子目录的remote目标

git remote add origin YOUR_NEW_REPO_GIT_URL

6) 把master的history push到新repo

git branch -M master
git push -uf origin master

7)把所有branchs/tags都push上去

git push --all origin
        

SLF4J 是什么?

Java的世界里有很多优秀的log库,比如logback,log4j2。SLF4J(Simple Logging Facade)作为一个日志框架的抽象层,以Facade模式帮助我们“无缝”地选择、切换这些log库。

如何使用SLF4J?

在项目中要使用SLF4J需要做的就是引入slf4j-api-2.0.7.jar,再根据你选择的底层log框架选择对应的provider。比如,
如果底层使用log4j2,就要导入log4j-slf4j-impl(provider/bridge)、log4j-api.jar(log4j接口)、log4j-core(log4j实现)。
如果底层使用logback,就要导入”ch.qos.logback:logback-classic”,这个会自动导入 slf4j-api-2.0.7.jar(provider/bridge)、logback-core-1.3.6.jar(logback实现)。

SpringBoot底层默认支持logbak,如果使用log4j2则需要显示的exclude logback。

<dependencies>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
      </exclusions>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-log4j2</artifactId>
  </dependency>
<dpendencies>

关于SLF4J的MDC

SLF4J, 在底层库支持MDC(Mapped Diagnostic Context)的情况下也是支持MDC的。MDC data can also be highly helpful in filtering messages or triggering certain actions, 目前也就log4j2, logback支持MDC。在当前微服务流行的情况下让各个微服务的日志格式遵循同一格式是困难的,而且MDC的跨服务传递也是不现实的,这个特性在实际工程中用的比较少。

关于log4j2自己的log。

  1. 如果我们希望看到log4j2自己的log,可以设置-Dlog4j2.debug=true,让它自己的log显示在console。
  2. Composite Configuration, 因为可以通过 log4j2.configurationFile 设置多个逗号分隔的配置文件,log4j有一套自己的逻辑对多个配置文件进行merge。所以除非项目中有特定的需求,显示的通过log4j2.configurationFile显示的指定配置文件可以减少不必要的困惑。
  3. 在连接上一个 K8S cluster 后执行下面的命令可以看到系统中的ingressclasses。这篇文字用来帮助自己理解下面几行简单的输出。

    ╰─$ kubectl get ingressclass        
    NAME       CONTROLLER                     PARAMETERS                             AGE
    awslb      ingress.k8s.aws/alb            IngressClassParams.elbv2.k8s.aws/alb   20d
    nginx      nginx.org/ingress-controller   <none>                                 30d
    os-nginx   k8s.io/ingress-nginx           <none>                                 30d
    

    Mental Model

    在Kubernets里经常会提到Pod,Service,Ingress,Ingress Controller, Ingress Class,那他们之间有什么逻辑关系呢?

    Pod

    Pod用于把几个相关的containers封装在一起对外提供业务服务,containers之间可以直接通过localhost通讯。而如果想访问POD服务只能凭借POD的IP,这个IP也是K8S集群内部可见,而POD的IP在每次重建后都会变化,这显然是不可接受的。

    Service

    Service就是为了解决这个问题而生,通过service.yaml可以定义 1)这个service的name/namespace;2)由selector定义这个service对应的PODs;3)再通过定义service port和pod port的映射关系,就可以通过Service的名称访问PODs提供的服务了。Service借助自己对Pod自动发现的能力、服务名到POD IP的解析能力、简单的负载均衡能力,成为在Kubernets集群内部暴露Pod的不二之选。

    Ingress / Ingress Controller / Ingerss Class

    Service解决了我们在k8s集群内部访问‘服务’的问题。如果想从集群外部访问‘服务’呢?这正是“Ingress机制”七层路由存在的意义。这里的Ingress机制由Ingress Controller、Ingress这两个概念组成。
    作为码农,接触较多的一般是Ingress。这是因为Ingress Controller一旦部署到Kubernetes Cluster就很少会再去改动,而需要经常改动的应用路由规则都是在Ingress这个Kubernets API对象(或者说是在ingress.yaml文件)完成的。实际上,Ingress Controller实例才是真正执行将用户请求路由到Service进而到Pod的部件。Ingress只是我们定义请求到Service的路由规则的对象。

    既然“ingress“的核心功能就是7层路由/反向代理,那么借助早已存在的Nginx、HAProxy等产品实现IngressController就是很自然的想法了。另一个ingress controller的实现类别可以划分到service mesh阵营,比如Istio Ingerss、Gloo等。
    k8s官网列出的
    一些Ingerss Controller实现
    这篇文章详细讲解了各种Ingress Controller的特性以方便我们根据自己项目的需求做出选择。直接贴上文章的干货图片:
    kubernetes-ingress-comparison

    在一个Kubernets集群里可以定义多个不同Ingress Controller实现/类型,那么Ingress对象如何知道自己的数据是提供给哪个Ingress Controller的呢?

    在Kubernetes 1.18之前,是通过在Ingress的一个annotation kubernets.io/ingress.class 来声明。
    在Kubernetes 1.18正式引入了一个新的k8s资源对象 IngressClass帮助Ingress定义它绑定到哪个IngressController
    下面是一个官网的IngressClass对象定义示例,spec.controll定义了IngressController的实现, spec.parameters 相当于定义了你可以在Ingerss对象里可以向这个IngressController对象能够传递的参数,当然这些参数也是这种IngressControll必须支持的。不同的Ingress Controller实现其需要的parameter肯定是不同的,而k8s 1.18之前通过annoation给IngerssController传递参数的方式就显得比较随意无章可循了,这应该也是IngressClass出现的一个原因。

    apiVersion: networking.k8s.io/v1
    kind: IngressClass
    metadata:
      name: external-lb
    spec:
      controller: example.com/ingress-controller
      parameters:
        apiGroup: k8s.example.com
        kind: IngressParameters
        name: external-lb
    

    有了IngressClass,那么在Ingress中只要设置 spec.ingressClassName 为某个IngerssClass的名字,那么就意味着这个Ingress的配置就会被这个IngerssClass所对应的IngressController所获取并被这个IngressControll生成为对应的路由rules,从而完成把一个集群外请求路由到Service的功能。

    以上就是关于Kubernetes里Ingerss的几个基本概念。

    有关Nginx的IngressController

    基于Nginx实现的IngressController分为Kubernets社区版Nginx版

    Kubernets社区版由Kubernetes社区和F5 Nginx工程师基于开源的Nginx实现,其官网 code doc

    Nginx版自己又分为免费的基于开源Nginx的IngressController实现和商业版。Nginx开源版code doc

    所以就开源的版本来说,一个是Kubernets社区版,一个是Nginx开源版,两个都是基于开源的Nginx实现的,只是owner不通。表现在IngressClass的定义中,就是字段 spec.controller 的值一个是Kubernets社区版的 k8s.io/ingress-nginx, 一个是nginx开源版的 nginx.org/ingress-controller

    下面这个表格列出了Nginx Ingress Controller的 Kubernets社区版和Nginx开源版的区别。可以看到,两者差别不大,k8s社区版功能略好于Nginx开源版。而Nginx开源版因为没有使用Lua性能又好于k8s社区版。

    Aspect or Feature kubernetes/ingress-nginx nginxinc/kubernetes-ingress with NGINX nginxinc/kubernetes-ingress with NGINX Plus Fundamental NGINX version Custom NGINX build that includes several third-party modules NGINX official mainline build NGINX Plus Commercial support Included Implemented in Go/Lua (while Nginx is written in C) Go/Python Go/Python Load balancing configuration via the Ingress resource HTTP load balancing extensions - Annotations See the supported annotations See the supported annotations See the supported annotations HTTP load balancing extensions – ConfigMap See the supported ConfigMap keys See the supported ConfigMap keys See the supported ConfigMap keys TCP/UDP Supported via a ConfigMap Supported via custom resources Supported via custom resources Websocket Supported Supported via an annotation Supported via an annotation TCP SSL Passthrough Supported via a ConfigMap Supported via custom resources Supported via custom resources JWT validation Not supported Not supported Supported Session persistence Supported via a third-party module Not supported Supported Canary testing (by header, cookie, weight) Supported via annotations Supported via custom resources Supported via custom resources Configuration templates See the template See the templates See the templates Load balancing configuration via Custom Resources Not supported See VirtualServer and VirtualServerRoute resources See VirtualServer and VirtualServerRoute resources TCP/UDP load balancing Not supported See TransportServer resource See TransportServer resource TCP SSL Passthrough load balancing Not supported See TransportServer resource See TransportServer resource Deployment TLS certificate and key for the default server Required as a command-line argument/ auto-generated Required as a command-line argument Required as a command-line argument Helm chart Supported Supported Supported Operator Not supported Supported Supported Operational Reporting the IP address(es) of the Ingress controller into Ingress resources Supported Supported Supported Extended Status Supported via a third-party module Not supported Supported Prometheus Integration Supported Supported Supported Dynamic reconfiguration of endpoints (no configuration reloading) Supported with a third-party Lua module Not supported Supported

    再回到文章开头的命令输出,是不是看到的更多了些?

    ╰─$ kubectl get ingressclass        
    NAME       CONTROLLER                     PARAMETERS                             AGE
    awslb      ingress.k8s.aws/alb            IngressClassParams.elbv2.k8s.aws/alb   20d
    nginx      nginx.org/ingress-controller   <none>                                 30d
    os-nginx   k8s.io/ingress-nginx           <none>                                 30d
    

    References:
    [1]: Ingress
    [2]: IngressController
    [3]: IngressClass
    [4]: Comparing Ingress Controllers for Kubernetes
    [5]: 基于Nginx的Ingress Controller在社区和商业版之间的比较
    [6]: Kubernetes社区版
    [7]: Nginx开源版
    [8]: Nginx Ingress Controll社区版和Nginx开源版的比较

    1. 根据什么数据进行授权

    认证成功后以GrantedAuthority 的形式保存在Authentication对象中的authorities会别用来进行授权运算。

    请求被 AuthenticationManager 认证之后,其 Principal 的 authorities 以一组 GrantedAuthority 的形式被保存在 Authentication 对象里。

    public interface GrantedAuthority extends Serializable {
        String getAuthority();
    

    在是否可以对security object (比如一个方法的调用、一个web request的处理)进行访问之前,需要使用 AuthorizationManager 来决定是否可以invoke 这些security objects。
    如果这些授权不能以String的形式表达出来,那么就用返回null的形式告诉 AuthorizationManager/AccessDecisionManager 需要自己进行特殊的处理。 Spring Security 源码里就有一个这样的例子,请参阅: WebExpressionConfigAttribute, WebExpressionVoter。

    2. 通过 AuthorizationManager 体会设计的改进

    AuthorizationManager 是在 spring security 5.5 中被加入。从spring-security-core-6.0开始,AccessDecisionManager AccessDecisionVoter 已经被 deprecated,由 AuthorizationManager 取代其作用。 对于之前定制化AccessDecisionManager AccessDecisionVoter的代码应该考虑迁移到AuthorizationManager。

    AuthorizationManager
    AuthorizationManager

    AccessDecisionManager
    AccessDecisionManager

    AccessDecisionVoter
    AccessDecisionVoter

  4. 之前AccessDecisionManager 通过抛出异常,现在default的verify也是通过exception。
  5. 明确返回AuthorizationDecision来标识。
  6. 之前有support方法,跟AuthenticationProvider的思路很像。
    从方法签名可以直接看出上面这几点。官方文档列出了更有意义的变化:
  7. AuthorizationManager的API相对于之前FilterSecurityInterCepter/AccessDecisionManager要使用metadata sources, config attributes, decison managers, voters进行授权判断要简化很多。
  8. 因为通过把AuthenticationManger放在Supplier中实现了对认证数据的延迟访问,这对一些无需认证的授权是有好处的。
  9. 支持基于Bean的配置。
  10. 3. AuthorizationManagers/AuthorizationFilter 以及 AccessDecisionManager/FilterSecurityInterceptor

    AuthorizationManagers 被 AuthorizationFilter 来使用,负责做出是否允许访问的决定。
    为了保证向后兼容,FilterSecurityInterceptor 作为用于授权的security fitler依旧是默认的 Spring Security Filters 之一,在倒数第二的位置上。

    ... ...
    SessionManagementFilter
    ExceptionTranslationFilter
    FilterSecurityInterceptor
    SwitchUserFilter
    

    4. 如何在 Security Filters 中使用AuthorizationFilter 或 FilterSecurityInterceptor?

    在通过HttpSecurity构建SecurityFilterChain的时候调用authorizeHttpRequests() 就会在security fitler chain中插入AuthorizationFilter,而调用****authorizeRequests()****则会插入 security filter FilterSecurityInterceptor

    @Bean
    SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated();
            // ...
        return http.build();
    

    5. 详细的使用范式

    关于AuthorizationFilter/AuthorizationManger,可参考:https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html

    关于 FilterSecurityInterceptor,可参考: https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-requests.html

    References:
    [1]: https://docs.spring.io/spring-security/reference/servlet/authorization/index.html

    关于Spring Security里的Authentication,官方文档总结的不错。理解这些classes的作用与关系是正确使用Spring Security Authentication的前提。

    认证的方式不同,认证逻辑就不同,这样每个认证方式都会有对应的fitler实现。执行认证的大致流程以 AbstractAuthenticationProcessingFilter 为例描述一下。不同类别的 Authentication Filter 的处理略有差异,但大体逻辑差不多:

  11. Authentication Filter 接收请求 http request。
  12. 从request中获取凭证(credential)等数据,封装在Authentication对象中,比如:OAuth2LoginAuthenticationToken, UsernamePasswordAuthenticationToken等。
  13. AuthenticationManager向对其传入的Authentication 进行实际的认证工作。
  14. 认证成功的处理,比如保存设置了授权信息的Authentication到SecurityContext中。
  15. 失败进行处理。
  16. 1) Authentication Filter 接收请求

    当用户发送了http request进行认证,将被负责authentication的filter处理。这些filters实际的认证工作大多数(不是全部)都是由 AuthenticationManager 完成的。比如,像AbstractPreAuthenticatedProcessingFilter这些类本身就是接收的是第三方已经认证的请求,所以无需AuthenticationManager。 另外像AnonymousAuthenticationFilter也无需AuthenticationManager的参与。
    所以虽然都是认证,但是因为不同场景处理的逻辑不同,所以与AuthenticationFilter相关类的父类并不相同。大致可以分成以下三类。

    1)继承自 AbstractAuthenticationProcessingFilter 的authentication fitler class 有3个。

    public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
    public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter
    public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProcessingFilter
    AbstractAuthenticationProcessingFilter

    2)继承自 AbstractPreAuthenticatedProcessingFilter 的类有5个。

    public class RequestHeaderAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter
    public class RequestAttributeAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter
    AbstractPreAuthenticatedProcessingFilter

    3)其它分别直接继承 OncePerRequestFilter 和 GenericFilterBean,比如:

    OncePerRequestFilter 较 GenericFilterBean可以保证只被filters处理一次。

    public class BearerTokenAuthenticationFilter extends OncePerRequestFilter
    public class BasicAuthenticationFilter extends OncePerRequestFilter
    public class AnonymousAuthenticationFilter extends GenericFilterBean
    public class RememberMeAuthenticationFilter extends GenericFilterBean
    

    2)Request to Authentication

    Authentication这个类在认证前主要用于承载认证需要的凭证信息,比如用户名密码。authentication 对象也就等同于一个authentication request的event,并包含请求者进行认证所必须的信息。

    3)AuthenticationManager

    authentication对象会传递给AuthenticationManager 的方法authenticate()做认证。
    AuthenticationManager是个interface,它的实现类如下图片所示。

    在认证后,principal的授权信息会被写在authentication对象的authorities字段。下图摘自《Spring Security in Action》,使用username password做认证。

    如上图所示的,具体的认证工作是委托给AuthenticationProvider完成的。在Spring Security的代码实现中,也并不是由AuthenticationManager直接包含一组AuthenticationProvider的方式完成,中间还有一个叫做ProviderManager的类,下面列出它的两个关键字段体会下。

    public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
        private List<AuthenticationProvider> providers = Collections.emptyList();
        private AuthenticationManager parent;
        ... ...
    

    可以看到ProviderManager包含的不是一个provider而是a list of providers。通过提供一组providers就向用户提供了更多灵活控制的可能性。当然随之而来的就是这里就需要明确定义providers的认证结果以谁为准的规则。源码authenticate()的doc说得很明白:

    Attempts to authenticate the passed Authentication object.
    The list of AuthenticationProviders will be successively tried until an AuthenticationProvider indicates it is capable of authenticating the type of Authentication object passed. Authentication will then be attempted with that AuthenticationProvider.
    If more than one AuthenticationProvider supports the passed Authentication object, the first one able to successfully authenticate the Authentication object determines the result, overriding any possible AuthenticationException thrown by earlier supporting AuthenticationProviders. On successful authentication, no subsequent AuthenticationProviders will be tried. If authentication was not successful by any supporting AuthenticationProvider the last thrown AuthenticationException will be rethrown.

    让我们对比一下AuthenticationManger和AuthenticationProvider这两个interface的定义。看了下面的定义,你会不会问一个问题:既然两个接口有一个一摸一样的方法authentication(),为什么不让AuthenticationProvider继承AuthenticationManager? 我想或许是为了明确两个类的职责吧。

    以图形的方式看看它们的关系:

    如果我们要实现某个特殊的在Spring里没有的认证方式,我们就需要实现自定的AuthenticationProvider并通过覆盖WebSecurityConfigurerAdapter里的configure()方法实现。

    @Configuration
    public class ProjectConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationProvider authenticationProvider;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(authenticationProvider);
    

    不过这个类已经被官方API文档标为Deprecated,并推荐使用HttpSecurity定义SecurityFilterChain的方式或者通过WebSecurityCustomizer来配置WebSecurity。参考源码:

    到这里我们应该已经知道具体的认证逻辑都在AuthenticationProvider里。想知道Spring Security提供了哪些开箱即用的provider吗?见下图,一共17个。

    再捋一下与认证相关的类,就结束这篇吧。虽然没有涉及过多细节,相信理解了这些脉略应该也能在copy-past代码的时候点点头了。。。
    SecurityContextHolder:保存SecurityContext的地方。
    SecurityContextHolderStrategy:定义SecurityContext在线程中共享的策略模式。如果要跨线越Spring管理的线程,请参考 。。。。
    SecurityContext - 认证成功后Authentication对象就放在这里。
    Authentication - 存放要认证的信息以及被 AuthenticationManager 认证后的结果,认证成功后被放入SecurityContext。
    GrantedAuthority - 请求认证的principal认证成功后被赋予的权限(i.e. roles, scopes, etc.)
    AuthenticationManager - authentication相关的filter调用这个对象做认证。
    AbstractAuthenticationProcessingFilter:各个认证相关filter的父类。
    ProviderManager - AuthenticationManager的一个实现.
    AuthenticationProvider - 由ProviderManager 用来做具体的认证。
    AuthenticationEntryPoint: 用于询问并接收用户的credentials,可以是重定向到一个网页或者发送http WWW-Authenticate response。

    我想现在我们看到下面这些类时,就应该能够大致知道/理解他们在Spring Security Authentication类图里的位置了吧?
    UserDetails, User
    UserDetailsService, UserDetailsManager, JdbcUserDetailsManager
    PasswordEncoder
    如果没有,一定是我还没描述清楚。

    References:
    [1]:
    https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html
    [2]: 《Spring Security in Action》

    在项目中实际使用Spring Security时,我们的大部分工作其实都是配置HttpSecurity。要么通过spring的 http xml element 来配置,要么通过配置类里的HttpSecurity class来配置,所以在理解了DelegatingFilterProxy,FilterChainProxy,SecurityFilterChain之间的关系之后就很有必要了解一下HttpSecurity类了。

    HttpSecurity这个类的名称与它的实际功用相差甚远,其实把它称为HttpSecurityFilterChainBuiler应该更合适,因为它的作用就是利用构造器模式构造出SecurityFilterChain的一个实例供FilterChainProxy使用。这点从它的类签名就能看出来。

    如果有多个 SecurityFilterChain 被配置、构造出来,它们的顺序可以通过注解 @Order来设定。没有@Order注解的优先级最低。同一order层级的,就可以通过 SecurityFilterChain 中的RequestMatcher 来决定了该chain是否与http request匹配了。我们应该尽量把特殊的匹配放在前面,通用的放在后面。

    1) 体会下HttpSecurity源码的定义部分:

    HttpSecurity

    2) 用xml配置http security:

    虽然现在基于Spring的开发都是基于注解的了,但是如果遇到遗留系统里通过http元素来定义HttpSecurity,那么俯视一下下面的schema应该也能大致了然了。。。
    http element in security namespace

    3) fitlers 的顺序定义:

    FilterOrderRegistration

    4) HttpSecurity build filter的套路

    看看与 authentication 相关的两个fitler的构建。

    1)从两个filter看规律

    Filter都是根据Configurer构建出来的。我们以BasicAuthenticationFilter和UsernamePasswordAuthenticationFilter的Configurer举例。
    FormLoginConfigurer 比较“特殊”,它定义了两个filter。一个是UsernamePasswordAuthenticationFilter, 一个是DefaultLoginPageGeneratingFilter,后者提供了一个让用户输入credential页面的filter。
    HttpBasicConfigurer 则定义了 BasicAuthenticationFilter。

    public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
      return getOrApply(new FormLoginConfigurer<>());
    public HttpBasicConfigurer<HttpSecurity> httpBasic() throws Exception {
      return getOrApply(new HttpBasicConfigurer<>());
    

    FormLoginConfigurer 是 AbstractAuthenticationFilterConfigurer 的子类, HttpBasicConfigurer是AbstractHttpConfigurer。这是因为basic的认证方式比起form形式的认证要简单得多。

    public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
        AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
    public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>
        extends AbstractHttpConfigurer<T, B> {
    public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<HttpBasicConfigurer<B>, B> {
    

    负责根据这些configuer构造出对象来的类是AbstractConfiguredSecurityBuilder。

    public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>> extends AbstractSecurityBuilder<O> {   
    

    2) 没有被使用的AuthenticationFilter

    很奇怪在Spring Security 的源码里没有看到 AuthenticationFilter 被使用。估计这是要让程序员通过提供自定义的 authenticationConverter 和 authenticationManagerResolver 来使用吧。
    对比三个与authentication相关的fitler体会下。

    BasicAuthenticationFilter

    UsernamePasswordAuthenticationFitler
    UsernamePasswordAuthenticationFitler

    AuthenticationFilter
    AuthenticationFilter

    References:
    [1]:
    https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-httpsecurity
    [2]: https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#_multiple_httpsecurity
    [3]: https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-custom-dsls
    [4]: https://www.baeldung.com/spring-onceperrequestfilter

    这段文字主要源于对 https://docs.spring.io/spring-security/reference/servlet/architecture.html 的学习和理解,其实就是对下图的理解。
    SecurityFilterChain
    上图表达了下面几个类之间的关系:
    DelegatingFilterProxy, FilterChainProxy (springSecurityFilterChain), SecurityFilterChain (security filter)
    通过调用 SecurityFilterChain API 把 Security Filters 组装成一个或多个chain,再设置给FilterChainProxy使用。

    对于下图这样FilterChainProxy有多个SecurityFilterChain的情况,只会触发第一个匹配的securityFilterChain。
    Multiple SecurityFilterChain

    1)使用 Spring Security

    要使用Spring Security,如果是在Spring Boot环境那么只需要导入security的starter,Spring Boot就会自动做下面的事。
    Creates a servlet Filter as a bean named springSecurityFilterChain. This bean is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the log in form, and so on) within your application.
    Registers the Filter with a bean named springSecurityFilterChain with the Servlet container for every request.
    Creates a UserDetailsService bean with a username of user and a randomly generated password that is logged to the console.

    不使用Spring Boot的情况下,就需要自己在web.xml文件中定义springSecurityFilterChain。

    <filter>
      <filter-name>springSecurityFilterChain</filter-name>
      <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
      <filter-name>springSecurityFilterChain</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>
    

    2)入口 DelegatingFilterProxy

    下面通过源码简单看下DelegatingFilterProxy实例化的过程。
    Tomcat启动时会在 web 容器中初始化 DelegatingFilterProxy 实例,

    DelegatingFilterProxy 本身既是一个Filter也是一个ServletContextAware的实例。 Spring 使用 ContextLoaderListener 来加载spring的bean。org.springframework.web.context.support.GenericWebApplicationContext 则是servlet context和spring context真正交汇的地方。
    从下面DelegatingFilterProxy实现的接口就可以感知到 DelegatingFilterProxy 最关键的作用就是作为Servlet Container 和 Spring Context的桥梁
    因为Spring要等web context初始化完成才能初始化自己的context,所以在spring中定义的filter beans就要延迟初始化才行。通过延迟初始化就解决了Filter必须定义在Servlet Container中的问题。 Spring很巧妙的通过FilterChain接口把这些filter beans串在一起

    public class DelegatingFilterProxy extends GenericFilterBean
    

    而 ServletContextAware 是Spring的一个接口。

    public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
        EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean 
    

    从DelegatingFilterProxy的两个关键fields:WebApplicationContext webApplicationContext 和 Filter delegate,也能知道DelegatingFilterProxy桥梁作用。

    上图中的delegate是FilterChainProxy的实例。

    3) FilterChainProxy 包装了 filers。

    下图中filterChian包含的filters是不做任何特殊配置时的16个filters。FilerChainProxy就是通过调用它拥有的filters起到了对request做filter处理的作用,这就是称它为代理的原因.

    4)springSecurityFilterChain 进入 DelegatingFilterProxy

    上面提到 DelegatingFilterProxy 对filter延迟初始化的作用,所以对delegate的赋值只发生处理第一个http request时。被命名为springSecurityFilterChain的FilterChainProxy会从spring context中被找出来并设置到DelegatingFilterProxy的delegate field。

    至此,应该对文首的第一个图能说出点儿什么了吧… …

    DelegatingFilterProxy 的 field ‘Filter delegate’ 就是名为springSecurityFilterChain 的 FilterChainProxy实例
    而springSecurityFilterChain默认包含16个SecurityFilterChain

    Reference:
    [1]:
    https://www.baeldung.com/spring-web-contexts
    [2]: https://docs.spring.io/spring-security/reference/servlet/architecture.html
    [3]: https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-security-filters

    项目中遇到多个微服务调用需要考虑和处理某个环节失败时的处理。虽然这里不需要很强的事务概念,但是需要对失败的动作进行重试等操作。这里的重试本质上就是rollback的另一种形式,在saga里算是“forward recovery”。
    借机又翻看了一下相关的文章,贴到了文末。

    Saga vs TCC

    1. Saga相比TCC的缺点是缺少预留动作,所以某些情况补偿的实现比较麻烦甚至无法撤销只能补救。不过没有预留动作也意味着不必担心资源释放的问题。
    2. TCC最少通信次数为2n,Saga为n(n=sub-transaction的数量)。
    3. 第三方服务没需要提供有Try接口。
      总体感觉下来SAGA更适合微服务的多数场景。
    4. Simple Saga

      解决这类问题当然可以直接引入一些已存在的saga框架,不过这里存在学习、部署等成本。如果只是小范围的解决问题,或许可以使用下面的形式。
      示意图
      上面示意图针对的场景是:服务的执行都需要较长时间、并且是异步调用。
      如果各个服务执行时间都不长,一个调用链下来小于几百毫秒,那么直接使用reactive style的编码也应该可以。
      因为各服务执行时间较长,所以不能使用同步调用。这里耗时指的是对于有UI的程序至少影响到到UI前的用户,如果是后台应用那么至少阻塞的时长影响到系统的资源可用性。
      即使服务执行时间短,同步调用也会使调用链的availability降低,所以微服务的场景下使用异步调用有天然的好处。

      从这个示意图其实可以看作是Chris演讲中提到的最最原始的模式。可以把callback看作是saga事务参与方发送消息到message broker。而调用链的第一个节点就充当了saga的协调者。
      各个微服务的updateStatus端点就是message的listner,只不过这里直接通过callback实现而没有利用消息队列。
      最开始的endpoint负责生成一个transactionId并依次传递给每个下游服务,每个下游服务通过callback把自己的状态更新给上游。

    5. getStatus() 端点提供给UI获取当前状态。
      UI上的状态显示

    6. transCheckAndAmend(trans_n) 每个服务暴露的业务方法都需要提供一个补偿方法。
      Compensating transactions

    7. 服务的入口方法其实充当了协调者, 更像orchestration的,而不是choreography的。

    8. Timer 是个后台定时器不停的检查服务状态,如果状态不成功就调用compensating endpoint.

      Reference:
      [1]: Saga的经典论文
      https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf
      [2]: 《Microservice Pattern》”Chapter4, Managing transactions with sagas”
      [3]: Chris Richardson 在2017年的演讲:https://www.youtube.com/watch?v=YPbGW3Fnmbc

      一些中文网文:
      [3] 分布式事务:Saga模式 https://www.jianshu.com/p/e4b662407c66
      [4] 七种分布式事务的解决方案 https://cloud.tencent.com/developer/article/1806989
      [5] 分布式事务六种解决方案 https://zhuanlan.zhihu.com/p/183753774

      本文基本是基于此efs workshop的记录和扩展。

      要创建一个EFS资源,大致有以下几个步骤:
      要在哪个VPC上创建 –> 这个VPC上子网的CIDR
      创建一个SG –> 设置这个SG的ingress rule: 对子网开放NFS的2049
      创建EFS,根据需求设置不同的参数比如是否加密、备份、performance mode、throughput-mode 等。
      找到VPC上的public subnet,在这些public subnet上创建Moint Target。

      有了 mount targets,这个NFS就已经可以对外提供服务了。
      如果需要对mount的网络文件系统的目录设置特定的user、group属性,那么可以通过在这个NFS上创建 Access Points 完成。

      因为EFS是可以跨region在这个region的所有AZ中可用的一个NFS,所以需要 VPC ID 应该是比较容易理解的。

      下面介绍一下如何通过 aws cli 创建EFS及其Access Points,完整的脚本可以在这里下载 create_efs.sh, create_access_points.sh

      设置参数

      这些变量定义了我们当前的aws环境以及要创建的资源名称等信息。

      首先我们可以设置一些变量定义当前环境

      AWS_PROFILE=myProfile
      AWS_REGION=us-west-2
      CLUSTER_NAME=myCluster
      

      设置中间过程中会用到的常量

      MOUNT_TARGET_GROUP_NAME=mySG4EFS
      MOUNT_TARGET_GROUP_DESC="NFS access to EFS from EKS worker nodes"
      EFS_NAME=myEfsName
      

      1)获取 VPC ID

      因为这里创建出来的EFS要供 EKS 的pod使用,所以VPC的获取是根据eks cluster得到的。

      VPC_ID=$(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION --name $CLUSTER_NAME \
              --query "cluster.resourcesVpcConfig.vpcId" --output text)
      echo "The $CLUSTER_NAME includes the VPC $VPC_ID"
      

      2)获取VPC下的 CIDR

      CIDR_BLOCK=$(aws ec2 describe-vpcs --profile $AWS_PROFILE --region $AWS_REGION \
                  --vpc-ids $VPC_ID --query "Vpcs[].CidrBlock" --output text)
      echo "The CIDR blocks in the $VPC_ID : $CIDR_BLOCK"
      

      3)在VPC上创建Security Group

      MOUNT_TARGET_GROUP_ID=$(aws ec2 create-security-group --profile $AWS_PROFILE --region $AWS_REGION \
                          --group-name $MOUNT_TARGET_GROUP_NAME \
                          --description "$MOUNT_TARGET_GROUP_DESC" \
                          --vpc-id $VPC_ID \
                          | jq --raw-output '.GroupId')
      

      4)设置去安全组的ingres对2049端口开放

      aws ec2 authorize-security-group-ingress --profile $AWS_PROFILE --region $AWS_REGION \
        --group-id $MOUNT_TARGET_GROUP_ID --protocol tcp --port 2049 --cidr $CIDR_BLOCK
      

      5)创建 EFS

      aws efs create-file-system 命令本身并没有选项用于设置资源名称,而是通过 Tag key=Name 首先的。这里要注意Name单词的大小写,使用小写的name并不能设置 efs name。
      通过使用creation-token 来做到创建操作的等幂性。如果你的系统希望efs资源的name是唯一的,那么的选择使用efs的名称作为creation-token是个不错的选择。

      FILE_SYSTEM_ID=$(aws efs create-file-system --profile $AWS_PROFILE --region $AWS_REGION \
        --performance-mode generalPurpose --throughput-mode bursting \
        --tags Key=Name,Value=$EFS_NAME \
        --backup --encrypted --creation-token "$EFS_NAME"_0 | jq --raw-output '.FileSystemId')
      echo "The EFS $FILE_SYSTEM_ID is created."
      

      查看某个efs:

      aws efs describe-file-systems --file-system-id $FILE_SYSTEM_ID
      

      EFS资源已经创建出来了,要让它能被使用就需要把它mount到VPC的 public subnets 上。
      一个subnet是public的还是private的,并不是通过subnet对象的某个属性标识的,而是要看路由表里这个subnet有没有通向0.0.0.0的internet gateway。下面的几个步骤就用于找到 public subnet 并把EFS mount到这些 public subnets。

      6) 得到 eks 里的 subnetIds

      eksSubnetIds=($(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION \
                      --name $CLUSTER_NAME --query "cluster.resourcesVpcConfig.subnetIds" \
                      --output text))
      echo "The eks cluster $CLUSTER_NAME VPC $VPC_ID includes the subnets: $eksSubnetIds"
      

      7) 找到 internet gateway

      IGW_ID=$(aws ec2 describe-internet-gateways  --profile $AWS_PROFILE --region $AWS_REGION \
              --filters Name=attachment.vpc-id,Values=${VPC_ID} \
              --query "InternetGateways[].InternetGatewayId" \
              | jq -r '.[0]')
      echo "The internet gateway in the VPC $VPC_ID is $IGW_ID"
      if [ "null" = "$IGW_ID" ] ; then
        echo "Can't find public IGW in VPN, exit ..."
      

      8) 找到 public subnets

      for subnetId in ${eksSubnetIds[@]}
            echo "Check the subnet " $subnetId
            IGW_IN_ROUTS=$(aws ec2 describe-route-tables --profile $AWS_PROFILE --region $AWS_REGION  \
                          --filter Name=association.subnet-id,Values=$subnetId \
                          --query "RouteTables[].Routes[]" \
                          | jq -r '.[] | select(.DestinationCidrBlock=="0.0.0.0/0") | .GatewayId')
            if [ -z $IGW_IN_ROUTS -o "null" = $IGW_IN_ROUTS ] ;  then
              echo "The subnet $subnetId is a private subnet."
              echo "The subnet $subnetId is a public subnet. $IGW_ID $IGW_IN_ROUTS" 
              if [ "$IGW_ID" = "$IGW_IN_ROUTS" ] ; then
                echo "Creating the mount target in the subnet $subnetId."
                aws efs create-mount-target --profile $AWS_PROFILE --region $AWS_REGION \
                                            --file-system-id $FILE_SYSTEM_ID \
                                            --subnet-id $subnetId \
                                            --security-groups $MOUNT_TARGET_GROUP_ID
              elif [ "null" != "$IGW_IN_ROUTS" ] ; then
                  echo "WARNING: The IGW id in routes does not equal with the one in VPC!"
      

      10) 创建 Access Point

      到这里这个NFS已经可以在这个VPC里提供服务了。如果你的目录需要更精细的用户、组的设置,可以通过下面的命令创建 Access Point 来做更精细的控制。

      ACCESS_POING_NAME=myAP
      FILE_SYSTEM_ID=fs-082697b352a3230d1
      AP_USER='{"Uid": 123, "Gid": 123, "SecondaryGids": [20]}'
      AP_ROOT_DIR='/myapp/logs,CreationInfo={OwnerUid=123,OwnerGid=123,Permissions=0755}'
      aws efs create-access-point --profile $AWS_PROFILE --region $AWS_REGION  \
      --tags Key=name,Value=$ACCESS_POING_NAME \
      --client-token "$ACCESS_POING_NAME" \
      --file-system-id $FILE_SYSTEM_ID \
      --posix-user $AP_USER \
      --root-directory Path=$AP_ROOT_DIR
      

      下面显示的是在eks中通过StorageClass自动分配EFS资源的场景下,如何如何设置相关属性。 参考 https://github.com/kubernetes-sigs/aws-efs-csi-driver 查看完整的parameter列表。

      kind: StorageClass
      apiVersion: storage.k8s.io/v1
      metadata:
        name: efs-sc
      provisioner: efs.csi.aws.com
      mountOptions:
        - tls
        - iam
      parameters:
        provisioningMode: efs-ap
        fileSystemId: fs-012345678901010
        directoryPerms: "700"
        gidRangeStart: "1000"
        gidRangeEnd: "2000"
        basePath: "/dynamic_provisioning"
      

      Reference:
      [1]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/file-storage.html
      [2]: https://docs.aws.amazon.com/efs/latest/ug/creating-using.html
      [3]: https://docs.aws.amazon.com/cli/latest/reference/efs/create-file-system.html
      [4]: https://docs.aws.amazon.com/cli/latest/reference/efs/create-access-point.html
      [5]: https://docs.aws.amazon.com/efs/latest/ug/creating-using-create-fs.html#creating-using-fs-part1-cli
      [6]: https://stackoverflow.com/questions/48830793/aws-vpc-identify-private-and-public-subnet
      [7]: https://www.baeldung.com/linux/jq-command-json
      [8]: https://aws.amazon.com/premiumsupport/knowledge-center/eks-troubleshoot-efs-volume-mount-issues/
      [9]: https://github.com/kubernetes-sigs/aws-efs-csi-driver

    9. Use Amazon S3 with Amazon EC2
    10. 下面的这张概念图很好的描述各种存储的位置层次。
      EC2 Storage Options
      在EKS的node如果是基于EC2的,那么PV就可以利用以上除去S3之外的其余三种作为底层存储。
      参考:https://docs.aws.amazon.com/eks/latest/userguide/storage.html

      关于EC2可以使用的存储的特性、使用场景,推荐阅读官方文档:https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Storage.html。

      使用 aws cli 创建一个EFS资源

      可以在AWS Console中创建EFS https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AmazonEFS.html。
      也可以通过使用aws cli在命令行创建,https://www.eksworkshop.com/beginner/190_efs/launching-efs/。
      下面是根据上文得到的,通过aws cli创建EFS的脚本。
      假设,你的aws profile是myAwsProfile、eks所在region是us-west-2、eks集群名称是myCluster,而要创建的EFS名称是 my-test-efs
      为了使用EFS需要创建SecurityGroup来允许对NFS端口2049的使用,这里设置SG名称为 SG_efs_demo。

      设置环境变量:


      # Set the input env vars
      export AWS_PROFILE=myAwsProfile
      export AWS_REGION=us-west-2
      export CLUSTER_NAME=myCluster
      
      # Set the output env vars
      export MOUNT_TARGET_GROUP_NAME=perf-test-efs-group
      export MOUNT_TARGET_GROUP_DESC="NFS access to EFS from EKS worker nodes"
      export EFS_NAME=my-test-efs
      

      获取 VPC ID


      # Get eks cluster's VPC ID.
      export VPC_ID=$(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION --name $CLUSTER_NAME --query "cluster.resourcesVpcConfig.vpcId" --output text)
      echo $VPC_ID
      

      获取 VPC 里的 subnets


      # Get the subnets's CIDR in the VPC.
      export CIDR_BLOCK=$(aws ec2 describe-vpcs --profile $AWS_PROFILE --region $AWS_REGION --vpc-ids $VPC_ID --query "Vpcs[].CidrBlock" --output text)
      echo $CIDR_BLOCK
      

      创建 Security Group


      # Create SG(allow port 2049 in ingress for all of the CIDR in VPC) for EFS
      export MOUNT_TARGET_GROUP_ID=$(aws ec2 create-security-group --profile $AWS_PROFILE --region $AWS_REGION --group-name $MOUNT_TARGET_GROUP_NAME --description "$MOUNT_TARGET_GROUP_DESC" --vpc-id $VPC_ID | jq --raw-output '.GroupId')
      aws ec2 authorize-security-group-ingress --profile $AWS_PROFILE --region $AWS_REGION --group-id $MOUNT_TARGET_GROUP_ID --protocol tcp --port 2049 --cidr $CIDR_BLOCK
      # Get back the security-group informaation.
      aws ec2 describe-security-groups --filters Name=group-name,Values=$MOUNT_TARGET_GROUP_NAME
      

      创建 EFS


      # Create EFS. https://docs.aws.amazon.com/cli/latest/reference/efs/create-file-system.html
      # https://docs.aws.amazon.com/efs/latest/ug/creating-using-create-fs.html#creating-using-fs-part1-cli
      # If no creation-token is provied, you'd better go to the AWS EFS Console 
      # to make sure the EFS is created or not to avoid too many EFS reources are created.
      # 使用efs name作为前缀加序号的方式做 creation-token 是个不错的选择,
      export FILE_SYSTEM_ID=$(aws efs create-file-system --profile $AWS_PROFILE --region $AWS_REGION \
        --performance-mode generalPurpose --throughput-mode bursting --tags Key=name,Value=$EFS_NAME \
        --backup --encrypted --creation-token "$EFS_NAME"_0 | jq --raw-output '.FileSystemId')
      

      找到 VPC 中的 pubilc subnets


      # Find out the public subtnets from the subnets of the eks cluster.
      # 得到eks VPC的所有 subnetIds
      # export eksSubnetIDs=($(aws eks describe-cluster --profile $AWS_PROFILE --region $AWS_REGION --name $CLUSTER_NAME --query "cluster.resourcesVpcConfig.subnetIds" --output text))
      # 找到 Internet GW
      export IGW_ID=`aws ec2 describe-internet-gateways \
        --filters Name=attachment.vpc-id,Values=${VPC_ID} \
        --query "InternetGateways[].InternetGatewayId" \
        | jq -r '.[0]'`
      # 找到 public subnets: https://stackoverflow.com/questions/48830793/aws-vpc-identify-private-and-public-subnet 
      export PUBLIC_SUBNETS=`aws ec2 describe-route-tables \
        --query  'RouteTables[*].Associations[].SubnetId' \
        --filters "Name=vpc-id,Values=${VPC_ID}" \
          "Name=route.gateway-id,Values=${IGW_ID}" \
        | jq . -c`
      

      把 EFS mount 到 public subnets,这样EKS里的worker nodes就都可以访问这些EFS了。


      for subnet in ${PUBLIC_SUBNETS[@]}
          echo "creating mount target in " $subnet
          aws efs create-mount-target --profile $AWS_PROFILE --region us-west-2 --file-system-id $FILE_SYSTEM_ID --subnet-id $subnet --security-groups $MOUNT_TARGET_GROUP_ID
      

      创建 Access Points

      有了EFS之后就可以创建 AccessPoints 供应用程序使用了。关于更详细的Access Points介绍:https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html

      ACCESS_POING_NAME=ap_db2
      FILE_SYSTEM_ID=fs-055b5f1fcc7df3e4b
      AP_DIR_USER='{"Uid": 70, "Gid": 70, "SecondaryGids": [70]}'
      AP_ROOT_DIR='/mydataPath,CreationInfo={OwnerUid=70,OwnerGid=70,Permissions=0755}'
      aws efs create-access-point --profile $AWS_PROFILE --region $AWS_REGION  \
      --tags Key=name,Value=$ACCESS_POING_NAME \
      --client-token "$ACCESS_POING_NAME"_2 \
      --file-system-id $FILE_SYSTEM_ID \
      --posix-user $AP_DIR_USER \
      --root-directory Path=$AP_ROOT_DIR
      
      1)Path的格式必须满足下面的正则
      ^(\/|(\/(?!\.)+[^$#<>;`|&?{}^*/\n]+){1,4})$
      

      所以path不能以 / 结尾。

      2)创建的access point 可以同名、可以同名同path,唯一标识access point的是 Access point ID
      3) 根据id删除一个 access point

      aws efs delete-access-point –profile $AWS_PROFILE –region $AWS_REGION –access-point-id fsap-0a8b1b7d9e0c1c9c3

 
推荐文章