前言
这篇文章我们来分析一下org.springframework.boot.actuate.security,org.springframework.boot.actuate.audit中的代码,这2个包的类是对spring security 的事件进行处理的.类图如下:
二、源码解析
2.1、AuditEvent事件类
AuditEvent–> 1个值对象–>代表了1个audit event: 在特定的时间,1个特定的用户或者代理,实施了1个特定类型的动作.AuditEvent记录了有关AuditEvent的细节.
其类上有如下注解:
@JsonInclude(Include.NON_EMPTY)
代表该类中为空(“”)或者为null的属性不会被序列化。
该类的字段如下:
private final Date timestamp; // 资源 private final String principal; private final String type; private final Mapdata;
2.2、AuditApplicationEvent事件类
AuditApplicationEvent–> 封装AuditEvent.代码如下:
public class AuditApplicationEvent extends ApplicationEvent { private final AuditEvent auditEvent; public AuditApplicationEvent(String principal, String type, Mapdata) { 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 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; } }
2.3、AbstractAuditListener
AbstractAuditListener –>处理AuditApplicationEvent事件的抽象类.代码如下:
public abstract class AbstractAuditListener implements ApplicationListener{ @Override public void onApplicationEvent(AuditApplicationEvent event) { onAuditEvent(event.getAuditEvent()); } protected abstract void onAuditEvent(AuditEvent event); }
2.4、AuditEventRepository
AuditEventRepository–> 关于AuditEvent的dao实现.声明了如下4个方法:
// 添加日志 void add(AuditEvent event); // 查询指定日期之后的AuditEvent Listfind(Date after); // 根据给定的Date和principal(资源)获得对应的AuditEvent List find(String principal, Date after); // 根据给的date,principal,type 类获取给定的AuditEvent List find(String principal, Date after, String type);
2.5、InMemoryAuditEventRepository
InMemoryAuditEventRepository –> AuditEventRepository接口的唯一实现.
该类的字段如下:
// 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]; }
AuditEventRepository中的方法实现如下:
@Override 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; } } @Override public Listfind(Date after) { return find(null, after, null); } @Override public List find(String principal, Date after) { return find(principal, after, null); } //上面两个方法最终调用这个方法 @Override 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; } //过滤不和条件的事件 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; } //获得最新的AuditEvent private AuditEvent resolveTailEvent(int offset) { int index = ((this.tail + this.events.length - offset) % this.events.length); return this.events[index]; }
返回结果集
这里有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次传入的是0,则index = (0+3-0)%3 = 0,获得的正是3.
第2次传入的是1,则index = (0+3-1)%3 = 2,获得的正是2.
第3次传入的是2,则index = (0+3-2)%3 = 1,获得的正是1.
因此说find(String, Date, String)获得的结果时按照添加的顺序倒序返回的.
自动装配:
声明在AuditAutoConfiguration类内的static 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.
2.6、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
2.7AbstractAuthenticationAuditListener
AbstractAuthenticationAuditListener–> 暴露 Spring Security AbstractAuthenticationEvent(认证事件) 将其转换为AuditEvent 的抽象ApplicationListener基类.
代码如下:
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)); } } }
2.8、AuthenticationAuditListener
AuthenticationAuditListener的默认实现
字段如下:
// 当发生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; }
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) { Mapdata = 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; Mapdata = 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) { Mapdata = new HashMap (); if (event.getAuthentication().getDetails() != null) { data.put("details", event.getAuthentication().getDetails()); } publish(new AuditEvent(event.getAuthentication().getName(), AUTHENTICATION_SUCCESS, data)); }
自动装配:
在AuditAutoConfiguration中进行了声明,代码如下:
@Bean @ConditionalOnClass(name = "org.springframework.security.authentication.event.AbstractAuthenticationEvent") @ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class) public AuthenticationAuditListener authenticationAuditListener() throws Exception { return new AuthenticationAuditListener(); }
1、@Bean –> 注册1个id为authenticationAuditListener, AuthenticationAuditListener的bean
2、@ConditionalOnClass(name = “org.springframework.security.authentication.event.AbstractAuthenticationEvent”)–> 当在当前类路径下存在org.springframework.security.authentication.event.AbstractAuthenticationEvent时生效
3、@ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class)–>beanFactory中不存在AbstractAuthenticationAuditListener类型的bean时生效.
2.9、AbstractAuthorizationAuditListener
AbstractAuthorizationAuditListener –>1个暴露AbstractAuthorizationEvent(授权事件)作为AuditEvent的抽象ApplicationListener基类.代码如下:
public abstract class AbstractAuthorizationAuditListener 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)); } } }
2.10、AuthorizationAuditListener
AuthorizationAuditListener–> AbstractAuthorizationAuditListener的默认实现
字段如下:
// 发生AuthorizationFailureEvent事件时对应的AuditEvent的类型 public static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE";
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) { Mapdata = 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) { Mapdata = 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)); }
自动装配:
在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时生效.
三、流程分析
准备工作
如果想让 spring boot 应用激活AuditEvent的事件的处理,需要加入spring-boot-starter-security依赖,代码如下:
org.springframework.boot spring-boot-starter-security
光加入依赖还不够,我们需要加入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"); } }
在configureGlobal中,我们在内存中生成了1个用户:用户名为user,密码为password,角色为USER.
在configure中我们配置了如下内容:
- 访问/error-log需要拥有ROLE_TEST的权限
- 访问/,/home不需要进行验证
- 登录页面为/login,不需要进行验证
- 登出页面为/logout
- 其他链接都需要进行验证
声明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"; } }
在src/main/resources/templates目录下创建如下几个页面:
hello.html,代码如下:
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">Hello World! Hello [[${#httpServletRequest.remoteUser}]]!
index.html,代码如下:
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">Spring Security入门 欢迎使用Spring Security!
点击 这里 打个招呼吧
login.html,代码如下:
xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">Spring Security Example if="${param.error}"> 用户名或密码错if="${param.logout}"> 您已注销成功
测试
启动应用后我们访问如下链接: http://127.0.0.1:8080/,返回的是如下页面:
点击index.html 中的超链接后,由于需要进行验证,返回到login页面,如图:
此时我们输入错误的用户名,密码,返回的页面如下:
此时我们输入user,password 后,返回的页面如下:
点击注销后,页面如下:
此时我们访问 http://127.0.0.1:8080/error-test,由于没有登录,还是调回到登录页面.
访问 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" } } } ] }
解析
- 当我们访问 http://127.0.0.1:8080/hello.html 时,由于需要验证,因此会发送AuthorizationFailureEvent事件,此时会交由AuthorizationAuditListener处理,调用onAuthorizationFailureEvent方法.发送AuditEvent事件
- AuditListener 监听该事件,最终执行onAuditEvent方法,将AuditEvent存入到InMemoryAuditEventRepository中.
- 此时在login.html中,我们输入错误的用户名,密码,此时会发送AuthenticationFailureBadCredentialsEvent事件,交由AuthenticationAuditListener处理,最终执行onAuthenticationFailureEvent方法,发送AuditEvent事件, 最终还是在AuditListener中将事件存到 InMemoryAuditEventRepository中
- 此时我们输入正确的用户名密码后,会发送AuthenticationSuccessEvent事件,交由AuthenticationAuditListener事件处理,最终执行onAuthenticationSuccessEvent方法,发送AuditEvent事件,最终还是在AuditListener中将事件存到InMemoryAuditEventRepository中
- 之后产生了SessionFixationProtectionEvent事件,AuthenticationAuditListener不进行处理
- 产生InteractiveAuthenticationSuccessEvent,AuthenticationAuditListener不进行处理.
- 此时跳转到hello.html ,我们点击注销后,没有产生任何的事件.
- 登录成功后,我们访问/error-log,由于需要ROLE_TEST权限,而user 只有ROLE_USER 权限,因此会产生AuthorizationFailureEvent事件,因此会交由AuthorizationAuditListener处理,发送AuditEvent事件,最终还是在AuditListener中将事件存到InMemoryAuditEventRepository中
zhuan:https://blog.csdn.net/qq_26000415/article/details/79138270