編輯歷史:
2021/01/06 15:13 :第三點“單體springboot項目怎么進行多認證方式”,增加一張原理說明圖。
正文:
一個正常的系統應用,都會要求安全性,不能讓人隨便亂搞。我們在開發web應用時,怎么保護我們的資源,這是十分重要的。
在以前jsp / servlet 時代,我們可能會直接在每個servlet上都加上用戶身份驗證,也會有系統是通過Filter來驗證用戶身份。
現在web應用中,主要有兩套安全框架:shiro 和 spring security。
功能上兩者都差不多,shiro有的功能spring security都有,而且spring security也還有一些額外的功能,例如對Oahtu的支持
上圖是網上的一些對比,如果是做單體項目,shiro足以,如果是分布式項目,推薦spring security 和 oauth2.0
下面我將會從四個方面講解spring security的使用:
1:單體springboot項目怎么用?
2:spring security 的認證過程!
3:單體springboot項目怎么進行多認證方式?
4:分布式項目怎么用spring security?
1:單體springboot項目怎么用?
1、引依賴(紅色是重點)
<dependencies>
<!--security的starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--security 的測試包-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--spring mvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--熱部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!--lombok小辣椒-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--測試包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
其實我們只要一引入spring security的依賴,整個項目的web請求都會被spring security攔截。
我們寫個啟動類,再寫個web接口,啟動項目
@SpringBootApplication @RestController public class SpringSecurityApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityApplication.class, args); } @RequestMapping("/say/{name}") public String echo(@PathVariable String name){ return "這里是控制器:" + name; } }
訪問 http://localhost:9999/say/123 時就會被重定向到一個登陸頁面
這個時候你看一下控制台,就會發現多了一串這玩意
這是spring security 自動幫你創建的一個隨機密碼,用戶名是user,登陸進去后就能正常訪問我們的接口了。
spring security不止提供了一個默認的登陸頁面,也提供了一個登出接口:http://localhost:9999/logout
有些版本會直接登出,有些新版本會有下面這個頁面。
當然我們的正常項目肯定是有自己的用戶模塊,不可能用它自帶的東西。下面將講怎樣接入我們自己的用戶模塊。
首先假設我們有一個用戶實體類
@Getter @Setter @AllArgsConstructor @ToString public class UserDO{ private Integer id; private String userName; private String password; private String realName; private List<String> roles; private List<String> permissions; }
然后呢我們有一個用戶業務類,專門去數據庫查詢用戶的。這里我們不連數據庫,我懶,不想弄,直接用一個Map充當數據庫
@Service public class UserServiceImpl { // 正經的業務代碼 public UserDO getUserByUserName(String userName){ if(userList == null){ return null; } UserDO userDO = userList.get(userName); return userDO; } // 用來模擬數據庫的一個Map private static Map<String,UserDO> userList; public UserServiceImpl(){ initUserList(); } /** * 模擬數據庫用戶 * */ private void initUserList(){ if (userList == null){ userList = new HashMap<>(3); userList.put("zhangsan",new UserDO(1,"zhangsan",password_123,"張三" , Arrays.asList("admin","role1"),Arrays.asList("p1","p2","p3"))); userList.put("lisi",new UserDO(2,"lisi",password_123,"李四" , Arrays.asList("role1"),Arrays.asList("p1","p2"))); userList.put("wangwu",new UserDO(2,"wangwu",password_123,"王五" , Arrays.asList("role2"),Arrays.asList("p3","p2"))); } } private static final String password_123 = "$2a$10$a0iYBZkmfqJnhd0g5ck9L.kfcf9RpdHFJ.mt5wf2sN2qzA6y9k/BC"; }
接下來就是重點了。
建一個類繼承 WebSecurityConfigurerAdapter ,進行spring security的配置
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; /** * 這個是spring security 的web安全配置類,這個類必須有 * @author hongcheng */ @Configuration @EnableWebSecurity // 啟動web的安全控制,這個注解好象不用也行 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; /** * 配置認證管理器 * */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置 userDetailsService ,這是用來查詢用戶信息的 auth.userDetailsService(userDetailsService);
// 設置擦除密碼,如果設置成false,那么在spring security中流轉的用戶認證信息都會帶有密碼,一般我們都會擦除 auth.eraseCredentials(true); super.configure(auth); } /** * 配置url安全認證攔截的 * */ @Override protected void configure(HttpSecurity http) throws Exception { /* * csrf().disable() 關閉跨域請求限制,如果開啟,可能會出現很多非get請求出現405 * .authorizeRequests() 啟動請求認證 * .antMatchers("/**").authenticated() 匹配指定的url地址進行認證判斷 * .anyRequest().permitAll() 對其他地址進行放行 * formLogin() 啟動表單登錄 * .loginPage("/login.html") 可以自定義登錄頁面 * .failureUrl("/login_fail.html") 表單登錄失敗的跳轉地址 * .defaultSuccessUrl("/login_s.html",true) 表單登錄成功的跳轉地址, * 參數2如果為false,登錄成功時會跳轉到攔截前的頁面,true時登錄成功固定跳轉給定的頁面 * .logout() 啟動用戶退出,security提供了默認退出地址:/logout * .logoutSuccessUrl("/logout.html") 成功推出后的跳轉地址 * */ http.csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .failureUrl("/login_fail.html") .defaultSuccessUrl("/login_s.html",true) .permitAll() // permitAll表示登錄相關的請求都放開,一定要加,不然你連登錄頁面都看不到 .and() .logout() .logoutSuccessUrl("/logout.html")
permitAll(); // 這里也要加,不然你退出后就看不到退出頁面了 } /** * 這個是配置密碼編碼器的<br/> * NoOpPasswordEncoder表示不進行密碼編碼 * */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 這個是我自己寫來加密密碼的 * */ public static void main(String[] args) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); System.err.println(passwordEncoder.encode("123")); } }
這是spring security的配置文件,大部分你們都能看懂,我說說這個url攔截配置
這兩個是一起用的,表示對哪些具體的url進行攔截,也可以模糊匹配
.authorizeRequests()
.anyRequest().authenticated()
這個and用於分割不同的攔截策略,例如上面的配置就用了兩個and方法,因為他有三組攔截策略,一組是針對所有url的,一組是針對登錄表單的,一組是針對登出的 .and()
這個是登錄的,spring security默認提供了一個登錄接口 http://localhost:9999/login 你也可以自己看登錄頁面的源碼,至於這個接口的實現在哪里,后面會詳細講,這里我們只需要直到默認有這個接口就行 .formLogin() .failureUrl("/login_fail.html") .defaultSuccessUrl("/login_s.html",true) .permitAll()


這是登出的,也有提供默認接口 http://localhost:9999/logout
.logout() .logoutSuccessUrl("/logout.html")
.permitAll();
配置完了后還有一步很關鍵,就是要告訴spring security去哪里獲取你的用戶信息
我們自己建一個類,繼承 UserDetailsService
import com.hongcheng.springsecurity.entity.UserDO; import com.hongcheng.springsecurity.service.user.UserServiceImpl; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; /** * 這個是配置用戶詳情查詢的,用於查詢用戶,給security用的 * @author hongcheng */ @Component public class MyUserDetailsServiceImpl implements UserDetailsService {
// 這個是我們自己的業務類,用來查數據庫 @Resource private UserServiceImpl userService; /** * 這個方法是一定要重寫的,spring security會根據頁面傳回來的用戶名去查這個用戶的權限和密碼 * */ @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { UserDO userDO = userService.getUserByUserName(userName); if(userDO == null){ throw new UsernameNotFoundException("賬號不存在"); } Set<String> authritiesSet = new HashSet<>(userDO.getPermissions().size() + userDO.getRoles().size()); authritiesSet.addAll(userDO.getPermissions()); // 這里需要注意,security里面角色和權限都是存在一個字段里面的,但是其角色會自動加上ROLE_前綴來將角色和權限進行區分, // 以便在進行判斷是否有某角色時可以進行判斷,但是我們一般使用是可以不用加前綴的,如@PreAuthorize("hasRole('role2')"),但是你加也沒問題 authritiesSet.addAll(userDO.getRoles().stream().map(role -> "ROLE_" + role).collect(Collectors.toList())); UserDetails userDetails = User.withUsername(userDO.getUserName()) .password(userDO.getPassword()) .authorities(authritiesSet.toArray(new String[authritiesSet.size()])) .build(); return userDetails; } }
UserDetailsService 是spring security提供的一個自定義查詢用戶的接口,和shiro的 Realm 是相同作用的
另外spring security封裝了一些認證異常,並不是說你想拋啥就拋啥,當然,你隨便拋也行,程序直接異常而已。
用spring security提供的異常,spring security就會幫你捕獲,並提示頁面。都是 AuthenticationException 的子類
完了之后你就可以啟動項目了,然后自己去測試。
這時你啟動項目,你會發現沒有隨機密碼了。用自己設置的賬號和密碼登錄,測試下不同情況吧。
spring security不僅可以將針對web請求進行攔截,還可以對具體方法進行攔截
web攔截:
web url的攔截是要在security的配置類中寫的,而且一定要注意,攔截規則是從上往下的,如果前面的攔截規則包含的url返回比下面的還要大,例如:
.antMatchers("/r/**").hasAuthority("p1")
.antMatchers("/r/r1").hasAuthority("p2")
這種情況下,/r/r1 攔截規則是必須有p1權限
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 開啟請求認證 .antMatchers("/r/r1").hasAuthority("p1") // /r/r1 地址的請求需要有p1權限 .antMatchers("/r/r2").hasAnyRole("p2") // /r/r2 地址的請求需要有p2角色 .antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')") // /r/r3 地址的請求需要同時有p1和p2這兩個權限
.antMatchers("/r/**").authenticated() // /r/** /r開頭的其他請求都需要認證 .anyRequest().permitAll() // 剩余沒有說明的地址全部開放,不用認證 .and() .formLogin() // ... }
保護URL常用的方法有:
authenticated() 保護URL,需要用戶登錄
permitAll() 指定URL無需保護,一般應用與靜態資源文件
hasRole(String role) 限制單個角色訪問,角色將被增加 “ROLE_” .所以”ADMIN” 將和 “ROLE_ADMIN”進行比較.
hasAuthority(String authority) 限制單個權限訪問
hasAnyRole(String… roles)允許多個角色訪問.
hasAnyAuthority(String… authorities) 允許多個權限訪問.
access(String attribute) 該方法使用 SpEL表達式, 所以可以創建復雜的限制.
hasIpAddress(String ipaddressExpression) 限制IP地址或子網
方法攔截:
這種方式主要是往類或者方法上面加注解來限制對該方法的訪問
Spring Security在方法的控制權限上支持三種類型的注解,JSR-250注解、@Secured注解和支持表達式的注解
JSR-250注解包括:@RolesAllowed、@PermitAll、@DenyAll
@RolesAllowed:表示訪問對應方法時所應具有的角色 ,例如:@RolesAllowed({“User”,“ADMIN”}) 該方法只要具有"User","Admin"任意一種權限就可以訪問。這里可以省略前綴ROLE_,實際的權限可能是ROLE_ADMIN,也可能是ADMIN
@PermitAll表示允許所有的角色進行訪問,也就是說不進行權限的控制
@DenyAll和@PermitAll相反的,表示無論什么角色都不能訪問
@Secured注解:這個注解和@RolesAllowed的作用是一樣的,不過@Secured不能省略前綴ROLE_,必須@Secured({“ROLE_User”,“ROLE_ADMIN”})
支持表達式的注解:@PreAuthorize、@PostAuthorize
@PreAuthorize:這是進入方法前進行權限判斷,常用
@PostAuthorize:這是方法執行后進行權限判斷,不常用
值可以是spring el表達式,一般我們都是用org.springframework.security.access.expression.SecurityExpressionRoot這個類里面的方法進行權限判斷,例如:
@RequestMapping("/say/{name}") @PreAuthorize("hasAnyAuthority('p1')") public String echo(@PathVariable String name){ return "這里是控制器:" + name; }
需要注意的還有一點:spring security默認不啟用方法注解權限判斷,我們必須手動加上。找個加了 @Configuration 注解的類,給他加上下面這個注解,你要用什么注解,你就設置他為true就行
@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true) // 啟動基於方法注解的控制
到目前為止,一個單體springboot項目使用spring security就算是完了。
接下來我們總計一下spring security的單體項目使用流程:
- 引入依賴 spring-boot-starter-security
- 編寫一個類繼承 WebSecurityConfigurerAdapter
- 配置spring security,包括認證配置器、url攔截配置、密碼匹配器
- 編寫一個類繼承 UserDetailsService ,以實現用戶信息的查找
- 給響應的方法加權限注解(可選)
自己看着寫個小例子玩玩,后面我們會講一下spring security的整體執行流程。
2:spring security 的認證過程!
源碼原理:
要了解他的執行流程,我們就需要調試代碼。首先我們知道UserDetailsService 這個的子類,就是我們自己實現的那個類,是用來獲取我們自己的用戶信息的,那么登錄時肯定會執行到這里,所以我們打個斷點,啟動項目,登錄。
進入斷點后我們一路看調用棧,因為我們的目標是看spring security的調用,所以我們一路往下找,找到最早被調用的security相關的地方
這里我們發現 org.springframework.security.web.FilterChainProxy#doFilter 這里是最早調用的security的方法。我們看下代碼
是不是和我們以前學的servlet時的Filter的寫法是一樣的。那我們看下這個類的情況
這里是不是就可以看出了,spring security 的 FilterChainProxy 這個類,實際上就是Filter的實現類,所以你是不是明白了,spring security的實現其實就是基於Filter來實現的。
好了,我們繼續往下追蹤,進入 doFilterInternal 這個方法看下
在 doFilterInternal 我們看到有意思的地方有三處。我們一一分析。
我們發現第一次有意思的地方只是在驗證請求的url是否正常,所以我們不管他先。看第二個地方
這里主要是遍歷這個過濾器鏈,判斷他和request是否匹配,究竟是匹配什么呢?我們進入match方法里面看看
繼續往下看,我們會發現有很多實現類,和security相關的也比較多,那我們就先不管他,繼續看第三個地方。
我們先看這一句: FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
發現VirtualFilterChain 是一個內部類,我們看下這個內部類,看他的屬性
originalChain :看參數名和實參,不難發現這就是實際的Filter過濾器鏈
additionalFilters : 這是一個額外的過濾器鏈,我們找下傳入的實參是什么,發現這就是我們第二步match匹配返回的一個過濾器鏈 List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
現在回想一下,是不是大概明白了點:一個spring security寫的 servlet.Filter 的過濾器,里面有個 SecurityFilterChain 類型的集合,在執行 doFilter 方法時悄悄 把List<SecurityFilterChain > 和 request 進行某種匹配
然后自己拿真實的 servlet.Filter 的過濾器鏈,和 security自己的SecurityFilterChain 鏈組合,形成一個新的虛擬過濾器鏈 VirtualFilterChain。
好,現在我們繼續回來,接下來就不是執行servlet.Filter 的過濾器鏈了,而是剛創建的虛擬過濾器鏈 VirtualFilterChain
我們進去這個方法里面,仔細讀下代碼,你會發現它分成兩部分,上面部分執行servlet.Filter原始過濾器鏈的,下面執行security的額外過濾器鏈的。
當然我們通過他打印的日志也能發現,這里是先執行額外鏈,后執行原始鏈的。
reached end of additional filter chain; proceeding with original chain
至於為什么是先執行額外鏈 ,后執行原始鏈呢?相比研究過 servlet 的 的Filter的人都知道,在執行Filter鏈時,每個Filter都是請求進來通過一次,返回響應通過一次,如果請求進來的時候你不處理這個額外鏈,以后就沒機會了。但是如果你先執行額外鏈,
由於 nextFilter.doFilter(request, response, this); 所以接下來執行的都是 VirtualFilterChain 里面的額外鏈,執行完了后還能繼續重新回到原始的 servlet.Filter 鏈
我們看下 VirtualFilterChain 里面都有啥
我們接着根據線程調用棧往下找security相關的,發現他在一個個執行 additionalFilters 中的Filter 。之前不是說過security 提供了一個登錄接口 /login 和一個登出接口 /logout 么,這里看 LogoutFilter 覺得是不是巧合,進去里面看一下代碼。
看下紅框標出的地方,熟不熟悉
聯想一下前面提到的 RequestMatcher 和 需要根據servlet 和 Filter進行某系匹配,匹配某個東西。現在是不是就明白了,那同樣的道理,能不能找到 /login 在哪里。
繼續往下看,找到 UsernamePasswordAuthenticationFilter,你就找到了 /login 了,接下來我們將會重點分析這里
仔細找你會發現 UsernamePasswordAuthenticationFilter 並沒有 doFilter 方法,我們去他父類 AbstractAuthenticationProcessingFilter 上找
這個方法里會去捕獲一些認證異常,如果我們胡亂拋出,就不能被是為認證失敗了,所以我們要遵守規則
好了,我們看一下關鍵語句:
authResult = attemptAuthentication(request, response);
我們點擊去看看具體實現,發現他又跳到了 UsernamePasswordAuthenticationFilter 這里來
關鍵的地方就這兩個點,1是創建了個Token,2是調用 AuthenticationManager 的 authenticate方法 ,去對傳入的賬號嗎和密碼進行認證
我們先看下 AuthenticationManager 是個啥玩意
看看他有啥具體實現類先:
行吧,沒啥好玩的,那 authenticate方法我們也不知道看哪個實現類,跳過,繼續看調用棧。
繼續往下看調用棧,就發現他走的是 org.springframework.security.authentication.ProviderManager#authenticate 這個方法,那我們進去看看代碼
代碼太長了,我就截關鍵點
這里我們可以看到 他在遍歷,遍歷啥呢? AuthenticationProvider 這是啥,不知道,但是看英文意思是認證提供者,先猜測會不會是用來認證的。
然后看下面那行, provider.supports(toTest) 在判斷是否支持,支持啥,我們前面看到的 UsernamePasswordAuthenticationToken ,如果支持就執行 result = provider.authenticate(authentication);
那綜合來講,這段代碼的意思是不是說“遍歷所有 AuthenticationProvider 認證提供者,判斷其是否支持 UsernamePasswordAuthenticationToken ,如果支持就執行 AuthenticationProvider 認證提供者的 authenticate 認證方法”。
我們繼續看看 ProviderManager 里面有多少個提供者。
好,繼續走線程調用棧
我們發現此時執行的是 DaoAuthenticationProvider 的父類的 authenticate 方法,我們看下這個方法具體代碼
這個方法里面有四個地方需要重點關注:
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
postAuthenticationChecks.check(user);
我們先看第一個,發現其在父類中是個抽象方法,具體實現在子類
你找下他返回的 UserDetailService 就會發現這其實就是我們自己自定義的UserDetailService
結合前面的,其實就是先根據Token去緩存中找一下,如果找不到,就通過我們自己的定義的 UserDetailService 實現類,去數據庫里面查,並組裝返回一個UserDetail對象。
那接下來我們看下
preAuthenticationChecks.check(user);
發現這兩個先后檢查都是內部類,主要檢查用戶是否被鎖定,是否過期超時,是否被禁用,密碼憑證是否過期超時。
最后看下 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
現在整個方法的內容基本都看完了,整理下 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate 做的事
判斷傳入的認證對象是不是UsernamePasswordAuthenticationToken
嘗試去緩存中獲取UserDetail
緩存中獲取不到,就通過自定義UserDetailService去查數據庫
前認證檢查,檢查用戶是否被鎖,是否過期超時,是否被禁用
額外認證檢查,檢查數據庫密碼和輸入密碼是否匹配
后認證檢查,檢查密碼憑證是否過期超時
重新生成一個認證成功對象
spring security的整個認證流程算是完了,我們來總結一下
spring security的認證流程 1:基於servlet.filter的過濾器 org.springframework.security.web.FilterChainProxy#doFilter 2:內部自己的過濾器鏈,會根據當前請求的url,將匹配的認證過濾器篩選保留 org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter 3:spring security自己的抽象過濾器 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter 4:spring security默認的一個賬號密碼認證過濾器,每個認證過濾器都會綁定一個登錄處理url和提交方式(post) org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication 5:由第4步組裝一個token對象,傳入第5步。ProviderManager會根據token的Class類型來判斷使用哪個Provider org.springframework.security.authentication.ProviderManager#authenticate 6:由具體的AuthenticationProvider來負責認證用戶信息 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate 7:UserDetailsService在AuthenticationProvider中獲取用戶信息 org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername
(這里我偷黑馬一張圖)
總結下出現過的重要的類和接口
1:FilterChainProxy 基於servlet.filter的過濾器,用於生成spring security自己內部的過濾器鏈 2:FilterChainProxy.VirtualFilterChain spring security的虛擬過濾器鏈,用於進行額外的過濾器鏈處理 3:AbstractAuthenticationProcessingFilter spring security額外過濾器鏈的父類,可以指定該過濾器處理哪些請求(url和請求method),同時在這里根據請求參數組裝一個AbstractAuthenticationToken的實現類,並傳給后續操作 4:UsernamePasswordAuthenticationFilter spring security提供的默認用戶名密碼過濾器鏈,處理POST方式的/login請求,構建UsernamePasswordAuthenticationToken類 5:ProviderManager 用於管理AuthenticationProvider列表,根據傳入的AbstractAuthenticationToken的實現類的不同,遍歷AuthenticationProvider列表選擇合適的AuthenticationProvider進行認證 6:AbstractUserDetailsAuthenticationProvider AuthenticationProvider的父類,提供了基本的認證過程代碼 7:DaoAuthenticationProvider spring security提供的默認用戶名密碼認證器,處理UsernamePasswordAuthenticationToken類型的認證 8:UserDetailsService spring security提供的用於自定義用戶查詢方式的接口 9:AbstractAuthenticationToken spring security的認證Token的基類,所有Token都必須繼承這個基類 10:UsernamePasswordAuthenticationToken spring security提供的默認用戶名密碼認證Token
3:單體springboot項目怎么進行多認證方式?
根據前面的說明,我們可以知道spring security提供了一個默認的用戶名密碼認證方式,這個用戶名密碼認證方式是怎么工作的呢?
1:UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken spring security提供的默認用戶名密碼認證Token 2:UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter spring security提供的默認用戶名密碼過濾器鏈,處理POST方式的/login請求,構建UsernamePasswordAuthenticationToken類 3:DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider spring security提供的默認用戶名密碼認證器,處理UsernamePasswordAuthenticationToken類型的認證 4:MyUserDetailsService extends UserDetailsService 自定義的用戶查詢類
據我們所了解的 ,AbstractAuthenticationProcessingFilter 和 AbstractUserDetailsAuthenticationProvider的實現類有多個,那我們能不能也自己實現一個,然后放進去呢?
模擬手機短信驗證碼登錄,我們先實現 AbstractAuthenticationProcessingFilter ,一些具體的代碼我們先抄 UsernamePasswordAuthenticationFilter 的先,后期跟進需要進行修改
/** * @author hongcheng */ public class SmsAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone"; public static final String SPRING_SECURITY_FORM_SMSCODE_KEY = "smsCode"; private String phoneParameter = SPRING_SECURITY_FORM_PHONE_KEY; private String smsCodeParameter = SPRING_SECURITY_FORM_SMSCODE_KEY; private boolean postOnly = true; public SmsAuthenticationProcessingFilter() { super(new AntPathRequestMatcher("/sms/login", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (postOnly && !"POST".equals(request.getMethod())) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String phone = request.getParameter(this.phoneParameter); String smsCode = request.getParameter(this.smsCodeParameter); if (phone == null) { phone = ""; } if (smsCode == null) { smsCode = ""; } phone = phone.trim(); SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, smsCode); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } }
期間發現需要用到Token的實現類,我們在實現一個 AbstractAuthenticationToken,基本的代碼也是抄 UsernamePasswordAuthenticationToken 的進行修改
/** * @author hongcheng */ public class SmsAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = -6437322217156360297L; private final Object phone; private Object smsCode; public SmsAuthenticationToken(Object phone, Object smsCode) { super(null); this.phone = phone; this.smsCode = smsCode; setAuthenticated(false); } public SmsAuthenticationToken(Object phone, Object smsCode, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.phone = phone; this.smsCode = smsCode; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.smsCode; } @Override public Object getPrincipal() { return this.phone; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); }else { super.setAuthenticated(false); } } @Override public void eraseCredentials() { super.eraseCredentials(); smsCode = null; } }
接下來我們實現 AbstractUserDetailsAuthenticationProvider,也是抄 DaoAuthenticationProvider
/** * @author hongcheng */ public class SmsAuthenticationProvider implements AuthenticationProvider { private PasswordEncoder passwordEncoder; private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { try{ String phone = (String) authentication.getPrincipal(); String smsCode = (String)authentication.getCredentials(); WebAuthenticationDetails details = (WebAuthenticationDetails)authentication.getDetails(); // 判斷手機號是否存在 UserDetails userDetails = userDetailsService.loadUserByUsername(phone); // 判斷驗證碼是否一致 HttpSession httpSession = ServletUtil.getHttpSession(); // 已經提前將手機驗證碼存在了session中,key為手機號 Object smsCodeObj = httpSession.getAttribute(phone); if(smsCodeObj == null){ throw new BadCredentialsException("手機驗證碼錯誤"); } String smsCodeInSession = (String)smsCodeObj.toString(); if(!StringUtils.hasText(smsCodeInSession) || !smsCodeInSession.equalsIgnoreCase(smsCode)){ throw new BadCredentialsException("手機驗證碼錯誤"); } httpSession.removeAttribute(phone); // 構建返回的用戶登錄成功的token return new SmsAuthenticationToken(userDetails.getUsername(), smsCode, userDetails.getAuthorities()); }catch (Exception e){ if(e instanceof AuthenticationException){ throw e; }else{ throw new AuthenticationServiceException("認證服務異常"); } } } @Override public boolean supports(Class<?> authentication) { /** * providerManager會遍歷所有 * security config中注冊的provider集合 * 根據此方法返回true或false來決定由哪個provider * 去校驗請求過來的authentication */ return (SmsAuthenticationToken.class .isAssignableFrom(authentication)); } public PasswordEncoder getPasswordEncoder() { return passwordEncoder; } public void setPasswordEncoder(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
最后我們再額外實現一個 UserDetailsService,根據手機號進行查詢用戶信息
/** * @author hongcheng */ @Service("SmsUserDetailsService") public class SmsUserDetailsServiceImpl implements UserDetailsService { @Resource private UserServiceImpl userService; @Override public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException { UserDO userDO = userService.getUserByPhone(phone); if(userDO == null){ throw new UsernameNotFoundException("手機號不存在"); } Set<String> authritiesSet = new HashSet<>(userDO.getPermissions().size() + userDO.getRoles().size()); authritiesSet.addAll(userDO.getPermissions()); // 這里需要注意,security里面角色和權限都是存在一個字段里面的,但是其角色會自動加上ROLE_前綴來將角色和權限進行區分, // 以便在進行判斷是否有某角色時可以進行判斷,但是我們一般使用是可以不用加前綴的,如@PreAuthorize("hasRole('role2')"),但是你加也沒問題 authritiesSet.addAll(userDO.getRoles().stream().map(role -> "ROLE_" + role).collect(Collectors.toList())); UserDetails userDetails = User.withUsername(userDO.getPhone()) .password(userDO.getPhone()) .authorities(authritiesSet.toArray(new String[authritiesSet.size()])) .build(); return userDetails; } }
/** * @author hongcheng */ @Service public class UserServiceImpl { public UserDO getUserByUserName(String userName){ if(userNameList == null){ return null; } UserDO userDO = userNameList.get(userName); return userDO; } public UserDO getUserByPhone(String phone){ if(userPhoneList == null){ return null; } UserDO userDO = userPhoneList.get(phone); return userDO; } private static Map<String,UserDO> userNameList; private static Map<String,UserDO> userPhoneList; public UserServiceImpl(){ initUserList(); } /** * 模擬數據庫用戶 * */ private void initUserList(){ if (userNameList == null){ userNameList = new HashMap<>(3); userNameList.put("zhangsan",new UserDO(1,"zhangsan",password_123,"張三" ,"10086", Arrays.asList("admin","role1"),Arrays.asList("p1","p2","p3"))); userNameList.put("lisi",new UserDO(2,"lisi",password_123,"李四" ,"10010", Arrays.asList("role1"),Arrays.asList("p1","p2"))); userNameList.put("wangwu",new UserDO(2,"wangwu",password_123,"王五" ,"10000", Arrays.asList("role2"),Arrays.asList("p3","p2"))); userPhoneList = new HashMap<>(3); userPhoneList.put("10086",new UserDO(1,"zhangsan",password_123,"張三" ,"10086", Arrays.asList("admin","role1"),Arrays.asList("p1","p2","p3"))); userPhoneList.put("10010",new UserDO(2,"lisi",password_123,"李四" ,"10010", Arrays.asList("role1"),Arrays.asList("p1","p2"))); userPhoneList.put("10000",new UserDO(2,"wangwu",password_123,"王五" ,"10000", Arrays.asList("role2"),Arrays.asList("p3","p2"))); } } private static final String password_123 = "$2a$10$a0iYBZkmfqJnhd0g5ck9L.kfcf9RpdHFJ.mt5wf2sN2qzA6y9k/BC"; }
/** * 模擬資源 * @author hongcheng */ @RestController @RequestMapping("/test") public class TestController { /** * @PreAuthorize("hasAnyAuthority('p1')") * 具體可以使用哪些方法,自己看 MethodSecurityExpressionRoot 這個類 * */ @PreAuthorize("hasAnyAuthority('p1')") @RequestMapping("/r1") public String test1(){ return "這里是資源1,只能p1權限訪問"; } @PreAuthorize("hasAnyAuthority('p2')") @RequestMapping("/r2") public String test2(){ return "這里是資源2,只能p2權限訪問"; } @PreAuthorize("hasAnyAuthority('p3')") @RequestMapping("/r3") public String test3(){ return "這里是資源3,只能p3權限訪問"; } @PreAuthorize("hasRole('admin')") @RequestMapping("/r4") public String test4(){ return "這里是資源4,只能admin角色訪問"; } @PreAuthorize("hasRole('ROLE_role1')") @RequestMapping("/r5") public String test5(){ return "這里是資源5,只能role1角色訪問"; } @PreAuthorize("hasRole('role2')") @RequestMapping("/r6") public String test6(){ return "這里是資源6,只能role2角色訪問"; } @PreAuthorize("isAuthenticated()") @RequestMapping("/r7") public String test7(){ return "這里是資源7,只要認證了就能訪問,認證可以是登錄,也可以是RememberMe。"; } @PreAuthorize("hasAnyAuthority('p3','p1')") @RequestMapping("/r8") public String test8(){ return "這里是資源8,只要有p1或者p3權限就訪問"; } @PreAuthorize("hasAnyAuthority('p2') and hasRole('role2')") @RequestMapping("/r9") public String test9(){ return "這里是資源9,只有同時擁有role2角色和p2權限才能訪問"; } /* 返回結果 {"authorities":[{"authority":"ROLE_admin"},{"authority":"ROLE_role1"},{"authority":"p1"},{"authority":"p2"},{"authority":"p3"}],"details":{"remoteAddress":"0:0:0:0:0:0:0:1",
"sessionId":"4ADC66B3D98E239C9B10AC04AD43BDE0"},"authenticated":true,"principal":{"password":null,"username":"zhangsan","authorities":[{"authority":"ROLE_admin"},{"authority":"ROLE_role1"},
{"authority":"p1"},{"authority":"p2"},{"authority":"p3"}],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true},"credentials":null,"name":"zhangsan"} * */ @PreAuthorize("isAuthenticated()") @RequestMapping("/user") public Object getLoginUser(){ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); return authentication; } @RequestMapping("/sms/code/{phone}") public Object getSmsCode(@PathVariable("phone") String phone){ Random r = new Random(); int i = r.nextInt(10); ServletUtil.getHttpSession().setAttribute(phone,i); return i; } }
好了,該實現了的都實現了,但是怎么放進去呢?目前我們有一個配置spring security的類,能不能在那里面設置?答案當然是可以的
/** * 這個是spring security 的web安全配置類,這個類必須有 * @author hongcheng */ @Configuration @EnableWebSecurity // 啟動web的安全控制,這個注解好象不用也行 @EnableGlobalMethodSecurity(prePostEnabled = true) // 啟動基於方法注解的控制 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource(name="MyUserDetailsService") private UserDetailsService MyUserDetailsService; @Resource(name="SmsUserDetailsService") private UserDetailsService SmsUserDetailsService; @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; /** * 這個必須重寫,才能使用AuthenticationManager,在成員變量注入進來,再注入過濾器中 * */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { // ******************************************************************** // 這里必須自己創建ProviderManager,並且把兩個provider傳進去,否則會一直創建新的ProviderManager // 然后不斷在自己和parentAuthenticationManager之間調用,最后棧溢出。 // 網上很多博客都是說在configure(AuthenticationManagerBuilder auth)方法中設置,我實際上測試在這個方法中設置是沒用的 ProviderManager authenticationManager = new ProviderManager(Arrays.asList( smsAuthenticationProvider(),daoAuthenticationProvider() )); return authenticationManager; } /** * 認證失敗的處理器 * */ @Bean public AuthenticationFailureHandler getFailureHandler(){ return new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { /** * 認證失敗的處理器 * 默認情況下,認證成功/失敗,都是重定向到一個頁面上的。 * 如果想要認證成功/失敗后,返回信息體,而不是重定向,就必須用AuthenticationHandler * 另外,絕對不要用httpServletResponse.getWriter(),一旦你用了Writer,那么setContentType("text/plain;charset=UTF-8")設置的 * 編碼utf-8就永遠不會被設置進去。具體原因自己看org.apache.catalina.connector.Response#setContentType(java.lang.String)376行 * */ httpServletResponse.getOutputStream().write(e.getMessage().getBytes("UTF-8")); httpServletResponse.setContentType("text/plain;charset=UTF-8"); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.flushBuffer(); } }; } /** * 認證成功的處理器,和上面一樣 * */ @Bean public AuthenticationSuccessHandler getSuccessHandler(){ return new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.getOutputStream().write("登錄成功".getBytes("UTF-8")); httpServletResponse.setContentType("text/plain;charset=UTF-8"); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.flushBuffer(); } }; } /** * 下面就是自定義的過濾器,配置一下攔截地址、認證成功失敗處理器、authenticationManager * 如果還有其他認證過濾器,則再這樣寫一個 * 自定義登錄過濾器 * @Author * @return */ @Bean public SmsAuthenticationProcessingFilter SmsAuthenticationProcessingFilter() throws Exception { SmsAuthenticationProcessingFilter filter = new SmsAuthenticationProcessingFilter(); /** * 自己額外添加的過濾器鏈必須在這里手動加上兩個處理器,不然他會胡亂重定向的。 * */ filter.setAuthenticationSuccessHandler(getSuccessHandler()); filter.setAuthenticationFailureHandler(getFailureHandler()); filter.setAuthenticationManager(authenticationManager); return filter; } /** * 自定義的認證器,這是用於提供認證服務的 * */ @Bean public SmsAuthenticationProvider smsAuthenticationProvider() { SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); smsAuthenticationProvider.setPasswordEncoder(noOpPasswordEncoder()); smsAuthenticationProvider.setUserDetailsService(SmsUserDetailsService); return smsAuthenticationProvider; } /** * DaoAuthenticationProvider是給UsernamePasswordAuthenticationFilter認證用的 * */ @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(MyUserDetailsService); daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder()); return daoAuthenticationProvider; } /** * 配置認證管理器 * */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 很多博客都是在這里加上這句來添加Provider的,如果你只在這里加了,百分百執行時的 // ProviderManager里面只有一個匿名的Provider。 // 記住,我們要自己手動創建一個ProviderManager對象,並且創建時就加上這倆個Provider // auth.authenticationProvider(daoAuthenticationProvider()); // auth.authenticationProvider(smsAuthenticationProvider()); super.configure(auth); } /** * 配置url安全認證攔截的 * */ @Override protected void configure(HttpSecurity http) throws Exception { /* * csrf().disable() 關閉跨域請求限制,如果開啟,可能會出現很多非get請求出現405 * .authorizeRequests() 啟動請求認證 * .antMatchers("/**").authenticated() 匹配指定的url地址進行認證判斷 * .anyRequest().permitAll() 對其他地址進行放行 * formLogin() 啟動表單登錄 * .loginPage("/login.html") 可以自定義登錄頁面 * .failureUrl("/login_fail.html") 表單登錄失敗的跳轉地址 * .defaultSuccessUrl("/login_s.html",true) 表單登錄成功的跳轉地址, * 參數2如果為false,登錄成功時會跳轉到攔截前的頁面,true時登錄成功固定跳轉給定的頁面 * .logout() 啟動用戶退出,security提供了默認退出地址:/logout * .logoutSuccessUrl("/logout.html") 成功推出后的跳轉地址 * */ http.cors(); http.csrf().disable() .authorizeRequests() .antMatchers("/sms/login","/test/sms/code/**","/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin() // .failureUrl("/login_fail.html") // .defaultSuccessUrl("/login_s.html",true) // .loginPage("/login.html") // .loginProcessingUrl("/login") .successHandler(getSuccessHandler()) // 認證成功時的處理器,返回自定義信息,而不是跳轉登錄頁 .failureHandler(getFailureHandler()) // 認證成功時的處理器,返回自定義信息,而不是跳轉登錄頁 .permitAll() .and() .logout() .logoutSuccessUrl("/logout.html") .and() .exceptionHandling() .authenticationEntryPoint(AjaxAuthenticationEntryPoint()) // 未登錄時的處理器,返回自定義信息,而不是跳轉登錄頁面 .accessDeniedHandler( accessDeniedHandler()); // 訪問拒絕時處理器,返回自定義信息 /** * 這里如果這定了頁面,Handler就無效,如果是不使用spring security自帶的登錄頁面,認證時需要始終返回json,就不要設置 * .failureUrl("/login_fail.html") * .defaultSuccessUrl("/login_s.html",true) * .loginPage("/login.html") * .loginProcessingUrl("/login") * */ http.addFilterBefore(SmsAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); } /** * 一般訪問資源時,如果沒有權限,就會返回內置異常信息,如需要返回自定義信息,需要重寫AccessDeniedHandler * 並需要在void configure(HttpSecurity http)方法中加 * .and() * .exceptionHandling() * .accessDeniedHandler( accessDeniedHandler()); // 訪問拒絕時處理器,返回自定義信息 * */ @Bean public AccessDeniedHandler accessDeniedHandler(){ return new AccessDeniedHandler(){ @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { HashMap<String, Object> responseBody = new HashMap<>(4); responseBody.put("status","403"); responseBody.put("msg",e.getMessage()); responseBody.put("data",null); responseBody.put("list",null); httpServletResponse.setStatus(401); httpServletResponse.getWriter().write(responseBody.toString()); } }; } /** * 一般訪問資源時,如果沒有登錄認證,就會跳轉到登錄頁,重寫AuthenticationEntryPoint返回自定義信息 * 需要在void configure(HttpSecurity http)方法中加 * .and() * .exceptionHandling() * .authenticationEntryPoint(AjaxAuthenticationEntryPoint()) // 未登錄時的處理器,返回自定義信息,而不是跳轉登錄頁面 * */ @Bean public AuthenticationEntryPoint AjaxAuthenticationEntryPoint(){ return new AuthenticationEntryPoint(){ @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { HashMap<String, Object> responseBody = new HashMap<>(4); responseBody.put("status","401"); responseBody.put("msg","Need Authorities!"); responseBody.put("data",null); responseBody.put("list",null); httpServletResponse.setStatus(401); httpServletResponse.getWriter().write(responseBody.toString()); } }; } /** * 這個是配置密碼編碼器的<br/> * BCryptPasswordEncoder表示使用BCrypt算法 * */ @Bean("bCryptPasswordEncoder") public PasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 這個是配置密碼編碼器的<br/> * NoOpPasswordEncoder表示不進行密碼編碼 * */ @Bean("noOpPasswordEncoder") public PasswordEncoder noOpPasswordEncoder() { return NoOpPasswordEncoder.getInstance(); } public static void main(String[] args) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); System.err.println(passwordEncoder.encode("123")); } }
接下來我們就可以進行測試了。
首先訪問我們隨便訪問一個資源 http://localhost:9999/test/r6
然后嘗試手機號驗證碼登錄,訪問 http://localhost:9999/test/sms/code/10086 獲取驗證碼
POST訪問 http://localhost:9999/sms/login 進行登錄,參數phone=10086,smsCode=上一步獲取到的驗證碼
訪問資源 http://localhost:9999/test/r1 和 http://localhost:9999/test/r6
訪問 http://localhost:9999/logout 進行登出
嘗試使用用戶名密碼進行登錄 http://localhost:9999/login ,參數username=zhangsan,password=123
到目前為止,多認證方式實現完畢
總結:
1:實現自定義SmsAuthenticationToken,繼承 AbstractAuthenticationToken 2:實現自定義SmsAuthenticationProcessingFilter,繼承 AbstractAuthenticationProcessingFilter 指定過濾器處理的URL地址,也就是指定一個認證地址,構建一個Token,傳給后續操作 3:實現自定義SmsAuthenticationProvider,繼承 AbstractUserDetailsAuthenticationProvider 指定如何處理Token,支持處理何種Token類型,如何認證用戶登錄是否合法 4:實現自定義SmsUserDetailsServiceImpl,實現 UserDetailsServiceImpl接口 自定如何根據傳入的參數去查詢用戶信息 5:重寫public AuthenticationManager authenticationManagerBean() 手動構建一個ProviderManager,將用到的AuthenticationProvider全部丟進去 6:創建一個SmsAuthenticationProcessingFilter的bean 設置其AuthenticationManager為我們自己構建的對象 7:創建一個SmsAuthenticationProvider的bean 設置其UserDetailsService、PasswordEncoder 8:創建一個DaoAuthenticationProvider的bean 設置其UserDetailsService、PasswordEncoder,因為我們自己手動構建了ProviderManager, 為了避免后續security沒有自動加入默認的DaoAuthenticationProvider,我們自己手動加入 9:修改protected void configure(HttpSecurity http) 開放新加的認證需要使用的url, 增加過濾器 http.addFilterBefore(SmsAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
4:分布式項目怎么用spring security?
待更新