国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
spring security 安全框架
spring security 安全框架
文章很長,而且持續(xù)更新,建議收藏起來,慢慢讀!瘋狂創(chuàng)客圈總目錄 博客園版 為您奉上珍貴的學(xué)習(xí)資源 :
免費贈送 :《尼恩Java面試寶典》 持續(xù)更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 經(jīng)典圖書:《Java高并發(fā)核心編程(卷1)加強(qiáng)版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領(lǐng)
免費贈送 經(jīng)典圖書:《Java高并發(fā)核心編程(卷2)加強(qiáng)版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領(lǐng)
免費贈送 經(jīng)典圖書:《Java高并發(fā)核心編程(卷3)加強(qiáng)版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領(lǐng)
免費贈送 經(jīng)典圖書:《尼恩Java面試寶典 最新版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領(lǐng)
免費贈送 資源寶庫: Java 必備 百度網(wǎng)盤資源大合集 價值>10000元 加尼恩領(lǐng)取
認(rèn)證與授權(quán)(Authentication and Authorization)
一般意義來說的應(yīng)用訪問安全性,都是圍繞認(rèn)證(Authentication)和授權(quán)(Authorization)這兩個核心概念來展開的。
即:
首先需要確定用戶身份,
再確定這個用戶是否有訪問指定資源的權(quán)限。
認(rèn)證這塊的解決方案很多,主流的有CAS、SAML2、OAUTH2等(不巧這幾個都用過-_-),我們常說的單點登錄方案(SSO)說的就是這塊,
授權(quán)的話主流的就是spring security和shiro。
shiro比較輕量級,相比較而言spring security確實架構(gòu)比較復(fù)雜。但是shiro與 ss,掌握一個即可。
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
什么是OAuth2 ?
OAuth2是一個關(guān)于授權(quán)的開放標(biāo)準(zhǔn),核心思路是通過各類認(rèn)證手段(具體什么手段OAuth2不關(guān)心)認(rèn)證用戶身份,
并頒發(fā)token(令牌),使得第三方應(yīng)用可以使用該令牌在限定時間、限定范圍訪問指定資源。
主要涉及的RFC規(guī)范有【RFC6749(整體授權(quán)框架)】、【RFC6750(令牌使用)】、【RFC6819(威脅模型)】這幾個,一般我們需要了解的就是RFC6749。
獲取令牌的方式主要有四種,分別是授權(quán)碼模式、簡單模式、密碼模式、客戶端模式,
總之:OAuth2是一個授權(quán)(Authorization)協(xié)議。
認(rèn)證(Authentication)證明的你是不是這個人,
而授權(quán)(Authorization)則是證明這個人有沒有訪問這個資源(Resource)的權(quán)限。
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
下面這張圖來源于OAuth 2.0 authorization framework RFC Document,是OAuth2的一個抽象流程。
+--------+                               +---------------+     |        |--(A)- Authorization Request ->|   Resource    |     |        |                               |     Owner     |     |        |<-(B)-- Authorization Grant ---|               |     |        |                               +---------------+     |        |     |        |                               +---------------+     |        |--(C)-- Authorization Grant -->| Authorization |     | Client |                               |     Server    |     |        |<-(D)----- Access Token -------|               |     |        |                               +---------------+     |        |     |        |                               +---------------+     |        |--(E)----- Access Token ------>|    Resource   |     |        |                               |     Server    |     |        |<-(F)--- Protected Resource ---|               |     +--------+                               +---------------+先來解釋一下上圖的名詞:
Resource Owner:資源所有者,即用戶
Client:客戶端應(yīng)用程序(Application)
Authorization Server:授權(quán)服務(wù)器
Resource Server:資源服務(wù)器
再來解釋一下上圖的大致流程:
(A) 用戶連接客戶端應(yīng)用程序以后,客戶端應(yīng)用程序要求用戶給予授權(quán)
(B) 用戶同意給予客戶端應(yīng)用程序授權(quán)
(C) 客戶端應(yīng)用程序使用上一步獲得的授權(quán)(Grant),向授權(quán)服務(wù)器申請令牌
(D) 授權(quán)服務(wù)器對客戶端應(yīng)用程序的授權(quán)(Grant)進(jìn)行驗證后,確認(rèn)無誤,發(fā)放令牌
(E) 客戶端應(yīng)用程序使用令牌,向資源服務(wù)器申請獲取資源
(F) 資源服務(wù)器確認(rèn)令牌無誤,同意向客戶端應(yīng)用程序開放資源
從上面的流程可以看出,如何獲取授權(quán)(Grant)才是關(guān)鍵。
在OAuth2中有4種授權(quán)類型:
Authorization Code(授權(quán)碼模式):
功能最完整、流程最嚴(yán)密的授權(quán)模式。通過第三方應(yīng)用程序服務(wù)器與認(rèn)證服務(wù)器進(jìn)行互動。廣泛用于各種第三方認(rèn)證。
Implicit(簡化模式):
不通過第三方應(yīng)用程序服務(wù)器,直接在瀏覽器中向認(rèn)證服務(wù)器申請令牌,更加適用于移動端的App及沒有服務(wù)器端的第三方單頁面應(yīng)用。
Resource Owner Password(密碼模式):
用戶向客戶端服務(wù)器提供自己的用戶名和密碼,用戶對客戶端高度信任的情況下使用,比如公司、組織的內(nèi)部系統(tǒng),SSO。
Client Credentials(客戶端模式):
客戶端服務(wù)器以自己的名義,而不是以用戶的名義,向認(rèn)證服務(wù)器進(jìn)行認(rèn)證。
下面主要講最常用的(1)和(3)。此外,還有一個模式叫Refresh Token,也會在下面介紹。
Resource Owner Password(密碼模式)
+----------+     | Resource |     |  Owner   |     |          |     +----------+          v          |    Resource Owner         (A) Password Credentials          |          v     +---------+                                  +---------------+     |         |>--(B)---- Resource Owner ------->|               |     |         |         Password Credentials     | Authorization |     | Client  |                                  |     Server    |     |         |<--(C)---- Access Token ---------<|               |     |         |    (w/ Optional Refresh Token)   |               |     +---------+                                  +---------------+            Figure 5: Resource Owner Password Credentials Flow它的步驟如下:
(A) 用戶(Resource Owner)向客戶端(Client)提供用戶名和密碼。
(B) 客戶端將用戶名和密碼發(fā)給認(rèn)證服務(wù)器(Authorization Server),向后者請求令牌。
(C) 認(rèn)證服務(wù)器確認(rèn)無誤后,向客戶端提供訪問令牌。
Authorization Code(授權(quán)碼模式)
+----------+     | Resource |     |   Owner  |     |          |     +----------+          ^          |         (B)     +----|-----+          Client Identifier      +---------------+     |         -+----(A)-- & Redirection URI ---->|               |     |  User-   |                                 | Authorization |     |  Agent  -+----(B)-- User authenticates --->|     Server    |     |          |                                 |               |     |         -+----(C)-- Authorization Code ---<|               |     +-|----|---+                                 +---------------+       |    |                                         ^      v      (A)  (C)                                        |      |       |    |                                         |      |       ^    v                                         |      |     +---------+                                      |      |     |         |>---(D)-- Authorization Code ---------      |     |  Client |          & Redirection URI                  |     |         |                                             |     |         |<---(E)----- Access Token -------------------     +---------+       (w/ Optional Refresh Token)   Note: The lines illustrating steps (A), (B), and (C) are broken into   two parts as they pass through the user-agent.它的步驟如下:
(A) 用戶(Resource Owner)通過用戶代理(User-Agent)訪問客戶端(Client),客戶端索要授權(quán),并將用戶導(dǎo)向認(rèn)證服務(wù)器(Authorization Server)。
(B) 用戶選擇是否給予客戶端授權(quán)。
(C) 假設(shè)用戶給予授權(quán),認(rèn)證服務(wù)器將用戶導(dǎo)向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權(quán)碼。
(D) 客戶端收到授權(quán)碼,附上早先的"重定向URI",向認(rèn)證服務(wù)器申請令牌。這一步是在客戶端的后臺的服務(wù)器上完成的,對用戶不可見。
(E) 認(rèn)證服務(wù)器核對了授權(quán)碼和重定向URI,確認(rèn)無誤后,向客戶端發(fā)送訪問令牌(access token)和更新令牌(refresh token)。這一步也對用戶不可見。
令牌刷新(refresh token)
+--------+                                           +---------------+  |        |--(A)------- Authorization Grant --------->|               |  |        |                                           |               |  |        |<-(B)----------- Access Token -------------|               |  |        |               & Refresh Token             |               |  |        |                                           |               |  |        |                            +----------+   |               |  |        |--(C)---- Access Token ---->|          |   |               |  |        |                            |          |   |               |  |        |<-(D)- Protected Resource --| Resource |   | Authorization |  | Client |                            |  Server  |   |     Server    |  |        |--(E)---- Access Token ---->|          |   |               |  |        |                            |          |   |               |  |        |<-(F)- Invalid Token Error -|          |   |               |  |        |                            +----------+   |               |  |        |                                           |               |  |        |--(G)----------- Refresh Token ----------->|               |  |        |                                           |               |  |        |<-(H)----------- Access Token -------------|               |  +--------+           & Optional Refresh Token        +---------------+當(dāng)我們申請token后,Authorization Server不僅給了我們Access Token,還有Refresh Token。
當(dāng)Access Token過期后,我們用Refresh Token訪問/refresh端點就可以拿到新的Access Token了。
我們要和Spring Security的認(rèn)證(Authentication)區(qū)別開來,
什么是Spring Security?
Spring Security是一套安全框架,可以基于RBAC(基于角色的權(quán)限控制)對用戶的訪問權(quán)限進(jìn)行控制,
核心思想是通過一系列的filter chain來進(jìn)行攔截過濾,對用戶的訪問權(quán)限進(jìn)行控制,
spring security 的核心功能主要包括:
認(rèn)證 (你是誰)
授權(quán) (你能干什么)
攻擊防護(hù) (防止偽造身份)
其核心就是一組過濾器鏈,項目啟動后將會自動配置。最核心的就是 Basic Authentication Filter 用來認(rèn)證用戶的身份,一個在spring security中一種過濾器處理一種認(rèn)證方式。
比如,對于username password認(rèn)證過濾器來說,
會檢查是否是一個登錄請求;
是否包含username 和 password (也就是該過濾器需要的一些認(rèn)證信息) ;
如果不滿足則放行給下一個。
下一個按照自身職責(zé)判定是否是自身需要的信息,basic的特征就是在請求頭中有 Authorization:Basic eHh4Onh4 的信息。中間可能還有更多的認(rèn)證過濾器。**最后一環(huán)是 FilterSecurityInterceptor**,這里會判定該請求是否能進(jìn)行訪問rest服務(wù),判斷的依據(jù)是  BrowserSecurityConfig中的配置,如果被拒絕了就會拋出不同的異常(根據(jù)具體的原因)。Exception  Translation Filter 會捕獲拋出的錯誤,然后根據(jù)不同的認(rèn)證方式進(jìn)行信息的返回提示。注意:綠色的過濾器可以配置是否生效,其他的都不能控制。
二、入門項目
首先創(chuàng)建spring boot項目HelloSecurity,其pom主要依賴如下:<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-thymeleaf</artifactId>    </dependency>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>        <scope>test</scope>    </dependency>    <dependency>        <groupId>org.springframework.security</groupId>        <artifactId>spring-security-test</artifactId>        <scope>test</scope>    </dependency></dependencies>然后在src/main/resources/templates/目錄下創(chuàng)建頁面:
home.html<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">    <head>        <title>Spring Security Example</title>    </head>    <body>        <h1>Welcome!</h1>        <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>    </body></html>我們可以看到, 在這個簡單的視圖中包含了一個鏈接: “/hello”. 鏈接到了如下的頁面,Thymeleaf模板如下:
hello.html<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">    <head>        <title>Hello World!</title>    </head>    <body>        <h1>Hello world!</h1>    </body></html>Web應(yīng)用程序基于Spring MVC。 因此,你需要配置Spring MVC并設(shè)置視圖控制器來暴露這些模板。 如下是一個典型的Spring MVC配置類。在src/main/java/hello目錄下(所以java都在這里):
@Configurationpublic 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("/login").setViewName("login");    }}  addViewControllers()方法(覆蓋WebMvcConfigurerAdapter中同名的方法)添加了四個視圖控制器。  兩個視圖控制器引用名稱為“home”的視圖(在home.html中定義),另一個引用名為“hello”的視圖(在hello.html中定義)。  第四個視圖控制器引用另一個名為“l(fā)ogin”的視圖。  將在下一部分中創(chuàng)建該視圖。此時,可以跳過來使應(yīng)用程序可執(zhí)行并運行應(yīng)用程序,而無需登錄任何內(nèi)容。然后啟動程序如下:@SpringBootApplicationpublic class Application {    public static void main(String[] args) throws Throwable {        SpringApplication.run(Application.class, args);    }}2、加入Spring Security
假設(shè)你希望防止未經(jīng)授權(quán)的用戶訪問“/ hello”。 此時,如果用戶點擊主頁上的鏈接,他們會看到問候語,請求被沒有被攔截。  你需要添加一個障礙,使得用戶在看到該頁面之前登錄。您可以通過在應(yīng)用程序中配置Spring Security來實現(xiàn)。 如果Spring  Security在類路徑上,則Spring Boot會使用“Basic認(rèn)證”來自動保護(hù)所有HTTP端點。  同時,你可以進(jìn)一步自定義安全設(shè)置。首先在pom文件中引入:<dependencies>    ...        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-security</artifactId>        </dependency>    ...</dependencies>如下是安全配置,使得只有認(rèn)證過的用戶才可以訪問到問候頁面:
@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http            .authorizeRequests()                .antMatchers("/", "/home").permitAll()                .anyRequest().authenticated()                .and()            .formLogin()                .loginPage("/login")                .permitAll()                .and()            .logout()                .permitAll();    }    @Autowired    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {        auth            .inMemoryAuthentication()                .withUser("user").password("password").roles("USER");    }} WebSecurityConfig類使用了@EnableWebSecurity注解 ,以**啟用Spring Security的Web安全支持**,并提供Spring MVC集成。它還擴(kuò)展了WebSecurityConfigurerAdapter,并覆蓋了一些方法來設(shè)置Web安全配置的一些細(xì)節(jié)。 **configure(HttpSecurity)方法定義了哪些URL路徑應(yīng)該被保護(hù)**,哪些不應(yīng)該。具體來說,“/”和“/ home”路徑被配置為不需要任何身份驗證。所有其他路徑必須經(jīng)過身份驗證。 當(dāng)用戶成功登錄時,它們將被重定向到先前請求的需要身份認(rèn)證的頁面。有一個由 loginPage()指定的自定義“/登錄”頁面,每個人都可以查看它。 對于configureGlobal(AuthenticationManagerBuilder) 方法,它將單個用戶設(shè)置在內(nèi)存中。該用戶的用戶名為“user”,密碼為“password”,角色為“USER”。 現(xiàn)在我們需要創(chuàng)建登錄頁面。前面我們已經(jīng)配置了“l(fā)ogin”的視圖控制器,因此現(xiàn)在只需要創(chuàng)建登錄頁面即可:login.html<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">    <head>        <title>Spring Security Example </title>    </head>    <body>        <div th:if="${param.error}">            Invalid username and password.        </div>        <div th:if="${param.logout}">            You have been logged out.        </div>        <form th:action="@{/login}" method="post">            <div><label> User Name : <input type="text" name="username"/> </label></div>            <div><label> Password: <input type="password" name="password"/> </label></div>            <div><input type="submit" value="Sign In"/></div>        </form>    </body></html> 你可以看到,這個Thymeleaf模板只是提供一個表單來獲取用戶名和密碼,并將它們提交到“/ login”。  根據(jù)配置,Spring Security提供了一個攔截該請求并驗證用戶的過濾器。 如果用戶未通過認(rèn)證,該頁面將重定向到“/  login?error”,并在頁面顯示相應(yīng)的錯誤消息。 注銷成功后,我們的應(yīng)用程序?qū)l(fā)送到“/  login?logout”,我們的頁面顯示相應(yīng)的登出成功消息。最后,我們需要向用戶提供一個顯示當(dāng)前用戶名和登出的方法。 更新hello.html  向當(dāng)前用戶打印一句hello,并包含一個“注銷”表單,如下所示:<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">    <head>        <title>Hello World!</title>    </head>    <body>        <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>        <form th:action="@{/logout}" method="post">            <input type="submit" value="Sign Out"/>        </form>    </body></html>說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
三、參數(shù)詳解
1、注解 @EnableWebSecurity
在 Spring boot 應(yīng)用中使用 Spring Security,用到了  @EnableWebSecurity注解,官方說明為,該注解和 @Configuration 注解一起使用, 注解  WebSecurityConfigurer 類型的類,或者利用@EnableWebSecurity 注解繼承  WebSecurityConfigurerAdapter的類,這樣就構(gòu)成了 Spring Security 的配置。2、抽象類 WebSecurityConfigurerAdapter
一般情況,會選擇繼承 WebSecurityConfigurerAdapter  類,其官方說明為:WebSecurityConfigurerAdapter 提供了一種便利的方式去創(chuàng)建  WebSecurityConfigurer的實例,只需要重寫 WebSecurityConfigurerAdapter  的方法,即可配置攔截什么URL、設(shè)置什么權(quán)限等安全控制。3、方法 configure(AuthenticationManagerBuilder auth) 和 configure(HttpSecurity http)
Demo 中重寫了 WebSecurityConfigurerAdapter 的兩個方法:   /**     * 通過 {@link #authenticationManager()} 方法的默認(rèn)實現(xiàn)嘗試獲取一個 {@link AuthenticationManager}.     * 如果被復(fù)寫, 應(yīng)該使用{@link AuthenticationManagerBuilder} 來指定 {@link AuthenticationManager}.     *     * 例如, 可以使用以下配置在內(nèi)存中進(jìn)行注冊公開內(nèi)存的身份驗證{@link UserDetailsService}:     *     * // 在內(nèi)存中添加 user 和 admin 用戶     * @Override     * protected void configure(AuthenticationManagerBuilder auth) {     *     auth     *       .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()     *         .withUser("admin").password("password").roles("USER", "ADMIN");     * }     *     * // 將 UserDetailsService 顯示為 Bean     * @Bean     * @Override     * public UserDetailsService userDetailsServiceBean() throws Exception {     *     return super.userDetailsServiceBean();     * }     *     */    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        this.disableLocalConfigureAuthenticationBldr = true;    }    /**     * 復(fù)寫這個方法來配置 {@link HttpSecurity}.      * 通常,子類不能通過調(diào)用 super 來調(diào)用此方法,因為它可能會覆蓋其配置。 默認(rèn)配置為:     *      * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();     *     */    protected void configure(HttpSecurity http) throws Exception {        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");        http            .authorizeRequests()                .anyRequest().authenticated()                .and()            .formLogin().and()            .httpBasic();    }4、final 類 HttpSecurity
HttpSecurity 常用方法及說明:
方法說明
openidLogin()用于基于 OpenId 的驗證
headers()將安全標(biāo)頭添加到響應(yīng)
cors()配置跨域資源共享( CORS )
sessionManagement()允許配置會話管理
portMapper()允許配置一個PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的對象使用 PortMapper 從 HTTP 重定向到 HTTPS 或者從 HTTPS 重定向到 HTTP。默認(rèn)情況下,Spring Security使用一個PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee()配置基于容器的預(yù)認(rèn)證。 在這種情況下,認(rèn)證由Servlet容器管理
x509()配置基于x509的認(rèn)證
rememberMe允許配置“記住我”的驗證
authorizeRequests()允許基于使用HttpServletRequest限制訪問
requestCache()允許配置請求緩存
exceptionHandling()允許配置錯誤處理
securityContext()在HttpServletRequests之間的SecurityContextHolder上設(shè)置SecurityContext的管理。 當(dāng)使用WebSecurityConfigurerAdapter時,這將自動應(yīng)用
servletApi()將HttpServletRequest方法與在其上找到的值集成到SecurityContext中。 當(dāng)使用WebSecurityConfigurerAdapter時,這將自動應(yīng)用
csrf()添加 CSRF 支持,使用WebSecurityConfigurerAdapter時,默認(rèn)啟用
logout()添加退出登錄支持。當(dāng)使用WebSecurityConfigurerAdapter時,這將自動應(yīng)用。默認(rèn)情況是,訪問URL”/ logout”,使HTTP Session無效來清除用戶,清除已配置的任何#rememberMe()身份驗證,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous()允許配置匿名用戶的表示方法。 當(dāng)與WebSecurityConfigurerAdapter結(jié)合使用時,這將自動應(yīng)用。 默認(rèn)情況下,匿名用戶將使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin()指定支持基于表單的身份驗證。如果未指定FormLoginConfigurer#loginPage(String),則將生成默認(rèn)登錄頁面
oauth2Login()根據(jù)外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份驗證
requiresChannel()配置通道安全。為了使該配置有用,必須提供至少一個到所需信道的映射
httpBasic()配置 Http Basic 驗證
addFilterAt()在指定的Filter類的位置添加過濾器
5、類 AuthenticationManagerBuilder
/*** {@link SecurityBuilder} used to create an {@link AuthenticationManager}. Allows for* easily building in memory authentication, LDAP authentication, JDBC based* authentication, adding {@link UserDetailsService}, and adding* {@link AuthenticationProvider}s.*/    意思是,AuthenticationManagerBuilder 用于創(chuàng)建一個  AuthenticationManager,讓我能夠輕松的實現(xiàn)內(nèi)存驗證、LADP驗證、基于JDBC的驗證、添加UserDetailsService、添加AuthenticationProvider。使用yaml文件定義的用戶名、密碼登錄
在application.yaml中定義用戶名密碼:
spring:  security:    user:      name: root      password: root使用root/root登錄,可以正常訪問/hello。
使用代碼中指定的用戶名、密碼登錄
使用configure(AuthenticationManagerBuilder) 添加認(rèn)證。
使用configure(httpSecurity) 添加權(quán)限
@Configurationpublic class MySecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth                .inMemoryAuthentication()                .withUser("admin") // 添加用戶admin                .password("{noop}admin")  // 不設(shè)置密碼加密                .roles("ADMIN", "USER")// 添加角色為admin,user                .and()                .withUser("user") // 添加用戶user                .password("{noop}user")                 .roles("USER")             .and()             .withUser("tmp") // 添加用戶tmp                .password("{noop}tmp")             .roles(); // 沒有角色    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .authorizeRequests()                .antMatchers("/product/**").hasRole("USER") //添加/product/** 下的所有請求只能由user角色才能訪問                .antMatchers("/admin/**").hasRole("ADMIN") //添加/admin/** 下的所有請求只能由admin角色才能訪問                .anyRequest().authenticated() // 沒有定義的請求,所有的角色都可以訪問(tmp也可以)。                .and()                .formLogin().and()                .httpBasic();    }}添加AdminController、ProductController
@RestController@RequestMapping("/admin")public class AdminController {    @RequestMapping("/hello")    public String hello(){        return "admin hello";    }}@RestController@RequestMapping("/product")public class ProductController {    @RequestMapping("/hello")    public String hello(){        return "product hello";    }}通過上面的設(shè)置,訪問http://localhost:8080/admin/hello只能由admin訪問,http://localhost:8080/product/hello admin和user都可以訪問,http://localhost:8080/hello 所有用戶(包括tmp)都可以訪問。
使用數(shù)據(jù)庫的用戶名、密碼登錄
添加依賴
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency>    <groupId>mysql</groupId>    <artifactId>mysql-connector-java</artifactId></dependency>添加數(shù)據(jù)庫配置
spring:  datasource:    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai    username: root    password: root    driver-class-name: com.mysql.cj.jdbc.Driver配置spring-security認(rèn)證和授權(quán)
@Configurationpublic class MySecurityConfiguration extends WebSecurityConfigurerAdapter {    @Autowired    private UserDetailsService userDetailsService;    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth.userDetailsService(userDetailsService)// 設(shè)置自定義的userDetailsService                .passwordEncoder(passwordEncoder());    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .authorizeRequests()                .antMatchers("/product/**").hasRole("USER")                .antMatchers("/admin/**").hasRole("ADMIN")                .anyRequest().authenticated() //                .and()                .formLogin()                .and()                .httpBasic()                .and().logout().logoutUrl("/logout");    }    @Bean    public PasswordEncoder passwordEncoder() {        return NoOpPasswordEncoder.getInstance();// 使用不使用加密算法保持密碼//        return new BCryptPasswordEncoder();    }}如果需要使用BCryptPasswordEncoder,可以先在測試環(huán)境中加密后放到數(shù)據(jù)庫中:
@Testvoid encode() {    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();    String password = bCryptPasswordEncoder.encode("user");    String password2 = bCryptPasswordEncoder.encode("admin");    System.out.println(password);    System.out.println(password2);}配置自定義UserDetailsService來進(jìn)行驗證
@Component("userDetailsService")public class CustomUserDetailsService implements UserDetailsService {   @Autowired   UserRepository userRepository;   @Override   public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {         // 1. 查詢用戶      User userFromDatabase = userRepository.findOneByLogin(login);      if (userFromDatabase == null) {         //log.warn("User: {} not found", login);       throw new UsernameNotFoundException("User " + login + " was not found in db");            //這里找不到必須拋異常      }       // 2. 設(shè)置角色      Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();      GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userFromDatabase.getRole());      grantedAuthorities.add(grantedAuthority);      return new org.springframework.security.core.userdetails.User(login,            userFromDatabase.getPassword(), grantedAuthorities);   }}配置JPA中的UserRepository
@Repositorypublic interface UserRepository extends JpaRepository<User, Long> {    User findOneByLogin(String login);}添加數(shù)據(jù)庫數(shù)據(jù)
CREATE TABLE `user` (  `id` int(28) NOT NULL,  `login` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,  `password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,  `role` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (1, user, user, ROLE_USER);INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (2, admin, admin, ROLE_ADMIN);默認(rèn)角色前綴必須是ROLE_,因為spring-security會在授權(quán)的時候自動使用match中的角色加上ROLE_后進(jìn)行比較。
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
四:獲取登錄信息
@RequestMapping("/info")public String info(){    String userDetails = null;    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();    if(principal instanceof UserDetails) {        userDetails = ((UserDetails)principal).getUsername();    }else {        userDetails = principal.toString();    }    return userDetails;}使用SecurityContextHolder.getContext().getAuthentication().getPrincipal();獲取當(dāng)前的登錄信息。
五: Spring Security 核心組件
SecurityContext
SecurityContext是安全的上下文,所有的數(shù)據(jù)都是保存到SecurityContext中。
可以通過SecurityContext獲取的對象有:
Authentication
SecurityContextHolder
SecurityContextHolder用來獲取SecurityContext中保存的數(shù)據(jù)的工具。通過使用靜態(tài)方法獲取SecurityContext的相對應(yīng)的數(shù)據(jù)。
SecurityContext context = SecurityContextHolder.getContext();Authentication
Authentication表示當(dāng)前的認(rèn)證情況,可以獲取的對象有:
UserDetails:獲取用戶信息,是否鎖定等額外信息。
Credentials:獲取密碼。
isAuthenticated:獲取是否已經(jīng)認(rèn)證過。
Principal:獲取用戶,如果沒有認(rèn)證,那么就是用戶名,如果認(rèn)證了,返回UserDetails。
UserDetails
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled();}UserDetailsService
UserDetailsService可以通過loadUserByUsername獲取UserDetails對象。該接口供spring security進(jìn)行用戶驗證。
通常使用自定義一個CustomUserDetailsService來實現(xiàn)UserDetailsService接口,通過自定義查詢UserDetails。
AuthenticationManager
AuthenticationManager用來進(jìn)行驗證,如果驗證失敗會拋出相對應(yīng)的異常。
PasswordEncoder
密碼加密器。通常是自定義指定。
BCryptPasswordEncoder:哈希算法加密
NoOpPasswordEncoder:不使用加密
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲取:語雀 或者 碼云
六:spring security session 無狀態(tài)支持權(quán)限控制(前后分離)
spring security會在默認(rèn)的情況下將認(rèn)證信息放到HttpSession中。
但是對于我們的前后端分離的情況,如app,小程序,web前后分離等,httpSession就沒有用武之地了。這時我們可以通過configure(httpSecurity)設(shè)置spring security是否使用httpSession。
@Configurationpublic class MySecurityConfiguration extends WebSecurityConfigurerAdapter {    // code...    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .sessionManagement()             //設(shè)置無狀態(tài),所有的值如下所示。                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)                // code...    }    // code...}共有四種值,其中默認(rèn)的是ifRequired。
always – a session will always be created if one doesn’t already exist,沒有session就創(chuàng)建。
ifRequired – a session will be created only if required (default),如果需要就創(chuàng)建(默認(rèn))。
never – the framework will never create a session itself but it will use one if it already exists
stateless – no session will be created or used by Spring Security 不創(chuàng)建不使用session
由于前后端不通過保存session和cookie來進(jìn)行判斷,所以為了保證spring security能夠記錄登錄狀態(tài),所以需要傳遞一個值,讓這個值能夠自我驗證來源,同時能夠得到數(shù)據(jù)信息。選型我們選擇JWT。對于java客戶端我們選擇使用jjwt。
添加依賴
<dependency>    <groupId>io.jsonwebtoken</groupId>    <artifactId>jjwt-api</artifactId>    <version>0.11.2</version></dependency><dependency>    <groupId>io.jsonwebtoken</groupId>    <artifactId>jjwt-impl</artifactId>    <version>0.11.2</version>    <scope>runtime</scope></dependency><dependency>    <groupId>io.jsonwebtoken</groupId>    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->    <version>0.11.2</version>    <scope>runtime</scope></dependency>創(chuàng)建工具類JWTProvider
JWTProvider需要至少提供兩個方法,一個用來創(chuàng)建我們的token,另一個根據(jù)token獲取Authentication。
provider需要保證Key密鑰是唯一的,使用init()構(gòu)建,否則會拋出異常。
@Component@Slf4jpublic class JWTProvider {    private Key key; // 私鑰    private long tokenValidityInMilliseconds; // 有效時間    private long tokenValidityInMillisecondsForRememberMe; // 記住我有效時間    @Autowired    private JJWTProperties jjwtProperties; // jwt配置參數(shù)    @Autowired    private UserRepository userRepository;     @PostConstruct    public void init() {        byte[] keyBytes;        String secret = jjwtProperties.getSecret();        if (StringUtils.hasText(secret)) {            log.warn("Warning: the JWT key used is not Base64-encoded. " +                    "We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security.");            keyBytes = secret.getBytes(StandardCharsets.UTF_8);        } else {            log.debug("Using a Base64-encoded JWT secret key");            keyBytes = Decoders.BASE64.decode(jjwtProperties.getBase64Secret());        }        this.key = Keys.hmacShaKeyFor(keyBytes); // 使用mac-sha算法的密鑰        this.tokenValidityInMilliseconds =                1000 * jjwtProperties.getTokenValidityInSeconds();        this.tokenValidityInMillisecondsForRememberMe =                1000 * jjwtProperties.getTokenValidityInSecondsForRememberMe();    }    public String createToken(Authentication authentication, boolean rememberMe) {        long now = (new Date()).getTime();        Date validity;        if (rememberMe) {            validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);        } else {            validity = new Date(now + this.tokenValidityInMilliseconds);        }        User user = userRepository.findOneByLogin(authentication.getName());        Map<String ,Object> map = new HashMap<>();        map.put("sub",authentication.getName());        map.put("user",user);        return Jwts.builder()                .setClaims(map) // 添加body                .signWith(key, SignatureAlgorithm.HS512) // 指定摘要算法                .setExpiration(validity) // 設(shè)置有效時間                .compact();    }    public Authentication getAuthentication(String token) {        Claims claims = Jwts.parserBuilder()                .setSigningKey(key)                .build()                .parseClaimsJws(token).getBody(); // 根據(jù)token獲取body        User principal;        Collection<? extends GrantedAuthority> authorities;        principal = userRepository.findOneByLogin(claims.getSubject());        authorities = principal.getAuthorities();        return new UsernamePasswordAuthenticationToken(principal, token, authorities);    }}注意這里我們創(chuàng)建的User需要實現(xiàn)UserDetails對象,這樣我們可以根據(jù)principal.getAuthorities()獲取到權(quán)限,如果不實現(xiàn)UserDetails,那么需要自定義authorities并添加到UsernamePasswordAuthenticationToken中。
@Data@Entity@Table(name="user")public class User implements UserDetails {    @Id    @Column    private Long id;    @Column    private String login;    @Column    private String password;    @Column    private String role;    @Override    // 獲取權(quán)限,這里就用簡單的方法    // 在spring security中,Authorities既可以是ROLE也可以是Authorities    public Collection<? extends GrantedAuthority> getAuthorities() {        return Collections.singleton(new SimpleGrantedAuthority(role));    }    @Override    public String getUsername() {        return login;    }    @Override    public boolean isAccountNonExpired() {        return true;    }    @Override    public boolean isAccountNonLocked() {        return false;    }    @Override    public boolean isCredentialsNonExpired() {        return true;    }    @Override    public boolean isEnabled() {        return true;    }}創(chuàng)建登錄成功,登出成功處理器
登錄成功后向前臺發(fā)送jwt。
認(rèn)證成功,返回jwt:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{    void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{        PrintWriter writer = response.getWriter();        writer.println(jwtProvider.createToken(authentication, true));    }}登出成功:
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {    void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException{        PrintWriter writer = response.getWriter();        writer.println("logout success");        writer.flush();    }}設(shè)置登錄、登出、取消csrf防護(hù)
登出無法對token進(jìn)行失效操作,可以使用數(shù)據(jù)庫保存token,然后在登出時刪除該token。
@Configurationpublic class MySecurityConfiguration extends WebSecurityConfigurerAdapter {    // code...    @Override    protected void configure(HttpSecurity http) throws Exception {       http           // code...           // 添加登錄處理器           .formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {           PrintWriter writer = response.getWriter();           writer.println(jwtProvider.createToken(authentication, true));       })           // 取消csrf防護(hù)           .and().csrf().disable()            // code...           // 添加登出處理器           .and().logout().logoutUrl("/logout").logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {           PrintWriter writer = response.getWriter();           writer.println("logout success");           writer.flush();       })         // code...    }    // code...}使用JWT集成spring-security
添加Filter供spring-security解析token,并向securityContext中添加我們的用戶信息。
在UsernamePasswordAuthenticationFilter.class之前我們需要執(zhí)行根據(jù)token添加authentication。關(guān)鍵方法是從jwt中獲取authentication,然后添加到securityContext中。
在SecurityConfiguration中需要設(shè)置Filter添加的位置。
創(chuàng)建自定義Filter,用于jwt獲取authentication:
@Slf4jpublic class JWTFilter extends GenericFilterBean {    private final static String HEADER_AUTH_NAME = "auth";    private JWTProvider jwtProvider;    public JWTFilter(JWTProvider jwtProvider) {        this.jwtProvider = jwtProvider;    }    @Override    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {        try {            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;            String authToken = httpServletRequest.getHeader(HEADER_AUTH_NAME);            if (StringUtils.hasText(authToken)) {                // 從自定義tokenProvider中解析用戶                Authentication authentication = this.jwtProvider.getAuthentication(authToken);                SecurityContextHolder.getContext().setAuthentication(authentication);            }            // 調(diào)用后續(xù)的Filter,如果上面的代碼邏輯未能復(fù)原“session”,SecurityContext中沒有想過信息,后面的流程會檢測出"需要登錄"            filterChain.doFilter(servletRequest, servletResponse);        } catch (Exception ex) {            throw new RuntimeException(ex);        }    }}向HttpSecurity添加Filter和設(shè)置Filter位置:
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {    // code...    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .sessionManagement()             //設(shè)置添加Filter和位置                .and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);                // code...    }    // code...}MySecurityConfiguration代碼
@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {    @Autowired    private UserDetailsService userDetailsService;    @Autowired    private JWTProvider jwtProvider;    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth.userDetailsService(userDetailsService)// 設(shè)置自定義的userDetailsService                .passwordEncoder(passwordEncoder());    }    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .sessionManagement()                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)//設(shè)置無狀態(tài)                .and()                .authorizeRequests() // 配置請求權(quán)限                .antMatchers("/product/**").hasRole("USER") // 需要角色                .antMatchers("/admin/**").hasRole("ADMIN")                .anyRequest().authenticated() // 所有的請求都需要登錄                .and()             // 配置登錄url,和登錄成功處理器                .formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {                    PrintWriter writer = response.getWriter();                    writer.println(jwtProvider.createToken(authentication, true));                })             // 取消csrf防護(hù)                .and().csrf().disable()                 .httpBasic()             // 配置登出url,和登出成功處理器 .and().logout().logoutUrl("/logout")             .logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {                    PrintWriter writer = response.getWriter();                    writer.println("logout success");                    writer.flush();                })             // 在UsernamePasswordAuthenticationFilter之前執(zhí)行我們添加的JWTFilter                .and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);    }    @Bean    public PasswordEncoder passwordEncoder() {        return NoOpPasswordEncoder.getInstance();    }    @Override    public void configure(WebSecurity web) {        // 添加不做權(quán)限的URL        web.ignoring()                .antMatchers("/swagger-resources/**")                .antMatchers("/swagger-ui.html")                .antMatchers("/webjars/**")                .antMatchers("/v2/**")                .antMatchers("/h2-console/**");    }}使用注解對方法進(jìn)行權(quán)限管理
需要在MySecurityConfiguration上添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解,prePostEnabled默認(rèn)為false,需要設(shè)置為true后才能全局的注解權(quán)限控制。
prePostEnabled設(shè)置為true后,可以使用四個注解:
添加實體類School:
@Datapublic class School implements Serializable {    private Long id;    private String name;    private String address;}@PreAuthorize
在訪問之前就進(jìn)行權(quán)限判斷
@RestControllerpublic class AnnoController {    @Autowired    private JWTProvider jwtProvider;    @RequestMapping("/annotation")//    @PreAuthorize("hasRole(ADMIN)")    @PreAuthorize("hasAuthority(ROLE_ADMIN)")    public String info(){        return "擁有admin權(quán)限";    }}hasRole和hasAuthority都會對UserDetails中的getAuthorities進(jìn)行判斷區(qū)別是hasRole會對字段加上ROLE_后再進(jìn)行判斷,上例中使用了hasRole(ADMIN),那么就會使用ROLE_ADMIN進(jìn)行判斷,如果是hasAuthority(ADMIN),那么就使用ADMIN進(jìn)行判斷。
@PostAuthorize
在請求之后進(jìn)行判斷,如果返回值不滿足條件,會拋出異常,但是方法本身是已經(jīng)執(zhí)行過了的。
@RequestMapping("/postAuthorize")@PreAuthorize("hasRole(ADMIN)")@PostAuthorize("returnObject.id%2==0")public School postAuthorize(Long id) {    School school = new School();    school.setId(id);    return school;}returnObject是內(nèi)置對象,引用的是方法的返回值。
如果returnObject.id%2==0為 true,那么返回方法值。如果為false,會返回403 Forbidden。
@PreFilter
在方法執(zhí)行之前,用于過濾集合中的值。
@RequestMapping("/preFilter")@PreAuthorize("hasRole(ADMIN)")@PreFilter("filterObject%2==0")public List<Long> preFilter(@RequestParam("ids") List<Long> ids) {    return ids;}filterObject是內(nèi)置對象,引用的是集合中的泛型類,如果有多個集合,需要指定filterTarget。
@PreFilter(filterTarget="ids", value="filterObject%2==0")public List<Long> preFilter(@RequestParam("ids") List<Long> ids,@RequestParam("ids") List<User> users,) {    return ids;}filterObject%2==0會對集合中的值會進(jìn)行過濾,為true的值會保留。
第一個例子返回的值在執(zhí)行前過濾返回2,4。
@PostFilter
會對返回的集合進(jìn)行過濾。
@RequestMapping("/postFilter")@PreAuthorize("hasRole(ADMIN)")@PostFilter("filterObject.id%2==0")public List<School> postFilter() {    List<School> schools = new ArrayList<School>();    School school;    for (int i = 0; i < 10; i++) {        school = new School();        school.setId((long)i);        schools.add(school);    }    return schools;}上面的方法返回結(jié)果為:id為0,2,4,6,8的School對象。
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
七、原理講解
1、校驗流程圖
2、源碼分析
AbstractAuthenticationProcessingFilter 抽象類
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)            throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest) req;        HttpServletResponse response = (HttpServletResponse) res;        if (!requiresAuthentication(request, response)) {            chain.doFilter(request, response);            return;        }        if (logger.isDebugEnabled()) {            logger.debug("Request is to process authentication");        }        Authentication authResult;        try {            authResult = attemptAuthentication(request, response);            if (authResult == null) {                // return immediately as subclass has indicated that it hasnt completed                // authentication                return;            }            sessionStrategy.onAuthentication(authResult, request, response);        }        catch (InternalAuthenticationServiceException failed) {            logger.error(                    "An internal error occurred while trying to authenticate the user.",                    failed);            unsuccessfulAuthentication(request, response, failed);            return;        }        catch (AuthenticationException failed) {            // Authentication failed            unsuccessfulAuthentication(request, response, failed);            return;        }        // Authentication success        if (continueChainBeforeSuccessfulAuthentication) {            chain.doFilter(request, response);        }        successfulAuthentication(request, response, chain, authResult);    }    **調(diào)用 requiresAuthentication(HttpServletRequest, HttpServletResponse) 決定是否需要進(jìn)行驗證操作**。如果需要驗證,則會調(diào)用 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法,有三種結(jié)果:
返回一個 Authentication 對象。配置的 SessionAuthenticationStrategy` 將被調(diào)用,然后 然后調(diào)用 successfulAuthentication(HttpServletRequest,HttpServletResponse,F(xiàn)ilterChain,Authentication) 方法。
驗證時發(fā)生 AuthenticationException。unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法將被調(diào)用。
返回Null,表示身份驗證不完整。假設(shè)子類做了一些必要的工作(如重定向)來繼續(xù)處理驗證,方法將立即返回。假設(shè)后一個請求將被這種方法接收,其中返回的Authentication對象不為空。
UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子類)
public Authentication attemptAuthentication(HttpServletRequest request,            HttpServletResponse response) throws AuthenticationException {        if (postOnly && !request.getMethod().equals("POST")) {            throw new AuthenticationServiceException(                    "Authentication method not supported: " + request.getMethod());        }        String username = obtainUsername(request);        String password = obtainPassword(request);        if (username == null) {            username = "";        }        if (password == null) {            password = "";        }        username = username.trim();        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(                username, password);        // Allow subclasses to set the "details" property        setDetails(request, authRequest);        return this.getAuthenticationManager().authenticate(authRequest);    }    **attemptAuthentication () 方法將 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 對象**,用于  AuthenticationManager 的驗證(即  this.getAuthenticationManager().authenticate(authRequest) )。默認(rèn)情況下注入  Spring 容器的 AuthenticationManager 是 ProviderManager。
ProviderManager(AuthenticationManager的實現(xiàn)類)
public Authentication authenticate(Authentication authentication)    throws AuthenticationException {    Class<? extends Authentication> toTest = authentication.getClass();    AuthenticationException lastException = null;    Authentication result = null;    boolean debug = logger.isDebugEnabled();    for (AuthenticationProvider provider : getProviders()) {        if (!provider.supports(toTest)) {            continue;        }        if (debug) {            logger.debug("Authentication attempt using "                         + provider.getClass().getName());        }        try {            result = provider.authenticate(authentication);            if (result != null) {                copyDetails(authentication, result);                break;            }        }        catch (AccountStatusException e) {            prepareException(e, authentication);            // SEC-546: Avoid polling additional providers if auth failure is due to            // invalid account status            throw e;        }        catch (InternalAuthenticationServiceException e) {            prepareException(e, authentication);            throw e;        }        catch (AuthenticationException e) {            lastException = e;        }    }    if (result == null && parent != null) {        // Allow the parent to try.        try {            result = parent.authenticate(authentication);        }        catch (ProviderNotFoundException e) {            // ignore as we will throw below if no other exception occurred prior to            // calling parent and the parent            // may throw ProviderNotFound even though a provider in the child already            // handled the request        }        catch (AuthenticationException e) {            lastException = e;        }    }    if (result != null) {        if (eraseCredentialsAfterAuthentication            && (result instanceof CredentialsContainer)) {            // Authentication is complete. Remove credentials and other secret data            // from authentication            ((CredentialsContainer) result).eraseCredentials();        }        eventPublisher.publishAuthenticationSuccess(result);        return result;    }    // Parent was null, or didnt authenticate (or throw an exception).    if (lastException == null) {        lastException = new ProviderNotFoundException(messages.getMessage(            "ProviderManager.providerNotFound",            new Object[] { toTest.getName() },            "No AuthenticationProvider found for {0}"));    }    prepareException(lastException, authentication);    throw lastException;}    **嘗試驗證 Authentication 對象**。AuthenticationProvider  列表將被連續(xù)嘗試,直到 AuthenticationProvider 表示它能夠認(rèn)證傳遞的過來的Authentication  對象。然后將使用該 AuthenticationProvider 嘗試身份驗證。如果有多個 AuthenticationProvider  支持驗證傳遞過來的Authentication 對象,那么由第一個來確定結(jié)果,覆蓋早期支持AuthenticationProviders  所引發(fā)的任何可能的AuthenticationException。  成功驗證后,將不會嘗試后續(xù)的AuthenticationProvider。如果最后所有的 AuthenticationProviders  都沒有成功驗證 Authentication 對象,將拋出 AuthenticationException。從代碼中不難看出,由  provider 來驗證 authentication, 核心點方法是:Authentication result = provider.authenticate(authentication);此處的 provider 是 AbstractUserDetailsAuthenticationProvider,AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的實現(xiàn),看看它的 authenticate(authentication) 方法:
// 驗證 authenticationpublic Authentication authenticate(Authentication authentication)            throws AuthenticationException {        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,                messages.getMessage(                        "AbstractUserDetailsAuthenticationProvider.onlySupports",                        "Only UsernamePasswordAuthenticationToken is supported"));        // Determine username        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"                : authentication.getName();        boolean cacheWasUsed = true;        UserDetails user = this.userCache.getUserFromCache(username);        if (user == null) {            cacheWasUsed = false;            try {                user = retrieveUser(username,                        (UsernamePasswordAuthenticationToken) authentication);            }            catch (UsernameNotFoundException notFound) {                logger.debug("User " + username + " not found");                if (hideUserNotFoundExceptions) {                    throw new BadCredentialsException(messages.getMessage(                            "AbstractUserDetailsAuthenticationProvider.badCredentials",                            "Bad credentials"));                }                else {                    throw notFound;                }            }            Assert.notNull(user,                    "retrieveUser returned null - a violation of the interface contract");        }        try {            preAuthenticationChecks.check(user);            additionalAuthenticationChecks(user,                    (UsernamePasswordAuthenticationToken) authentication);        }        catch (AuthenticationException exception) {            if (cacheWasUsed) {                // There was a problem, so try again after checking                // were 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;            }        }        postAuthenticationChecks.check(user);        if (!cacheWasUsed) {            this.userCache.putUserInCache(user);        }        Object principalToReturn = user;        if (forcePrincipalAsString) {            principalToReturn = user.getUsername();        }        return createSuccessAuthentication(principalToReturn, authentication, user);    }AbstractUserDetailsAuthenticationProvider 內(nèi)置了緩存機(jī)制,從緩存中獲取不到的 UserDetails 信息的話,就調(diào)用如下方法獲取用戶信息,然后和 用戶傳來的信息進(jìn)行對比來判斷是否驗證成功。
// 獲取用戶信息UserDetails user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);retrieveUser() 方法在 DaoAuthenticationProvider 中實現(xiàn),DaoAuthenticationProvider 是 AbstractUserDetailsAuthenticationProvider的子類。具體實現(xiàn)如下:
protected final UserDetails retrieveUser(String username,            UsernamePasswordAuthenticationToken authentication)            throws AuthenticationException {        UserDetails loadedUser;        try {            loadedUser = this.getUserDetailsService().loadUserByUsername(username);        }        catch (UsernameNotFoundException notFound) {            if (authentication.getCredentials() != null) {                String presentedPassword = authentication.getCredentials().toString();                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,                        presentedPassword, null);            }            throw notFound;        }        catch (Exception repositoryProblem) {            throw new InternalAuthenticationServiceException(                    repositoryProblem.getMessage(), repositoryProblem);        }        if (loadedUser == null) {            throw new InternalAuthenticationServiceException(                    "UserDetailsService returned null, which is an interface contract violation");        }        return loadedUser;    }可以看到此處的返回對象 userDetails 是由 UserDetailsService 的 #loadUserByUsername(username) 來獲取的。
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
八、玩轉(zhuǎn)自定義登錄
1. form 登錄的流程
下面是 form 登錄的基本流程:
只要是 form 登錄基本都能轉(zhuǎn)化為上面的流程。接下來我們看看 Spring Security 是如何處理的。
3. Spring Security 中的登錄
默認(rèn)它提供了三種登錄方式:
formLogin() 普通表單登錄
oauth2Login() 基于 OAuth2.0 認(rèn)證/授權(quán)協(xié)議
openidLogin() 基于 OpenID 身份認(rèn)證規(guī)范
以上三種方式統(tǒng)統(tǒng)是 AbstractAuthenticationFilterConfigurer 實現(xiàn)的,
4. HttpSecurity 中的 form 表單登錄
啟用表單登錄通過兩種方式一種是通過 HttpSecurity 的 apply(C configurer) 方法自己構(gòu)造一個 AbstractAuthenticationFilterConfigurer 的實現(xiàn),這種是比較高級的玩法。 另一種是我們常見的使用 HttpSecurity 的 formLogin() 方法來自定義 FormLoginConfigurer 。我們先搞一下比較常規(guī)的第二種。
4.1 FormLoginConfigurer
該類是 form 表單登錄的配置類。它提供了一些我們常用的配置方法:
loginPage(String loginPage) : 登錄 頁面而并不是接口,對于前后分離模式需要我們進(jìn)行改造 默認(rèn)為 /login。
loginProcessingUrl(String loginProcessingUrl) 實際表單向后臺提交用戶信息的 Action,再由過濾器UsernamePasswordAuthenticationFilter 攔截處理,該 Action 其實不會處理任何邏輯。
usernameParameter(String usernameParameter) 用來自定義用戶參數(shù)名,默認(rèn) username 。
passwordParameter(String passwordParameter) 用來自定義用戶密碼名,默認(rèn) password
failureUrl(String authenticationFailureUrl) 登錄失敗后會重定向到此路徑, 一般前后分離不會使用它。
failureForwardUrl(String forwardUrl) 登錄失敗會轉(zhuǎn)發(fā)到此, 一般前后分離用到它。 可定義一個 Controller (控制器)來處理返回值,但是要注意 RequestMethod。
defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默認(rèn)登陸成功后跳轉(zhuǎn)到此 ,如果 alwaysUse 為 true 只要進(jìn)行認(rèn)證流程而且成功,會一直跳轉(zhuǎn)到此。一般推薦默認(rèn)值 false
successForwardUrl(String forwardUrl) 效果等同于上面 defaultSuccessUrl 的 alwaysUse 為 true 但是要注意 RequestMethod。
successHandler(AuthenticationSuccessHandler successHandler) 自定義認(rèn)證成功處理器,可替代上面所有的 success 方式
failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定義失敗成功處理器,可替代上面所有的 failure 方式
permitAll(boolean permitAll) form 表單登錄是否放開
知道了這些我們就能來搞個定制化的登錄了。
5. Spring Security 聚合登錄 實戰(zhàn)
接下來是我們最激動人心的實戰(zhàn)登錄操作。 有疑問的可認(rèn)真閱讀 Spring 實戰(zhàn) 的一系列預(yù)熱文章。
5.1 簡單需求
我們的接口訪問都要通過認(rèn)證,登陸錯誤后返回錯誤信息(json),成功后前臺可以獲取到對應(yīng)數(shù)據(jù)庫用戶信息(json)(實戰(zhàn)中記得脫敏)。
我們定義處理成功失敗的控制器:
@RestController @RequestMapping("/login") public class LoginController {     @Resource     private SysUserService sysUserService;      /**      * 登錄失敗返回 401 以及提示信息.      *      * @return the rest      */     @PostMapping("/failure")     public Rest loginFailure() {          return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登錄失敗了,老哥");     }      /**      * 登錄成功后拿到個人信息.      *      * @return the rest      */     @PostMapping("/success")     public Rest loginSuccess() {           // 登錄成功后用戶的認(rèn)證信息 UserDetails會存在 安全上下文寄存器 SecurityContextHolder 中         User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();         String username = principal.getUsername();         SysUser sysUser = sysUserService.queryByUsername(username);         // 脫敏         sysUser.setEncodePassword("[PROTECT]");         return RestBody.okData(sysUser,"登錄成功");     } }然后 我們自定義配置覆寫 void configure(HttpSecurity http) 方法進(jìn)行如下配置(這里需要禁用crsf):
@Configuration @ConditionalOnClass(WebSecurityConfigurerAdapter.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class CustomSpringBootWebSecurityConfiguration {      @Configuration     @Order(SecurityProperties.BASIC_AUTH_ORDER)     static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {         @Override         protected void configure(AuthenticationManagerBuilder auth) throws Exception {             super.configure(auth);         }          @Override         public void configure(WebSecurity web) throws Exception {             super.configure(web);         }          @Override         protected void configure(HttpSecurity http) throws Exception {             http.csrf().disable()                     .cors()                     .and()                     .authorizeRequests().anyRequest().authenticated()                     .and()                     .formLogin()                     .loginProcessingUrl("/process")                     .successForwardUrl("/login/success").                     failureForwardUrl("/login/failure");          }     } }使用 Postman 或者其它工具進(jìn)行 Post 方式的表單提交 http://localhost:8080/process?username=Felordcn&password=12345 會返回用戶信息:
{     "httpStatus": 200,     "data": {         "userId": 1,         "username": "Felordcn",         "encodePassword": "[PROTECT]",         "age": 18     },     "msg": "登錄成功",     "identifier": "" }把密碼修改為其它值再次請求認(rèn)證失敗后 :
{      "httpStatus": 401,      "data": null,      "msg": "登錄失敗了,老哥",      "identifier": "-9999"  }6. 多種登錄方式的簡單實現(xiàn)
就這么完了了么?現(xiàn)在登錄的花樣繁多。常規(guī)的就有短信、郵箱、掃碼 ,第三方是以后我要講的不在今天范圍之內(nèi)。 如何應(yīng)對想法多的產(chǎn)品經(jīng)理? 我們來搞一個可擴(kuò)展各種姿勢的登錄方式。我們在上面 2. form 登錄的流程 中的 用戶 和 判定 之間增加一個適配器來適配即可。 我們知道這個所謂的 判定就是 UsernamePasswordAuthenticationFilter 。
我們只需要保證 uri 為上面配置的/process 并且能夠通過 getParameter(String name) 獲取用戶名和密碼即可 。
我突然覺得可以模仿 DelegatingPasswordEncoder 的搞法, 維護(hù)一個注冊表執(zhí)行不同的處理策略。當(dāng)然我們要實現(xiàn)一個 GenericFilterBean 在 UsernamePasswordAuthenticationFilter 之前執(zhí)行。同時制定登錄的策略。
6.1 登錄方式定義
定義登錄方式枚舉 ``。
public enum LoginTypeEnum {        /**       * 原始登錄方式.       */      FORM,      /**       * Json 提交.       */      JSON,      /**       * 驗證碼.       */      CAPTCHA    }6.2 定義前置處理器接口
定義前置處理器接口用來處理接收的各種特色的登錄參數(shù) 并處理具體的邏輯。這個借口其實有點隨意 ,重要的是你要學(xué)會思路。我實現(xiàn)了一個 默認(rèn)的 form 表單登錄 和 通過RequestBody放入json` 的兩種方式,篇幅限制這里就不展示了。具體的 DEMO 參見底部。
public interface LoginPostProcessor {                /**        * 獲取 登錄類型        *        * @return the type        */       LoginTypeEnum getLoginTypeEnum();          /**        * 獲取用戶名        *        * @param request the request        * @return the string        */       String obtainUsername(ServletRequest request);          /**        * 獲取密碼        *        * @param request the request        * @return the string        */       String obtainPassword(ServletRequest request);      }6.3 實現(xiàn)登錄前置處理過濾器
該過濾器維護(hù)了 LoginPostProcessor 映射表。 通過前端來判定登錄方式進(jìn)行策略上的預(yù)處理,最終還是會交給 UsernamePasswordAuthenticationFilter 。通過 HttpSecurity 的 addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)方法進(jìn)行前置。
package cn.felord.spring.security.filter;  import cn.felord.spring.security.enumation.LoginTypeEnum; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.web.filter.GenericFilterBean;  import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Map;  import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY; import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;  /**  * 預(yù)登錄控制器  *  * @author Felordcn  * @since 16 :21 2019/10/17  */ public class PreLoginFilter extends GenericFilterBean {       private static final String LOGIN_TYPE_KEY = "login_type";       private RequestMatcher requiresAuthenticationRequestMatcher;     private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();       public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {         Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");         requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");         LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();         processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);          if (!CollectionUtils.isEmpty(loginPostProcessors)) {             loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));         }      }       private LoginTypeEnum getTypeFromReq(ServletRequest request) {         String parameter = request.getParameter(LOGIN_TYPE_KEY);          int i = Integer.parseInt(parameter);         LoginTypeEnum[] values = LoginTypeEnum.values();         return values[i];     }       /**      * 默認(rèn)還是Form .      *      * @return the login post processor      */     private LoginPostProcessor defaultLoginPostProcessor() {         return new LoginPostProcessor() {               @Override             public LoginTypeEnum getLoginTypeEnum() {                  return LoginTypeEnum.FORM;             }              @Override             public String obtainUsername(ServletRequest request) {                 return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);             }              @Override             public String obtainPassword(ServletRequest request) {                 return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);             }         };     }       @Override     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {         ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);         if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {              LoginTypeEnum typeFromReq = getTypeFromReq(request);              LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);               String username = loginPostProcessor.obtainUsername(request);              String password = loginPostProcessor.obtainPassword(request);               parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);             parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);          }          chain.doFilter(parameterRequestWrapper, response);       } }6.4 驗證
通過 POST 表單提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0 可以請求成功?;蛘咭韵铝蟹绞揭部梢蕴峤怀晒Γ?div style="height:15px;">
更多的方式 只需要實現(xiàn)接口 LoginPostProcessor 注入 PreLoginFilter
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
九 整合JWT做登錄認(rèn)證
JWT是JSON Web Token的縮寫,是目前最流行的跨域認(rèn)證解決方法。
互聯(lián)網(wǎng)服務(wù)認(rèn)證的一般流程是:
用戶向服務(wù)器發(fā)送賬號、密碼
服務(wù)器驗證通過后,將用戶的角色、登錄時間等信息保存到當(dāng)前會話中
同時,服務(wù)器向用戶返回一個session_id(一般保存在cookie里)
用戶再次發(fā)送請求時,把含有session_id的cookie發(fā)送給服務(wù)器
服務(wù)器收到session_id,查找session,提取用戶信息
上面的認(rèn)證模式,存在以下缺點:
cookie不允許跨域
因為每臺服務(wù)器都必須保存session對象,所以擴(kuò)展性不好
JWT認(rèn)證原理是:
用戶向服務(wù)器發(fā)送賬號、密碼
服務(wù)器驗證通過后,生成token令牌返回給客戶端(token可以包含用戶信息)
用戶再次請求時,把token放到請求頭Authorization里
服務(wù)器收到請求,驗證token合法后放行請求
JWT token令牌可以包含用戶身份、登錄時間等信息,這樣登錄狀態(tài)保持者由服務(wù)器端變?yōu)榭蛻舳耍?wù)器變成無狀態(tài)了;token放到請求頭,實現(xiàn)了跨域
JWT的組成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cJWT由三部分組成:
Header(頭部)
Payload(負(fù)載)
Signature(簽名)
表現(xiàn)形式為:Header.Payload.Signature
Header 部分是一個 JSON 對象,描述 JWT 的元數(shù)據(jù),通常是下面的樣子:
{  "alg": "HS256",  "typ": "JWT"}上面代碼中,alg屬性表示簽名的算法(algorithm),默認(rèn)是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的類型(type),JWT 令牌統(tǒng)一寫為JWT。
上面的 JSON 對象使用 Base64URL 算法轉(zhuǎn)成字符串
Payload
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的數(shù)據(jù)。JWT 規(guī)定了7個官方字段:
iss (issuer):簽發(fā)人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發(fā)時間
jti (JWT ID):編號
當(dāng)然,用戶也可以定義私有字段。
這個 JSON 對象也要使用 Base64URL 算法轉(zhuǎn)成字符串
Signature
Signature 部分是對前兩部分的簽名,防止數(shù)據(jù)篡改
簽名算法如下:
HMACSHA256(  base64UrlEncode(header) + "." +  base64UrlEncode(payload),  your-256-bit-secret)算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"."分隔
JWT認(rèn)證和授權(quán)
Security是基于AOP和Servlet過濾器的安全框架,為了實現(xiàn)JWT要重寫那些方法、自定義那些過濾器需要首先了解security自帶的過濾器。security默認(rèn)過濾器鏈如下:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
SecurityContextPersistenceFilter
這個過濾器有兩個作用:
用戶發(fā)送請求時,從session對象提取用戶信息,保存到SecurityContextHolder的securitycontext中
當(dāng)前請求響應(yīng)結(jié)束時,把SecurityContextHolder的securitycontext保存的用戶信息放到session,便于下次請求時共享數(shù)據(jù);同時將SecurityContextHolder的securitycontext清空
由于禁用session功能,所以該過濾器只剩一個作用即把SecurityContextHolder的securitycontext清空。舉例來說明為何要清空securitycontext:用戶1發(fā)送一個請求,由線程M處理,當(dāng)響應(yīng)完成線程M放回線程池;用戶2發(fā)送一個請求,本次請求同樣由線程M處理,由于securitycontext沒有清空,理應(yīng)儲存用戶2的信息但此時儲存的是用戶1的信息,造成用戶信息不符
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter繼承自AbstractAuthenticationProcessingFilter,處理邏輯在doFilter方法中:
當(dāng)請求被UsernamePasswordAuthenticationFilter攔截時,判斷請求路徑是否匹配登錄URL,若不匹配繼續(xù)執(zhí)行下個過濾器;否則,執(zhí)行步驟2
調(diào)用attemptAuthentication方法進(jìn)行認(rèn)證。UsernamePasswordAuthenticationFilter重寫了attemptAuthentication方法,負(fù)責(zé)讀取表單登錄參數(shù),委托AuthenticationManager進(jìn)行認(rèn)證,返回一個認(rèn)證過的token(null表示認(rèn)證失?。?div style="height:15px;">
判斷token是否為null,非null表示認(rèn)證成功,null表示認(rèn)證失敗
若認(rèn)證成功,調(diào)用successfulAuthentication。該方法把認(rèn)證過的token放入securitycontext供后續(xù)請求授權(quán),同時該方法預(yù)留一個擴(kuò)展點(AuthenticationSuccessHandler.onAuthenticationSuccess方法),進(jìn)行認(rèn)證成功后的處理
若認(rèn)證失敗,同樣可以擴(kuò)展uthenticationFailureHandler.onAuthenticationFailure進(jìn)行認(rèn)證失敗后的處理
只要當(dāng)前請求路徑匹配登錄URL,那么無論認(rèn)證成功還是失敗,當(dāng)前請求都會響應(yīng)完成,不再執(zhí)行過濾器鏈
UsernamePasswordAuthenticationFilter的attemptAuthentication方法,執(zhí)行邏輯如下:
從請求中獲取表單參數(shù)。因為使用HttpServletRequest.getParameter方法獲取參數(shù),它只能處理Content-Type為application/x-www-form-urlencoded或multipart/form-data的請求,若是application/json則無法獲取值
把步驟1獲取的賬號、密碼封裝成UsernamePasswordAuthenticationToken對象,創(chuàng)建未認(rèn)證的token。UsernamePasswordAuthenticationToken有兩個重載的構(gòu)造方法,其中public UsernamePasswordAuthenticationToken(Object principal, Object credentials)創(chuàng)建未經(jīng)認(rèn)證的token,public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)創(chuàng)建已認(rèn)證的token
獲取認(rèn)證管理器AuthenticationManager,其缺省實現(xiàn)為ProviderManager,調(diào)用其authenticate進(jìn)行認(rèn)證
ProviderManager的authenticate是個模板方法,它遍歷所有AuthenticationProvider,直至找到支持認(rèn)證某類型token的AuthenticationProvider,調(diào)用AuthenticationProvider.authenticate方法認(rèn)證,AuthenticationProvider.authenticate加載正確的賬號、密碼進(jìn)行比較驗證
AuthenticationManager.authenticate方法返回一個已認(rèn)證的token
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter負(fù)責(zé)創(chuàng)建匿名token:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {        if (SecurityContextHolder.getContext().getAuthentication() == null) {            SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));            if (this.logger.isTraceEnabled()) {                this.logger.trace(LogMessage.of(() -> {                    return "Set SecurityContextHolder to " + SecurityContextHolder.getContext().getAuthentication();                }));            } else {                this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");            }        } else if (this.logger.isTraceEnabled()) {            this.logger.trace(LogMessage.of(() -> {                return "Did not set SecurityContextHolder since already authenticated " + SecurityContextHolder.getContext().getAuthentication();            }));        }        chain.doFilter(req, res);    }如果當(dāng)前用戶沒有認(rèn)證,會創(chuàng)建一個匿名token,用戶是否能讀取資源交由FilterSecurityInterceptor過濾器委托給決策管理器判斷是否有權(quán)限讀取
實現(xiàn)思路
JWT認(rèn)證思路:
利用Security原生的表單認(rèn)證過濾器驗證用戶名、密碼
驗證通過后自定義AuthenticationSuccessHandler認(rèn)證成功處理器,由該處理器生成token令牌
JWT授權(quán)思路:
使用JWT目的是讓服務(wù)器變成無狀態(tài),不用session共享數(shù)據(jù),所以要禁用security的session功能(http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS))
token令牌數(shù)據(jù)結(jié)構(gòu)設(shè)計時,payload部分要儲存用戶名、角色信息
token令牌有兩個作用:
認(rèn)證, 用戶發(fā)送的token合法即代表認(rèn)證成功
授權(quán),令牌驗證成功后提取角色信息,構(gòu)造認(rèn)證過的token,將其放到securitycontext,具體權(quán)限判斷交給security框架處理
自己實現(xiàn)一個過濾器,攔截用戶請求,實現(xiàn)(3)中所說的功能
代碼實現(xiàn) 創(chuàng)建JWT工具類
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.12.0</version> </dependency>我們對java-jwt提供的API進(jìn)行封裝,便于創(chuàng)建、驗證、提取claim
@Slf4jpublic class JWTUtil {    // 攜帶token的請求頭名字    public final static String TOKEN_HEADER = "Authorization";    //token的前綴    public final static String TOKEN_PREFIX = "Bearer ";    // 默認(rèn)密鑰    public final static String DEFAULT_SECRET = "mySecret";    // 用戶身份    private final static String ROLES_CLAIM = "roles";    // token有效期,單位分鐘;    private final static long EXPIRE_TIME = 5 * 60 * 1000;    // 設(shè)置Remember-me功能后的token有效期    private final static long EXPIRE_TIME_REMEMBER = 7 * 24 * 60 * 60 * 1000;    // 創(chuàng)建token    public static String createToken(String username, List role, String secret, boolean rememberMe) {        Date expireDate = rememberMe ? new Date(System.currentTimeMillis() + EXPIRE_TIME_REMEMBER) : new Date(System.currentTimeMillis() + EXPIRE_TIME);        try {            // 創(chuàng)建簽名的算法實例            Algorithm algorithm = Algorithm.HMAC256(secret);            String token = JWT.create()                    .withExpiresAt(expireDate)                    .withClaim("username", username)                    .withClaim(ROLES_CLAIM, role)                    .sign(algorithm);            return token;        } catch (JWTCreationException jwtCreationException) {            log.warn("Token create failed");            return null;        }    }    // 驗證token    public static boolean verifyToken(String token, String secret) {        try{            Algorithm algorithm = Algorithm.HMAC256(secret);            // 構(gòu)建JWT驗證器,token合法同時pyload必須含有私有字段username且值一致            // token過期也會驗證失敗            JWTVerifier verifier = JWT.require(algorithm)                    .build();            // 驗證token            DecodedJWT decodedJWT = verifier.verify(token);            return true;        } catch (JWTVerificationException jwtVerificationException) {            log.warn("token驗證失敗");            return false;        }    }    // 獲取username    public static String getUsername(String token) {        try {            // 因此獲取載荷信息不需要密鑰            DecodedJWT jwt = JWT.decode(token);            return jwt.getClaim("username").asString();        } catch (JWTDecodeException jwtDecodeException) {            log.warn("提取用戶姓名時,token解碼失敗");            return null;        }    }    public static List<String> getRole(String token) {        try {            // 因此獲取載荷信息不需要密鑰            DecodedJWT jwt = JWT.decode(token);            // asList方法需要指定容器元素的類型            return jwt.getClaim(ROLES_CLAIM).asList(String.class);        } catch (JWTDecodeException jwtDecodeException) {            log.warn("提取身份時,token解碼失敗");            return null;        }    }}代碼實現(xiàn)認(rèn)證
驗證賬號、密碼交給UsernamePasswordAuthenticationFilter,不用修改代碼
認(rèn)證成功后,需要生成token返回給客戶端,我們通過擴(kuò)展AuthenticationSuccessHandler.onAuthenticationSuccess方法實現(xiàn)
@Componentpublic class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {    @Override    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {        ResponseData responseData = new ResponseData();        responseData.setCode("200");        responseData.setMessage("登錄成功!");         // 提取用戶名,準(zhǔn)備寫入token        String username = authentication.getName();        // 提取角色,轉(zhuǎn)為List<String>對象,寫入token        List<String> roles = new ArrayList<>();        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();        for (GrantedAuthority authority : authorities){            roles.add(authority.getAuthority());        }         // 創(chuàng)建token        String token = JWTUtil.createToken(username, roles, JWTUtil.DEFAULT_SECRET, true);        httpServletResponse.setCharacterEncoding("utf-8");        // 為了跨域,把token放到響應(yīng)頭WWW-Authenticate里        httpServletResponse.setHeader("WWW-Authenticate", JWTUtil.TOKEN_PREFIX + token); // 寫入響應(yīng)里        ObjectMapper mapper = new ObjectMapper();        mapper.writeValue(httpServletResponse.getWriter(), responseData);    }}為了統(tǒng)一返回值,我們封裝了一個ResponseData對象
代碼實現(xiàn) 授權(quán)
自定義一個過濾器JWTAuthorizationFilter,驗證token,token驗證成功后認(rèn)為認(rèn)證成功
@Slf4jpublic class JWTAuthorizationFilter extends OncePerRequestFilter {    @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {        String token = getTokenFromRequestHeader(request);        Authentication verifyResult = verefyToken(token, JWTUtil.DEFAULT_SECRET);        if (verifyResult == null) {            // 即便驗證失敗,也繼續(xù)調(diào)用過濾鏈,匿名過濾器生成匿名令牌            chain.doFilter(request, response);            return;        } else {            log.info("token令牌驗證成功");            SecurityContextHolder.getContext().setAuthentication(verifyResult);            chain.doFilter(request, response);        }    }     // 從請求頭獲取token    private String getTokenFromRequestHeader(HttpServletRequest request) {        String header = request.getHeader(JWTUtil.TOKEN_HEADER);        if (header == null || !header.startsWith(JWTUtil.TOKEN_PREFIX)) {            log.info("請求頭不含JWT token, 調(diào)用下個過濾器");            return null;        }        String token = header.split(" ")[1].trim();        return token;    }     // 驗證token,并生成認(rèn)證后的token    private UsernamePasswordAuthenticationToken verefyToken(String token, String secret) {        if (token == null) {            return null;        }         // 認(rèn)證失敗,返回null        if (!JWTUtil.verifyToken(token, secret)) {            return null;        }        // 提取用戶名        String username = JWTUtil.getUsername(token);        // 定義權(quán)限列表        List<GrantedAuthority> authorities = new ArrayList<>();        // 從token提取角色        List<String> roles = JWTUtil.getRole(token);        for (String role : roles) {            log.info("用戶身份是:" + role);            authorities.add(new SimpleGrantedAuthority(role));        }        // 構(gòu)建認(rèn)證過的token        return new UsernamePasswordAuthenticationToken(username, null, authorities);    }}OncePerRequestFilter`保證當(dāng)前請求中,此過濾器只被調(diào)用一次,執(zhí)行邏輯在`doFilterInternal代碼實現(xiàn) security配置
@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {    @Autowired    private AjaxAuthenticationEntryPoint ajaxAuthenticationEntryPoint;    @Autowired    private JWTAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;    @Autowired    private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }    protected void configure(HttpSecurity http) throws Exception {        http.csrf().disable()                .authorizeRequests().anyRequest().authenticated()                .and()                .formLogin()                .successHandler(jwtAuthenticationSuccessHandler)                .failureHandler(ajaxAuthenticationFailureHandler)                .permitAll()                .and()                .addFilterAfter(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)                .and()                .exceptionHandling().authenticationEntryPoint(ajaxAuthenticationEntryPoint);    }}配置里取消了session功能,把我們定義的過濾器添加到過濾鏈中;同時,定義ajaxAuthenticationEntryPoint處理未認(rèn)證用戶訪問未授權(quán)資源時拋出的異常
@Componentpublic class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {    @Override    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {        ResponseData responseData = new ResponseData();        responseData.setCode("401");        responseData.setMessage("匿名用戶,請先登錄再訪問!");        httpServletResponse.setCharacterEncoding("utf-8");        ObjectMapper mapper = new ObjectMapper();        mapper.writeValue(httpServletResponse.getWriter(), responseData);    }}過濾器鏈(filter chain)的介紹
上一節(jié)中,主要講了Spring Security認(rèn)證和授權(quán)的核心組件及核心方法。但是,什么時候調(diào)用這些方法呢?答案就是Filter和AOP。Spring Security在我們進(jìn)行用戶認(rèn)證以及授予權(quán)限的時候,通過各種各樣的攔截器來控制權(quán)限的訪問。
對于基于HttpRequest的方式對端點進(jìn)行保護(hù),我們使用一個Filter Chain來保護(hù);對于基于方法調(diào)用進(jìn)行保護(hù),我們使用AOP來保護(hù)。本篇重點講Spring Security中過濾器鏈的種類及過濾器中如何實現(xiàn)的認(rèn)證和授權(quán)。
Spring Security會默認(rèn)為我們添加15個過濾器,我們可以從WebSecurity(WebSecurity是Spring Security加載的一個重要對象,將在下節(jié)具體講述)的performBuild()方法中看到過濾器鏈SecurityFilterChain的構(gòu)建過程,并交由FilterChainProxy對象代理。我們從SecurityFilterChain的默認(rèn)實現(xiàn)類DefaultSecurityFilterChain中的log看出,Spring Security由以下過濾器組成了過濾器鏈:
Creating filter chain: any request, [  org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7f353a0f,  org.springframework.security.web.context.SecurityContextPersistenceFilter@4735d6e5,  org.springframework.security.web.header.HeaderWriterFilter@314a31b0,  org.springframework.security.web.csrf.CsrfFilter@4ef2ab73,  org.springframework.security.web.authentication.logout.LogoutFilter@57efc6fd,  org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@d88f893,  org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2cd388f5,  org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7ea2412c,  org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2091833,  org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4dad0eed,  org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@16132f21,  org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1c93b51e,  org.springframework.security.web.session.SessionManagementFilter@59edb4f5,  org.springframework.security.web.access.ExceptionTranslationFilter@104dc1a2,  org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1de0641b]下面就是各個過濾器的功能,
其中SecurityContextPersistenceFilter,UsernamePasswordAuthenticationFilter及FilterSecurityInterceptor分別對應(yīng)了SecurityContext,AuthenticationManager,AccessDecisionManager的處理。
[WebAsyncManagerIntegrationFilter] (異步方式)提供了對securityContext和WebAsyncManager的集成。
方式是通過SecurityContextCallableProcessingInterceptor的beforeConcurrentHandling(NativeWebRequest, Callable)方法來將SecurityContext設(shè)置到Callable上。
其實就是把SecurityContext設(shè)置到異步線程中,使其也能獲取到用戶上下文認(rèn)證信息。
[SecurityContextPersistenceFilter] (同步方式)在請求之前從SecurityContextRepository(默認(rèn)實現(xiàn)是HttpSessionSecurityContextRepository)獲取信息并填充SecurityContextHolder(如果沒有,則創(chuàng)建一個新的ThreadLocal的SecurityContext),并在請求完成并清空SecurityContextHolder并更新SecurityContextRepository。
在Spring Security中,雖然安全上下文信息被存儲于Session中,但實際的Filter中不應(yīng)直接操作Session(過濾器一般負(fù)責(zé)核心的處理流程,而具體的業(yè)務(wù)實現(xiàn),通常交給其中聚合的其他實體類),而是用如HttpSessionSecurityContextRepository中l(wèi)oadContext(),saveContext()來存取session。
[HeaderWriterFilter] 用來給http響應(yīng)添加一些Header,比如X-Frame-Options,X-XSS-Protection*,X-Content-Type-Options。
[CsrfFilter] 默認(rèn)開啟,用于防止csrf攻擊的過濾器
[LogoutFilter] 處理注銷的過濾器
[UsernamePasswordAuthenticationFilter] 表單提交了username和password,被封裝成UsernamePasswordAuthenticationToken對象進(jìn)行一系列的認(rèn)證,便是主要通過這個過濾器完成的,即調(diào)用AuthenticationManager.authenticate()。在表單認(rèn)證的方法中,這是最最關(guān)鍵的過濾器。具體過程是:
(1)調(diào)用AbstractAuthenticationProcessingFilter.doFilter()方法執(zhí)行過濾器
(2)調(diào)用UsernamePasswordAuthenticationFilter.attemptAuthentication()方法
(3)調(diào)用AuthenticationManager.authenticate()方法(實際上委托給AuthenticationProvider的實現(xiàn)類來處理)
[DefaultLoginPageGeneratingFilter] & [DefaultLogoutPageGeneratingFilter] 如果沒有配置/login及l(fā)ogin page, 系統(tǒng)則會自動配置這兩個Filter。
[BasicAuthenticationFilter] Processes a HTTP requests BASIC authorization headers, putting the result into the SecurityContextHolder.
[RequestCacheAwareFilter] 內(nèi)部維護(hù)了一個RequestCache,用于緩存request請求
[SecurityContextHolderAwareRequestFilter] 此過濾器對ServletRequest進(jìn)行了一次包裝,使得request具有更加豐富的API(populates the ServletRequest with a request wrapper which implements servlet API security methods)
[AnonymousAuthenticationFilter] 匿名身份過濾器,spring security為了兼容未登錄的訪問,也走了一套認(rèn)證流程,只不過是一個匿名的身份。它位于身份認(rèn)證過濾器(e.g. UsernamePasswordAuthenticationFilter)之后,意味著只有在上述身份過濾器執(zhí)行完畢后,SecurityContext依舊沒有用戶信息,AnonymousAuthenticationFilter該過濾器才會有意義。
[SessionManagementFilter] 和session相關(guān)的過濾器,內(nèi)部維護(hù)了一個SessionAuthenticationStrategy來執(zhí)行任何與session相關(guān)的活動,比如session-fixation protection mechanisms or checking for multiple concurrent logins。
[ExceptionTranslationFilter] 異常轉(zhuǎn)換過濾器,這個過濾器本身不處理異常,而是將認(rèn)證過程中出現(xiàn)的異常(AccessDeniedException and AuthenticationException)交給內(nèi)部維護(hù)的一些類去處理。它
位于整個springSecurityFilterChain的后方,用來轉(zhuǎn)換整個鏈路中出現(xiàn)的異常,將其轉(zhuǎn)化,顧名思義,轉(zhuǎn)化以意味本身并不處理。一般其只處理兩大類異常:AccessDeniedException訪問異常和AuthenticationException認(rèn)證異常。
它將Java中的異常和HTTP的響應(yīng)連接在了一起,這樣在處理異常時,我們不用考慮密碼錯誤該跳到什么頁面,賬號鎖定該如何,只需要關(guān)注自己的業(yè)務(wù)邏輯,拋出相應(yīng)的異常便可。如果該過濾器檢測到AuthenticationException,則將會交給內(nèi)部的AuthenticationEntryPoint去處理,如果檢測到AccessDeniedException,需要先判斷當(dāng)前用戶是不是匿名用戶,如果是匿名訪問,則和前面一樣運行AuthenticationEntryPoint,否則會委托給AccessDeniedHandler去處理,而AccessDeniedHandler的默認(rèn)實現(xiàn),是AccessDeniedHandlerImpl。
[FilterSecurityInterceptor] 這個過濾器決定了訪問特定路徑應(yīng)該具備的權(quán)限,這些受限的資源訪需要什么權(quán)限或角色,這些判斷和處理都是由該類進(jìn)行的。
(1)調(diào)用FilterSecurityInterceptor.invoke()方法執(zhí)行過濾器
(2)調(diào)用AbstractSecurityInterceptor.beforeInvocation()方法
(3)調(diào)用AccessDecisionManager.decide()方法決策判斷是否有該權(quán)限
說明:本文會以pdf格式持續(xù)更新,更多最新尼恩3高pdf筆記,請從下面的鏈接獲?。?a target="_blank" >語雀 或者 碼云
參考
JSON Web Token 入門教程
Spring Security-5-認(rèn)證流程梳理
Spring Security3源碼分析(5)-SecurityContextPersistenceFilter分析
Spring Security addFilter() 順序問題
前后端聯(lián)調(diào)之Form Data與Request Payload,你真的了解嗎?
Spring Boot 2 + Spring Security 5 + JWT 的單頁應(yīng)用 Restful 解決方案
SpringBoot實戰(zhàn)派-第十章源碼
https://www.cnblogs.com/cjsblog/p/9184173.html
https://www.cnblogs.com/storml/p/10937486.html
分類: Java web , springboot , java基礎(chǔ)
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Spring Boot 鑒權(quán)之——JWT鑒權(quán)
Spring Security(2):過濾器鏈(filter chain)的介紹
設(shè)計一個可擴(kuò)展的用戶登錄系統(tǒng)
spring-security 登陸認(rèn)證之初次探究
實現(xiàn)自定義的 AuthenticationProvider
Spring Security筆記:Remember Me(下次自動登錄)
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服