spring security 安全框架


編輯歷史:

  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的單體項目使用流程:

  1.   引入依賴 spring-boot-starter-security 
  2.        編寫一個類繼承 WebSecurityConfigurerAdapter
  3.        配置spring security,包括認證配置器、url攔截配置、密碼匹配器
  4.        編寫一個類繼承 UserDetailsService ,以實現用戶信息的查找
  5.        給響應的方法加權限注解(可選)

 

 

自己看着寫個小例子玩玩,后面我們會講一下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?

待更新

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM