Spring?Security登錄表單配置示例詳解
Spring Security登錄表單配置
1.引入pom依賴
? 創(chuàng)建一個(gè)Spring Boot工程,引入Web和Spring Security依賴:
<?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 http://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.7.0</version>
</parent>
<groupId>com.kapcb.ccc</groupId>
<artifactId>springsecurity-helloworld</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>2.bootstrap.yml添加配置
? 工程創(chuàng)建完成之后,為了方便測(cè)試,需要在bootstrap.yml配置文件中添加如下配置,將登錄用戶名和密碼固定下來(lái):
spring:
security:
user:
name: kapcb
password: 1234563.創(chuàng)建login.html
? 在resources/static目錄下創(chuàng)建login.html頁(yè)面,這個(gè)就是自定義登錄頁(yè)面:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/loginSystem" method="post">
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? 這個(gè)login.html核心內(nèi)容就是一個(gè)登陸表單,登陸表單中有三個(gè)需要注意的地方:
form標(biāo)簽中的action熟悉,這里是/loginSystem,表示表單要提交請(qǐng)求到/loginSystem接口上。- 用戶名框的
name屬性值為username。 - 密碼框的
name屬性為password。
4.創(chuàng)建配置類
? 定義好login.html之后,接下來(lái)就定義兩個(gè)測(cè)試接口,作為受保護(hù)資源。當(dāng)用戶登陸成功后,就可以訪問(wèn)受保護(hù)的資源。接口定義如下:
package com.kapcb.security.helloworld.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <a>Title: HelloWorldController </a>
* <a>Author: Kapcb <a>
* <a>Description: HelloWorldController <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/12 22:05
* @since 1.0
*/
@RestController
public class HelloWorldController {
@GetMapping("index")
public String index() {
return "login success!";
}
@GetMapping("hello")
public String hello() {
return "Hello, World!";
}
}? 最后再提供Spring Security的配置類,需要注意的是在Spring Security 5.7版本中已經(jīng)廢棄WebSecurityConfigurerAdapter:
package com.kapcb.security.helloworld.configuration;
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.web.SecurityFilterChain;
/**
* <a>Title: SecurityConfiguration </a>
* <a>Author: Kapcb <a>
* <a>Description: SecurityConfiguration <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/13 22:23
* @since 1.0
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.defaultSuccessUrl("/index")
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
}
? 這里直接使用聲明SecurityFilterChain類型的Bean的方式即可配置Spring Security登陸表單。
- authorizeHttpRequests()方法表示開(kāi)啟權(quán)限配置。
- anyRequest().authenticated():表示所有請(qǐng)求都需要經(jīng)過(guò)認(rèn)證。
- and():方法會(huì)返回httpSecurityBuilder對(duì)象的一個(gè)子類,實(shí)際上就是HttpSecurity。所以and()方法相當(dāng)于返回一個(gè)HttpSecurity實(shí)例,重新開(kāi)啟新一輪配置。
- formLogin():表示開(kāi)啟表單登陸配置。
- loginPage("/login.html"):用于配置默認(rèn)登錄頁(yè)的請(qǐng)求地址。
- loginProcessingUrl("/loginSystem"):用于配置登錄請(qǐng)求接口地址。
- defaultSuccessUrl("/index"):表示登錄請(qǐng)求處理成功后的跳轉(zhuǎn)地址。
- failureUrl("/login.html"):表示登陸失敗跳轉(zhuǎn)的地址。
- usernameParameter("username"):表示登陸用戶名的參數(shù)名稱。
- passwordParameter("password"):表示登錄密碼的參數(shù)名稱。
- permitAll():表示跟登錄相關(guān)的頁(yè)面和接口不做攔截,直接允許訪問(wèn)。
- csrf().disable():表示禁用CSRF防御功能。Spring Security自帶了CSRF防御機(jī)制。為了測(cè)試方便,這里先關(guān)閉了。
? 需要注意的是,上面的loginPage()、loginProcessingUrl()、defaultSuccessUrl()、usernameParameter()、passwordParameter()需要和之前創(chuàng)建的login.html中登錄表單的配置一致。
? 完成上述配置之后,啟動(dòng)工程,訪問(wèn)http://localhost:9096/index,會(huì)自動(dòng)跳轉(zhuǎn)到http://localhost:9096/login.html頁(yè)面。輸入之前設(shè)置好的用戶名密碼,登陸成功之后,就可以訪問(wèn)到/index頁(yè)面了。
5.配置細(xì)節(jié)
? 在前面的配置中,使用defaultSuccessUrl()方法配置了用戶登陸成功之后的跳轉(zhuǎn)地址。使用failureUrl()方法配置了用戶登錄失敗之后的跳轉(zhuǎn)地址。關(guān)于用戶登錄成功與登陸失敗,除了這兩個(gè)方法可以進(jìn)行配置之外,還有另外兩個(gè)方法也可配置。
6.登陸成功
? 當(dāng)用戶登錄成功之后,除了使用defaultSuccessUrl()方法可以實(shí)現(xiàn)登錄成功后的跳轉(zhuǎn)之外,successForwardUrl()方法也可以配置實(shí)現(xiàn)登錄成功之后的跳轉(zhuǎn),配置代碼如下:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.successForwardUrl("/index")
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
}
? defaultSuccessUrl()和successForwardUrl()方法配置的區(qū)別如下:
- defaultSuccessUrl()方法表示當(dāng)用戶登陸成功之后,會(huì)自動(dòng)重定向到登錄之前用戶訪問(wèn)的地址上。如果用戶本身就是直接訪問(wèn)的登陸頁(yè)面,則登錄成功之后就會(huì)重定向到defaultSuccessUrl()指定的頁(yè)面。
- successForwardUrl()方法則不會(huì)考慮用戶登錄之前訪問(wèn)的地址,只要用戶登陸成功,就會(huì)通過(guò)服務(wù)器端跳轉(zhuǎn)到successForwardUrl()所指定的頁(yè)面。
- defaultSuccessUrl()方法有一個(gè)重載方法,如果重載方法的第二個(gè)參數(shù)傳入true,則defaultSuccessUrl()方法的效果與successForwardUrl()方法類似,即不考慮用戶之前的訪問(wèn)地址,只要登陸成功,就重定向到defaultSuccessUrl()所指定的頁(yè)面。不同之處在于,defaultSuccessUrl()方法是通過(guò)重定向?qū)崿F(xiàn)的客戶端跳轉(zhuǎn),successForwardUrl()則是通過(guò)服務(wù)端跳轉(zhuǎn)實(shí)現(xiàn)的。
? 無(wú)論是defaultSuccessUrl()還是successForwardUrl()方法配置登錄成功跳轉(zhuǎn)頁(yè)面,最終所配置的都是AuthenticationSuccessHandler接口的實(shí)例。
? 在Spring Security中專門提供了AuthenticationSuccessHandler接口來(lái)處理用戶登陸成功事項(xiàng),AuthenticationSuccessHandler接口源碼如下:
public interface AuthenticationSuccessHandler {
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
this.onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
? AuthenticationSuccessHandler接口中定義了兩個(gè)方法,其中一個(gè)是default方法,此方法是Spring Security 5.2版本中添加進(jìn)來(lái)的,在處理特定的認(rèn)證請(qǐng)求Authentication Filter中會(huì)用到。另外一個(gè)非default方法,則用來(lái)處理登陸成功的具體事項(xiàng),其中Authentication參數(shù)保存了登陸成功的用戶信息。
? AuthenticationSuccessHandler接口在Spring Security源碼中一共有三個(gè)實(shí)現(xiàn)類:
- SimpleUrlAuthenticationSuccessHandler繼承自AbstractAuthenticationTargetUrlRequestHandler。通過(guò)AbstractAuthenticationTargetUrlRequestHandler中的handler()方法實(shí)現(xiàn)請(qǐng)求重定向。
- SavedRequestAwareAuthenticationSuccessHandler在SimpleUrlAuthenticationSuccessHandler的基礎(chǔ)上增加了請(qǐng)求緩存的功能,可以記錄用戶登陸之前請(qǐng)求的地址,進(jìn)而在登陸成功之后重定向到一開(kāi)始訪問(wèn)的地址。
- ForwardAuthenticationSuccessHandler的實(shí)現(xiàn)比較簡(jiǎn)單,就是一個(gè)服務(wù)端跳轉(zhuǎn)。
? 通過(guò)defaultSuccessUrl()方法來(lái)配置登陸成功之后重定向的請(qǐng)求地址時(shí),實(shí)際上對(duì)應(yīng)的實(shí)現(xiàn)類就是SavedRequestAwareAuthenticationSuccessHandler。SavedRequestAwareAuthenticationSuccessHandler源碼如下:
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
public SavedRequestAwareAuthenticationSuccessHandler() {
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
} else {
String targetUrlParameter = this.getTargetUrlParameter();
if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.clearAuthenticationAttributes(request);
String targetUrl = savedRequest.getRedirectUrl();
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
} else {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
? SavedRequestAwareAuthenticationSuccessHandler中的核心方法就是onAuthenticationSuccess()。
- 從RequestCache中獲取緩存下來(lái)的請(qǐng)求,如果沒(méi)有獲取到緩存的請(qǐng)求地址,就代表用戶在登錄之前并沒(méi)有訪問(wèn)其它頁(yè)面。此時(shí)直接調(diào)用父類的onAuthenticationSuccess()方法來(lái)處理,最終重定向到defaultSuccessUrl()方法指定的地址。
- 接下里會(huì)獲取一個(gè)targetUrlParameter,這個(gè)地址是用戶顯示指定的、希望登錄成功之后重定向的地址。例如用戶發(fā)送的登錄請(qǐng)求是http://127.0.0.1:9096/loginSystem?target=/hello,這就表示當(dāng)用戶登陸成功之后,希望主動(dòng)重定向到/hello接口。targetUrlParameter()方法就是獲取重定向地址參數(shù)的key,就是上面舉例中的target。獲取到target之后就可以獲取到重定向的地址了。
- 如果targetUrlParameter存在,或者開(kāi)發(fā)者設(shè)置了isAlwaysUseDefaultTargetUrl()為true,此時(shí)緩存下來(lái)的請(qǐng)求就失去了意義。此時(shí)會(huì)直接移除掉緩存的請(qǐng)求地址,直接調(diào)用父類的onAuthenticationSuccess()方法完成重定向。targetUrlParameter存在,則直重定向到targetUrlParameter指定的地址。isAlwaysUseDefaultTargetUrl為true,則直接重定向到defaultSuccessUrl指定的地址。如果targetUrlParameter存在并且isAlwaysUseDefaultTargetUrl為true,則直接重定向到defaultSuccessUrl指定的地址。
- 如果上述條件均不滿足,那么最終會(huì)從緩存請(qǐng)求SavedRequest對(duì)象中獲取重定向地址,然后進(jìn)行重定向操作。
? 這就是SavedRequestAwareAuthenticationSuccessHandler的實(shí)現(xiàn)邏輯,開(kāi)發(fā)者也可配置自己的SavedRequestAwareAuthenticationSuccessHandler,如下:
package com.kapcb.security.helloworld.configuration;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
/**
* <a>Title: SecurityConfiguration </a>
* <a>Author: Kapcb <a>
* <a>Description: SecurityConfiguration <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/13 22:23
* @since 1.0
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.successHandler(successHandler())
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected SavedRequestAwareAuthenticationSuccessHandler successHandler() {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl("/index");
successHandler.setTargetUrlParameter("target");
return successHandler;
}
}
? 在上面的配置代碼中,制定了targetUrlParameter為target,這樣用戶就可以在登錄請(qǐng)求中,通過(guò)target來(lái)指定跳轉(zhuǎn)地址。修改一下上面的login.html中的form表單:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/loginSystem?target=/hello" method="post">
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? 修改form表單的action屬性為/loginSystem?target=/hello。當(dāng)用戶登陸成功之后,就始終會(huì)跳轉(zhuǎn)到/hello接口了。
? 在使用successForwardUrl()方法設(shè)置登陸成功后重定向的地址時(shí),實(shí)際上對(duì)應(yīng)的實(shí)現(xiàn)類是ForwardAuthenticationSuccessHandler,ForwardAuthenticationSuccessHandler類的源碼非常簡(jiǎn)單,就是一個(gè)服務(wù)端轉(zhuǎn)發(fā),ForwardAuthenticationSuccessHandler源碼如下:
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String forwardUrl;
public ForwardAuthenticationSuccessHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
return "'" + forwardUrl + "' is not a valid forward URL";
});
this.forwardUrl = forwardUrl;
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}? 主要功能就是調(diào)用request.getRequestDispatcher(this.forwardUrl).forward(request, response)方法實(shí)現(xiàn)服務(wù)端請(qǐng)求轉(zhuǎn)發(fā)。
? 啟動(dòng)服務(wù)之后訪問(wèn)登錄頁(yè)面,登錄成功后即可自動(dòng)重定向到/hello接口。
? AuthenticationSuccessHandler默認(rèn)的三個(gè)實(shí)現(xiàn)類,無(wú)論是哪一種,都是用來(lái)處理頁(yè)面跳轉(zhuǎn)的。隨著前后端分離架構(gòu)的盛行,頁(yè)面跳轉(zhuǎn)并不能滿足我們的業(yè)務(wù)需求,用戶登錄成功后,后端返回JSON數(shù)據(jù)給前端即可。告訴前端當(dāng)前用戶是登陸成功還是失敗即可,前端拿到后端響應(yīng)結(jié)果后自行處理即可。像這種需求,可以使用自定義AuthenticationSuccessHandler的實(shí)現(xiàn)類完成。
package com.kapcb.security.helloworld.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* <a>Title: CustomizeAuthenticationSuccessHandler </a>
* <a>Author: Kapcb <a>
* <a>Description: CustomizeAuthenticationSuccessHandler <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/16 23:47
* @since 1.0
*/
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", null);
resultMap.put("data", "1111111");
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}
}
? 修改配置類:
package com.kapcb.security.helloworld.configuration;
import com.kapcb.security.helloworld.handler.CustomizeAuthenticationSuccessHandler;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
/**
* <a>Title: SecurityConfiguration </a>
* <a>Author: Kapcb <a>
* <a>Description: SecurityConfiguration <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/13 22:23
* @since 1.0
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureUrl("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
}
? 在使用自定義AuthenticationSuccessHandler的實(shí)現(xiàn)類來(lái)完成登錄成功之后的處理邏輯之后,還需要同步修改login.html。
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/loginSystem" method="post">
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? 將form表單中的action屬性修改為之前的/loginSystem。因?yàn)榇藭r(shí)使用的是自定義的AuthenticationSuccessHandler。邏輯與SavedRequestAwareAuthenticationSuccessHandler是不一樣的。
? 所有的改動(dòng)完成之后,重啟工程。當(dāng)用戶登錄成功之后,就不會(huì)在進(jìn)行頁(yè)面跳轉(zhuǎn)了,而是返回了一段JSON字符串到頁(yè)面上。
7.登陸失敗
? Spring Security登陸失敗的處理邏輯。為了方便在前端頁(yè)面展示登錄失敗的異常信息,首先在項(xiàng)目的pom.xml文件中引入thymeleaf模板引擎的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
? 在resources目錄下創(chuàng)建templates文件夾,新建loginTemplate.html文件:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>loginTemplate</title>
</head>
<body>
<form action="/loginSystem" method="post">
<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
<label>username: <input type="text" name="username"/></label></br>
<label>password: <input type="password" name="password"/></label></br>
<input type="submit" name="submit" value="login">
</form>
</body>
</html>
? loginTemplate.html文件和前面的login.html文件基本類似。login.html在static目錄下,屬于靜態(tài)頁(yè)面,loginTemplate.html是template模板頁(yè)面。loginTemplate.html在form標(biāo)簽內(nèi)新增了<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>標(biāo)簽,用于展示Spring Security在處理登陸失敗時(shí)的異常信息。在Spring Security中,用戶登陸失敗時(shí),異常信息會(huì)放在request中返回給前端,開(kāi)發(fā)者可將其直接提取出來(lái)展示。
? loginTemplate.html屬于動(dòng)態(tài)頁(yè)面,所以就不能像訪問(wèn)static/login.html那樣直接訪問(wèn),需要后端為其提供訪問(wèn)控制器:
package com.kapcb.security.helloworld.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* <a>Title: RouterController </a>
* <a>Author: Kapcb <a>
* <a>Description: RouterController <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/6/17 23:32
* @since 1.0
*/
@Controller
@RequestMapping("/router")
public class RouterController {
@RequestMapping("loginTemplate")
public String loginTemplate() {
return "loginTemplate";
}
}
? 最后在Spring Security配置類中配置登陸頁(yè)面:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureUrl("/router/loginTemplate")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
}? failureUrl()表示登陸失敗后重定向到/router/loginTemplate接口請(qǐng)求地址,也就是loginTemplate.html頁(yè)面。重定向是一種客戶端跳轉(zhuǎn),重定向不方便攜帶請(qǐng)求失敗的異常信息,只能放在URL中。
? 如果希望在前端展示請(qǐng)求失敗的異常信息,可以使用下面這種方式:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureForwardUrl("/router/loginTemplate")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
}
? failureForwardUrl()方法從名字上就可以看出,這種跳轉(zhuǎn)是一種服務(wù)器端跳轉(zhuǎn)。服務(wù)器端跳轉(zhuǎn)的好處是可以攜帶登錄異常信息。如果用戶登陸失敗,自動(dòng)跳轉(zhuǎn)回登錄頁(yè)面后,就可以將錯(cuò)誤信息展示出來(lái)。

? 無(wú)論是failureUrl()還是failureForwardUrl()方法,最終所配置的都是AuthenticationFailureHandler接口的實(shí)現(xiàn)類。Spring Security中提供了AuthenticationFailureHandler接口,為登陸失敗下的處理方式提供頂級(jí)拓展。AuthenticationFailureHandler源碼如下:
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException;
}
? AuthenticationFailureHandler接口中只定義了一個(gè)onAuthenticationFailure()方法,用于處理登陸失敗的請(qǐng)求。AuthenticationException表示登陸失敗的異常信息。Spring Security中為AuthenticationFailureHandler一共提供了五個(gè)實(shí)現(xiàn)類:
- SimpleUrlAuthenticationFailureHandler:默認(rèn)的處理邏輯就是通過(guò)重定向跳轉(zhuǎn)到登陸頁(yè)面,當(dāng)然也可以通過(guò)配置forwardToDestination屬性將重定向改為服務(wù)器端跳轉(zhuǎn),failureUrl()方法底層實(shí)現(xiàn)邏輯就是SimpleUrlAuthenticationFailureHandler。
- ExceptionMappingAuthenticationFailureHandler:可以實(shí)現(xiàn)根據(jù)不同異常類型,映射到不同路徑。
- ForwardAuthenticationFailureHandler:表示通過(guò)服務(wù)器端跳轉(zhuǎn)來(lái)重新回到登陸頁(yè)面,failureForwardUrl()方法底層實(shí)現(xiàn)邏輯就是ForwardAuthenticationFailureHandler。
- AuthenticationEntryPointFailureHandler:是Spring Security 5.2新引入的處理類,可以通過(guò)AuthenticationEntryPoint來(lái)處理登陸異常。
- DelegatingAuthenticationFailureHandler:可以實(shí)現(xiàn)為不同的異常類型配置不同的登錄失敗處理回調(diào)。
? 舉個(gè)簡(jiǎn)單的例子。假設(shè)不使用failureForwardUrl()方法進(jìn)行登陸失敗的處理邏輯配置,同時(shí)又想在登陸失敗后通過(guò)服務(wù)器端跳轉(zhuǎn)回到登陸頁(yè)面,那么可以自定義SimpleUrlAuthenticationFailureHandler配置,并將forwardToDestination屬性設(shè)置為true,代碼如下:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(failureHandler())
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected SimpleUrlAuthenticationFailureHandler failureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
failureHandler.setDefaultFailureUrl("/loginFail");
failureHandler.setUseForward(true);
return failureHandler;
}
}
? 這樣配置之后,用戶再次登陸失敗,就會(huì)通過(guò)服務(wù)端跳轉(zhuǎn)重新回到登陸頁(yè)面,同時(shí)頁(yè)面上也會(huì)展示相應(yīng)的錯(cuò)誤信息,效果和failureForwardUrl()一樣。
? SimpleUrlAuthenticationFailureHandler的源碼也很簡(jiǎn)單:
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private String defaultFailureUrl;
private boolean forwardToDestination = false;
private boolean allowSessionCreation = true;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public SimpleUrlAuthenticationFailureHandler() {
}
public SimpleUrlAuthenticationFailureHandler(String defaultFailureUrl) {
this.setDefaultFailureUrl(defaultFailureUrl);
}
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
} else {
this.logger.debug("Sending 401 Unauthorized error");
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
} else {
this.saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
} else {
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
}
protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
if (this.forwardToDestination) {
request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
} else {
HttpSession session = request.getSession(false);
if (session != null || this.allowSessionCreation) {
request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
}
}
}
public void setDefaultFailureUrl(String defaultFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(defaultFailureUrl), () -> {
return "'" + defaultFailureUrl + "' is not a valid redirect URL";
});
this.defaultFailureUrl = defaultFailureUrl;
}
protected boolean isUseForward() {
return this.forwardToDestination;
}
public void setUseForward(boolean forwardToDestination) {
this.forwardToDestination = forwardToDestination;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return this.redirectStrategy;
}
protected boolean isAllowSessionCreation() {
return this.allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
}
? SimpleUrlAuthenticationFailureHandler提供了無(wú)參和有參構(gòu)造器。使用有參構(gòu)造器構(gòu)造SimpleUrlAuthenticationFailureHandler對(duì)象時(shí),可傳入defaultFailureUrl。defaultFailureUrl也就是登陸失敗時(shí)要跳轉(zhuǎn)的地址。
? onAuthenticationFailure()方法中,首先會(huì)判斷defaultFailureUrl是否為null,如果為null則直接通過(guò)response返回401狀態(tài)碼Unauthorized。
? 如果defaultFailureUrl不為空,則調(diào)用saveException()方。在saveException()方法中,如果forwardToDestination屬性為true,表示此時(shí)需要通過(guò)服務(wù)器端跳轉(zhuǎn)回登錄首頁(yè),此時(shí)就將異常信息放到request中。在回到onAuthenticationFailure()方法中,如果forwardToDestination屬性為true,就通過(guò)服務(wù)器端跳轉(zhuǎn)回到登錄頁(yè)面,否則通過(guò)重定向回到登陸頁(yè)面。
? 如果是前后端分離開(kāi)發(fā),登陸失敗時(shí)就不需要進(jìn)行頁(yè)面跳轉(zhuǎn)了,只需要返回登錄失敗的JSON響應(yīng)給前端即可。這種場(chǎng)景下通過(guò)AuthenticationFailureHandler的實(shí)現(xiàn)類來(lái)完成,實(shí)現(xiàn)代碼如下:
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 500);
resultMap.put("msg", exception.getMessage());
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}
}
? 在Spring Security配置類中配置自定義登陸失敗處理器:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 配置完成后,當(dāng)用戶再次登陸失敗,就不會(huì)進(jìn)行頁(yè)面跳轉(zhuǎn)了,而是直接返回JSON字符串。
8.注銷登錄
? Spring Security中提供了默認(rèn)的注銷頁(yè)面,開(kāi)發(fā)者也可以根據(jù)自己的需求對(duì)注銷登錄進(jìn)行定制。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
- logout():表示開(kāi)啟注銷登錄配置。
- logoutUrl():表示指定了注銷登錄請(qǐng)求地址,默認(rèn)是GET請(qǐng)求,路徑為.logout。
- invalidateHttpSession():表示是否使session失效,默認(rèn)為true。
- clearAuthentication():表示是否清除認(rèn)真信息,默認(rèn)為true。
- logoutSuccessUrl():表示注銷登錄后的跳轉(zhuǎn)地址。
? 配置完成后,再次啟動(dòng)工程,登陸成功后,在瀏覽器中輸入http://127.0.0.1:9096/logout就可以發(fā)起注銷登錄請(qǐng)求了。注銷成功后,會(huì)自動(dòng)跳轉(zhuǎn)到loginTemplate.html頁(yè)面。
? 開(kāi)發(fā)者也可以配置多個(gè)注銷登錄的請(qǐng)求,同時(shí)還可以指定請(qǐng)求的方法:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()),
new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
))
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/router/loginTemplate")
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 在logoutRequestMatcher()的配置表示注銷請(qǐng)求路徑,分別有兩個(gè):
- 第一個(gè)是
/logout1,請(qǐng)求方法是GET。 - 第二個(gè)是
/logout2,請(qǐng)求方法是POST
? 使用其中的任意一個(gè)請(qǐng)求都可以完成登陸注銷。
? 如果項(xiàng)目采用的是前后端分離架構(gòu),注銷成功后就需要頁(yè)面跳轉(zhuǎn)了。只需要將注銷成功的信息返回給前端即可,此時(shí)可以自定義返回內(nèi)容:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()),
new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
))
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessHandler((request, response, auth) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", "logout success!");
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
})
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 配置logoutSuccessHandler()和logoutSuccessUrl()類似于之前的successHandler和defaultSuccessUrl之間的關(guān)系,只是類不同。
? 配置完成之后,重啟項(xiàng)目,登陸成功后再注銷登錄。無(wú)論是/logout1還是/logout2進(jìn)行注銷,只要注銷成功,就會(huì)返回JSON。
? 如果開(kāi)發(fā)者希望為不同的注銷地址返回不同的結(jié)果,如下配置即可:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeHttpRequests()
.antMatchers("/loginFail")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/router/loginTemplate")
.loginProcessingUrl("/loginSystem")
.successHandler(customizeSuccessHandler())
.failureHandler(customizeFailureHandler())
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()),
new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
))
.invalidateHttpSession(true)
.clearAuthentication(true)
.defaultLogoutSuccessHandlerFor((request, response, auth) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", "logout1 success!");
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}, new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()))
.defaultLogoutSuccessHandlerFor((request, response, auth) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> resultMap = new HashMap<>(4);
resultMap.put("code", 200);
resultMap.put("msg", "logout2 success!");
resultMap.put("data", null);
String jsonResult = JSON.toJSONString(resultMap);
response.getWriter().write(jsonResult);
response.getWriter().close();
}, new AntPathRequestMatcher("/logout2", HttpMethod.POST.name()))
.permitAll()
.and()
.csrf().disable().build();
}
protected CustomizeAuthenticationSuccessHandler customizeSuccessHandler() {
return new CustomizeAuthenticationSuccessHandler();
}
protected CustomizeAuthenticationFailureHandler customizeFailureHandler() {
return new CustomizeAuthenticationFailureHandler();
}
}
? 通過(guò)defaultLogoutSuccessHandlerFor()方法可以注冊(cè)多個(gè)不同的注銷成功回調(diào)函數(shù),該方法第一個(gè)參數(shù)是注銷成功回調(diào),第二個(gè)參數(shù)則是具體的注銷登錄請(qǐng)求。當(dāng)用戶注銷成功之后,請(qǐng)求哪個(gè)注銷地址,就會(huì)返回對(duì)應(yīng)的響應(yīng)信息。
到此這篇關(guān)于Spring Security登錄表單配置的文章就介紹到這了,更多相關(guān)Spring Security登錄表單內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity表單配置之登錄成功及頁(yè)面跳轉(zhuǎn)原理解析
- SpringSecurity?表單登錄的實(shí)現(xiàn)
- SpringBoot基于SpringSecurity表單登錄和權(quán)限驗(yàn)證的示例
- SpringSecurity 自定義表單登錄的實(shí)現(xiàn)
- SpringSecurity 默認(rèn)表單登錄頁(yè)展示流程源碼
- Spring Security 表單登錄功能的實(shí)現(xiàn)方法
- Spring Security在標(biāo)準(zhǔn)登錄表單中添加一個(gè)額外的字段
- 最新Spring?Security實(shí)戰(zhàn)教程之表單登錄定制到處理邏輯的深度改造(最新推薦)
相關(guān)文章
SpringMVC+EasyUI實(shí)現(xiàn)頁(yè)面左側(cè)導(dǎo)航菜單功能
這篇文章主要介紹了SpringMVC+EasyUI實(shí)現(xiàn)頁(yè)面左側(cè)導(dǎo)航菜單功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09
Mybatis傳遞多個(gè)參數(shù)的解決辦法(三種)
這篇文章主要介紹了Mybatis傳遞多個(gè)參數(shù)的解決辦法(三種),個(gè)人覺(jué)得第三種解決辦法比較好用,有需要的朋友一起學(xué)習(xí)吧2016-05-05
ShardingSphere JDBC強(qiáng)制路由使用的項(xiàng)目實(shí)踐
在某些特定場(chǎng)景下,可能需要繞過(guò)分片規(guī)則直接定位到特定的數(shù)據(jù)庫(kù)或表,這種情況下就可以使用HintRouting,本文就來(lái)介紹一下ShardingSphere JDBC強(qiáng)制路由使用的項(xiàng)目實(shí)踐,感興趣的可以了解一下2024-06-06
Java中的Semaphore信號(hào)量簡(jiǎn)析
這篇文章主要介紹了Java中的Semaphore信號(hào)量簡(jiǎn)析,Semaphore:信號(hào)量,用來(lái)限制能同時(shí)訪問(wèn)共享資源的線程上限,使用Semaphore實(shí)現(xiàn)簡(jiǎn)單連接池,對(duì)比享元模式下的實(shí)現(xiàn)(用wait和notify),性能和可讀性要更好,需要的朋友可以參考下2023-12-12
Java基于Base64實(shí)現(xiàn)編碼解碼圖片文件
這篇文章主要介紹了Java基于Base64實(shí)現(xiàn)編碼解碼圖片文件,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03
詳解Spring-boot中讀取config配置文件的兩種方式
這篇文章主要介紹了詳解Spring-boot中讀取config配置文件的兩種方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
寧可用Lombok也不把成員設(shè)置為public原理解析
這篇文章主要為大家介紹了寧可用Lombok也不把成員設(shè)置為public原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

