項目集成Spring Security

前言

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

花了半天時間整理的筆記,但願能對你有所幫助。前端

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

1、加入依賴

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

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

在僅僅添加完依賴的狀況下,啓動項目看看:spring

1.1 控制檯打印

控制檯打印了一串密碼,以下圖所示:數據庫

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

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

奇怪,怎麼本身跳到 /login 路徑下了,並且還讓登錄?後端

1.2 帳號登陸

在登錄 from 表單裏輸入以下:瀏覽器

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

點擊 Sign in 而後跳轉到了目標地址:ruby

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

2、基本原理

Spring Security的整個工做流程以下所示:

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

Security 有兩種認證方式:

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

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

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

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

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

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

3、自定義認證邏輯

實際開發中是不可能使用上方 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();是帳戶是否有效。
}

主要看後四個方法:

一、isAccountNonExpired() 帳戶沒有過時 返回true 表示沒有過時
二、isAccountNonLocked() 帳戶沒有鎖定
三、isCredentialsNonExpired() 密碼是否過時
四、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 實現類。

4、個性化認證流程

一樣的在實際的開發中,對於用戶的登陸認證,不可能使用 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

相關文章
相關標籤/搜索