Spring Security系列之動手實現一個IP_Login(五)

蔣老溼發表於2018-12-17

Spring Security系列之動手實現一個IP_Login(五)
文章來源

為什麼我們不自己寫一個表單認證,用過濾器即可完成,大費周章引入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。

動手實現一個IP_Login

定義需求

在表單登入中,一般使用資料庫中配置的使用者表,許可權表,角色表,許可權組表…這取決於你的許可權粒度,但本質都是藉助了一個持久化儲存,維護了使用者的角色許可權,而後給出一個/login作為登入端點,使用表單提交使用者名稱和密碼,而後完成登入後可自由訪問受限頁面。

在我們的IP登入demo中,也是類似的,使用IP地址作為身份,記憶體中的一個ConcurrentHashMap維護IP地址和許可權的對映,如果在認證時找不到相應的許可權,則認為認證失敗。

實際上,在表單登入中,使用者的IP地址已經被存放在Authentication.getDetails()中了,完全可以只重寫一個AuthenticationProvider認證這個IP地址即可,但是,本demo是為了理清Spring Security內部工作原理而設定,為了設計到更多的類,我完全重寫了IP過濾器。

設計概述

我們的參考完全是表單認證,在之前章節中,已經瞭解了表單認證相關的核心流程,將此圖再貼一遍:

Spring Security系列之動手實現一個IP_Login(五)
在IP登入的demo中,使用IpAuthenticationProcessingFilter攔截IP登入請求,同樣使用ProviderManager作為全域性AuthenticationManager介面的實現類,將ProviderManager內部的DaoAuthenticationProvider替換為IpAuthenticationProvider,而UserDetailsService則使用一個ConcurrentHashMap代替。更詳細一點的設計:

  • IpAuthenticationProcessingFilter—>UsernamePasswordAuthenticationFilter
  • IpAuthenticationToken—>UsernamePasswordAuthenticationToken
  • ProviderManager—>ProviderManager
  • IpAuthenticationProvider—>DaoAuthenticationProvider
  • ConcurrentHashMap—>UserDetailsService

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類設計中很常見。

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));
    }
}
複製程式碼
  1. AbstractAuthenticationProcessingFilter這個過濾器在前面一節介紹過,是UsernamePasswordAuthenticationFilter的父類,我們的IpAuthenticationProcessingFilter也繼承了它
  2. 構造器中傳入了/ipVerify作為IP登入的端點
  3. attemptAuthentication()方法中載入請求的IP地址,之後交給內部的AuthenticationManager去認證

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。

配置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。剩下的一些細節,註釋中基本都寫了出來。

配置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");

    }

}
複製程式碼

頁面的具體內容和表單登入基本一致,可以在文末的原始碼中檢視。

執行效果

成功的流程

http://127.0.0.1:8080/訪問首頁,其中here連結到的地址為:http://127.0.0.1:8080/hello

Spring Security系列之動手實現一個IP_Login(五)
點選here,由於http://127.0.0.1:8080/hello是受保護資源,所以跳轉到了校驗IP的頁面。此時若點選Sign In by IP按鈕,將會提交到/ipVerify端點,進行IP的認證。
Spring Security系列之動手實現一個IP_Login(五)
登入校驗成功之後,頁面被成功重定向到了原先訪問的
Spring Security系列之動手實現一個IP_Login(五)

失敗的流程

注意此時已經登出了上次的登入,並且,使用了localhost(localhost和127.0.0.1是兩個不同的IP地址,我們的記憶體中只有127.0.0.1的使用者,沒有localhost的使用者)

Spring Security系列之動手實現一個IP_Login(五)
點選here後,由於沒有認證過,依舊跳轉到登入頁面
Spring Security系列之動手實現一個IP_Login(五)
此時,我們發現使用localhost,並沒有認證成功,符合我們的預期
Spring Security系列之動手實現一個IP_Login(五)

總結

一個簡單的使用Spring Security來進行驗證IP地址的登入demo就已經完成了,這個demo主要是為了更加清晰地闡釋Spring Security內部工作的原理設定的,其本身沒有實際的專案意義,認證IP其實也不應該通過Spring Security的過濾器去做,退一步也應該交給Filter去做(這個Filter不存在於Spring Security的過濾器鏈中),而真正專案中,如果真正要做黑白名單這樣的功能,一般選擇在閘道器層或者nginx的擴充套件模組中做。再次特地強調下,怕大家誤解。

本節的程式碼可以在github中下載原始碼:github.com/lexburner/s…

Spring Security系列之動手實現一個IP_Login(五)

相關文章