更新時(shí)間:2022-11-17 來(lái)源:黑馬程序員 瀏覽量:
一、文章導(dǎo)讀
SpringSecurity是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問(wèn)控制解決方案的安全框架。當(dāng)我們使用SpringSecurity自定義登錄頁(yè)面的時(shí)候,如果發(fā)生登陸失敗的情況,如何在自己的頁(yè)面上顯示相應(yīng)的錯(cuò)誤信息?
針對(duì)上面所描述的問(wèn)題,本次我們將通過(guò)具體的案例來(lái)將問(wèn)題進(jìn)行描述并提出對(duì)應(yīng)的解決方案,我們將從如下方面進(jìn)行具體講解:
SpringSecurity環(huán)境搭建
更換自己的登錄頁(yè)面
在登錄頁(yè)面顯示錯(cuò)誤信息
自定義用戶名不存在的錯(cuò)誤信息
自定義用戶密碼錯(cuò)誤的錯(cuò)誤信息
二、SpringSecurity環(huán)境搭建
1.創(chuàng)建maven工程項(xiàng)目
使用IDEA進(jìn)行Maven項(xiàng)目的創(chuàng)建,并將項(xiàng)目構(gòu)建為web項(xiàng)目,創(chuàng)建方式如下:
2.在項(xiàng)目的pom.xml添加依賴
<dependencies> <!--SpringMVC與Spring相關(guān)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添加委托過(guò)濾器代理類(lèi)
<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"/> <!--采用默認(rèn)springSecurity提供的默認(rèn)登陸頁(yè)面 --> <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實(shí)現(xiàn)類(lèi)"></bean> </beans>
5.添加UserDetailsService與TbUser
UserService類(lèi)
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類(lèi)
public class TbUser { private Integer id; private String username; private String password; private String role; //setter...getter...constructor...省略 }
6.運(yùn)行測(cè)試
三、更換自己的登錄頁(yè)面
1.修改springsecurity.xml
在配置文件中添加security:form-login相關(guān)配置,配置內(nèi)容如下:
<security:http use-expressions="false"> <security:intercept-url pattern="/**" access="ROLE_ADMIN"/> <!--采用默認(rèn)springSecurity提供的默認(rèn)登陸頁(yè)面 --> <!--<security:form-login/>--> <!--更換自己的登陸頁(yè)面 login-page:設(shè)定登陸login.html頁(yè)面 default-target-url:默認(rèn)登錄成功后跳轉(zhuǎn)的url authentication-failure-forward-url:登陸失敗后跳轉(zhuǎn)的頁(yè)面 --> <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下添加登錄頁(yè)面
<!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>自定義登陸頁(yè)面</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.啟動(dòng)服務(wù),運(yùn)行測(cè)試
遇到問(wèn)題后,如何在登陸失敗后像SpringSecurity原帶登陸頁(yè)面那樣在頁(yè)面上顯示出對(duì)應(yīng)的錯(cuò)誤信息呢?
四、在登錄頁(yè)面顯示錯(cuò)誤信息
1.改springsecurity.xml配置文件
添加上述內(nèi)容后,當(dāng)再次訪問(wèn)頁(yè)面,如果用戶名或密碼輸入錯(cuò)誤,則會(huì)重新跳回到登陸頁(yè)面,并且會(huì)在瀏覽器上把error=true顯示在瀏覽器中,如下圖所示:
在頁(yè)面加載完后,獲取瀏覽器URL后面的error屬性,判斷如果值為true,則說(shuō)明登陸失敗,發(fā)送請(qǐng)求從后臺(tái)獲取登陸錯(cuò)誤信息,修改代碼如下:
2.在后臺(tái)提供查詢錯(cuò)誤信息服務(wù)
編寫(xiě)Controller類(lèi),用來(lái)提供前端頁(yè)面對(duì)錯(cuò)誤信息查詢的服務(wù)
@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.啟動(dòng)服務(wù),運(yùn)行測(cè)試
雖然錯(cuò)誤信息能在頁(yè)面上進(jìn)行展示錯(cuò)誤信息,但是錯(cuò)誤信息為英文,如何將錯(cuò)誤信息轉(zhuǎn)換成中文呢?
五、自定義用戶名不存在的錯(cuò)誤信息
1.更改錯(cuò)誤信息
在UserService中,當(dāng)用戶名不存在,可以通過(guò)拋出異常的方式來(lái)更改錯(cuò)誤信息,修改代碼如下:
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;對(duì)比上述代碼,修改內(nèi)容如下 throw new UsernameNotFoundException("用戶名不存在"); } } }
2.啟動(dòng)服務(wù),運(yùn)行測(cè)試
重新啟動(dòng)服務(wù)器,測(cè)試后會(huì)發(fā)現(xiàn)頁(yè)面顯示的錯(cuò)誤信息更改為如下內(nèi)容:
3.分析問(wèn)題及解決方案
為什么沒(méi)有顯示"用戶名不存在"的錯(cuò)誤信息呢?經(jīng)過(guò)讀取源碼可發(fā)現(xiàn)如下內(nèi)容,DaoAuthenticationProvider類(lèi)中有如下代碼:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //會(huì)調(diào)用自定義UserService中提供的loadUserByUsername方法,當(dāng)該方法拋出異常后會(huì)被捕獲到 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); } }
當(dāng)上述方法出現(xiàn)異常后,會(huì)拋給父類(lèi)進(jì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,所以當(dāng)出現(xiàn)UsernameNotFoundException會(huì)被封裝成 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); }
上述父類(lèi)的代碼中的hideUserNotFoundExceptions默認(rèn)為true,所以UsernameNotFoundException會(huì)被封裝成BadCredentialsException異常信息,所以需要修改hideUserNotFoundExceptions的值,通過(guò)以下配置文件進(jìn)行設(shè)置,springsecurity.xml修改內(nèi)容如下:
啟動(dòng)運(yùn)行測(cè)試結(jié)果如下:
但是當(dāng)用戶輸入的密碼錯(cuò)誤依然是"Bad credentials",那該如何解決?
六、自定義用戶密碼錯(cuò)誤的錯(cuò)誤信息
1.更改國(guó)際化文件
默認(rèn)情況下springsecurity會(huì)從messages.properties中取對(duì)應(yīng)的錯(cuò)誤信息,在當(dāng)前包下,會(huì)有一個(gè)對(duì)應(yīng)的messages_zh_CN.properties中有對(duì)應(yīng)的中文錯(cuò)誤信息,該如何更改默認(rèn)獲取錯(cuò)誤的文件?
課有采用如下方式進(jìn)行修改:
修改完后,啟動(dòng)測(cè)試,效果如下:
2.更改顯示內(nèi)容
上面已經(jīng)能將錯(cuò)誤信息顯示成"壞的憑證"用戶看著不是特別明白,所以可以通過(guò)自定義的方式進(jìn)行錯(cuò)誤信息展示,在自己項(xiàng)目中的resources目錄下新建一個(gè) messages目錄,在messages目錄下創(chuàng)建一個(gè)名字為messages_zh_CN.properties的配置文件,在配置文件中添加如下內(nèi)容:
并將springsecurity.xml配置文件中將獲取錯(cuò)誤信息的配置文件路徑更改成如下內(nèi)容:
3.啟動(dòng)服務(wù),運(yùn)行測(cè)試
至此,對(duì)應(yīng)的錯(cuò)誤信息就已經(jīng)能夠按照我們的意愿進(jìn)行顯示。
七、總結(jié)
通過(guò)上述方式即可將錯(cuò)誤信息進(jìn)行自定義展示,當(dāng)前springsecurity默認(rèn)的錯(cuò)誤信息還有非常多,大家可以根據(jù)自己的需要將錯(cuò)誤信息進(jìn)行自定義展示。下面提供一個(gè)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"/> <!--采用默認(rèn)springSecurity提供的默認(rèn)登陸頁(yè)面 --> <!--<security:form-login/>--> <!--更換自己的登陸頁(yè)面 login-page:設(shè)定登陸login.html頁(yè)面 default-target-url:默認(rèn)登錄成功后跳轉(zhuǎn)的url authentication-failure-forward-url:登陸失敗后跳轉(zhuǎn)的頁(yè)面 --> <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實(shí)現(xiàn)類(lèi)"></bean> </beans>