參考視訊,程式設計不良人
由前面的學習可以知道,SS的預設的攔截規則很簡單,我們在項目中實際使用的時候往往需要更加複雜的攔截規則,這個時候就需要自定義一些攔截規則。
自定義攔截規則
在我們的項目中,資源往往是需要不同的權限才能操作的,可以分為下面幾種:
- 公共資源:可以随意通路
- 認證通路:隻有登入了之後的使用者才能通路。
- 授權通路:登入的使用者必須具有響應的權限才能夠通路。
我們想要自定義認證邏輯,就需要建立一些原來不存在的bean,這個時候就可以使
@ConditionalOnMissingBean
注解發現建立預設的實作類失效。
測試環境搭建
@RequestMapping("/public/test")
public String justatest(){
return "just a test,這個是公共資源!";
}
@RequestMapping("/private/t1")
public String t1(){
return "通路受限資源!";
}
下面我們重寫一個配置類去替換内部預設的配置類
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//ss裡面要求放行的資源要寫在任何請求的前面
http.authorizeRequests()//開啟請求的權限管理
.mvcMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();//表單驗證的方式
}
}
下面測試,通路公共資源
通路/private/t1跳轉到
輸入賬号密碼之後通路到
自定義登入界面
在前面的學習中我們知道了預設的登入界面是在過濾器
DefaultLoginPageGeneratingFilter
裡面實作的,現在我們想要自定義一個登入界面。
- 首先引入thymeleaf依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 在templates目錄下面建立一個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接口用于跳轉到我們自己寫的登入頁面,
這裡的字首預設就是在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 #可以讓我們的修改立即生效
另外把認證相關的接口放行
@Override
protected void configure(HttpSecurity http) throws Exception {
//ss裡面要求放行的資源要寫在任何請求的前面
http.authorizeRequests()//開啟請求的權限管理
.mvcMatchers("/public/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login");//表單驗證的方式,同時指定預設的登入界面
}
這個時候再去通路頁面就會跳轉到下面這個頁面
這個時候登入會發現還是條狀到登入頁面,這裡要注意,一旦指自定義了登入頁面就需要指定登入的url,是以我們在接口裡面添加下面的代碼
//ss裡面要求放行的資源要寫在任何請求的前面
http.authorizeRequests()//開啟請求的權限管理
.mvcMatchers("/public/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
//一旦自定義登入界面必須指定登入url
.loginProcessingUrl("/login")
.and()
.csrf().disable();
這個時候就可以登入成功了。
但是這時候要注意源碼中指定了登入的參數名,隻能是username和password。
這個時候可以進行修改如下
http.authorizeRequests()//開啟請求的權限管理
.mvcMatchers("/public/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
//一旦自定義登入界面必須指定登入url
.loginProcessingUrl("/login")
.usernameParameter("uname")//指定登入的參數
.passwordParameter("pwd")
// .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後
//直接跳轉到這個指定的位址,原來的位址不跳轉了。
.defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之
//後會記住原來通路的路徑,也可以再傳遞一個boolean參數指定位址預設false
.and()
.csrf().disable();
前後端分離項目路徑跳轉
前面介紹了前後端不分離項目的登入認證成功之後的路徑跳轉,但是針對于前後端分離項目,比如有的時候可能會發送AJAX請求,這個時候怎麼處理呢?
我們可以自定義一個類實作
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()//開啟請求的權限管理
.mvcMatchers("/public/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
//一旦自定義登入界面必須指定登入url
.loginProcessingUrl("/login")
// .usernameParameter("uname")
// .passwordParameter("pwd")
// .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後直接跳轉到這個指定的位址,原來的位址不跳轉了。
// .defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之後會記住原來通路的路徑
.successHandler(new MyAuthenticationSuccessHandler())//前後端分離的處理方案
.and()
.csrf().disable();
這個時候登入成功傳回的是一個json字元串。
身份驗證失敗跳轉
首先點進
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);
的實作
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);
}
}
可以發現這裡就是去一開始我們學習的map裡面找到對應使用者名和密碼,這裡面應該會報出異常。這個異常後面會被這個方法接收
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;
}
}
}
執行
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作用域當中
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, failed);
if (authenticationFailureHandler != null) {
authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
}
}
這裡配置請求轉發
protected void configure(HttpSecurity http) throws Exception {
//ss裡面要求放行的資源要寫在任何請求的前面
http.authorizeRequests()//開啟請求的權限管理
.mvcMatchers("/public/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
//一旦自定義登入界面必須指定登入url
.loginProcessingUrl("/login")
// .usernameParameter("uname")
// .passwordParameter("pwd")
// .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後直接跳轉到這個指定的位址,原來的位址不跳轉了。
// .defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之後會記住原來通路的路徑
.successHandler(new MyAuthenticationSuccessHandler())//前後端分離的處理方案
.failureForwardUrl("/login.html")//登入失敗之後的請求轉發頁面
// .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作用域中。如果是請求轉發就會放到reques作用域中。
前後端分離項目認證失敗處理
實作接口
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);
}
}
配置認證失敗接口實作類
protected void configure(HttpSecurity http) throws Exception {
//ss裡面要求放行的資源要寫在任何請求的前面
http.authorizeRequests()//開啟請求的權限管理
.mvcMatchers("/public/**").permitAll()
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")//表單驗證的方式,同時指定預設的登入界面
//一旦自定義登入界面必須指定登入url
.loginProcessingUrl("/login")
// .usernameParameter("uname")
// .passwordParameter("pwd")
// .successForwardUrl("")//預設驗證成功之後的跳轉,這個是請求轉發, 登入成功之後直接跳轉到這個指定的位址,原來的位址不跳轉了。
// .defaultSuccessUrl("")//這個也是成功之後的跳轉路徑,預設是請求重定向。 登入成功之後會記住原來通路的路徑
// .successHandler(new MyAuthenticationSuccessHandler())//前後端分離的處理方案
// .failureForwardUrl("/login.html")//登入失敗之後的請求轉發頁面
.failureUrl("/login.html")//登入失敗之後的重定向頁面
.failureHandler(new MyAuthenticationHandler())
.and()
.csrf().disable();
}