在開始這篇文章之前,我們似乎應該思考下為什麼需要搞清楚Spring Security的內部工作原理?按照第二篇文章中的配置,一個簡單的表單認證不就達成了嗎?更有甚者,為什麼我們不自己寫一個表單認證,用過濾器即可完成,大費周章引入Spring Security,看起來也並沒有方便多少。對的,在引入Spring Security之前,我們得首先想到,是什麼需求讓我們引入了Spring Security,以及為什麼是Spring Security,而不是shiro等等其他安全框架。我的理解是有如下幾點:
1 在前文的介紹中,Spring Security支援防止csrf攻擊,session-fixation protection,支援表單認證,basic認證,rememberMe…等等一些特性,有很多是開箱即用的功能,而大多特性都可以透過配置靈活的變更,這是它的強大之處。
2 Spring Security的兄弟的專案Spring Security SSO,OAuth2等支援了多種協議,而這些都是基於Spring Security的,方便了專案的擴充套件。
3 SpringBoot的支援,更加保證了Spring Security的開箱即用。
4 為什麼需要理解其內部工作原理?一個有自我追求的程式員都不會滿足於淺嘗輒止,如果一個開源技術在我們的日常工作中十分常用,那麼我偏向於閱讀其原始碼,這樣可以讓我們即使排查不期而至的問題,也方便日後需求擴充套件。
5 Spring及其子專案的官方檔案是我見過的最良心的檔案!~~相比較於Apache的部分檔案~~
這一節,為了對之前分析的Spring Security原始碼和元件有一個清晰的認識,介紹一個使用IP完成登入的簡單demo。
作者:老徐
原文地址:https://www.cnkirito.moe/2017/10/01/spring-security-5/
友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。
友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。
友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。
5 動手實現一個IP_Login
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));
}
}
returnnewIpAuthenticationToken(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(AuthenticationManagerBuilderauth)
便可以新增我們自定義的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的擴充套件模組中做。再次特地強調下,怕大家誤解。
最後祝大家國慶玩的開心~
本節的程式碼可以在github中下載原始碼:https://github.com/lexburner/spring-security-ipLogin