spring boot 源码解析31-AuthenticationAuditListener,AuthorizationAuditListener

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】

前言

这篇文章我们来分析一下org.springframework.boot.actuate.security,org.springframework.boot.actuate.audit中的代码,这2个包的类是对spring security 的事件进行处理的.类图如下:

20191017100453\_1.png

解析

AuditEvent

AuditEvent–> 1个值对象–>代表了1个audit event: 在特定的时间,1个特定的用户或者代理,实施了1个特定类型的动作.AuditEvent记录了有关AuditEvent的细节.

  1. 其类上有如下注解:

        @JsonInclude(Include.NON_EMPTY)

    代表该类中为空(“”)或者为null的属性不会被序列化

  2. 该类的字段如下:

        private final Date timestamp;
    
        // 资源
        private final String principal;
    
        private final String type;
    
        private final Map data;

AuditApplicationEvent

AuditApplicationEvent–> 封装AuditEvent.代码如下:

    public class AuditApplicationEvent extends ApplicationEvent {

        private final AuditEvent auditEvent;

        public AuditApplicationEvent(String principal, String type,
                Map<String, Object> data) {
            this(new AuditEvent(principal, type, data));
        }

        AuditApplicationEvent(String principal, String type, String... data) {
            this(new AuditEvent(principal, type, data));
        }

        public AuditApplicationEvent(Date timestamp, String principal, String type,
                Map<String, Object> data) {
            this(new AuditEvent(timestamp, principal, type, data));
        }

        public AuditApplicationEvent(AuditEvent auditEvent) {
            super(auditEvent);
            Assert.notNull(auditEvent, "AuditEvent must not be null");
            this.auditEvent = auditEvent;
        }

        public AuditEvent getAuditEvent() {
            return this.auditEvent;
        }

    }

AbstractAuditListener

AbstractAuditListener –>处理AuditApplicationEvent事件的抽象类.代码如下:

    public abstract class AbstractAuditListener implements ApplicationListener<AuditApplicationEvent> {

        @Override
        public void onApplicationEvent(AuditApplicationEvent event) {
            onAuditEvent(event.getAuditEvent());
        }

        protected abstract void onAuditEvent(AuditEvent event);

    }

AuditEventRepository

AuditEventRepository–> 关于AuditEvent的dao实现.声明了如下4个方法:

    // 添加日志
    void add(AuditEvent event);

    // 查询指定日期之后的AuditEvent
    List<AuditEvent> find(Date after);

    // 根据给定的Date和principal(资源)获得对应的AuditEvent
    List<AuditEvent> find(String principal, Date after);

    // 根据给的date,principal,type 类获取给定的AuditEvent
    List<AuditEvent> find(String principal, Date after, String type);

InMemoryAuditEventRepository

InMemoryAuditEventRepository –> AuditEventRepository接口的唯一实现.

  1. 该类的字段如下:

        // AuditEvent数组默认的默认大小
        private static final int DEFAULT_CAPACITY = 4000;
    
        // 用于对events进行操作时 加的锁
        private final Object monitor = new Object();
    
        /** * Circular buffer of the event with tail pointing to the last element. * 循环数组 */
        private AuditEvent[] events;
    
        // 最后1个元素的下标
        private volatile int tail = -1;

    构造器如下:

        public InMemoryAuditEventRepository() {
            this(DEFAULT_CAPACITY);
        }
    
        public InMemoryAuditEventRepository(int capacity) {
            this.events = new AuditEvent[capacity];
        }
  2. AuditEventRepository中的方法实现如下:

    1. add,代码如下:

          public void add(AuditEvent event) {
              Assert.notNull(event, "AuditEvent must not be null");
              synchronized (this.monitor) {
                  this.tail = (this.tail + 1) % this.events.length;
                  this.events[this.tail] = event;
              }
          }
    2. find(java.util.Date) 实现如下:

          public List find(Date after) {
              return find(null, after, null);
          }
    3. find(String, Date) 实现如下:

          public List find(String principal, Date after) {
              return find(principal, after, null);
          }

      find(java.util.Date)和find(String, Date) 最终都调用find(String, Date, String)来实现.

    4. find(String, Date, String),代码如下:

          public List find(String principal, Date after, String type) {
              LinkedList events = new LinkedList();
              synchronized (this.monitor) {
                  // 1. 遍历events
                  for (int i = 0; i < this.events.length; i++) {
                      // 1.1 获得最新的AuditEvent
                      AuditEvent event = resolveTailEvent(i);
                      // 1.2 如果AuditEvent 不等于null并且符合查询要求的话,就加入到events中
                      if (event != null && isMatch(principal, after, type, event)) {
                          events.addFirst(event);
                      }
                  }
              }
              // 2. 返回结果集
              return events;
          }
      1. 遍历events

        1. 获得最新的AuditEvent,代码如下:

              private AuditEvent resolveTailEvent(int offset) {
                  int index = ((this.tail + this.events.length - offset) % this.events.length);
                  return this.events[index];
              }
        2. 如果AuditEvent 不等于null并且符合查询要求的话,就加入到events中.符合查询要求的代码如下:

              private boolean isMatch(String principal, Date after, String type, AuditEvent event) {
                  boolean match = true;
                  match = match && (principal == null || event.getPrincipal().equals(principal));
                  match = match && (after == null || event.getTimestamp().compareTo(after) >= 0);
                  match = match && (type == null || event.getType().equals(type));
                  return match;
              }
      2. 返回结果集

      这里有2个问题:

      1. 前面说过访问events的时候都需要进行加锁,为什么resolveTailEvent方法没有加锁?

        原因如下: resolveTailEvent的调用点只有1个,就是在find(String Date , String)中,而在该方法中已经加锁了,因此该方法不需要加锁.

      2. resolveTailEvent方法加锁可以吗

        答: 可以,原因是synchronized 是可重入的.但是不推荐,如果加上,会产生性能损耗.

      关于这个方法的实现原理我们还是举个例子比较好.假设我们的数组长度为3个,此时已经放满数组了,如下:

      [0,1,2]

      此时tail = 2, 然后我们继续放入3,则数组如下:

      [3,1,2],此时tail = 0. 然后我们调用find.在该方法中会调用resolveTailEvent.

      1. 第1次传入的是0,则index = (0+3-0)%3 = 0,获得的正是3.
      2. 第2次传入的是1,则index = (0+3-1)%3 = 2,获得的正是2.
      3. 第3次传入的是2,则index = (0+3-2)%3 = 1,获得的正是1.

      因此说find(String, Date, String)获得的结果时按照添加的顺序倒序返回的.

  3. 自动装配:

    声明在AuditEventRepositoryConfiguration中,代码如下:

        @ConditionalOnMissingBean(AuditEventRepository.class)
        protected static class AuditEventRepositoryConfiguration {
    
            @Bean
            public InMemoryAuditEventRepository auditEventRepository() throws Exception {
                return new InMemoryAuditEventRepository();
            }
    
        }

    当beanFactory中不存在 AuditEventRepository类型的bean时生效.注册1个id为auditEventRepository,类型为InMemoryAuditEventRepository的bean.

AuditListener

AuditListener–> AbstractAuditListener的默认实现.监听AuditApplicationEvent事件然后存储到AuditEventRepository中. 代码如下:

    public class AuditListener extends AbstractAuditListener {

        private static final Log logger = LogFactory.getLog(AuditListener.class);

        private final AuditEventRepository auditEventRepository;

        public AuditListener(AuditEventRepository auditEventRepository) {
            this.auditEventRepository = auditEventRepository;
        }

        @Override
        protected void onAuditEvent(AuditEvent event) {
            if (logger.isDebugEnabled()) {
                logger.debug(event);
            }
            this.auditEventRepository.add(event);
        }

    }

监听到AuditApplicationEvent时,直接将其封装的AuditEvent加入到AuditEventRepository中.还是比较简单的.

自动装配如下:

在AuditAutoConfiguration中进行了声明,代码如下:

    @Bean
    @ConditionalOnMissingBean(AbstractAuditListener.class)
    public AuditListener auditListener() throws Exception {
        return new AuditListener(this.auditEventRepository);
    }
  • @Bean–> 注册1个id为auditListener,类型为AuditListener的bean
  • @ConditionalOnMissingBean(AbstractAuditListener.class) –> 当beanFactory中不存在类型为AbstractAuditListener的bean时生效.

注意,在AuditListener中注入的是InMemoryAuditEventRepository

AbstractAuthenticationAuditListener

  1. AbstractAuthenticationAuditListener–> 暴露 Spring Security AbstractAuthenticationEvent(认证事件) 将其转换为AuditEvent 的抽象ApplicationListener基类.
  2. 代码如下:

        public abstract class AbstractAuthenticationAuditListener implements
            ApplicationListener, ApplicationEventPublisherAware {
    
            private ApplicationEventPublisher publisher;
    
            @Override
            public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
                this.publisher = publisher;
            }
    
            protected ApplicationEventPublisher getPublisher() {
                return this.publisher;
            }
    
            protected void publish(AuditEvent event) {
                if (getPublisher() != null) {
                    getPublisher().publishEvent(new AuditApplicationEvent(event));
                }
            }
        }

AuthenticationAuditListener

  1. AuthenticationAuditListener的默认实现
  2. 字段如下:

        // 当发生AuthenticationSuccessEvent事件时添加到AuditEvent中的type
        public static final String AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS";
    
        // 当发生AbstractAuthenticationFailureEvent事件时添加到AuditEvent中的type
        public static final String AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE";
    
        // 当发生AuthenticationSwitchUserEvent事件时添加到AuditEvent中的type
        public static final String AUTHENTICATION_SWITCH = "AUTHENTICATION_SWITCH";
    
        private static final String WEB_LISTENER_CHECK_CLASS = "org.springframework.security.web.authentication.switchuser.AuthenticationSwitchUserEvent";
    
        private WebAuditListener webListener = maybeCreateWebListener();
    
        // 只要加入spring-boot-starter-security的依赖,就会在当前类路径下存在org.springframework.security.web.authentication.switchuser.AuthenticationSwitchUserEvent
        // 因此会返回WebAuditListener
        private static WebAuditListener maybeCreateWebListener() {
            if (ClassUtils.isPresent(WEB_LISTENER_CHECK_CLASS, null)) {
                return new WebAuditListener();
            }
            return null;
        }
  3. onApplicationEvent 方法实现如下:

        public void onApplicationEvent(AbstractAuthenticationEvent event) {
            // 1. 如果验证失败,
            if (event instanceof AbstractAuthenticationFailureEvent) {
                onAuthenticationFailureEvent((AbstractAuthenticationFailureEvent) event);
            }
            // 2.如果webListener不等于null.并且该事件为AuthenticationSwitchUserEvent
            else if (this.webListener != null && this.webListener.accepts(event)) {
                this.webListener.process(this, event);
            }
            // 3. 如果是AuthenticationSuccessEvent
            else if (event instanceof AuthenticationSuccessEvent) {
                onAuthenticationSuccessEvent((AuthenticationSuccessEvent) event);
            }
        }
    1. 如果验证失败(AbstractAuthenticationFailureEvent),则发送AuditEvent事件,其type为AUTHENTICATION_FAILURE.代码如下:

          private void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) {
              Map data = new HashMap();
              data.put("type", event.getException().getClass().getName());
              data.put("message", event.getException().getMessage());
              if (event.getAuthentication().getDetails() != null) {
                  data.put("details", event.getAuthentication().getDetails());
              }
              publish(new AuditEvent(event.getAuthentication().getName(),
                      AUTHENTICATION_FAILURE, data));
          }
    2. 如果webListener不等于null.并且该事件为AuthenticationSwitchUserEvent,则发送AuditEvent事件,其type为AUTHENTICATION_SWITCH.代码如下:

          public void process(AuthenticationAuditListener listener,
                  AbstractAuthenticationEvent input) {
              if (listener != null) {
                  AuthenticationSwitchUserEvent event = (AuthenticationSwitchUserEvent) input;
                  Map data = new HashMap();
                  if (event.getAuthentication().getDetails() != null) {
                      data.put("details", event.getAuthentication().getDetails());
                  }
                  data.put("target", event.getTargetUser().getUsername());
                  listener.publish(new AuditEvent(event.getAuthentication().getName(),
                          AUTHENTICATION_SWITCH, data));
              }
      
          }
    3. 如果是AuthenticationSuccessEvent,则发送AuditEvent事件,其type为AUTHENTICATION_SUCCESS.代码如下:

          private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) {
              Map data = new HashMap();
              if (event.getAuthentication().getDetails() != null) {
                  data.put("details", event.getAuthentication().getDetails());
              }
              publish(new AuditEvent(event.getAuthentication().getName(),
                      AUTHENTICATION_SUCCESS, data));
          }
  4. 自动装配:

    在AuditAutoConfiguration中进行了声明,代码如下:

        @Bean
        @ConditionalOnClass(name = "org.springframework.security.authentication.event.AbstractAuthenticationEvent")
        @ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class)
        public AuthenticationAuditListener authenticationAuditListener() throws Exception {
            return new AuthenticationAuditListener();
        }
    • @Bean –> 注册1个id为authenticationAuditListener, AuthenticationAuditListener的bean
    • @ConditionalOnClass(name = “org.springframework.security.authentication.event.AbstractAuthenticationEvent”)–> 当在当前类路径下存在org.springframework.security.authentication.event.AbstractAuthenticationEvent时生效
    • @ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class)–>beanFactory中不存在AbstractAuthenticationAuditListener类型的bean时生效.

AbstractAuthorizationAuditListener

AbstractAuthorizationAuditListener –>1个暴露AbstractAuthorizationEvent(授权事件)作为AuditEvent的抽象ApplicationListener基类.代码如下:

    public abstract class AbstractAuthorizationAuditListener implements
            ApplicationListener<AbstractAuthorizationEvent>, ApplicationEventPublisherAware {

        private ApplicationEventPublisher publisher;

        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
            this.publisher = publisher;
        }

        protected ApplicationEventPublisher getPublisher() {
            return this.publisher;
        }

        protected void publish(AuditEvent event) {
            if (getPublisher() != null) {
                getPublisher().publishEvent(new AuditApplicationEvent(event));
            }
        }

    }

AuthorizationAuditListener

  1. AuthorizationAuditListener–> AbstractAuthorizationAuditListener的默认实现
  2. 字段如下:

        // 发生AuthorizationFailureEvent事件时对应的AuditEvent的类型
        public static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE";
  3. onApplicationEvent代码如下:

        public void onApplicationEvent(AbstractAuthorizationEvent event) {
            // 1. 如果是AuthenticationCredentialsNotFoundEvent事件,则发送AuditEvent事件,type为AUTHENTICATION_FAILURE
            if (event instanceof AuthenticationCredentialsNotFoundEvent) {
                onAuthenticationCredentialsNotFoundEvent(
                        (AuthenticationCredentialsNotFoundEvent) event);
            }
            // 2. 如果是AuthorizationFailureEvent事件,则发送AuditEvent事件,type为AUTHORIZATION_FAILURE
            else if (event instanceof AuthorizationFailureEvent) {
                onAuthorizationFailureEvent((AuthorizationFailureEvent) event);
            }
        }
    1. 如果是AuthenticationCredentialsNotFoundEvent事件,则发送AuditEvent事件,type为AUTHENTICATION_FAILURE.代码如下:

          private void onAuthenticationCredentialsNotFoundEvent(
                  AuthenticationCredentialsNotFoundEvent event) {
              Map data = new HashMap();
              data.put("type", event.getCredentialsNotFoundException().getClass().getName());
              data.put("message", event.getCredentialsNotFoundException().getMessage());
              publish(new AuditEvent("",
                      AuthenticationAuditListener.AUTHENTICATION_FAILURE, data));
          }
    2. 如果是AuthorizationFailureEvent事件,则发送AuditEvent事件,type为AUTHORIZATION_FAILURE.代码如下:

          private void onAuthorizationFailureEvent(AuthorizationFailureEvent event) {
              Map data = new HashMap();
              data.put("type", event.getAccessDeniedException().getClass().getName());
              data.put("message", event.getAccessDeniedException().getMessage());
              if (event.getAuthentication().getDetails() != null) {
                  data.put("details", event.getAuthentication().getDetails());
              }
              publish(new AuditEvent(event.getAuthentication().getName(), AUTHORIZATION_FAILURE,
                      data));
          }
  4. 自动装配:

    在AuditAutoConfiguration中进行了装配,代码如下:

        @Bean
        @ConditionalOnClass(name = "org.springframework.security.access.event.AbstractAuthorizationEvent")
        @ConditionalOnMissingBean(AbstractAuthorizationAuditListener.class)
        public AuthorizationAuditListener authorizationAuditListener() throws Exception {
            return new AuthorizationAuditListener();
        }
    • @Bean –> 注册1个id为authorizationAuditListener,类型为AuthorizationAuditListener的bean
    • @ConditionalOnClass(name = “org.springframework.security.access.event.AbstractAuthorizationEvent”)–> 在当前类路径下存在org.springframework.security.access.event.AbstractAuthorizationEvent时生效
    • @ConditionalOnMissingBean(AbstractAuthorizationAuditListener.class)–> beanFactory中不存在AbstractAuthorizationAuditListener类型的bean时生效.

流程分析

准备工作

  1. 如果想让 spring boot 应用激活AuditEvent的事件的处理,需要加入spring-boot-starter-security依赖,代码如下:

        
            org.springframework.boot
            spring-boot-starter-security
        
  2. 光加入依赖还不够,我们需要加入security的配置,不然AuthorizationAuditListener,AuthenticationAuditListener 监听什么事件呢? 因此,我们加入如下代码:

        @Configuration
        @EnableWebSecurity
        public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests().antMatchers("/error-log").hasAuthority("ROLE_TEST").antMatchers("/", "/home")
                        .permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().and()
                        .logout().logoutUrl("/logout").permitAll().and().authorizeRequests();
    
            }
    
            @Autowired
            public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
                auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
            }
        }
    1. 在configureGlobal中,我们在内存中生成了1个用户:用户名为user,密码为password,角色为USER.
    2. 在configure中我们配置了如下内容:

      1. 访问/error-log需要拥有ROLE_TEST的权限
      2. 访问/,/home不需要进行验证
      3. 登录页面为/login,不需要进行验证
      4. 登出页面为/logout
      5. 其他链接都需要进行验证
  3. 声明1个UserController,代码如下:

        @Controller
        public class UserController {
    
            @RequestMapping("/")
            public String index() {
                return "index";
            }
    
            @RequestMapping("/hello")
            public String hello() {
                return "hello";
            }
    
            @RequestMapping(value = "/login", method = RequestMethod.GET)
            public String login() {
                return "login";
            }
    
            @RequestMapping("/error-test")
            public String error() {
                return "1";
            }   
        } 
  4. 在src/main/resources/templates目录下创建如下几个页面:

    1. hello.html,代码如下:

          
          
          
              Hello World!
          
          
          

      Hello [[${#httpServletRequest.remoteUser}]]!

    2. index.html,代码如下:

          
          
          
              Spring Security入门
          
          
          

      欢迎使用Spring Security!

      点击 这里 打个招呼吧

    3. login.html,代码如下:

          
          
              
                  Spring Security Example 
              
              
                  
      用户名或密码错
      您已注销成功
  5. 测试

    1. 启动应用后我们访问如下链接: http://127.0.0.1:8080/,返回的是如下页面:

      20191017100453\_2.png

    2. 点击index.html 中的超链接后,由于需要进行验证,返回到login页面,如图:

      20191017100453\_3.png

    3. 此时我们输入错误的用户名,密码,返回的页面如下:

      20191017100453\_4.png

    4. 此时我们输入user,password 后,返回的页面如下:

      20191017100453\_5.png

    5. 点击注销后,页面如下:

      20191017100453\_6.png

    6. 此时我们访问 http://127.0.0.1:8080/error-test,由于没有登录,还是调回到登录页面.
    7. 访问 http://127.0.0.1:8080/auditevents,返回的结果如下:

          {
          events: [
              {
              timestamp: "2018-01-23T03:52:13+0000",
              principal: "anonymousUser",
              type: "AUTHORIZATION_FAILURE",
              data: { details: { remoteAddress: "127.0.0.1", sessionId: null },
              type: "org.springframework.security.access.AccessDeniedException",
              message: "Access is denied"
              }
          },
              {
              timestamp: "2018-01-23T03:54:21+0000",
              principal: "aaa",
              type: "AUTHENTICATION_FAILURE",
              data: { details: { remoteAddress: "127.0.0.1", sessionId: "DFDB023AEEF41BBD8079EC32402CBFD8" },
              type: "org.springframework.security.authentication.BadCredentialsException",
              message: "Bad credentials"
              }
              },
              {
              timestamp: "2018-01-23T03:55:50+0000",
              principal: "user",
              type: "AUTHENTICATION_SUCCESS",
              data: { details: { remoteAddress: "127.0.0.1", sessionId: "DFDB023AEEF41BBD8079EC32402CBFD8" }
              }
              },
              {
              timestamp: "2018-01-23T03:58:38+0000",
              principal: "anonymousUser",
              type: "AUTHORIZATION_FAILURE",
              data: { details: { remoteAddress: "127.0.0.1", sessionId: "6E6E614D638B6F5EE5B7E8CF516E2534" },
              type: "org.springframework.security.access.AccessDeniedException",
              message: "Access is denied"
              }
              },
              {
              timestamp: "2018-01-23T04:00:01+0000",
              principal: "anonymousUser",
              type: "AUTHORIZATION_FAILURE",
              data: { details: { remoteAddress: "127.0.0.1", sessionId: "6E6E614D638B6F5EE5B7E8CF516E2534" },
              type: "org.springframework.security.access.AccessDeniedException",
              message: "Access is denied"
              }
              },
              {
              timestamp: "2018-01-23T04:00:12+0000",
              principal: "user",
              type: "AUTHENTICATION_SUCCESS",
              data: { details: { remoteAddress: "127.0.0.1", sessionId: "6E6E614D638B6F5EE5B7E8CF516E2534" }
              }
              }
          ]
          }

解析

  1. 当我们访问 http://127.0.0.1:8080/hello.html 时,由于需要验证,因此会发送AuthorizationFailureEvent事件,此时会交由AuthorizationAuditListener处理,调用onAuthorizationFailureEvent方法.发送AuditEvent事件
  2. AuditListener 监听该事件,最终执行onAuditEvent方法,将AuditEvent存入到InMemoryAuditEventRepository中.
  3. 此时在login.html中,我们输入错误的用户名,密码,此时会发送AuthenticationFailureBadCredentialsEvent事件,交由AuthenticationAuditListener处理,最终执行onAuthenticationFailureEvent方法,发送AuditEvent事件, 最终还是在AuditListener中将事件存到 InMemoryAuditEventRepository中
  4. 此时我们输入正确的用户名密码后,会发送AuthenticationSuccessEvent事件,交由AuthenticationAuditListener事件处理,最终执行onAuthenticationSuccessEvent方法,发送AuditEvent事件,最终还是在AuditListener中将事件存到InMemoryAuditEventRepository中
  5. 之后产生了SessionFixationProtectionEvent事件,AuthenticationAuditListener不进行处理
  6. 产生InteractiveAuthenticationSuccessEvent,AuthenticationAuditListener不进行处理.
  7. 此时跳转到hello.html ,我们点击注销后,没有产生任何的事件.
  8. 登录成功后,我们访问/error-log,由于需要ROLE_TEST权限,而user 只有ROLE_USER 权限,因此会产生AuthorizationFailureEvent事件,因此会交由AuthorizationAuditListener处理,发送AuditEvent事件,最终还是在AuditListener中将事件存到InMemoryAuditEventRepository中

来源:[]()

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » spring boot 源码解析31-AuthenticationAuditListener,AuthorizationAuditListener

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏