SpringSecurity自定義資源攔截規(guī)則及登錄界面跳轉(zhuǎn)問題
由前面的學(xué)習(xí)可以知道,SS的默認的攔截規(guī)則很簡單,我們在項目中實際使用的時候往往需要更加復(fù)雜的攔截規(guī)則,這個時候就需要自定義一些攔截規(guī)則。
自定義攔截規(guī)則
在我們的項目中,資源往往是需要不同的權(quán)限才能操作的,可以分為下面幾種:
- 公共資源:可以隨意訪問
- 認證訪問:只有登錄了之后的用戶才能訪問。
- 授權(quán)訪問:登錄的用戶必須具有響應(yīng)的權(quán)限才能夠訪問。
我們想要自定義認證邏輯,就需要創(chuàng)建一些原來不存在的bean,這個時候就可以使@ConditionalOnMissingBean注解發(fā)現(xiàn)創(chuàng)建默認的實現(xiàn)類失效。
測試環(huán)境搭建
@RequestMapping("/public/test")
public String justatest(){
return "just a test,這個是公共資源!";
}
@RequestMapping("/private/t1")
public String t1(){
return "訪問受限資源!";
}下面我們重寫一個配置類去替換內(nèi)部默認的配置類
@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í)中我們知道了默認的登錄界面是在過濾器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)到我們自己寫的登錄頁面,


這里的前綴默認就是在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 #可以讓我們的修改立即生效另外把認證相關(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");//表單驗證的方式,同時指定默認的登錄界面
}這個時候再去訪問頁面就會跳轉(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")//表單驗證的方式,同時指定默認的登錄界面
//一旦自定義登錄界面必須指定登錄url
.loginProcessingUrl("/login")
.and()
.csrf().disable();這個時候就可以登錄成功了。
但是這時候要注意源碼中指定了登錄的參數(shù)名,只能是username和password。

這個時候可以進行修改如下
http.authorizeRequests()//開啟請求的權(quán)限管理
.mvcMatchers("/public/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")//表單驗證的方式,同時指定默認的登錄界面
//一旦自定義登錄界面必須指定登錄url
.loginProcessingUrl("/login")
.usernameParameter("uname")//指定登錄的參數(shù)
.passwordParameter("pwd")
// .successForwardUrl("")//默認驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后
//直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。
.defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎn)路徑,默認是請求重定向。 登錄成功之
//后會記住原來訪問的路徑,也可以再傳遞一個boolean參數(shù)指定地址默認false
.and()
.csrf().disable();前后端分離項目路徑跳轉(zhuǎ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")//表單驗證的方式,同時指定默認的登錄界面
//一旦自定義登錄界面必須指定登錄url
.loginProcessingUrl("/login")
// .usernameParameter("uname")
// .passwordParameter("pwd")
// .successForwardUrl("")//默認驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。
// .defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎn)路徑,默認是請求重定向。 登錄成功之后會記住原來訪問的路徑
.successHandler(new MyAuthenticationSuccessHandler())//前后端分離的處理方案
.and()
.csrf().disable();這個時候登錄成功返回的是一個json字符串。

身份驗證失敗跳轉(zhuǎn)
首先點進



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);
}然后最后一句代碼authenticate(authRequest)會進入
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;
}
}這一塊會進入
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")//表單驗證的方式,同時指定默認的登錄界面
//一旦自定義登錄界面必須指定登錄url
.loginProcessingUrl("/login")
// .usernameParameter("uname")
// .passwordParameter("pwd")
// .successForwardUrl("")//默認驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。
// .defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎ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作用域中。
前后端分離項目認證失敗處理
實現(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);
}
}配置認證失敗接口實現(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")//表單驗證的方式,同時指定默認的登錄界面
//一旦自定義登錄界面必須指定登錄url
.loginProcessingUrl("/login")
// .usernameParameter("uname")
// .passwordParameter("pwd")
// .successForwardUrl("")//默認驗證成功之后的跳轉(zhuǎn),這個是請求轉(zhuǎn)發(fā), 登錄成功之后直接跳轉(zhuǎn)到這個指定的地址,原來的地址不跳轉(zhuǎn)了。
// .defaultSuccessUrl("")//這個也是成功之后的跳轉(zhuǎ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)文章希望大家以后多多支持腳本之家!
- SpringSecurity攔截器鏈的使用詳解
- springsecurity實現(xiàn)攔截器的使用示例
- SpringBoot整合SpringSecurity實現(xiàn)認證攔截的教程
- Swagger2不被SpringSecurity框架攔截的配置及說明
- Spring Boot security 默認攔截靜態(tài)資源的解決方法
- SpringSecurity實現(xiàn)動態(tài)url攔截(基于rbac模型)
- Spring Security攔截器引起Java CORS跨域失敗的問題及解決
- SpringBoot+SpringSecurity 不攔截靜態(tài)資源的實現(xiàn)
- 淺談Spring Security 對于靜態(tài)資源的攔截與放行
- spring Security配置攔截規(guī)則小結(jié)
相關(guān)文章
WebUploader+SpringMVC實現(xiàn)文件上傳功能
WebUploader是由Baidu團隊開發(fā)的一個簡單的以HTML5為主,F(xiàn)LASH為輔的現(xiàn)代文件上傳組件。這篇文章主要介紹了WebUploader+SpringMVC實現(xiàn)文件上傳功能,需要的朋友可以參考下2017-06-06
Java Collections.shuffle()方法案例詳解
這篇文章主要介紹了Java Collections.shuffle()方法案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08
Spring中Websocket身份驗證和授權(quán)的實現(xiàn)
在Web應(yīng)用開發(fā)中,安全一直是非常重要的一個方面,本文主要介紹了Spring中Websocket身份驗證和授權(quán)的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-08-08
idea新建mapper.xml文件詳細步驟如:mybatis-config
這篇文章主要介紹了idea新建xml模板設(shè)置,例如:mybatis-config,本文分步驟通過圖文并茂的形式給大家介紹的非常詳細,需要的朋友可以參考下2023-07-07
從前端Vue到后端Spring Boot接收JSON數(shù)據(jù)的正確姿勢(常見錯誤及問題)
這篇文章主要介紹了從前端Vue到后端Spring Boot接收JSON數(shù)據(jù)的正確姿勢(常見錯誤及問題),本文將從前端Vue到后端Spring Boot,詳細介紹接收JSON數(shù)據(jù)的正確姿勢,幫助開發(fā)人員更好地處理JSON數(shù)據(jù),感興趣的朋友一起看看吧2024-02-02
SpringCloud分布式鏈路追蹤組件Sleuth配置詳解
這篇文章主要介紹了SpringCloud鏈路追蹤組件Sleuth配置方法解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-11-11

