SpringSecurity自定義資源攔截規(guī)則及登錄界面跳轉(zhuǎn)問題
由前面的學(xué)習(xí)可以知道,SS的默認(rèn)的攔截規(guī)則很簡單,我們在項目中實際使用的時候往往需要更加復(fù)雜的攔截規(guī)則,這個時候就需要自定義一些攔截規(guī)則。
自定義攔截規(guī)則
在我們的項目中,資源往往是需要不同的權(quán)限才能操作的,可以分為下面幾種:
- 公共資源:可以隨意訪問
- 認(rèn)證訪問:只有登錄了之后的用戶才能訪問。
- 授權(quán)訪問:登錄的用戶必須具有響應(yīng)的權(quán)限才能夠訪問。
我們想要自定義認(rèn)證邏輯,就需要創(chuàng)建一些原來不存在的bean,這個時候就可以使@ConditionalOnMissingBean
注解發(fā)現(xiàn)創(chuàng)建默認(rèn)的實現(xiàn)類失效。
測試環(huán)境搭建
@RequestMapping("/public/test") public String justatest(){ return "just a test,這個是公共資源!"; } @RequestMapping("/private/t1") public String t1(){ return "訪問受限資源!"; }
下面我們重寫一個配置類去替換內(nèi)部默認(rèn)的配置類
@Configuration public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { //ss里面要求放行的資源要寫在任何請求的前面 http.authorizeRequests()//開啟請求的權(quán)限管理 .mvcMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin();//表單驗證的方式 } }
下面測試,訪問公共資源
訪問/private/t1跳轉(zhuǎn)到
輸入賬號密碼之后訪問到
自定義登錄界面
在前面的學(xué)習(xí)中我們知道了默認(rèn)的登錄界面是在過濾器DefaultLoginPageGeneratingFilter
里面實現(xiàn)的,現(xiàn)在我們想要自定義一個登錄界面。
首先引入thymeleaf依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
在templates目錄下面創(chuàng)建一個login的html頁面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>冬木自定義用戶登錄</title> </head> <body> <form th:action="@{/login}" method="post"> 用戶名:<input type="text" name="username"><br> 密碼:<input type="text" name="password"><br> <input type="submit" name="登錄"> </form> </body> </html>
編寫一個controller接口用于跳轉(zhuǎn)到我們自己寫的登錄頁面,
這里的前綴默認(rèn)就是在templates下面因此我下面直接return login
package com.dongmu.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class LoginController { @RequestMapping("/login.html") public String login(){ return "login"; } }
添加配置路徑,
spring: thymeleaf: cache: false #可以讓我們的修改立即生效
另外把認(rèn)證相關(guān)的接口放行
@Override protected void configure(HttpSecurity http) throws Exception { //ss里面要求放行的資源要寫在任何請求的前面 http.authorizeRequests()//開啟請求的權(quán)限管理 .mvcMatchers("/public/**").permitAll() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login");//表單驗證的方式,同時指定默認(rèn)的登錄界面 }
這個時候再去訪問頁面就會跳轉(zhuǎn)到下面這個頁面
這個時候登錄會發(fā)現(xiàn)還是條狀到登錄頁面,這里要注意,一旦指自定義了登錄頁面就需要指定登錄的url,所以我們在接口里面添加下面的代碼
//ss里面要求放行的資源要寫在任何請求的前面 http.authorizeRequests()//開啟請求的權(quán)限管理 .mvcMatchers("/public/**").permitAll() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定默認(rèn)的登錄界面 //一旦自定義登錄界面必須指定登錄url .loginProcessingUrl("/login") .and() .csrf().disable();
這個時候就可以登錄成功了。
但是這時候要注意源碼中指定了登錄的參數(shù)名,只能是username和password。
這個時候可以進(jìn)行修改如下
http.authorizeRequests()//開啟請求的權(quán)限管理 .mvcMatchers("/public/**").permitAll() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定默認(rèn)的登錄界面 //一旦自定義登錄界面必須指定登錄url .loginProcessingUrl("/login") .usernameParameter("uname")//指定登錄的參數(shù) .passwordParameter("pwd") // .successForwardUrl("")//默認(rèn)驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后 //直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。 .defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎn)路徑,默認(rèn)是請求重定向。 登錄成功之 //后會記住原來訪問的路徑,也可以再傳遞一個boolean參數(shù)指定地址默認(rèn)false .and() .csrf().disable();
前后端分離項目路徑跳轉(zhuǎn)
前面介紹了前后端不分離項目的登錄認(rèn)證成功之后的路徑跳轉(zhuǎn),但是針對于前后端分離項目,比如有的時候可能會發(fā)送AJAX請求,這個時候怎么處理呢?
我們可以自定義一個類實現(xiàn)AuthenticationSuccessHandler
接口即可。
package com.dongmu.config; import com.fasterxml.jackson.databind.ObjectMapper; 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; public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { HashMap<String,Object> hashMap = new HashMap<>(); hashMap.put("msg","登錄成功"); hashMap.put("code",200); hashMap.put("auth",authentication); response.setContentType("application/json;charset=utf-8"); String s = new ObjectMapper().writeValueAsString(hashMap); response.getWriter().write(s); } }
在successHandler里面配置即可
//ss里面要求放行的資源要寫在任何請求的前面 http.authorizeRequests()//開啟請求的權(quán)限管理 .mvcMatchers("/public/**").permitAll() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定默認(rèn)的登錄界面 //一旦自定義登錄界面必須指定登錄url .loginProcessingUrl("/login") // .usernameParameter("uname") // .passwordParameter("pwd") // .successForwardUrl("")//默認(rèn)驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。 // .defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎn)路徑,默認(rèn)是請求重定向。 登錄成功之后會記住原來訪問的路徑 .successHandler(new MyAuthenticationSuccessHandler())//前后端分離的處理方案 .and() .csrf().disable();
這個時候登錄成功返回的是一個json字符串。
身份驗證失敗跳轉(zhuǎn)
首先點進(jìn)
UsernamePasswordAuthenticationFilter
這個類里面由一個方法attemptAuthentication
進(jìn)行身份的驗證
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); }
然后最后一句代碼authenticate(authRequest)
會進(jìn)入
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException e) { lastException = parentException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } // If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } // If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it if (parentException == null) { prepareException(lastException, authentication); } throw lastException; }
上面代碼中
try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } }
這一塊會進(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); }
這里面user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
的實現(xiàn)
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { 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); } }
可以發(fā)現(xiàn)這里就是去一開始我們學(xué)習(xí)的map里面找到對應(yīng)用戶名和密碼,這里面應(yīng)該會報出異常。這個異常后面會被這個方法接收
private void doAuthenticate(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Authentication authResult; Object principal = getPreAuthenticatedPrincipal(request); Object credentials = getPreAuthenticatedCredentials(request); if (principal == null) { if (logger.isDebugEnabled()) { logger.debug("No pre-authenticated principal found in request"); } return; } if (logger.isDebugEnabled()) { logger.debug("preAuthenticatedPrincipal = " + principal + ", trying to authenticate"); } try { PreAuthenticatedAuthenticationToken authRequest = new PreAuthenticatedAuthenticationToken( principal, credentials); authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); authResult = authenticationManager.authenticate(authRequest); successfulAuthentication(request, response, authResult); } catch (AuthenticationException failed) { unsuccessfulAuthentication(request, response, failed); if (!continueFilterChainOnUnsuccessfulAuthentication) { throw failed; } } }
執(zhí)行unsuccessfulAuthentication
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Cleared security context due to exception", failed); } //這里會把異常信息放到request作用域當(dāng)中 request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, failed); if (authenticationFailureHandler != null) { authenticationFailureHandler.onAuthenticationFailure(request, response, failed); } }
這里配置請求轉(zhuǎn)發(fā)
protected void configure(HttpSecurity http) throws Exception { //ss里面要求放行的資源要寫在任何請求的前面 http.authorizeRequests()//開啟請求的權(quán)限管理 .mvcMatchers("/public/**").permitAll() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定默認(rèn)的登錄界面 //一旦自定義登錄界面必須指定登錄url .loginProcessingUrl("/login") // .usernameParameter("uname") // .passwordParameter("pwd") // .successForwardUrl("")//默認(rèn)驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。 // .defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎn)路徑,默認(rèn)是請求重定向。 登錄成功之后會記住原來訪問的路徑 .successHandler(new MyAuthenticationSuccessHandler())//前后端分離的處理方案 .failureForwardUrl("/login.html")//登錄失敗之后的請求轉(zhuǎn)發(fā)頁面 // .failureUrl("/login.html")//登錄失敗之后的重定向頁面 .and() .csrf().disable(); }
可以直接從request作用域中獲取異常
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>冬木自定義用戶登錄</title> </head> <h2> <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div> </h2> <body> <form th:action="@{/login}" method="post"> 用戶名:<input type="text" name="username"><br> 密碼:<input type="text" name="password"><br> <input type="submit" name="登錄"> </form> </body> </html>
如果是在重定向就會放在session作用域中。如果是請求轉(zhuǎn)發(fā)就會放到reques作用域中。
前后端分離項目認(rèn)證失敗處理
實現(xiàn)接口AuthenticationFailureHandler
package com.dongmu.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; 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; public class MyAuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { HashMap<String,Object> hashMap = new HashMap<>(); hashMap.put("msg","登錄成功"); hashMap.put("code",200); hashMap.put("auth",authentication); response.setContentType("application/json;charset=utf-8"); String s = new ObjectMapper().writeValueAsString(hashMap); response.getWriter().write(s); } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { HashMap<String,Object> hashMap = new HashMap<>(); hashMap.put("code",403); hashMap.put("msg",exception.getMessage()); response.setContentType("application/json;charset=utf-8"); String s = new ObjectMapper().writeValueAsString(hashMap); response.getWriter().write(s); } }
配置認(rèn)證失敗接口實現(xiàn)類
protected void configure(HttpSecurity http) throws Exception { //ss里面要求放行的資源要寫在任何請求的前面 http.authorizeRequests()//開啟請求的權(quán)限管理 .mvcMatchers("/public/**").permitAll() .mvcMatchers("/login.html").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login.html")//表單驗證的方式,同時指定默認(rèn)的登錄界面 //一旦自定義登錄界面必須指定登錄url .loginProcessingUrl("/login") // .usernameParameter("uname") // .passwordParameter("pwd") // .successForwardUrl("")//默認(rèn)驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。 // .defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎn)路徑,默認(rèn)是請求重定向。 登錄成功之后會記住原來訪問的路徑 // .successHandler(new MyAuthenticationSuccessHandler())//前后端分離的處理方案 // .failureForwardUrl("/login.html")//登錄失敗之后的請求轉(zhuǎn)發(fā)頁面 .failureUrl("/login.html")//登錄失敗之后的重定向頁面 .failureHandler(new MyAuthenticationHandler()) .and() .csrf().disable(); }
到此這篇關(guān)于SpringSecurity自定義資源攔截規(guī)則以及登錄界面跳轉(zhuǎn)的文章就介紹到這了,更多相關(guān)SpringSecurity登錄界面跳轉(zhuǎn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java利用url實現(xiàn)網(wǎng)頁內(nèi)容的抓取
本文主要介紹了java利用url實現(xiàn)網(wǎng)頁內(nèi)容抓取的示例。具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03使用feign調(diào)用接口時調(diào)不到get方法的問題及解決
這篇文章主要介紹了使用feign調(diào)用接口時調(diào)不到get方法的問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03