基于Spring-Security自定義登陸錯誤提示信息
實現效果如圖所示:
首先公布實現代碼:
一. 自定義實現
import.org.springframework.security.core.userdetails.UserDetailsService類
并且拋出BadCredentialsException異常,否則頁面無法獲取到錯誤信息。
@Slf4j @Service public class MyUserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserService userService; @Autowired private PermissionService permissionService; private String passwordParameter = "password"; @Override public UserDetails loadUserByUsername(String username) throws AuthenticationException { HttpServletRequest request = ContextHolderUtils.getRequest(); String password = request.getParameter(passwordParameter); log.error("password = {}", password); SysUser sysUser = userService.getByUsername(username); if (null == sysUser) { log.error("用戶{}不存在", username); throw new BadCredentialsException("帳號不存在,請重新輸入"); } // 自定義業(yè)務邏輯校驗 if ("userli".equals(sysUser.getUsername())) { throw new BadCredentialsException("您的帳號有違規(guī)記錄,無法登錄!"); } // 自定義密碼驗證 if (!password.equals(sysUser.getPassword())){ throw new BadCredentialsException("密碼錯誤,請重新輸入"); } List<SysPermission> permissionList = permissionService.findByUserId(sysUser.getId()); List<SimpleGrantedAuthority> authorityList = new ArrayList<>(); if (!CollectionUtils.isEmpty(permissionList)) { for (SysPermission sysPermission : permissionList) { authorityList.add(new SimpleGrantedAuthority(sysPermission.getCode())); } } User myUser = new User(sysUser.getUsername(), passwordEncoder.encode(sysUser.getPassword()), authorityList); log.info("登錄成功!用戶: {}", myUser); return myUser; } }
二. 實現自定義登陸頁面
前提是,你們已經解決了自定義登陸頁面配置的問題,這里不做討論。
通過 thymeleaf 表達式獲取錯誤信息(我們選擇thymeleaf模板引擎)
<p style="color: red" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></p>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>XX相親網</title> <meta name="description" content="Ela Admin - HTML5 Admin Template"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body class="mui-content"> <div id="d1"> <div class="first"> <img class="hosp" th:src="@{/images/dashboard/hospital.png}"/> <div class="hospital">XX相親網</div> </div> <div class="sufee-login d-flex align-content-center flex-wrap"> <div class="container"> <div class="login-content"> <div class="login-logo"> <h1 style="color: #385978;font-size: 24px">XX相親網</h1> <h1 style="color: #385978;font-size: 24px">登錄</h1> </div> <div class="login-form"> <form th:action="@{/login}" method="post"> <div class="form-group"> <input type="text" class="form-control" name="username" placeholder="請輸入帳號"> </div> <div class="form-group"> <input type="password" class="form-control" name="password" placeholder="請輸入密碼"> </div> <div> <button type="submit" class="button-style"> <span class="in">登錄</span> </button> </div> <p style="color: red" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"> </p> </form> </div> </div> </div> </div> </div> </body> </html>
Spring-Security登陸表單提交過程
當用戶從登錄頁提交賬號密碼的時候,首先由
org.springframework.security.web.authentication包下的UsernamePasswordAuthenticationFilter類attemptAuthentication()
方法來處理登陸邏輯。
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
1. 該類內部默認的登錄請求url是"/login",并且只允許POST方式的請求。
2. obtainUsername()方法參數名為"username"和"password"從HttpServletRequest中獲取用戶名和密碼(由此可以找到突破口,我們可以在自定義實現的loadUserByUsername方法中獲取到提交的賬號和密碼,進而檢查正則性)。
3. 通過構造方法UsernamePasswordAuthenticationToken,將用戶名和密碼分別賦值給principal和credentials。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); }
super(null)調用的是父類的構造方法,傳入的是權限集合,因為目前還沒有認證通過,所以不知道有什么權限信息,這里設置為null,然后將用戶名和密碼分別賦值給principal和credentials,同樣因為此時還未進行身份認證,所以setAuthenticated(false)。
到此為止,用戶提交的表單信息已加載完成,繼續(xù)往下則是校驗表單提交的賬號和密碼是否正確。
那么異常一下是如何傳遞給前端的呢
前面提到用戶登錄驗證的過濾器是UsernamePasswordAuthenticationFilter,它繼承自AbstractAuthenticationProcessingFilter。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); }
從代碼片段中看到Spring將異常捕獲后交給了unsuccessfulAuthentication這個方法來處理。
unsuccessfulAuthentication又交給了failureHandler(AuthenticationFailureHandler)來處理,然后追蹤failureHandler
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString(), failed); logger.debug("Updated SecurityContextHolder to contain null Authentication"); logger.debug("Delegating to authentication failure handler " + failureHandler); } rememberMeServices.loginFail(request, response); failureHandler.onAuthenticationFailure(request, response, failed); }
Ctrl + 左鍵 追蹤failureHandler引用的類是,SimpleUrlAuthenticationFailureHandler。
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
找到SimpleUrlAuthenticationFailureHandler類中的,onAuthenticationFailure()方法。
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (defaultFailureUrl == null) { logger.debug("No failure URL set, sending 401 Unauthorized error"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed: " + exception.getMessage()); } else { saveException(request, exception); if (forwardToDestination) { logger.debug("Forwarding to " + defaultFailureUrl); request.getRequestDispatcher(defaultFailureUrl) .forward(request, response); } else { logger.debug("Redirecting to " + defaultFailureUrl); redirectStrategy.sendRedirect(request, response, defaultFailureUrl); } } }
追蹤到saveException(request, exception)的內部實現。
protected final void saveException(HttpServletRequest request, AuthenticationException exception) { if (forwardToDestination) { request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } else { HttpSession session = request.getSession(false); if (session != null || allowSessionCreation) { request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } } }
此處的
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
就是存儲到session中的錯誤信息,key就是
public static final String AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";
因此我們通過thymeleaf模板引擎的表達式可獲得session的信息。
獲取方式
<p style="color: red" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"> </p>
需要注意:saveException保存的是Session對象所以需要使用${SPRING_SECURITY_LAST_EXCEPTION.message}獲取。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
深入淺出分析Java抽象類和接口【功能,定義,用法,區(qū)別】
這篇文章主要介紹了Java抽象類和接口,結合實例形式深入淺出的分析了java抽象類與接口的功能功能,定義,用法及區(qū)別,需要的朋友可以參考下2017-08-08Java?常量池詳解之class文件常量池?和class運行時常量池
這篇文章主要介紹了Java?常量池詳解之class文件常量池?和class運行時常量池,常量池主要存放兩大類常量:字面量,符號引用,本文結合示例代碼對java class常量池相關知識介紹的非常詳細,需要的朋友可以參考下2022-12-12Java索引越界異常Exception java.lang.IndexOutOfBoundsException
本文主要介紹了Java索引越界異常Exception java.lang.IndexOutOfBoundsException的解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-06-06Google Kaptcha 框架實現登錄驗證碼功能(SSM 和 SpringBoot)
這篇文章主要介紹了Google Kaptcha 實現登錄驗證碼(SSM 和 SpringBoot)功能,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2018-12-12Springboot報錯java.lang.NullPointerException: null問題
這篇文章主要介紹了Springboot報錯java.lang.NullPointerException: null問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11