項目集成Spring Security


前言

之前寫的 塗塗影院管理系統 這個 demo 是基於 shiro 來鑒權的,項目前后端分離后,顯然集成 Spring Security 更加方便一些,畢竟,都用 Spring 了,權限管理當然 Spring Security.

花了半天時間整理的筆記,希望能對你有所幫助。

Spring Security 一句話概述:一組 filter 過濾器鏈組成的權限認證。

一、加入依賴

環境:項目采用 Spring Initializr 快速構建 Spring Boot ,版本交由 spring-boot-starter-parent 管理。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

在僅僅添加完依賴的情況下,啟動項目看看:

1.1 控制台打印

控制台打印了一串密碼,如下圖所示:

訪問一下項目中的某個方法:

http://localhost:7777/tmax/videoCategory/getAll

奇怪,怎么自己跳到 /login 路徑下了,而且還讓登陸?

1.2 賬號登錄

在登陸 from 表單里輸入如下:

  • 用戶名:user
  • 密碼:0839a4ba-c8a3-4aee-8a6e-cd19c1d0b0c1(控制台打印的)

點擊 Sign in 然后跳轉到了目標地址:

添加 Spring Security 依賴后,實際觸發了兩件事,一時將系統中所有的連接服務都保護起來, 再就是會有默認配置 form 表單認證。

二、基本原理

Spring Security的整個工作流程如下所示:

綠色認證方式可以配置, 橘黃色和藍色的位置不可更改。

Security 有兩種認證方式:

  • httpbasic
  • formLogin 默認的,如上邊那種方式

同樣,Security 也提供兩種過濾器類:

  • UsernamePasswordAuthenticationFilter 表示表單登陸過濾器
  • BasicAuthenticationFilter 表示 httpbaic 方式登陸過濾器

圖中橙色的 FilterSecurityInterceptor 是最終的過濾器,它會決定當前的請求可不可以訪問Controller,判斷規則放在這個里面。

當不通過時會把異常拋給在這個過濾器的前面的 ExceptionTranslationFilter 過濾器。

ExceptionTranslationFilter 接收到異常信息時,將跳轉頁面引導用戶進行認證,如上方所示的用戶登陸界面。

三、自定義認證邏輯

實際開發中是不可能使用上方 Spring Security 默認的這種方式的,如何去覆蓋掉 Spring Security 默認的配置呢?

我們以:將默認的 form 認證方式改為 httpbasic 方式為例。

創建SpringSecurity自定義配置類:WebSecurityConfig.java

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();

        registry.and()
            表單登錄方式
            .formLogin()
            .permitAll()
            .and()
            .logout()
            .permitAll()
            .and()
            .authorizeRequests()
            任何請求
            .anyRequest()
            需要身份認證
            .authenticated()
            .and()
            關閉跨站請求防護
            .csrf().disable()
            前后端分離采用JWT 不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }
}

重新啟動項目,已經看到修改后的 httpbasic 方式認證了。

在這里我們依然采用的默認提供的用戶名 user,以及每次服務器啟動自動生成的 password,那么可不可以自定義認證邏輯呢?比如采用數據庫中的用戶登陸?

答案是肯定的。

自定義用戶認證邏輯需要了解三步:
  1. 處理用戶信息獲取邏輯
  2. 處理用戶校驗邏輯
  3. 處理密碼加密解密

接下來我們來看一下這三步,然后實現自定義登陸:

3.1 處理用戶信息獲取邏輯

Spring Security 中用戶信息獲取邏輯的獲取邏輯是封裝在一個接口里的:UserDetailService,代碼如下:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

這個接口中只有一個方法,loadUserByUsername(), 該接收一個 String 類型的 username 參數,然后返回一個 UserDetails 的對象。

那么這個方法到底是干啥的呢?

通過前台用戶輸入的用戶名,然后去數據庫存儲中獲取對應的用戶信息,然后封裝在 UserDetail 實現類里面。

封裝到 UserDetail 實現類返回以后,Spring Srcurity 會拿着用戶信息去做校驗,如果校驗通過了,就會把用戶放在 session 里面,否則,拋出 UsernameNotFoundException 異常,Spring Security 捕獲后做出相應的提示信息。

想要處理用戶信息獲取邏輯,那么我們就需要自己去實現 UserDetailsService

新建 UserDetailsServiceImpl.java

@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService{

    @Autowired
    private UserService userService;

    /**
     * 從數據庫中獲取用戶信息,返回一個 UserDetails 對象,
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        通過用戶名獲取用戶
        User user = userService.findByUsername(username);
        將 user 對象轉化為 UserDetails 對象
        return new SecurityUserDetails(user);
    }
}

SecurityUserDetail.java

public class SecurityUserDetails extends User implements UserDetails {

    private static final long serialVersionUID = 1L;

    public SecurityUserDetails(User user) {

        if(user!=null) {
            this.setUsername(user.getUsername());
            this.setPassword(user.getPassword());
            this.setStatus(user.getStatus());
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        理想型返回 admin 權限,可自已處理這塊
        return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
    }

    /**
     * 賬戶是否過期
     * @return
     */

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 是否禁用
     * @return
     */

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 密碼是否過期
     * @return
     */

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否啟用
     * @return
     */

    @Override
    public boolean isEnabled() {
        return true;
    }
}

至此,處理用戶信息獲取邏輯 部分完成了,主要實現 UserDetailsService 接口的 loadUserByname 方法。

為何會用到 SecurityUserDetail 類進行轉換一下?

其實完全可以直接返回一個 User 對象,但是需要注意的是,如果直接返回 User 對象的話,返回的是 security 包下的 user。

至於為何這樣處理,如果返回的是 security 包下的 user,這樣就失去了使用本地數據庫的意義,下方自定義登陸邏輯詳細說明。

再來登陸試一下:

其中 niceyoo、 為數據庫用戶信息,如下圖為成功跳轉:

3.2 處理用戶校驗邏輯

關於用戶的校驗邏輯主要包含兩方面:

  1. 密碼是否匹配【由Sprin Security處理,只需要告訴其密碼即可】
  2. 密碼是否過期、或者賬戶是否被凍結等

前者,已經通過實現 UserDetailsService 的 loadUserByname() 方法實現了,接下來主要看看后者。

用戶密碼是否過期、是否被凍結等等需要實現 UserDetails 接口:

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();授權列表;

    String getPassword();從數據庫中查詢到的密碼;

    String getUsername();用戶輸入的用戶名;

    boolean isAccountNonExpired();當前賬戶是否過期;

    boolean isAccountNonLocked();賬戶是否被鎖定;

    boolean isCredentialsNonExpired();賬戶的認證時間是否過期;

    boolean isEnabled();是賬戶是否有效。
}

主要看后四個方法:

1、isAccountNonExpired() 賬戶沒有過期 返回true 表示沒有過期
2、isAccountNonLocked() 賬戶沒有鎖定
3、isCredentialsNonExpired() 密碼是否過期
4、isEnabled() 是否被刪除

如上四個方法,皆可根據實際情況做響應處理。

3.3 處理密碼加密解密

再回到 WebSecurityConfig 自定義配置類。加入:

@Autowired
private UserDetailsServiceImpl userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());//加密
}

配置了這個 configure 方法以后,從前端傳遞過來的密碼就會被加密,所以從數據庫查詢到的密碼必須是經過加密的,而這個過程都是在用戶注冊的時候進行加密的。

補充:UserDetailsServiceImpl 為自定義的 UserDetailsService 實現類。

四、個性化認證流程

同樣的在實際的開發中,對於用戶的登錄認證,不可能使用 Spring Security 自帶的方式或者頁面,需要自己定制適用於項目的登錄流程。

Spring Security 支持用戶在配置文件中配置自己的登錄頁面,如果用戶配置了,則采用用戶自己的頁面,否則采用模塊內置的登錄頁面。

WebSecurityConfig 配置類中增加 成功、失敗過濾器。

@Autowired
private AuthenticationSuccessHandler successHandler;

@Autowired
private AuthenticationFailHandler failHandler;

@Override
protected void configure(HttpSecurity http) throws Exception 
{

    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
            .authorizeRequests();

    registry.and()
        表單登錄方式
        .formLogin()
        .permitAll()
        成功處理類
        .successHandler(successHandler)
        失敗
        .failureHandler(failHandler)
        .and()
        .logout()
        .permitAll()
        .and()
        .authorizeRequests()
        任何請求
        .anyRequest()
        需要身份認證
        .authenticated()
        .and()
        關閉跨站請求防護
        .csrf().disable()
        前后端分離采用JWT 不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

在添加 AuthenticationSuccessHandler、AuthenticationFailHandler 后會幫我們自動導包,但是,既然是個性化認證流程,自然要我們自己去實現~

那我們究竟要實現什么效果呢?

自定義登陸成功處理:

自定義登陸失敗處理:

為何要采用這種返回新式?

用戶登錄成功后,Spring Security 的默認處理方式是跳轉到原來的鏈接上,這也是企業級開發的常見方式,但是有時候采用的是 Ajax 方式發送的請求,往往需要返回 Json 數據,如圖中:登陸成功后,會把 token 返回給前台,失敗時則返回失敗信息。

AuthenticationSuccessHandler:

Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        String username = ((UserDetails)authentication.getPrincipal()).getUsername();
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails)authentication.getPrincipal()).getAuthorities();
        List<String> list = new ArrayList<>();
        for(GrantedAuthority g : authorities){
            list.add(g.getAuthority());
        }
        登陸成功生成token
        String  token = UUID.randomUUID().toString().replace("-""");
    token 需要保存至服務器一份,實現方式:redis or jwt
        輸出到瀏覽器
        ResponseUtil.out(response, ResponseUtil.resultMap(true,200,"登錄成功", token));
    }
}

SavedRequestAwareAuthenticationSuccessHandle r是 Spring Security 默認的成功處理器,默認方式是跳轉。這里將認證信息作為 Json 數據進行了返回,也可以返回其他數據,這個是根據業務需求來定的,比如,上方代碼在用戶登陸成功后返回來 token,需要注意的是,此 token 需要在服務器備份一份,畢竟要用做下次的身份認證嘛~

AuthenticationFailHandler:

@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {

        ## 默認情況下,不管你是用戶名不存在,密碼錯誤,SS 都會報出 Bad credentials 異常信息
        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"用戶名或密碼錯誤"));
        } else if (e instanceof DisabledException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"賬戶被禁用,請聯系管理員"));
        } else {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"登錄失敗,其他內部錯誤"));
        }
    }

}

失敗處理器跟成功處理此雷同。

ResponseUtil:

@Slf4j
public class ResponseUtil {

    /**
     *  使用response輸出JSON
     * @param response
     * @param resultMap
     */

    public static void out(HttpServletResponse response, Map<String, Object> resultMap){

        ServletOutputStream out = null;
        try {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=UTF-8");
            out = response.getOutputStream();
            out.write(new Gson().toJson(resultMap).getBytes());
        } catch (Exception e) {
            log.error(e + "輸出JSON出錯");
        } finally{
            if(out!=null){
                try {
                    out.flush();
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

其中用到 gson 依賴:

<!-- Gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>

最后

下一篇將集成 jwt 實現用戶身份認證。

SpringSecurity 整合 JWT

 


免責聲明!

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



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