Spring Security 解析(二) —— 認證過程

来源:https://www.cnblogs.com/bug9/archive/2019/08/22/11396981.html
-Advertisement-
Play Games

Spring Security 解析(二) —— 認證過程   在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth2 等許可權、認證相關的內容、原理及設計學 ...


Spring Security 解析(二) —— 認證過程

  在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth2 等許可權、認證相關的內容、原理及設計學習並整理一遍。本系列文章就是在學習的過程中加強印象和理解所撰寫的,如有侵權請告知。

項目環境:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

一、@EnableGlobalAuthentication 配置 解析

  還記得上一篇講解授權過程中提到@EnableWebSecurity 引用了 WebSecurityConfiguration 配置類 和 @EnableGlobalAuthentication 註解嗎? 當時只是講解了下 WebSecurityConfiguration 配置類 ,這次該輪到 @EnableGlobalAuthentication 配置了。

  查看 @EnableGlobalAuthentication 註解源碼,我們可以看到其引用了AuthenticationConfiguration 配置類。其中有一個方法值得我們註意,那就是 getAuthenticationManager() (還記得授權過程中調用了 AuthenticationManager().authenticate() 進行認證麽?), 我們來看下其源碼內部大致邏輯:

public AuthenticationManager getAuthenticationManager() throws Exception {

        ......
        // 1 調用 authenticationManagerBuilder 方法獲取 authenticationManagerBuilder 對象,用於 build  authenticationManager 對象
        AuthenticationManagerBuilder authBuilder = authenticationManagerBuilder(
                this.objectPostProcessor, this.applicationContext);
        .....
        // 2  build 方法調用同授權過程中的 webSecurity.build()  一樣,都是通過父類 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 方法進行 build, 只是這裡不再是通過其子類 HttpSecurity.performBuild() ,而是通過 AuthenticationManagerBuilder.performBuild() 
        authenticationManager = authBuilder.build();

        .......
        
        return authenticationManager;
    }

根據源碼我們可以概括其邏輯分2部分:

  • 1、 通過調用 authenticationManagerBuilder() 方法獲取 authenticationManagerBuilder 對象
  • 2、 調用authenticationManagerBuilder 對象的 build() 創建 authenticationManager 對象並返回

  我們再詳細看下這個build的過程,可以發現其 build 調用跟授權過程中build securityFilterChain 一樣 都是通過 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 進行構建, 不過這次不再是調用其子類 HttpSecurity.performBuild() 而是 AuthenticationManagerBuilder.performBuild() 。
我們來看下 AuthenticationManagerBuilder.performBuild() 方法內部實現:

protected ProviderManager performBuild() throws Exception {
        if (!isConfigured()) {
            logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
            return null;
        }
        // 1  創建了一個包含  authenticationProviders  參數 的 ProviderManager 對象
        ProviderManager providerManager = new ProviderManager(authenticationProviders,
                parentAuthenticationManager);
        if (eraseCredentials != null) {
            providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
        }
        if (eventPublisher != null) {
            providerManager.setAuthenticationEventPublisher(eventPublisher);
        }
        providerManager = postProcess(providerManager);
        return providerManager;
    }

   這裡我們主要關註其內部 創建了一個包含 authenticationProviders 參數 的 ProviderManager (ProviderManager 是 AuthenticationManager 的實現類)對象並返回。

回過頭,我們來看下 AuthenticationManager 介面 源碼:

public interface AuthenticationManager {
    // 認證介面
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

   可以看到,內部就只有一個我們在授權過程中提到過的 authenticate(),其介面接收一個 Authentication(這個對象我們也不陌生,之前授權過程中提到過的 UsernamePasswordAuthrnticationToken 等都是其實現子類) 對象作為參數。

  至此認證的部分關鍵類或介面已經浮出水面了,它們分別是 AuthenticationManager 、ProviderManager、AuthenticationProvider、Authentication, 接下來我們就圍繞這幾個類或介面進行剖析。

二、AuthenticationManager

  正如我們之前看到的一項,它是整個認證的入口,其定義的認證介面 authenticate() 接收一個 Authentication 對象作為參數。AuthenticationManager 它只是提供了一個認證介面方法,因為在實際使用中,我們不僅有賬戶密碼的登錄方式,還有簡訊驗證碼登錄、郵箱登錄等等,所以它本身不做任何認證,其具體做認證的是 ProviderManager 子類,但正如我們說過的認證方式有很多,如果僅僅依靠 ProviderManager 本身來實現 authenticate() 介面,那我們要支持這麼多認證方式不得寫多少個 if 判斷,而且以後如果我們想要支持指紋登錄,那又不得不在這個方法內部加個if,這種不利於系統擴展的寫法肯定是不可取的,所以 ProviderManager 本身會維護一個List<AuthenticationProvider>列表 ,用於存放多種認證方式,然後通過委托的方式,調用 AuthenticationProvider 來真正實現認證邏輯的 。 而 Authentication 就是我們需要認證的信息(當然不僅僅只包括賬戶信息),通過authenticate() 介面認證成功後返回的 Authentication 就是一個被標識認證成功的對象 。 這裡為什麼要解釋下 AuthenticationManager、ProviderManager、AuthenticationProvider 的關係,主要是一開始容易搞混它們,相信經過這樣一段描述更容易理解了吧。。。

三、Authentication

   如果 沒有看過源碼的同學可能會認為 Authentication 是一個類吧,可實際上它是一個 介面,其內部並未存在任何屬性欄位,它僅僅定義了和規範好了認證對象需要的介面方法,我們來看看其定義的介面方法有哪些,分別又什麼作用:

public interface Authentication extends Principal, Serializable { 

    // 1  獲取許可權信息(不能僅僅理解未角色許可權,還有菜單許可權等等),預設是GrantedAuthority介面的實現類
    Collection<? extends GrantedAuthority> getAuthorities();

    // 2 獲取用戶密碼信息 ,認證成功後會被刪除掉
    Object getCredentials();
    
    // 3  主要存放訪問著的ip等信息
    Object getDetails();

    // 4  重點!! 最重要的身份信息。 大部分情況下是 UserDetails 介面的實現 類,比如 我們 之前配置的 User 對象   
    Object getPrincipal();

    // 5  是否認證(成功)
    boolean isAuthenticated();

    // 6  設置認證標識 
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

   既然 Authentication 定義了這些介面方法,那麼其子類實現肯定都按照這個標準或者稱之為規範定製了實現,這裡就不羅列出其子類的具體實現了,有興趣的同學可以去看下 我們最常用的 UsernamePasswordAuthenticationToken 實現(包括其 父類 AbstractAuthenticationToken)

四、ProviderManager

   它是 AuthenticationManager 的實現子類之一,也是我們最常用的一個實現。正如我們前面提到過的,其內部維護了 一個 List<AuthenticationProvider> 對象, 用於支持和擴展 多種形式的認證方式。我們來看下 其 實現 authenticate() 的源碼:

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
            
        ......
        
        // 1 通過 getProviders() 方法獲取到內部維護的 List<AuthenticationProvider> 對象 並 通過遍歷的方式 去 認證,只要認證成功 就 break 
        for (AuthenticationProvider provider : getProviders()) {
            //  2 正如前面看到的有 很多 AuthenticationProvider 實現,如果每次都是驗證失敗後再掉用下一個 AuthenticationProvider 這種實現是不是很不高效? 所以 這裡通過  supports() 方法來驗證是否可以使用 該 AuthenticationProvider 進行驗證,不可以就直接換下一個 
            if (!provider.supports(toTest)) {
                continue;
            }
            try {
                // 3  重點,這裡是 調用真實的認證方法
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }
        
        if (result == null && parent != null) {
            try {
                // 4 前面都認證不成功,調用父類(嚴格意思不是調用父類,而是其他的 AuthenticationManager 實現類)認證方法
                result = parentResult = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
        
            }
            catch (AuthenticationException e) {
                lastException = parentException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                //  5  刪除認證成功後的 密碼信息,保證安全
                ((CredentialsContainer) result).eraseCredentials();
            }
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }
        
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        throw lastException;
    }

   梳理下整個方法內部實現邏輯:

  • 通過 getProviders() 方法獲取到內部維護的 List 對象 並 通過遍歷的方式 去 認證
  • 通過 provider.supports() 方法 來驗證是否可用當前的 AuthenticationProvider 進行驗證,不可以就直接換下一個 ( 其實方法內部就是驗證當前 的 Authentication 對象是不是其某個子類,比如 我們最常用到的 DaoAuthenticationProvider 的 supports 方法就是判斷當前 的 Authentication 是不是 UsernamePasswordAuthenticationToken )
  • 通過 provider.authenticate() 調用 其真正的認證實現
  • 如果 前面的所有 AuthenticationProvider 均不能認證成功,嘗試調用 parent.authenticate() 方法 :調用父類(嚴格意思不是調用父類,而是其他的 AuthenticationManager 實現類)認證方法
  • 最後 通過 ((CredentialsContainer) result).eraseCredentials() 刪除認證成功後的 密碼信息,保證安全

五、AuthenticationProvider(DaoAuthenticationProvider)

   正如我們想象的一樣,AuthenticationProvider 是一個介面,本身定義了一個 和 AuthenticationManager 一樣的 authenticate 認證介面方法,外加一個 supports() 用於 判別當前 Authentication 是否可以進行處理。

public interface AuthenticationProvider {
    // 定義認證介面方法
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    // 定義判斷是否可以認證處理的介面方法
    boolean supports(Class<?> authentication);
}

   這裡我們就拿我們用得最多的一個 AuthenticationProvider 實現類 DaoAuthenticationProvider(註意,這裡和UsernamePasswordAuthenticationFilter 類似,都是通過父類來實現介面,然後內部處理方法再調用 其 子類進行處理) 來看其內部 這2個抽象方法的實現:

  • supports 實現:
public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
    }

   可以看到僅僅只是判斷當前的 authentication 是否為 UsernamePasswordAuthenticationToken(或其子類)

  • authrnticate 實現
// 1 註意這裡的實現方法是 DaoAuthenticationProvider 的父類 AbstractUserDetailsAuthenticationProvider 實現的
public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
    
        // 2 從 authentication 中獲取 用戶名
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        
        // 3 根據username 從緩存中獲取 認證成功的 UserDetails 信息
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                // 4 如果緩存中沒有用戶信息 需要 獲取用戶信息(由 DaoAuthenticationProvider 實現 ) 
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                ......
            }
        }

        try {
            // 5 前置檢查賬戶是否鎖定,過期,凍結(由DefaultPreAuthenticationChecks類實現)
            preAuthenticationChecks.check(user);
            // 6 主要是驗證 獲取到的用戶密碼與傳入的用戶密碼是否一致
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            // 這裡官方發現緩存可能導致了某些問題,又重新去認證一次
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }
        // 7 後置檢查用戶密碼是否 過期
        postAuthenticationChecks.check(user);
        
        // 8 驗證成功後的用戶信息存入緩存
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        // 9 重新創建一個 authenticated 為true (即認證成功)的 UsernamePasswordAuthenticationToken 對象並返回 
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

   梳理下authenticate(這裡的方法的實現是由 AbstractUserDetailsAuthenticationProvider 提供的)方法內部實現邏輯:

  • 從 入參 authentication 對象中獲取到 username 信息
  • (這裡忽略緩存的處理) 調用 retrieveUser() 方法(由 DaoAuthenticationProvider 實現)根據 username 獲取到 系統(一般來說是從資料庫中) 中獲取到 UserDetails** 對象**
  • 通過 preAuthenticationChecks.check() 方法檢測 當前獲取到的 UserDetails 是否過期、凍結、鎖定(如果任意一個條件 為 true 將拋出 相應 的異常)
  • 通過 additionalAuthenticationChecks() (由 DaoAuthenticationProvider 實現) 判斷 密碼是否一致
  • 通過 postAuthenticationChecks.check() 檢測 UserDetails 的密碼是否過期
  • 最後通過 createSuccessAuthentication() 重新創建一個 authenticated 為true (即認證成功)的 UsernamePasswordAuthenticationToken 對象並返回

  雖然我們知道其驗證邏輯, 但其內部很多方法我們不清楚其內部實現,以及這裡新增的一個 關鍵認證類 UserDetails 是怎麼設計的,如何驗證其是否過期等等。

六、 UserDetailsService 和 UserDetails

  繼續深入看下 retrieveUser() 方法,首先我們註意到其返回對象是一個 UserDetails,那麼我們先從 UserDetails 入手。

UserDetails:

   我們先來看下 UserDetails 源碼:

public interface UserDetails extends Serializable {
    
    // 1 與 Authentication 的 一樣,都是獲取 許可權信息 
    Collection<? extends GrantedAuthority> getAuthorities();

    // 2 獲取用戶正確的密碼   
    String getPassword();

    // 3 獲取賬戶名
    String getUsername();

    // 4 賬戶是否過期
    boolean isAccountNonExpired();

    // 5 賬戶是否鎖定
    boolean isAccountNonLocked();

    // 6 密碼是否過期 
    boolean isCredentialsNonExpired();

    // 7 賬戶是否凍結
    boolean isEnabled();
}

   從上面的 4,5,6,7 介面我們就能夠知道 preAuthenticationChecks.check() 和 postAuthenticationChecks.check() 是如何檢測的了,這裡2個方法的檢測細節就不再深究了,有興趣的同學可以看看源碼,我們只要知道檢測失敗會拋出異常就行了。

  咋呼一看,這個UserDetails 和 Authentication 很相似,其實它們之間還真有關係,在createSuccessAuthentication() 傳教Authentication 對象時,它的authorities 就是UserDetails 傳入的。

UserDetailsService:

  retrieveUser() 方法是系統通過傳入的賬戶名獲取對應的賬戶信息的唯一方法,我們來看下其內部源碼邏輯:

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
        
            // 通過 UserDetailsService 的loadUserByUsername 方法 獲取用戶信息
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            ......
        }
    }

   相信看到這裡,一切都關聯上了,這裡的 UserDetailsService.loadUserByUsername() 就是我們在 上一篇 授權過程中 我們自己實現的。 這裡就不再 貼出UserDetailsService 源碼了。

   還有additionalAuthenticationChecks() 密碼驗證沒有講到,這裡簡單提下,其內部就是通過 PasswordEncoder.matches() 方法進行密碼匹配的。不過這裡要註意一下,這裡的 PasswordEncoder 在 Security 5 開始預設 替換成了 DelegatingPasswordEncoder 這裡也是和我們之前 討論 loadUserByUsername 方法內部創建User (UserDeatails 實現類之一)是一定要用到 PasswordEncoderFactories.createDelegatingPasswordEncoder().encode() 加密是相應的。

七、個人總結

   認證的頂級管理員 AuthenticationManager 為我們提供了 認證入口( authenticate()介面),但是呢,我們也知道大老闆一般不直接參与實質的工作,所以它把任務安排給它的下屬,也就是我們的 ProviderManager 部門領導 ,部門領導 肩負起 認證的工作(authenticate() 認證的實現),其實呢,我們也知道部門領導也是 直接參數 認證工作的,它都是將實際任務安排給小組長的, 也就是我們的 AuthrnticationProvider ,部門領導 開個會議,聚集了所有小組長 ,讓它們自行判斷(通過
support()) 大老闆交下來的任務 該由誰來完成, 小組長 領到任務後,就把任務 分發給各個小組成員,比如 成員1(UserDetailsService) 只需要 完成 retrieveUser() 的工作,然後成員2 完成 additionalAuthenticationChecks() 的工作,最後由項目經理 ( createSuccessAuthentication() ) 將結果彙報給小組長,然後小組長彙報給部門領導,部門領導 審核一下結果,覺得小組長做得不夠好,然後又做了一些操作 ( eraseCredentials() 擦除密碼信息 ),最後認為 結果 可以了就彙報給老闆,老闆呢,也不多看,直接將結果給了客戶(filter)。

   按照慣例,上流程圖:

   本文介紹認證過程的代碼可以訪問代碼倉庫中的 security 模塊 ,項目的github 地址 : https://github.com/BUG9/spring-security

         如果您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前段時間和室友一起給某個公司做了一個管理系統,每個人分2W多。這裡和大家分享一下做完項目後一點點感受,想到啥就說點啥。 核心競爭力 兩個月就掙了2W塊,掙了我爸媽兩個人一年的收入,每天還賊辛苦,披星戴月的感覺,我還沒睡醒,我爸媽就出去大早上賣菜去了,等我睡醒了,還沒有回來(你站在別動,我去買個橘子) ...
  • 類spring ioc 泛型保留 什麼是泛型擦除 Java並不會傳遞泛型類,舉個直觀的慄子: 這裡 嘗試列印泛型類型, 泛型指定了 類,來個測試看看 是否能被獲取到? 依賴腳本build.gradle 運行可以看到結果是,spring ioc並不能註入獲取泛型 自定義IOC泛型註入 在解決sprin ...
  • 摘要: 的兩大核心技術就是 和`AOP AOP Spring AOP CGLIB Spring AOP Spring AOP`的一個運行過程。知其然,知其所以然,才能更好的駕馭這門核心技術。 所有的 驅動技術都得看他的 ,所以上面最重要的是這一句 ,下麵看看它 是一個項容器註冊自動代理創建器 說明 ...
  • 對於剛入門的springboot的新手來說,學的過程中碰到的一些問題記錄下。 1. 首先,配置好Maven環境及本地倉庫 之後進入Maven安裝目錄conf文件夾下的settings.xml配置文件,用Notepadd++打開文件。 配置本地倉庫指向自己創建的本地倉庫,如圖 把jdk版本固定為1.8 ...
  • 對Series的理解也源於對其相關的代碼操作,本次僅貼一些代碼來加深理解以及記憶 ...
  • 第十二章 Django框架 12.1 伺服器程式和應用程式 伺服器程式負責對socket伺服器進行封裝,併在請求到來時,對請求的各種數據進行整理。應用程式則負責具體的邏輯處理。為了方便應用程式的開發,就出現了眾多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的開發方 ...
  • 一、使用JSONObject來解析JSON數據官方提供的,所以不需要導入第三方jar包;直接上代碼,如下 步驟解讀: 定義一個JSON數組,用於將伺服器返回的數據傳入到一個JSONArray對象中; 然後迴圈遍歷這個JSONArray,從中取出每一個元素(JSONObject對象),接下來只需調用g ...
  • String轉成jsonObject JsonObject json = JsonObject.fromObject(String str) String轉成JsonArray JsonArray jsonArray = JsonArray.fromObject(String str) 在開發過程中 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...