## 動手實現一個IP_Login
在開始這篇文章之前,我們似乎應該思考下為什么需要搞清楚Spring Security的內部工作原理?按照第二篇文章中的配置,一個簡單的表單認證不就達成了嗎?更有甚者,為什么我們不自己寫一個表單認證,用過濾器即可完成,大費周章引入Spring Security,看起來也并沒有方便多少。對的,在引入Spring Security之前,我們得首先想到,是什么需求讓我們引入了Spring Security,以及為什么是Spring Security,而不是shiro等等其他安全框架。我的理解是有如下幾點:
* 在前文的介紹中,Spring Security支持防止csrf攻擊,session-fixation protection,支持表單認證,basic認證,rememberMe…等等一些特性,有很多是開箱即用的功能,而大多特性都可以通過配置靈活的變更,這是它的強大之處。
* Spring Security的兄弟的項目Spring Security SSO,OAuth2等支持了多種協議,而這些都是基于Spring Security的,方便了項目的擴展。
* SpringBoot的支持,更加保證了Spring Security的開箱即用。
* 為什么需要理解其內部工作原理?一個有自我追求的程序員都不會滿足于淺嘗輒止,如果一個開源技術在我們的日常工作中十分常用,那么我偏向于閱讀其源碼,這樣可以讓我們即使排查不期而至的問題,也方便日后需求擴展。
* Spring及其子項目的官方文檔是我見過的最良心的文檔!相比較于Apache的部分文檔
*
這一節,為了對之前分析的Spring Security源碼和組件有一個清晰的認識,介紹一個使用IP完成登錄的簡單demo。
### 5.1 定義需求
在表單登錄中,一般使用數據庫中配置的用戶表,權限表,角色表,權限組表…這取決于你的權限粒度,但本質都是借助了一個持久化存儲,維護了用戶的角色權限,而后給出一個/login作為登錄端點,使用表單提交用戶名和密碼,而后完成登錄后可自由訪問受限頁面。
在我們的IP登錄demo中,也是類似的,使用IP地址作為身份,內存中的一個ConcurrentHashMap維護IP地址和權限的映射,如果在認證時找不到相應的權限,則認為認證失敗。
實際上,在表單登錄中,用戶的IP地址已經被存放在Authentication.getDetails()中了,完全可以只重寫一個AuthenticationProvider認證這個IP地址即可,但是,本demo是為了厘清Spring Security內部工作原理而設置,為了設計到更多的類,我完全重寫了IP過濾器。
### 5.2 設計概述
我們的參考完全是表單認證,在之前章節中,已經了解了表單認證相關的核心流程,將此圖再貼一遍:

在IP登錄的demo中,使用IpAuthenticationProcessingFilter攔截IP登錄請求,同樣使用ProviderManager作為全局AuthenticationManager接口的實現類,將ProviderManager內部的DaoAuthenticationProvider替換為IpAuthenticationProvider,而UserDetailsService則使用一個ConcurrentHashMap代替。更詳細一點的設計:
* IpAuthenticationProcessingFilter–>UsernamePasswordAuthenticationFilter
* IpAuthenticationToken–>UsernamePasswordAuthenticationToken
* ProviderManager–>ProviderManager
* IpAuthenticationProvider–>DaoAuthenticationProvider
* ConcurrentHashMap–>UserDetailsService
### 5.3 IpAuthenticationToken
~~~
public class IpAuthenticationToken extends AbstractAuthenticationToken {
private String ip;
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public IpAuthenticationToken(String ip) {
super(null);
this.ip = ip;
super.setAuthenticated(false);//注意這個構造方法是認證時使用的
}
public IpAuthenticationToken(String ip, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.ip = ip;
super.setAuthenticated(true);//注意這個構造方法是認證成功后使用的
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.ip;
}
}
~~~
兩個構造方法需要引起我們的注意,這里設計的用意是模仿的UsernamePasswordAuthenticationToken,第一個構造器是用于認證之前,傳遞給認證器使用的,所以只有IP地址,自然是未認證;第二個構造器用于認證成功之后,封裝認證用戶的信息,此時需要將權限也設置到其中,并且setAuthenticated(true)。這樣的設計在諸多的Token類設計中很常見。
### 5.4 IpAuthenticationProcessingFilter
~~~
public class IpAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
//使用/ipVerify該端點進行ip認證
IpAuthenticationProcessingFilter() {
super(new AntPathRequestMatcher("/ipVerify"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//獲取host信息
String host = request.getRemoteHost();
//交給內部的AuthenticationManager去認證,實現解耦
return getAuthenticationManager().authenticate(new IpAuthenticationToken(host));
}
}
~~~
AbstractAuthenticationProcessingFilter這個過濾器在前面一節介紹過,是UsernamePasswordAuthenticationFilter的父類,我們的IpAuthenticationProcessingFilter也繼承了它
構造器中傳入了/ipVerify作為IP登錄的端點
attemptAuthentication()方法中加載請求的IP地址,之后交給內部的AuthenticationManager去認證
### 5.5 IpAuthenticationProvider
~~~
public class IpAuthenticationProvider implements AuthenticationProvider {
final static Map<String, SimpleGrantedAuthority> ipAuthorityMap = new ConcurrenHashMap();
//維護一個ip白名單列表,每個ip對應一定的權限
static {
ipAuthorityMap.put("127.0.0.1", new SimpleGrantedAuthority("ADMIN"));
ipAuthorityMap.put("10.236.69.103", new SimpleGrantedAuthority("ADMIN"));
ipAuthorityMap.put("10.236.69.104", new SimpleGrantedAuthority("FRIEND"));
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
IpAuthenticationToken ipAuthenticationToken = (IpAuthenticationToken) authentication;
String ip = ipAuthenticationToken.getIp();
SimpleGrantedAuthority simpleGrantedAuthority = ipAuthorityMap.get(ip);
//不在白名單列表中
if (simpleGrantedAuthority == null) {
return null;
} else {
//封裝權限信息,并且此時身份已經被認證
return new IpAuthenticationToken(ip, Arrays.asList(simpleGrantedAuthority));
}
}
//只支持IpAuthenticationToken該身份
@Override
public boolean supports(Class<?> authentication) {
return (IpAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
~~~
return new IpAuthenticationToken(ip, Arrays.asList(simpleGrantedAuthority));使用了IpAuthenticationToken的第二個構造器,返回了一個已經經過認證的IpAuthenticationToken。
### 5.6 配置WebSecurityConfigAdapter
~~~
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//ip認證者配置
@Bean
IpAuthenticationProvider ipAuthenticationProvider() {
return new IpAuthenticationProvider();
}
//配置封裝ipAuthenticationToken的過濾器
IpAuthenticationProcessingFilter ipAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
IpAuthenticationProcessingFilter ipAuthenticationProcessingFilter = new IpAuthenticationProcessingFilter();
//為過濾器添加認證器
ipAuthenticationProcessingFilter.setAuthenticationManager(authenticationManager);
//重寫認證失敗時的跳轉頁面
ipAuthenticationProcessingFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/ipLogin?error"));
return ipAuthenticationProcessingFilter;
}
//配置登錄端點
@Bean
LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint(){
LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint = new LoginUrlAuthenticationEntryPoint
("/ipLogin");
return loginUrlAuthenticationEntryPoint;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.antMatchers("/ipLogin").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/ipLogin")
.authenticationEntryPoint(loginUrlAuthenticationEntryPoint())
;
//注冊IpAuthenticationProcessingFilter 注意放置的順序 這很關鍵
http.addFilterBefore(ipAuthenticationProcessingFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(ipAuthenticationProvider());
}
}
~~~
WebSecurityConfigAdapter提供了我們很大的便利,不需要關注AuthenticationManager什么時候被創建,只需要使用其暴露的configure(AuthenticationManagerBuilder auth)便可以添加我們自定義的ipAuthenticationProvider。剩下的一些細節,注釋中基本都寫了出來。
### 5.7 配置SpringMVC
~~~
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/ip").setViewName("ipHello");
registry.addViewController("/ipLogin").setViewName("ipLogin");
}
}
~~~
頁面的具體內容和表單登錄基本一致,可以在文末的源碼中查看。
### 5.8 運行效果
#### 成功的流程
http://127.0.0.1:8080/ 訪問首頁,其中here鏈接到的地址為:http://127.0.0.1:8080/hello
點擊here,由于http://127.0.0.1:8080/hello是受保護資源,所以跳轉到了校驗IP的頁面。此時若點擊Sign In by IP按鈕,將會提交到/ipVerify端點,進行IP的認證。
登錄校驗成功之后,頁面被成功重定向到了原先訪問的
#### 失敗的流程
注意此時已經注銷了上次的登錄,并且,使用了localhost(localhost和127.0.0.1是兩個不同的IP地址,我們的內存中只有127.0.0.1的用戶,沒有localhost的用戶)
點擊here后,由于沒有認證過,依舊跳轉到登錄頁面
此時,我們發現使用localhost,并沒有認證成功,符合我們的預期
### 5.9 總結
一個簡單的使用Spring Security來進行驗證IP地址的登錄demo就已經完成了,這個demo主要是為了更加清晰地闡釋Spring Security內部工作的原理設置的,其本身沒有實際的項目意義,認證IP其實也不應該通過Spring Security的過濾器去做,退一步也應該交給Filter去做(這個Filter不存在于Spring Security的過濾器鏈中),而真正項目中,如果真正要做黑白名單這樣的功能,一般選擇在網關層或者nginx的擴展模塊中做。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux