更新時間:2022-11-17 來源:黑馬程序員 瀏覽量:

一、文章導讀
SpringSecurity是一個能夠為基于Spring的企業(yè)應用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架。當我們使用SpringSecurity自定義登錄頁面的時候,如果發(fā)生登陸失敗的情況,如何在自己的頁面上顯示相應的錯誤信息?
針對上面所描述的問題,本次我們將通過具體的案例來將問題進行描述并提出對應的解決方案,我們將從如下方面進行具體講解:
SpringSecurity環(huán)境搭建
更換自己的登錄頁面
在登錄頁面顯示錯誤信息
自定義用戶名不存在的錯誤信息
自定義用戶密碼錯誤的錯誤信息
二、SpringSecurity環(huán)境搭建
1.創(chuàng)建maven工程項目
使用IDEA進行Maven項目的創(chuàng)建,并將項目構建為web項目,創(chuàng)建方式如下:

2.在項目的pom.xml添加依賴
<dependencies> <!--SpringMVC與Spring相關jar包略 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>4.1.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>4.1.0.RELEASE</version> </dependency> </dependencies>
3.web.xml添加委托過濾器代理類
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
4.添加springSecurity的配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <!--配置放行資源 --> <security:http pattern="/login.html" security="none"/> <security:http pattern="/js/**" security="none"/> <!--配置攔截規(guī)則 --> <security:http use-expressions="false"> <security:intercept-url pattern="/**" access="ROLE_ADMIN"/> <!--采用默認springSecurity提供的默認登陸頁面 --> <security:form-login/> <security:csrf disabled="true"/> </security:http> <security:authentication-manager> <security:authentication-provider user-service-ref="userDetailService"/> </security:authentication-manager> <bean id="userDetailService" class="UserDetailService實現(xiàn)類"></bean> </beans>
5.添加UserDetailsService與TbUser
UserService類
public class UserService implements UserDetailsService {
public static Map<String, TbUser> users = new HashMap<String, TbUser>();
static {
users.put("root", new TbUser(1, "root", "root", "ROOT"));
users.put("admin", new TbUser(1, "admin", "admin", "ADMIN"));
}
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
if (users.containsKey(username)) {
//查詢到用戶信息
TbUser tbUser = users.get(username);
List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(new SimpleGrantedAuthority(tbUser.getRole()));
return new User(username, tbUser.getPassword(), list);
} else {
//未查詢到用戶信息
return null;
}
}
}TbUser類
public class TbUser {
private Integer id;
private String username;
private String password;
private String role;
//setter...getter...constructor...省略
} 6.運行測試

三、更換自己的登錄頁面
1.修改springsecurity.xml
在配置文件中添加security:form-login相關配置,配置內(nèi)容如下:
<security:http use-expressions="false"> <security:intercept-url pattern="/**" access="ROLE_ADMIN"/> <!--采用默認springSecurity提供的默認登陸頁面 --> <!--<security:form-login/>--> <!--更換自己的登陸頁面 login-page:設定登陸login.html頁面 default-target-url:默認登錄成功后跳轉的url authentication-failure-forward-url:登陸失敗后跳轉的頁面 --> <security:form-login login-page="/login.html" default-target-url="/index.html" authentication-failure-url="/login.html" /> <security:csrf disabled="true"/> </security:http>
2.在webapp下添加登錄頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script type="text/javascript" src="jquery.min.js"></script> <script type="text/javascript"> </script> </head> <body> <h1>自定義登陸頁面</h1> <form action="/login" method="post"> 用戶姓名:<input type="text" name="username"/><br/> 用戶密碼:<input type="password" name="password"/><br/> <input type="submit" value="登陸"/> </form> </body> </html>
3.啟動服務,運行測試

遇到問題后,如何在登陸失敗后像SpringSecurity原帶登陸頁面那樣在頁面上顯示出對應的錯誤信息呢?
四、在登錄頁面顯示錯誤信息
1.改springsecurity.xml配置文件

添加上述內(nèi)容后,當再次訪問頁面,如果用戶名或密碼輸入錯誤,則會重新跳回到登陸頁面,并且會在瀏覽器上把error=true顯示在瀏覽器中,如下圖所示:

在頁面加載完后,獲取瀏覽器URL后面的error屬性,判斷如果值為true,則說明登陸失敗,發(fā)送請求從后臺獲取登陸錯誤信息,修改代碼如下:

2.在后臺提供查詢錯誤信息服務
編寫Controller類,用來提供前端頁面對錯誤信息查詢的服務
@RestController
@RequestMapping("/login")
public class LoginController {
@RequestMapping("/getErrorMsg")
public Map getErrorMsg(HttpSession session){
Map map = new HashMap();
Exception e =(Exception)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
map.put("error",e.getMessage());
return map;
}
}3.啟動服務,運行測試

雖然錯誤信息能在頁面上進行展示錯誤信息,但是錯誤信息為英文,如何將錯誤信息轉換成中文呢?
五、自定義用戶名不存在的錯誤信息
1.更改錯誤信息
在UserService中,當用戶名不存在,可以通過拋出異常的方式來更改錯誤信息,修改代碼如下:
public class UserService implements UserDetailsService {
public static Map<String, TbUser> users = new HashMap<String, TbUser>();
static {
users.put("root", new TbUser(1, "root", "root", "ROOT"));
users.put("admin", new TbUser(1, "admin", "admin", "ADMIN"));
}
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
if (users.containsKey(username)) {
//查詢到用戶信息
TbUser tbUser = users.get(username);
List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(new SimpleGrantedAuthority(tbUser.getRole()));
return new User(username, tbUser.getPassword(), list);
} else {
//未查詢到用戶信息
//return null;對比上述代碼,修改內(nèi)容如下
throw new UsernameNotFoundException("用戶名不存在");
}
}
} 2.啟動服務,運行測試
重新啟動服務器,測試后會發(fā)現(xiàn)頁面顯示的錯誤信息更改為如下內(nèi)容:

3.分析問題及解決方案
為什么沒有顯示"用戶名不存在"的錯誤信息呢?經(jīng)過讀取源碼可發(fā)現(xiàn)如下內(nèi)容,DaoAuthenticationProvider類中有如下代碼:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//會調用自定義UserService中提供的loadUserByUsername方法,當該方法拋出異常后會被捕獲到
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}當上述方法出現(xiàn)異常后,會拋給父類進行捕獲
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
//此處hideUserNotFoundException為true,所以當出現(xiàn)UsernameNotFoundException會被封裝成 Bad Credentials信息
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
//....
return createSuccessAuthentication(principalToReturn, authentication, user);
}上述父類的代碼中的hideUserNotFoundExceptions默認為true,所以UsernameNotFoundException會被封裝成BadCredentialsException異常信息,所以需要修改hideUserNotFoundExceptions的值,通過以下配置文件進行設置,springsecurity.xml修改內(nèi)容如下:
啟動運行測試結果如下:

但是當用戶輸入的密碼錯誤依然是"Bad credentials",那該如何解決?
六、自定義用戶密碼錯誤的錯誤信息
1.更改國際化文件

默認情況下springsecurity會從messages.properties中取對應的錯誤信息,在當前包下,會有一個對應的messages_zh_CN.properties中有對應的中文錯誤信息,該如何更改默認獲取錯誤的文件?
課有采用如下方式進行修改:

修改完后,啟動測試,效果如下:

2.更改顯示內(nèi)容
上面已經(jīng)能將錯誤信息顯示成"壞的憑證"用戶看著不是特別明白,所以可以通過自定義的方式進行錯誤信息展示,在自己項目中的resources目錄下新建一個 messages目錄,在messages目錄下創(chuàng)建一個名字為messages_zh_CN.properties的配置文件,在配置文件中添加如下內(nèi)容:

并將springsecurity.xml配置文件中將獲取錯誤信息的配置文件路徑更改成如下內(nèi)容:

3.啟動服務,運行測試

至此,對應的錯誤信息就已經(jīng)能夠按照我們的意愿進行顯示。
七、總結
通過上述方式即可將錯誤信息進行自定義展示,當前springsecurity默認的錯誤信息還有非常多,大家可以根據(jù)自己的需要將錯誤信息進行自定義展示。下面提供一個springsecurity.xml的完整配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <!--配置放行資源 --> <security:http pattern="/login.html" security="none"/> <security:http pattern="/js/**" security="none"/> <security:http pattern="/login/getErrorMsg.do" security="none"/> <!--配置攔截規(guī)則 --> <security:http use-expressions="false"> <security:intercept-url pattern="/**" access="ROLE_ADMIN"/> <!--采用默認springSecurity提供的默認登陸頁面 --> <!--<security:form-login/>--> <!--更換自己的登陸頁面 login-page:設定登陸login.html頁面 default-target-url:默認登錄成功后跳轉的url authentication-failure-forward-url:登陸失敗后跳轉的頁面 --> <security:form-login login-page="/login.html" login-processing-url="/login" default-target-url="/index.html" authentication-failure-url="/login.html?error=true"/> <security:csrf disabled="true"/> </security:http> <security:authentication-manager> <!--<security:authentication-provider user-service-ref="userDetailService"> </security:authentication-provider>--> <security:authentication-provider ref="daoAuthenticationProvider"> </security:authentication-provider> </security:authentication-manager> <bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="hideUserNotFoundExceptions" value="false"/> <property name="userDetailsService" ref="userDetailService"/> <property name="messageSource" ref="messageSource"/> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basenames" value="classpath:messages/messages_zh_CN"/> </bean> <bean id="userDetailService" class="UserDetailService實現(xiàn)類"></bean> </beans>