Spring Security 自定義用戶認證

Spring Boot 集成 Spring Security 這篇文章中,咱們介紹瞭如何在 Spring Boot 項目中快速集成 Spring Security,同時也介紹瞭如何更改系統默認生成的用戶名和密碼。接下來本文將基於 Spring Boot 集成 Spring Security 這篇文章中所建立的項目,進一步介紹在 Spring Security 中如何實現自定義用戶認證。html

閱讀更多關於 Angular、TypeScript、Node.js/Java 、Spring 等技術文章,歡迎訪問個人我的博客 —— 全棧修仙之路

1、自定義認證過程

本項目所使用的開發環境及主要框架版本:前端

  • java version "1.8.0_144"
  • spring boot 2.2.0.RELEASE
  • spring security 5.2.0.RELEASE

1.0 配置項目 pom.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.semlinker</groupId>
    <artifactId>custom-user-authentication</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>custom-user-authentication</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
      
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 省略spring-boot-starter-test、spring-security-test及spring-boot-devtools -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.1 自定義用戶模型

首先建立一個 MyUser 類,用於存儲模擬的用戶信息(實際開發中通常從數據庫中獲取真實的用戶信息):java

// com/semlinker/domain/MyUser.java
@Data
public class MyUser implements Serializable {
    private static final long serialVersionUID = -1090551705063344205L;

    private String userName;
    private String password;
    private boolean accountNonExpired = true; // 表示帳號是否未過時
    private boolean accountNonLocked = true; // 表示帳號是否未鎖定
    private boolean credentialsNonExpired = true; // 表示用戶憑證未過時,好比用戶密碼
    private boolean enabled = true; // 表示用戶是否啓用
}

1.2 自定義 Security 配置類及 PasswordEncoder 對象

接着配置 PasswordEncoder 對象,顧名思義該對象用於密碼加密。在下面的 UserDetailsService 服務中須要用到此對象,所以這裏咱們須要提早作好配置。PasswordEncoder 是一個密碼加密接口,在 Spring Security 中有許多實現類,好比 BCryptPasswordEncoder、Pbkdf2PasswordEncoder 和 LdapShaPasswordEncoder 等。git

固然咱們也能夠自定義 PasswordEncoder,但 Spring Security 中實現的 BCryptPasswordEncoder 功能已經足夠強大,它對相同的密碼進行加密後能夠生成不一樣的結果,這樣就大大提升了系統的安全性。即儘管系統中使用相同密碼的某些用戶不當心泄露了密碼,也不會致使其餘用戶密碼泄露。既然 BCryptPasswordEncoder 功能那麼強大,咱們確定直接使用它,具體的配置方式以下:github

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

1.3 自定義 UserDetailsService 服務

自定義 UserDetailsService 服務,須要實現 UserDetailsService 接口,該接口只包含一個 loadUserByUsername 方法,用於經過 username 來加載匹配的用戶。當找不到 username 對應用戶時,會拋出 UsernameNotFoundException 異常。UserDetailsService 接口的定義以下:web

// org/springframework/security/core/userdetails/UserDetailsService.java
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername 方法返回 UserDetails 對象,這裏的 UserDetails 也是一個接口,它的定義以下:spring

// org/springframework/security/core/userdetails/UserDetails.java
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

顧名思義,UserDetails 表示詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,具體的實現類對它進行了擴展。以上方法的具體做用以下:數據庫

  • getPassword():用於獲取密碼;
  • getUsername():用於獲取用戶名;
  • isAccountNonExpired():用於判斷帳號是否未過時;
  • isAccountNonLocked():用於判斷帳號是否未鎖定;
  • isCredentialsNonExpired():用於判斷用戶憑證是否未過時,即密碼是否未過時;
  • isEnabled():用於判斷用戶是否可用。

介紹完上述內容,下面咱們來建立一個 MyUserDetailsService 類並實現 UserDetailsService 接口,具體以下:apache

// com/semlinker/service/MyUserDetailsService.java
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser myUser = new MyUser();
        myUser.setUserName(username);
        myUser.setPassword(this.passwordEncoder.encode("hello"));

        // 使用Spring Security內部UserDetails的實現類User,來建立User對象
        return new User(username, myUser.getPassword(), myUser.isEnabled(),
                myUser.isAccountNonExpired(), myUser.isCredentialsNonExpired(),
                myUser.isAccountNonLocked(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

1.4 配置 UserDetailsService Bean 及配置 AuthenticationManagerBuilder 對象

在 Spring Security 中使用咱們自定義的 MyUserDetailsService,還須要在 WebSecurityConfig 類中進行配置:json

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    UserDetailsService myUserDetailService() {
        return new MyUserDetailsService();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder());
    }
}

在以上 configure 方法中,咱們配置了自定義的 MyUserDetailsService 和 PasswordEncoder 對象。

1.5 建立相關 Controller 及自定義登陸頁和首頁

在 Spring Security 中 DefaultLoginPageGeneratingFilter 過濾器會爲咱們生成默認登陸界面:

user-login-page.jpg

相信不少小夥伴都 「看不慣」 這個頁面,下面咱們就來對這個頁面進行 「整容」。

HomeController 類
// com/semlinker/controller/HomeController.java
@Controller
public class HomeController {

    @GetMapping("/")
    public String index() {
        return "index";
    }

}
UserController 類
// com/semlinker/controller/UserController.java
@Controller
public class UserController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Semlinker修仙之路首頁 </title>
</head>
<body>
   <h3>歡迎您來到Semlinker修仙之路首頁</h3>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Semlinker修仙之路登陸頁</title>
</head>
<body>
<form class="login-form" method="post" action="/login">
    <h1>Login</h1>
    <div class="form-field">
        <i class="fas fa-user"></i>
        <input type="text" name="username" id="username" class="form-field" 
               placeholder=" " required>
        <label for="username">Username</label>
    </div>
    <div class="form-field">
        <i class="fas fa-lock"></i>
        <input type="password" name="password" id="password" class="form-field" 
               placeholder=" " required>
        <label for="password">Password</label>
    </div>
    <button type="submit" value="Login" class="btn">Login</button>
</form>
</body>
</html>

1.6 配置默認的登陸頁

在建立完登陸頁以後,還須要在 WebSecurityConfig 類中進行配置才能生效,對應的配置方式以下:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 省略前面已設置的內容
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .loginPage("/login");
    }
}

完成上述配置後,咱們來測試一下效果,首先啓動 Spring Boot 應用,待啓動完成後在瀏覽器中打開 http://localhost:8080/login 地址,若一切順利的話,你將看到如下界面:

custom-login-page.jpg

(頁面來源於 https://codepen.io/alphardex/...

接下來咱們來執行登陸操做,這裏的用戶名能夠是任意的,密碼是前面咱們所設置的 hello。但當咱們輸入正確的用戶名和密碼點擊登陸以後,映入眼簾的倒是如下的異常頁面:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon Oct 28 14:27:25 CST 2019
There was an unexpected error (type=Forbidden, status=403).
Forbidden

這是什麼緣由呢?爲啥被禁止訪問了,小夥伴們先別急,首先打開當前項目 src/main/resources/ 目錄下的 application.properties 文件,而後輸入如下配置信息:

logging.level.org.springframework.security.web.FilterChainProxy=DEBUG

待完成配置以後,重啓一下應用,而後從新執行一次上述的登陸操做。若是沒猜錯的話,你從新執行登陸,輸入的用戶名和密碼也沒有錯,但仍看見 Whitelabel Error Page 頁面。其實剛纔咱們已經啓用的 Security FilterChainProxy 的 DEBUG 調試模式,因此咱們來看一下控制檯輸出的異常信息:

filter-chain-proxy-debug.jpg

經過上圖能夠發現 /login 請求,通過 CsrfFilter 過濾器就再也不往下繼續執行了。這裏的 CsrfFilter 過濾器是用來處理跨站請求僞造攻擊的過濾器,跨站請求僞造(英語:Cross-site request forgery),也被稱爲 one-click attack 或者 session riding,一般縮寫爲 CSRF 或者 XSRF, 是一種挾制用戶在當前已登陸的 Web 應用程序上執行非本意的操做的攻擊方法。

如今咱們已經大體知道緣由了,因爲咱們的登陸頁暫不須要開啓 Csrf 防護,因此咱們先把 Csrf 過濾器禁用掉:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .and().csrf().disable();
    }
}

更新完 WebSecurityConfig 配置類,再從新跑一次前面的登陸流程,此次當你點擊登陸以後,你將會在當前頁面看到歡迎您來到Semlinker修仙之路首頁這行內容。

2、處理不一樣類型的請求

默認狀況下,當用戶經過瀏覽器訪問被保護的資源時,會默認自動重定向到預設的登陸地址。這對於傳統的 Web 項目來講,是沒有多大問題,但這種方式就不適用於先後端分離的項目。對於先後端分離的項目,服務端通常只須要對外提供返回 JSON 格式的 API 接口。

針對上述的問題,有以下一種方案可供參考。即根據請求是否以 .html 爲結尾來對應不一樣的處理方法。若是是以 .html 結尾,那麼重定向到登陸頁面,不然返回 」訪問的資源須要身份認證!」 信息,而且 HTTP 狀態碼爲401(HttpStatus.UNAUTHORIZED)。

要實現上述的功能,咱們先來定義一個 WebSecurityController 類,具體實現以下:

// com/semlinker/controller/WebSecurityController.java
@Slf4j
@RestController
public class WebSecurityController {
    // 原請求信息的緩存及恢復
    private RequestCache requestCache = new HttpSessionRequestCache();

    // 用於執行重定向操做
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    /**
     * 默認的登陸頁,用於處理不一樣的登陸認證邏輯
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String requireAuthenication(HttpServletRequest request, 
      HttpServletResponse response) throws Exception {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl = savedRequest.getRedirectUrl();
            log.info("引起跳轉的請求是:" + targetUrl);
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, "/login.html");
            }
        }

        return "訪問的服務須要身份認證,請引導用戶到登陸頁";
    }
}

接着將 formLogin 的默認登陸頁,修改成 /authentication/require,並經過 antMatchers 方法設置免攔截:

// com/semlinker/config/WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/authentication/require")
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", "/login.html").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
        ;
    }
}

同時也要修改一下前面定義的 UserController 類,讓其支持 /login.html 路徑映射:

// com/semlinker/controller/UserController.java
@Controller
public class UserController {

    @GetMapping({"login", "/login.html"})
    public String login() {
        return "login";
    }

}

完成上述調整後,到咱們訪問 http://localhost:8080/index 的時候,頁面會自動跳轉到 http://localhost:8080/authentication/require,而且輸出 "訪問的服務須要身份認證,請引導用戶到登陸頁"。而當咱們訪問 http://localhost:8080/index.html 的時候,頁面會跳轉到登陸頁面。

3、自定義處理登陸成功和失敗邏輯

在先後端分離項目中,當用戶登陸成功或登陸失敗時,須要向前端返回相應的信息,而不是直接進行頁面跳轉。針對先後端分離的場景,能夠利用 Spring Security 中的 AuthenticationSuccessHandlerAuthenticationFailureHandler 這兩個接口或繼承 SimpleUrlAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandler 類來實現自定義登陸成功和登陸失敗的處理邏輯。

3.1 自定義登陸成功處理邏輯

這裏咱們選用繼承 SimpleUrlAuthenticationSuccessHandler 類,來實現自定義登陸成功處理邏輯:

// com/semlinker/handler/MyAuthenctiationSuccessHandler.java
@Slf4j
@Component
public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

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

        log.info("登陸成功");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

3.2 自定義登陸失敗處理邏輯

一樣咱們也選用繼承 SimpleUrlAuthenticationFailureHandler 類,來實現自定義登陸失敗處理邏輯:

// com/semlinker/handler/MyAuthenctiationFailureHandler.java
@Slf4j
@Component
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

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

        log.info("登陸失敗");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
    }
}

3.3 配置 MyAuthenctiationSuccessHandler 和 MyAuthenctiationFailureHandler

最後要讓自定義處理登陸成功和失敗邏輯生效,還須要在 WebSecurityConfig 類中配置 FormLoginConfigurer 對象的 successHandler 和 failureHandler 屬性,到目前爲止 WebSecurityConfig 類的完整配置以下:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;

    @Autowired
    private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    UserDetailsService myUserDetailService() {
        return new MyUserDetailsService();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder());
    }

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .successHandler(myAuthenctiationSuccessHandler)
                .failureHandler(myAuthenctiationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", "/login").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
        ;
    }
}

前面本文已經介紹了在 Spring Security 中實現自定義用戶認證的流程,在學習過程當中若是小夥伴們遇到其它問題的話,建議能夠開啓 FilterChainProxy 的 DEBUG 模式進行日誌排查。

本文項目地址: Github - custom-user-authentication

4、參考資源

full-stack-logo

相關文章
相關標籤/搜索