前言
這篇文章咱們來分析一下org.springframework.boot.actuate.security,org.springframework.boot.actuate.audit中的代碼,這2個包的類是對spring security 的事件進行處理的.類圖以下:html
AuditEvent–> 1個值對象–>表明了1個audit event: 在特定的時間,1個特定的用戶或者代理,實施了1個特定類型的動做.AuditEvent記錄了有關AuditEvent的細節.web
其類上有以下註解:spring
@JsonInclude(Include.NON_EMPTY)
表明該類中爲空(「」)或者爲null的屬性不會被序列化。數組
該類的字段以下:session
private final Date timestamp; // 資源 private final String principal; private final String type; private final Map<String, Object> data;
AuditApplicationEvent–> 封裝AuditEvent.代碼以下:app
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 –>處理AuditApplicationEvent事件的抽象類.代碼以下:ide
public abstract class AbstractAuditListener implements ApplicationListener<AuditApplicationEvent> { @Override public void onApplicationEvent(AuditApplicationEvent event) { onAuditEvent(event.getAuditEvent()); } protected abstract void onAuditEvent(AuditEvent event); }
AuditEventRepository–> 關於AuditEvent的dao實現.聲明瞭以下4個方法:spring-boot
// 添加日誌 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 –> AuditEventRepository接口的惟一實現.post
該類的字段以下:性能
// 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 List<AuditEvent> find(Date after) { return find(null, after, null); } @Override public List<AuditEvent> find(String principal, Date after) { return find(principal, after, null); } //上面兩個方法最終調用這個方法 @Override public List<AuditEvent> find(String principal, Date after, String type) { LinkedList<AuditEvent> events = new LinkedList<AuditEvent>(); 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個問題:
一、前面說過訪問events的時候都須要進行加鎖,爲何resolveTailEvent方法沒有加鎖?
緣由以下: resolveTailEvent的調用點只有1個,就是在find(String Date , String)中,而在該方法中已經加鎖了,所以該方法不須要加鎖.
二、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.
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–> 暴露 Spring Security AbstractAuthenticationEvent(認證事件) 將其轉換爲AuditEvent 的抽象ApplicationListener基類.
代碼以下:
public abstract class AbstractAuthenticationAuditListener implements ApplicationListener<AbstractAuthenticationEvent>, 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的默認實現
字段以下:
// 當發生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); } }
一、若是驗證失敗(AbstractAuthenticationFailureEvent),則發送AuditEvent事件,其type爲AUTHENTICATION_FAILURE.代碼以下:
private void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { Map<String, Object> data = new HashMap<String, Object>(); 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)); }
二、若是webListener不等於null.而且該事件爲AuthenticationSwitchUserEvent,則發送AuditEvent事件,其type爲AUTHENTICATION_SWITCH.代碼以下:
public void process(AuthenticationAuditListener listener, AbstractAuthenticationEvent input) { if (listener != null) { AuthenticationSwitchUserEvent event = (AuthenticationSwitchUserEvent) input; Map<String, Object> data = new HashMap<String, Object>(); 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)); } }
三、若是是AuthenticationSuccessEvent,則發送AuditEvent事件,其type爲AUTHENTICATION_SUCCESS.代碼以下:
private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { Map<String, Object> data = new HashMap<String, Object>(); 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(); }
一、@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 –>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–> 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); } }
一、若是是AuthenticationCredentialsNotFoundEvent事件,則發送AuditEvent事件,type爲AUTHENTICATION_FAILURE.代碼以下:
private void onAuthenticationCredentialsNotFoundEvent( AuthenticationCredentialsNotFoundEvent event) { Map<String, Object> data = new HashMap<String, Object>(); data.put("type", event.getCredentialsNotFoundException().getClass().getName()); data.put("message", event.getCredentialsNotFoundException().getMessage()); publish(new AuditEvent("<unknown>", AuthenticationAuditListener.AUTHENTICATION_FAILURE, data)); }
二、若是是AuthorizationFailureEvent事件,則發送AuditEvent事件,type爲AUTHORIZATION_FAILURE.代碼以下:
private void onAuthorizationFailureEvent(AuthorizationFailureEvent event) { Map<String, Object> data = new HashMap<String, Object>(); 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(); }
準備工做
若是想讓 spring boot 應用激活AuditEvent的事件的處理,須要加入spring-boot-starter-security依賴,代碼以下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
光加入依賴還不夠,咱們須要加入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中咱們配置了以下內容:
聲明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,代碼以下:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="註銷"/>
</form>
</body>
</html>
index.html,代碼以下:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security入門</title>
</head>
<body>
<h1>歡迎使用Spring Security!</h1>
<p>點擊 <a th:href="@{/hello}">這裏</a> 打個招呼吧</p>
</body>
</html>
login.html,代碼以下:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Spring Security Example </title> </head> <body> <div th:if="${param.error}"> 用戶名或密碼錯 </div> <div th:if="${param.logout}"> 您已註銷成功 </div> <form th:action="@{/login}" method="post"> <div><label> 用戶名 : <input type="text" name="username"/> </label></div> <div><label> 密 碼 : <input type="password" name="password"/> </label></div> <div><input type="submit" value="登陸"/></div> </form> </body> </html>
測試
啓動應用後咱們訪問以下連接: 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" } } } ] }
解析
zhuan:https://blog.csdn.net/qq_26000415/article/details/79138270