歡迎光臨
每天分享高質量文章

Spring Security(五)–動手實現一個IP_Login

在開始這篇文章之前,我們似乎應該思考下為什麼需要搞清楚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代替。更詳細一點的設計:

  1. IpAuthenticationProcessingFilter–>UsernamePasswordAuthenticationFilter

  2. IpAuthenticationToken–>UsernamePasswordAuthenticationToken

  3. ProviderManager–>ProviderManager

  4. IpAuthenticationProvider–>DaoAuthenticationProvider

  5. ConcurrentHashMap–>UserDetailsService

5.3 IpAuthenticationToken

  1. public class IpAuthenticationToken extends AbstractAuthenticationToken {

  2.    private String ip;

  3.    public String getIp() {

  4.        return ip;

  5.    }

  6.    public void setIp(String ip) {

  7.        this.ip = ip;

  8.    }

  9.    public IpAuthenticationToken(String ip) {

  10.        super(null);

  11.        this.ip = ip;

  12.        super.setAuthenticated(false);//註意這個構造方法是認證時使用的

  13.    }

  14.    public IpAuthenticationToken(String ip, Collection extends GrantedAuthority> authorities) {

  15.        super(authorities);

  16.        this.ip = ip;

  17.        super.setAuthenticated(true);//註意這個構造方法是認證成功後使用的

  18.    }

  19.    @Override

  20.    public Object getCredentials() {

  21.        return null;

  22.    }

  23.    @Override

  24.    public Object getPrincipal() {

  25.        return this.ip;

  26.    }

  27. }

兩個構造方法需要引起我們的註意,這裡設計的用意是模仿的UsernamePasswordAuthenticationToken,第一個建構式是用於認證之前,傳遞給認證器使用的,所以只有IP地址,自然是未認證;第二個建構式用於認證成功之後,封裝認證使用者的資訊,此時需要將許可權也設定到其中,並且setAuthenticated(true)。這樣的設計在諸多的Token類設計中很常見。

5.4 IpAuthenticationProcessingFilter

  1. public class IpAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

  2.    //使用/ipVerify該端點進行ip認證

  3.    IpAuthenticationProcessingFilter() {

  4.        super(new AntPathRequestMatcher("/ipVerify"));

  5.    }

  6.    @Override

  7.    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

  8.        //獲取host資訊

  9.        String host = request.getRemoteHost();

  10.        //交給內部的AuthenticationManager去認證,實現解耦

  11.        return getAuthenticationManager().authenticate(new IpAuthenticationToken(host));

  12.    }

  13. }

  1. AbstractAuthenticationProcessingFilter這個過濾器在前面一節介紹過,是UsernamePasswordAuthenticationFilter的父類,我們的IpAuthenticationProcessingFilter也繼承了它

  2. 建構式中傳入了/ipVerify作為IP登入的端點

  3. attemptAuthentication()方法中載入請求的IP地址,之後交給內部的AuthenticationManager去認證

5.5 IpAuthenticationProvider

  1. public class IpAuthenticationProvider implements AuthenticationProvider {

  2.    final static Map<String, SimpleGrantedAuthority> ipAuthorityMap = new ConcurrenHashMap();

  3.    //維護一個ip白名單串列,每個ip對應一定的許可權

  4.    static {

  5.        ipAuthorityMap.put("127.0.0.1", new SimpleGrantedAuthority("ADMIN"));

  6.        ipAuthorityMap.put("10.236.69.103", new SimpleGrantedAuthority("ADMIN"));

  7.        ipAuthorityMap.put("10.236.69.104", new SimpleGrantedAuthority("FRIEND"));

  8.    }

  9.    @Override

  10.    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

  11.        IpAuthenticationToken ipAuthenticationToken = (IpAuthenticationToken) authentication;

  12.        String ip = ipAuthenticationToken.getIp();

  13.        SimpleGrantedAuthority simpleGrantedAuthority = ipAuthorityMap.get(ip);

  14.        //不在白名單串列中

  15.        if (simpleGrantedAuthority == null) {

  16.            return null;

  17.        } else {

  18.            //封裝許可權資訊,並且此時身份已經被認證

  19.            return new IpAuthenticationToken(ip, Arrays.asList(simpleGrantedAuthority));

  20.        }

  21.    }

  22.    //只支援IpAuthenticationToken該身份

  23.    @Override

  24.    public boolean supports(Class> authentication) {

  25.        return (IpAuthenticationToken.class

  26.                .isAssignableFrom(authentication));

  27.    }

  28. }

returnnewIpAuthenticationToken(ip,Arrays.asList(simpleGrantedAuthority));使用了IpAuthenticationToken的第二個建構式,傳回了一個已經經過認證的IpAuthenticationToken。

5.6 配置WebSecurityConfigAdapter

  1. @Configuration

  2. @EnableWebSecurity

  3. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  4.    //ip認證者配置

  5.    @Bean

  6.    IpAuthenticationProvider ipAuthenticationProvider() {

  7.        return new IpAuthenticationProvider();

  8.    }

  9.    //配置封裝ipAuthenticationToken的過濾器

  10.    IpAuthenticationProcessingFilter ipAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {

  11.        IpAuthenticationProcessingFilter ipAuthenticationProcessingFilter = new IpAuthenticationProcessingFilter();

  12.        //為過濾器新增認證器

  13.        ipAuthenticationProcessingFilter.setAuthenticationManager(authenticationManager);

  14.        //重寫認證失敗時的跳轉頁面

  15.        ipAuthenticationProcessingFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/ipLogin?error"));

  16.        return ipAuthenticationProcessingFilter;

  17.    }

  18.    //配置登入端點

  19.    @Bean

  20.    LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint(){

  21.        LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint = new LoginUrlAuthenticationEntryPoint

  22.                ("/ipLogin");

  23.        return loginUrlAuthenticationEntryPoint;

  24.    }

  25.    @Override

  26.    protected void configure(HttpSecurity http) throws Exception {

  27.        http

  28.            .authorizeRequests()

  29.                .antMatchers("/", "/home").permitAll()

  30.                .antMatchers("/ipLogin").permitAll()

  31.                .anyRequest().authenticated()

  32.                .and()

  33.            .logout()

  34.                .logoutSuccessUrl("/")

  35.                .permitAll()

  36.                .and()

  37.            .exceptionHandling()

  38.                .accessDeniedPage("/ipLogin")

  39.                .authenticationEntryPoint(loginUrlAuthenticationEntryPoint())

  40.        ;

  41.        //註冊IpAuthenticationProcessingFilter  註意放置的順序 這很關鍵

  42.        http.addFilterBefore(ipAuthenticationProcessingFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);

  43.    }

  44.    @Override

  45.    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

  46.        auth.authenticationProvider(ipAuthenticationProvider());

  47.    }

  48. }

WebSecurityConfigAdapter提供了我們很大的便利,不需要關註AuthenticationManager什麼時候被建立,只需要使用其暴露的 configure(AuthenticationManagerBuilderauth)便可以新增我們自定義的ipAuthenticationProvider。剩下的一些細節,註釋中基本都寫了出來。

5.7 配置SpringMVC

  1. @Configuration

  2. public class MvcConfig extends WebMvcConfigurerAdapter {

  3.    @Override

  4.    public void addViewControllers(ViewControllerRegistry registry) {

  5.        registry.addViewController("/home").setViewName("home");

  6.        registry.addViewController("/").setViewName("home");

  7.        registry.addViewController("/hello").setViewName("hello");

  8.        registry.addViewController("/ip").setViewName("ipHello");

  9.        registry.addViewController("/ipLogin").setViewName("ipLogin");

  10.    }

  11. }

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

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

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖