Spring Security - 重定向



在 Web 應用程式中,我們經常需要根據使用者配置檔案跳轉到不同的頁面。例如,普通使用者可能跳轉到使用者主頁,而管理員可能跳轉到管理員控制檯。我們可以使用 Spring Security 很容易地實現此要求,它提供支援來處理登入成功,並根據使用者角色決定向使用者顯示哪個頁面,或者簡單地將使用者重定向到所需的頁面。

為了實現重定向,我們需要在 Spring Security 配置中處理 formLogin 的 successHandler,如下所示

protected void configure(HttpSecurity http) throws Exception {
http 
   // ... 
   // key should be unique
   .formLogin(form -> form.loginPage("/login")
      .defaultSuccessUrl("/")
      .failureUrl("/login?error=true")
      .successHandler(authenticationSuccessHandler())
      .permitAll())   
   //
   .build();
}

這裡 authenticationSuccessHandler() 方法是另一個處理登入成功並將使用者重定向到所需頁面的 bean。

@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
   return new AuthenticationHandler();
}

AuthenticationSuccessHandler

為了實現重定向,我們首先需要建立一個類來實現 AuthenticationSuccessHandler,如下所示。在這個類中,我們必須實現 onAuthenticationSuccess() 方法。onAuthenticationSuccess() 方法在使用者成功登入後呼叫。現在使用 Authentication 物件,我們可以檢查已登入使用者的角色,然後確定重定向 URL。然後,使用 HttpServletResponse.sendRedirect() 方法,我們可以將使用者重定向到所需的頁面。

public class AuthenticationHandler implements AuthenticationSuccessHandler {

   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
      String redirect = request.getContextPath();

      if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
         redirect = "/admin";
      } else if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
         redirect = "/user";
      }

      response.sendRedirect(redirect);
   }
}

讓我們開始使用 Spring Security 進行實際程式設計。在開始使用 Spring 框架編寫您的第一個示例之前,您必須確保已正確設定 Spring 環境,如Spring Security - 環境搭建章節中所述。我們還假設您對 Spring Tool Suite IDE 有些瞭解。

現在讓我們繼續編寫一個基於 Spring MVC 並由 Maven 管理的應用程式,該應用程式將要求使用者登入,對使用者進行身份驗證,然後使用 Spring Security 表單登入功能提供登出選項。

使用 Spring Initializr 建立專案

Spring Initializr 是開始 Spring Boot 專案的好方法。它提供了一個易於使用的使用者介面來建立專案、新增依賴項、選擇 Java 執行時等。它生成一個骨架專案結構,下載後可以在 Spring Tool Suite 中匯入,然後我們可以繼續使用現成的專案結構。

我們選擇一個 Maven 專案,將專案命名為 formlogin,Java 版本為 21。新增以下依賴項:

  • Spring Web

  • Spring Security

  • Thymeleaf

  • Spring Boot DevTools

Spring Initializr

Thymeleaf 是 Java 的模板引擎。它允許我們快速開發靜態或動態網頁以便在瀏覽器中呈現。它具有極高的可擴充套件性,允許我們詳細定義和定製模板的處理。此外,我們可以點選此連結瞭解更多關於 Thymeleaf 的資訊。

讓我們繼續生成專案並下載它。然後,我們將它解壓到我們選擇的資料夾中,並使用任何 IDE 開啟它。我將使用Spring Tools Suite 4。它可以從https://springframework.tw/tools網站免費下載,並針對 Spring 應用程式進行了最佳化。

包含所有相關依賴項的 pom.xml

讓我們看一下我們的 pom.xml 檔案。它應該看起來類似於以下內容:

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>3.3.1</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.tutorialspoint.security</groupId>
   <artifactId>formlogin</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>formlogin</name>
   <description>Demo project for Spring Boot</description>
   <url/>
   <licenses>
      <license/>
   </licenses>
   <developers>
      <developer/>
   </developers>
   <scm>
      <connection/>
      <developerConnection/>
      <tag/>
      <url/>
   </scm>
   <properties>
      <java.version>21</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.thymeleaf.extras</groupId>
         <artifactId>thymeleaf-extras-springsecurity6</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-devtools</artifactId>
         <scope>runtime</scope>
         <optional>true</optional>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>
      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-test</artifactId>
         <scope>test</scope>
      </dependency>
   </dependencies>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
</project>

Spring Security 配置類

在我們的 config 包中,我們建立了 WebSecurityConfig 類。我們將使用此類進行安全配置,因此讓我們使用 @Configuration 註解和 @EnableWebSecurity 對其進行註解。因此,Spring Security 知道將此類視為配置類。如我們所見,Spring 使配置應用程式變得非常容易。

WebSecurityConfig

package com.tutorialspoint.security.formlogin.config; 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

@Configuration 
@EnableWebSecurity
public class WebSecurityConfig  { 

   @Bean 
   protected UserDetailsService userDetailsService() {
      UserDetails user = User.builder()
         .username("user")
         .password(passwordEncoder().encode("user123"))
         .roles("USER")
         .build();
      UserDetails admin = User.builder()
         .username("admin")
         .password(passwordEncoder().encode("admin123"))
         .roles("USER", "ADMIN")
         .build();
      return new InMemoryUserDetailsManager(user, admin);
   }

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

   @Bean
   protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 
      return http
         .csrf(AbstractHttpConfigurer::disable)
         .authorizeHttpRequests(
            request -> request.requestMatchers("/login").permitAll()
            .requestMatchers("/**").authenticated()
         )
         .formLogin(form -> form.loginPage("/login")
            .defaultSuccessUrl("/")
            .failureUrl("/login?error=true")
            .successHandler(authenticationSuccessHandler())
            .permitAll())       
         .rememberMe(config -> config.key("123456")
         .tokenValiditySeconds(3600))   
         .logout(config -> config  
            .logoutUrl("/logout") 
            .logoutSuccessUrl("/login")
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID"))
         .build();
   }  
   
   @Bean
   public AuthenticationSuccessHandler authenticationSuccessHandler() {
      return new AuthenticationHandler();
   }
}

在這裡,我們在 successHandler() 方法中使用了 authenticationSuccessHandler() 來進行所需的重定向。AuthenticationHandler 類應該在同一個包中。

AuthenticationHandler 類

下面的類實現了 AuthenticationSuccessHandler。在這個類中,我們實現了 onAuthenticationSuccess() 方法。onAuthenticationSuccess() 方法在使用者成功登入後呼叫。

AuthenticationHandler

package com.tutorialspoint.security.formlogin.config;

import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class AuthenticationHandler implements AuthenticationSuccessHandler {

   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
      String redirect = request.getContextPath();

      if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
         redirect = "/admin";
      } else if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
         redirect = "/user";
      }

      response.sendRedirect(redirect);
   }
}

控制器類

在這個類中,我們為 "/" 端點和 "/login" 建立了對映,用於此應用程式的索引頁面和登入頁面。對於 user.html 和 admin.html,我們添加了兩個方法 user() 和 admin()。

AuthController.java

package com.tutorialspoint.security.formlogin.controllers; 

import org.springframework.stereotype.Controller; 
import org.springframework.web.bind.annotation.GetMapping; 

@Controller 
public class AuthController { 
   @GetMapping("/") 
   public String home() { 
      return "index"; 
   }
   @GetMapping("/login") 
   public String login() { 
      return "login"; 
   }
   @GetMapping("/user")
   public String user() {
      return "user";
   }
   @GetMapping("/admin")
   public String admin() {
      return "admin";
   }
}

檢視

讓我們在/src/main/resources/templates資料夾中建立 index.html,其內容如下,作為主頁並顯示已登入使用者名稱。

index.html

<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml" 
   xmlns:th="https://www.thymeleaf.org" 
   xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> 
   <head> 
      <title>
         Hello World!
      </title> 
   </head>
   <body> 
      <h1 th:inline="text">Hello <span sec:authentication="name"></span>!</h1> 
      <form th:action="@{/logout}" method="post"> 
         <input type="submit" value="Sign Out"/> 
      </form>
   </body> 
<html> 

login.html

讓我們在/src/main/resources/templates資料夾中建立 login.html,其內容如下,作為登入頁面。我們使用預設名稱usernamepasswordremember-me作為文字欄位。如果名稱不同,我們也需要在 Spring Security 配置類中設定相同的名稱。

<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml"      
   xmlns:th="https://www.thymeleaf.org" 
   xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> 
   <head> 
      <title>Spring Security Example</title> 
   </head> 
   <body> 
      <div th:if="${param.error}"> 
         <p>Bad Credentials</p>
      </div> 
      <div th:if="${param.logout}">You have been logged out.</div> 
      <form th:action="@{/login}" method="post">
         <h1>Please sign in</h1>
         <table>
            <tr>
               <td><label for="username"><b>Username</b></label></td>
               <td><input type="text" placeholder="Enter Username" name="username" id="username" required></td>
            </tr>
            <tr>
               <td><label for="password"><b>Password</b></label></td>
               <td><input type="password" placeholder="Enter Password" name="password" id="password" required></td>
            </tr>
            <tr>
               <td><label for="remember-me"><b>Remember Me</b></label> </td>
               <td><input type="checkbox" name="remember-me" /></td>
            </tr>
            <tr>
               <td> </td>
               <td><input type="submit"  value="Sign In" /></td>
            </tr>		
         </table>       
      </form> 
   </body>
</html>

在登入表單中,我們使用 POST 方法登入,同時使用名稱和 ID 為usernamepasswordremember-me複選框的輸入欄位。如果選中複選框,則使用者登入應用程式時將建立一個 remember-me Cookie。

admin.html

讓我們在/src/main/resources/templates資料夾中建立 admin.html,其內容如下,作為使用者使用 ADMIN 角色憑據登入時的主頁。

<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml" 
   xmlns:th="https://www.thymeleaf.org" 
   xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> 
   <head> 
      <title>
         Hello Admin!
      </title> 
   </head>
   <body> 
      <p>Admin Console</p>
      <h1 th:inline="text">Welcome <span sec:authentication="name"></span>!</h1> 
      <a href="/logout" alt="logout">Sign Out</a>
   </body> 
<html> 

user.html

讓我們在/src/main/resources/templates資料夾中建立 user.html,其內容如下,作為使用者使用 USER 角色憑據登入時的主頁。

<!DOCTYPE html> 
<html xmlns="http://www.w3.org/1999/xhtml" 
   xmlns:th="https://www.thymeleaf.org" 
   xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> 
   <head> 
      <title>
         Hello User!
      </title> 
   </head>
   <body> 
      <p>User Dashboard</p>
      <h1 th:inline="text">Hello <span sec:authentication="name"></span>!</h1> 
      <a href="/logout" alt="logout">Sign Out</a>
   </body> 
<html> 

執行應用程式

由於我們所有的元件都已準備就緒,讓我們執行應用程式。右鍵單擊專案,選擇Run As,然後選擇Spring Boot App

它將啟動應用程式,並且應用程式啟動後,我們可以執行 localhost:8080 來檢查更改。

輸出

現在開啟 localhost:8080,您可以看到我們的登入頁面。

輸入使用者詳細資訊的登入頁面

Login Form with User credential

使用者主頁

當我們輸入使用者的有效憑據時,它將載入使用者主頁,即 user.html

User Home Page

現在單擊“登出”連結登出,然後在登入頁面上使用管理員詳細資訊。

Login Form with Admin credential

管理員主頁

當我們輸入管理員的有效憑據時,它將載入管理員主頁,即 admin.html

Admin Home Page
廣告