SpringSecurity學習筆記
前言
版本說明
platform-bom=Cairo-SR7
spring-cloud-dependencies=Finchley.SR4
相關鏈接
- SpringSecurity 官網: https://spring.io/projects/spring-security
- SpringSecurity 官方文檔: https://docs.spring.io/spring-security/site/docs/5.2.2.BUILD-SNAPSHOT/reference/htmlsingle/
- maven 地址:https://mvnrepository.com/artifact/org.springframework.security/spring-security-web
- maven 地址:https://mvnrepository.com/artifact/org.springframework.security/spring-security-config
- maven 地址:https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
SpringSecurity 核心功能
- 認證
- 授權
- 攻擊防護
SpringSecurity 基本原理
自定義 SpringSecurity
SpringSecurity 核心配置
- 定義
SpringSecurityCoreConfig
需要繼承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
- 在
SpringSecurityCoreConfig
添加註解@EnableWebSecurity
,該類可以使用SpringSecurity
配置和擴展SpringSecurity
- 簡單配置後(使用 SpringSecurity 默認配置項),啓動項目,SpringSeucirty 默認用戶爲
user
, 密碼控制檯自動生成,可以搜索Using generated security password
查詢密碼;
package top.simba1949.config.security;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
// 使用 SpringSecurity 默認配置
}
簡單自定義用戶名和密碼配置
application.yml 配置文件
spring:
security:
# 簡單配置用戶信息
user:
name: user
password: password
自定義用戶認證流程邏輯
- 創建一個類(示例:
MyUserDetailsService
),實現org.springframework.security.core.userdetails.UserDetailsService
接口,重寫loadUserByUsername
方法 - 自定義用戶名和密碼
- 在
SpringSecurityCoreConfig
配置加密方式
SpringSecurityCoreConfig 類
package top.simba1949.config.security;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
// 使用 SpringSecurity 默認配置
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
MyUserDetailsService 類
package top.simba1949.config.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定義 SpringSecurity 認證流程
*
* @AUTHOR Theodore
* @DATE 2019/12/30 10:42
*/
@Slf4j
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("登錄用戶名:{}", username);
// 根據用戶名去數據庫中查詢用戶信息包括密碼
String password = passwordEncoder.encode("password");
log.info("從數據庫中查詢的密碼爲:{}", password);
// 根據用戶名去數據查詢用戶的信息,判斷用戶是否啓用,賬號是否過期,密碼是否過期,賬號是否沒有凍結
// 用戶是否啓用
boolean enabled = true;
// 賬號是否沒有過期
boolean accountNonExpired = true;
// 密碼是否過期
boolean credentialsNonExpired = true;
// 賬號是否沒有凍結
boolean accountNonLocked = true;
// 第一個參數表示用戶名,第二個參數表示加密後的密碼,最後一個個參數表示該用戶擁有哪些權限
return new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
自定義用戶登錄頁面
- 創建一個自定義登陸頁面
login.html
- 在
SpringSecurityCoreConfig
類,添加如下配置
自定義登陸頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>自定義登錄頁面</title>
</head>
<body>
<h1>自定義登錄頁面</h1>
<!--/user/login 是自定義處理登錄的uri-->
<form action="/user/login" method="post">
<table>
<tr> <td>用戶名:</td><td><input type="text" name="username"></td></tr>
<tr> <td>密 碼:</td><td><input type="password" name="password"></td></tr>
<tr> <td><button type="submit">登錄</button></tr>
</table>
</form>
</body>
</html>
SpringSecurityCoreConfig 配置自定義登錄頁面和處理登錄的URI
package top.simba1949.config.security;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login").permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
自定義用戶登錄成功後處理邏輯
備註:SpringSecurity 默認用戶登錄成功後進入用戶上一次登錄的URL
- 首先需要實現
org.springframework.security.web.authentication.AuthenticationSuccessHandler
接口或者繼承org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler
類,重寫onAuthenticationSuccess
方法,根據業務需求處理 - 在
SpringSecurityCoreConfig
中配置自定義登錄成功處理的類
定義認證成功處理邏輯
package top.simba1949.config.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定義認證成功後業務處理邏輯
*
* @AUTHOR Theodore
* @DATE 2019/12/30 14:23
*/
@Slf4j
@Component(value = "authenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* HttpSessionRequestCache 存儲用戶上次訪問的url
*/
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 記錄用戶登錄ip、時間等或者其他業務處理邏輯
log.info("用戶登錄成功後的業務處理邏輯");
// 自己獲取用戶上一次登錄url,然後實行跳轉
// 和調用父類方法同樣效果哈
SavedRequest savedRequest = requestCache.getRequest(request, response);
String targetUrl = savedRequest.getRedirectUrl();
getRedirectStrategy().sendRedirect(request, response, targetUrl);
// 調用父類的方法,跳轉到用戶上次登錄url中
// super.onAuthenticationSuccess(request, response, authentication);
}
}
配置自定義認證處理邏輯
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login").permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
自定義用戶登錄失敗後處理邏輯
- 首先需要實現
org.springframework.security.web.authentication.AuthenticationFailureHandler
接口或者繼承org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
類,重寫onAuthenticationFailure
方法 - 配置自定義登錄失敗處理的類
定義認證失敗處理邏輯
package top.simba1949.config.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定義認證失敗處理邏輯
*
* @AUTHOR Theodore
* @DATE 2019/12/30 14:40
*/
@Slf4j
@Component("authenticationFailureHandler")
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("用戶認證失敗後的業務處理邏輯");
// 使用 response 寫給前端
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("認證失敗啦,請重新認證");
// SpringSecurity 默認跳轉到提交認證的頁面
// super.onAuthenticationFailure(request, response, exception);
}
}
SpringSecurityCoreConfig 配置自定義登錄頁面和處理登錄的URI
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login").permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
獲取當前用戶信息
package top.simba1949.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 獲取當前用戶信息
*
* @AUTHOR Theodore
* @DATE 2019/12/30 15:02
*/
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("me0")
public Authentication authentication(){
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("me1")
public Authentication authentication(Authentication authentication){
return authentication;
}
@GetMapping("me2")
public UserDetails authentication(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
}
記住我
記住我基本原理
前端 記住我 的 name 爲 **remember-me
**
- 配置
PersistentTokenRepository
類的bean
- 注入
UserDetailsService
,並設置 - 在
SpringSecurityCoreConfig
的配置類添加配置
配置 PersistentTokenRepository,注入 UserDetailsService
,在 SpringSecurityCoreConfig
添加配置
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
/**
* 注入自定義認證流程
*/
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
/**
* 定義 token 存儲方式,示例使用功能 <b>數據庫存儲</b>
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動時候自動創建 tokenRepository 需要的表結構,第一次啓動時可以配置爲true,以後不能配置爲true
// 或者進入 JdbcTokenRepositoryImpl 類,執行類中對應的SQL,下面一行設置爲false或者刪除即可
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 記住我配置:其實remember-me功能是將token存儲起來,
// 在未過期時,用戶打開瀏覽器 remember-me 的 cookie還存在,後臺自動捕捉到並登錄,只是用戶無感知而已
.rememberMe()
// 設置 token 存儲位置
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login").permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
自定義登出後處理邏輯
SpringSecurity 默認訪問:/logout
路徑即爲退出,
SpringSecurity 登出之後會做如下:
- 使當前的 session 失效
- 清楚與當前用戶相關的 remember-me 記錄
- 清空當前的 SecurityContext
- 重定向到登錄頁面
實現自定義退出成功後業務邏輯
- 自定義退出成功類
MyLogoutSuccessHandler
,實現org.springframework.security.web.authentication.logout.LogoutSuccessHandler
接口或者基礎org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler
,重寫onLogoutSuccess
方法 - 在
SpringSecurityCoreConfig
中配置
MyLogoutSuccessHandler 自定義退成成功業務
package top.simba1949.config.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定義退出成功後業務邏輯
*
* @AUTHOR Theodore
* @DATE 2019/12/30 15:33
*/
@Slf4j
@Component
public class MyLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
/**
* Spring 封裝的重定向
*/
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 比如記錄日誌,根據自己項目進行業務
log.info("用戶已經成功退出系統,記錄日誌");
// 使用 RedirectStrategy 自定義跳轉頁面
String logoutRedirectUrl = "/logout-redirect.html";
redirectStrategy.sendRedirect(request, response, logoutRedirectUrl);
// SpringSecurity 默認進入 '/logout.html'
// super.onLogoutSuccess(request, response, authentication);
}
}
SpringSecurityCoreConfig 配置
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
@Qualifier("myLogoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;
/**
* 注入自定義認證流程
*/
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
/**
* 定義 token 存儲方式,示例使用功能 <b>數據庫存儲</b>
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動時候自動創建 tokenRepository 需要的表結構,第一次啓動時可以配置爲true,以後不能配置爲true
// 或者進入 JdbcTokenRepositoryImpl 類,執行類中對應的SQL,下面一行設置爲false或者刪除即可
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 記住我配置:其實remember-me功能是將token存儲起來,
// 在未過期時,用戶打開瀏覽器 remember-me 的 cookie還存在,後臺自動捕捉到並登錄,只是用戶無感知而已
.rememberMe()
// 設置 token 存儲位置
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
.logout()
// 自定義退出的url
.logoutUrl("/user/logout")
// 退出成功後,訪問的地址
// .logoutSuccessUrl("/logout.html")
// 配置成功退出後的業務邏輯
.logoutSuccessHandler(logoutSuccessHandler)
// 瀏覽器退出後清空指定cookie
.deleteCookies("SESSION")
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login", "/user/logout", "/logout.html", "/logout-redirect.html").permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
SpringSession 管理
SpringSession 支持存儲 :redis、mongo、jdbc、hazelcast、hashMap、none
session 過期時間配置,SpringSession
項目最少默認過期時間爲一分鐘 (配置過低則配置不生效,實際生效爲一分鐘)
- 在
application.yml
中配置SpringSession
- 自定義會話過期業務處理邏輯類
MyExpiredSessionStrategy
,實現org.springframework.security.web.session.SessionInformationExpiredStrategy
接口,重寫onExpiredSessionDetected
- 在
SpringSecurityCoreConfig
中配置
SpringSession
在 application.yml
中配置如下
spring:
# spring-session 配置
session:
# spring-session 存儲配置,none 表示關閉,配置SpringSession存儲即配置SpringSession集羣管理
store-type: redis
# 10s 最少默認過期時間爲一分鐘(配置過低則配置不生效,實際生效爲一分鐘)
timeout: 10
自定義會話過期業務處理邏輯類 MyExpiredSessionStrategy
package top.simba1949.config.security;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 16:07
*/
@Component
public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("會話過期業務處理邏輯");
}
}
session 在 SpringSecurityCoreConfig
中配置
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import javax.sql.DataSource;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
@Qualifier("myLogoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
/**
* 注入自定義認證流程
*/
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
/**
* 定義 token 存儲方式,示例使用功能 <b>數據庫存儲</b>
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動時候自動創建 tokenRepository 需要的表結構,第一次啓動時可以配置爲true,以後不能配置爲true
// 或者進入 JdbcTokenRepositoryImpl 類,執行類中對應的SQL,下面一行設置爲false或者刪除即可
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 記住我配置:其實remember-me功能是將token存儲起來,
// 在未過期時,用戶打開瀏覽器 remember-me 的 cookie還存在,後臺自動捕捉到並登錄,只是用戶無感知而已
.rememberMe()
// 設置 token 存儲位置
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
.logout()
// 自定義退出的url
.logoutUrl("/user/logout")
// 退出成功後,訪問的地址
// .logoutSuccessUrl("/logout.html")
// 配置成功退出後的業務邏輯
.logoutSuccessHandler(logoutSuccessHandler)
// 瀏覽器退出後清空指定cookie
.deleteCookies("SESSION")
.and()
// session 管理配置
.sessionManagement()
// 會話失效時,跳轉url和業務邏輯不能工共存
// session 失效的時候跳轉url路徑
// .invalidSessionUrl("/session-expired.html")
// 設置同一用戶 session 最大數量,控制session併發數
.maximumSessions(1)
// 當會話過期的業務處理邏輯
.expiredSessionStrategy(sessionInformationExpiredStrategy)
// 當同一用戶的 session 數量達到最大數時,阻止後面的用戶登錄
// .maxSessionsPreventsLogin(true)
.and()
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login", "/user/logout", "/logout.html",
"/logout-redirect.html", "/session-expired.html"
).permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
實現圖形驗證碼功能
- 生成圖形驗證碼(生成圖形驗證碼,並將驗證碼存到
session
中,最後將生成的圖片寫到接口的響應中) - 自定義圖形驗證碼校驗的過濾器
- 在
SpringSecurityCoreConfig
配置中添加自定義的圖形驗證碼過濾器
圖形驗證碼的生成與存儲
備註:示例中使用 hutool
工具 CaptchaUtil
類
package top.simba1949.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 驗證碼生成接口
* @AUTHOR Theodore
* @DATE 2019/12/30 15:58
*/
@Slf4j
@RestController
@RequestMapping("img/validate")
public class ImgValidateController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
@GetMapping("create")
public void createImgValidate(HttpServletRequest request, HttpServletResponse response) throws IOException {
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(100, 40, 4, 20);
String code = lineCaptcha.getCode();
log.info(code);
request.getSession().setAttribute(SESSION_KEY, code);
lineCaptcha.write(response.getOutputStream());
response.flushBuffer();
}
}
根據 SpringSecurity
自定義驗證碼異常類
package top.simba1949.exception;
import org.springframework.security.core.AuthenticationException;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 22:03
*/
public class ValidateException extends AuthenticationException {
public ValidateException(String msg) {
super(msg);
}
}
自定義圖形驗證碼校驗的過濾器
package top.simba1949.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import top.simba1949.config.security.MyAuthenticationFailureHandler;
import top.simba1949.controller.ImgValidateController;
import top.simba1949.exception.ValidateException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 21:45
*/
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
private final AuthenticationFailureHandler authenticationFailureHandler = new MyAuthenticationFailureHandler();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("驗證碼過濾器");
// 如果是登錄請求,進行校驗
String requestURI = request.getRequestURI();
String method = request.getMethod();
// 登錄時進行圖形驗證碼校驗,如果其他uri也需要校驗,可自行添加
if ("/user/login".equalsIgnoreCase(requestURI) && "POST".equalsIgnoreCase(method)){
try {
// 校驗驗證碼
validateCode(request);
} catch (AuthenticationException e) {
// 身份認證失敗,由失敗處理器進行處理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
// 認證失敗無需往下走
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 校驗圖形驗證碼
* @param request
*/
private void validateCode(HttpServletRequest request) throws AuthenticationException {
HttpSession session = request.getSession();
String sessionValidateCode = null;
String reqCode = null;
try {
reqCode = ServletRequestUtils.getStringParameter(request, "code");
} catch (ServletRequestBindingException e) {
throw new ValidateException("the error is what can't get code");
}
if (StringUtils.isEmpty(reqCode)){
throw new ValidateException("can't get code");
}
try {
sessionValidateCode = (String) session.getAttribute(ImgValidateController.SESSION_KEY);
} catch (Exception e) {
throw new ValidateException("the code is expired");
}
if (StringUtils.isEmpty(sessionValidateCode)){
throw new ValidateException("the code is expired");
}
if (!reqCode.equalsIgnoreCase(sessionValidateCode)){
throw new ValidateException("the code doesn't right");
}
// 驗證通過後,刪除 session 中的驗證碼
session.removeAttribute(ImgValidateController.SESSION_KEY);
}
}
在認證流程之前加入圖形驗證碼的校驗
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import top.simba1949.filter.ValidateCodeFilter;
import javax.sql.DataSource;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
@Qualifier("myLogoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
/**
* 注入自定義認證流程
*/
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
/**
* 定義 token 存儲方式,示例使用功能 <b>數據庫存儲</b>
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動時候自動創建 tokenRepository 需要的表結構,第一次啓動時可以配置爲true,以後不能配置爲true
// 或者進入 JdbcTokenRepositoryImpl 類,執行類中對應的SQL,下面一行設置爲false或者刪除即可
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 圖形驗證碼過濾器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
http
// 圖形驗證碼過濾器要在 UsernamePasswordAuthenticationFilter 之前執行
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 記住我配置:其實remember-me功能是將token存儲起來,
// 在未過期時,用戶打開瀏覽器 remember-me 的 cookie還存在,後臺自動捕捉到並登錄,只是用戶無感知而已
.rememberMe()
// 設置 token 存儲位置
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
.logout()
// 自定義退出的url
.logoutUrl("/user/logout")
// 退出成功後,訪問的地址
// .logoutSuccessUrl("/logout.html")
// 配置成功退出後的業務邏輯
.logoutSuccessHandler(logoutSuccessHandler)
// 瀏覽器退出後清空指定cookie
.deleteCookies("SESSION")
.and()
// session 管理配置
.sessionManagement()
// 會話失效時,跳轉url和業務邏輯不能工共存
// session 失效的時候跳轉url路徑
.invalidSessionUrl("/session-expired.html")
// 設置同一用戶 session 最大數量,控制session併發數
.maximumSessions(1)
// 當會話過期的業務處理邏輯
.expiredSessionStrategy(sessionInformationExpiredStrategy)
// 當同一用戶的 session 數量達到最大數時,阻止後面的用戶登錄
//.maxSessionsPreventsLogin(true)
.and()
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login", "/user/logout", "/logout.html",
"/logout-redirect.html", "/session-expired.html","/img/validate/create"
).permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
實現郵件/短信登錄
- 根據
org.springframework.security.authentication.UsernamePasswordAuthenticationToken
提供AccountAuthenticationToke
- 根據
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
提供AccountAuthenticationFilter
,拿到請求的數據,用於組裝成 Token - 根據
org.springframework.security.authentication.dao.DaoAuthenticationProvider
提供AccountAuthenticationProvider
,用於認證處理 - 自定義配置類
AccountAuthenticationSecurityConfig
,將上面AccountAuthenticationFilter
和AccountAuthenticationProvider
配置到SpringSecurity
過濾器鏈中 - 在
SpringSecurityCoreConfig
中將自定義配置加入到SecurityBuilder
中 - 發送短信/Email驗證碼接口
- 和配置圖像驗證碼過濾器一樣,配置校驗短信驗證碼過濾器(示例中使用同一)
AccountAuthenticationToke
package top.simba1949.config.security.account;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 22:36
*/
public class AccountAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* principal 實際上是認證信息,認證之前是放置的手機號碼或者電子郵箱,認證之後放置的是認證信息
*/
private final Object principal;
public AccountAuthenticationToken(String account) {
super(null);
this.principal = account;
setAuthenticated(false);
}
public AccountAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
AccountAuthenticationFilter
package top.simba1949.config.security.account;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 22:28
*/
public class AccountAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// =====================================================================================
/**
* account 可以是 email 也可以是 mobile,請求中接收到賬號的name
*/
public static final String SPRING_SECURITY_FORM_ACCOUNT_KEY = "account";
private String accountParameter = SPRING_SECURITY_FORM_ACCOUNT_KEY;
private boolean postOnly = true;
public AccountAuthenticationFilter() {
// 匹配哪些路徑
super(new AntPathRequestMatcher("/user/account", "POST"));
}
/**
* 認證流程
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
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 account = obtainAccount(request);
if (account == null) {
account = "";
}
account = account.trim();
AccountAuthenticationToken authRequest = new AccountAuthenticationToken(account);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 獲取賬號
* @param request
* @return
*/
protected String obtainAccount(HttpServletRequest request) {
return request.getParameter(accountParameter);
}
/**
*
* @param request
* @param authRequest
*/
protected void setDetails(HttpServletRequest request, AccountAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setAccountParameter(String accountParameter) {
Assert.hasText(accountParameter, "Account parameter must not be empty or null");
this.accountParameter = accountParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getAccountParameter() {
return accountParameter;
}
}
AccountAuthenticationProvider
package top.simba1949.config.security.account;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 22:42
*/
public class AccountAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
/**
* 認證
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AccountAuthenticationToken authenticationToken = (AccountAuthenticationToken) authentication;
// 讀取用戶信息
UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationToken.getPrincipal().toString());
if (null == userDetails){
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
// 第一個參數傳遞用戶信息userDetails, 第二個參數傳遞用戶的權限
AccountAuthenticationToken authenticationResult = new AccountAuthenticationToken(userDetails.getUsername(), userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
// 判斷傳入的是否是 AccountAuthenticationToken 類
return AccountAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
AccountAuthenticationSecurityConfig
package top.simba1949.config.security.account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @AUTHOR Theodore
* @DATE 2019/12/31 8:04
*/
@Configuration
public class AccountAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
// 配置過濾器
AccountAuthenticationFilter accountAuthenticationFilter = new AccountAuthenticationFilter();
accountAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
accountAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
accountAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
// 配置自定義的 AuthenticationProvider
AccountAuthenticationProvider accountAuthenticationProvider = new AccountAuthenticationProvider();
accountAuthenticationProvider.setUserDetailsService(userDetailsService);
// 將自定義的 過濾器和 Provider 配置到 SpringSecurity 過濾器鏈中
http.authenticationProvider(accountAuthenticationProvider)
.addFilterAfter(accountAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
SpringSecurityCoreConfig
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import top.simba1949.config.security.account.AccountAuthenticationSecurityConfig;
import top.simba1949.filter.ValidateCodeFilter;
import javax.sql.DataSource;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
@Qualifier("myLogoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Autowired
private AccountAuthenticationSecurityConfig accountAuthenticationSecurityConfig;
/**
* 注入自定義認證流程
*/
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
/**
* 定義 token 存儲方式,示例使用功能 <b>數據庫存儲</b>
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動時候自動創建 tokenRepository 需要的表結構,第一次啓動時可以配置爲true,以後不能配置爲true
// 或者進入 JdbcTokenRepositoryImpl 類,執行類中對應的SQL,下面一行設置爲false或者刪除即可
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 圖形驗證碼過濾器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
http
// 圖形驗證碼過濾器要在 UsernamePasswordAuthenticationFilter 之前執行
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 記住我配置:其實remember-me功能是將token存儲起來,
// 在未過期時,用戶打開瀏覽器 remember-me 的 cookie還存在,後臺自動捕捉到並登錄,只是用戶無感知而已
.rememberMe()
// 設置 token 存儲位置
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
.logout()
// 自定義退出的url
.logoutUrl("/user/logout")
// 退出成功後,訪問的地址
// .logoutSuccessUrl("/logout.html")
// 配置成功退出後的業務邏輯
.logoutSuccessHandler(logoutSuccessHandler)
// 瀏覽器退出後清空指定cookie
.deleteCookies("SESSION")
.and()
// session 管理配置
.sessionManagement()
// 會話失效時,跳轉url和業務邏輯不能工共存
// session 失效的時候跳轉url路徑
.invalidSessionUrl("/session-expired.html")
// 設置同一用戶 session 最大數量,控制session併發數
.maximumSessions(1)
// 當會話過期的業務處理邏輯
.expiredSessionStrategy(sessionInformationExpiredStrategy)
// 當同一用戶的 session 數量達到最大數時,阻止後面的用戶登錄
//.maxSessionsPreventsLogin(true)
.and()
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login", "/user/logout", "/logout.html",
"/logout-redirect.html", "/session-expired.html","/img/validate/create",
"/account/send-code"
).permitAll()
.anyRequest()
.authenticated()
.and()
// 禁用跨站請求防護
.csrf()
.disable()
// 添加自定義短信郵箱認證的配置
.apply(accountAuthenticationSecurityConfig)
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
發送短信/Email驗證碼接口
package top.simba1949.controller;
import cn.hutool.core.util.RandomUtil;
import com.google.common.base.Preconditions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 22:50
*/
@Slf4j
@RestController
@RequestMapping("account")
public class AccountController {
public static final String SESSION_KEY_ACCOUNT = "AccountKey";
@GetMapping("send-code")
public void sendCode(HttpServletRequest request) throws ServletRequestBindingException {
String account = ServletRequestUtils.getStringParameter(request, "account");
// 判斷 account 是手機號碼還是郵件賬號
Preconditions.checkArgument(null != account, "the account doesn't blank");
// 生成隨機驗證碼
String code = RandomUtil.randomNumbers(4);
if (account.contains("@")){
// 發送郵箱驗證碼
log.info("已經發送郵箱驗證碼爲:{}", code);
}else {
// 發送手機驗證碼
log.info("已經發送手機驗證碼爲:{}", code);
}
// 存儲驗證碼(推薦使用redis等中間件存儲,示例使用session存儲)
request.getSession().setAttribute(account, code);
log.info("account code is {}", request.getSession().getAttribute(account).toString());
}
}
配置校驗短信驗證碼過濾器
package top.simba1949.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import top.simba1949.config.security.MyAuthenticationFailureHandler;
import top.simba1949.controller.ImgValidateController;
import top.simba1949.exception.ValidateException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* @AUTHOR Theodore
* @DATE 2019/12/30 21:45
*/
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
private final AuthenticationFailureHandler authenticationFailureHandler = new MyAuthenticationFailureHandler();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("驗證碼過濾器");
// 如果是登錄請求,進行校驗
String requestURI = request.getRequestURI();
String method = request.getMethod();
// 登錄時進行圖形驗證碼校驗,如果其他uri也需要校驗,可自行添加
if ("/user/login".equalsIgnoreCase(requestURI) && "POST".equalsIgnoreCase(method)){
try {
// 校驗驗證碼
String reqKey = "code";
String sessionKey = ImgValidateController.SESSION_KEY;
validateCode(request, reqKey, sessionKey);
} catch (AuthenticationException e) {
// 身份認證失敗,由失敗處理器進行處理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
// 認證失敗無需往下走
return;
}
}
// 用戶賬號登錄(手機或者Email)
if ("/user/account".equalsIgnoreCase(requestURI) && "POST".equalsIgnoreCase(method)){
try {
String account = ServletRequestUtils.getStringParameter(request, "account");
// 校驗驗證碼
String reqKey = "code";
String sessionKey = account;
validateCode(request, reqKey, sessionKey);
} catch (AuthenticationException e) {
// 身份認證失敗,由失敗處理器進行處理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
// 認證失敗無需往下走
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 校驗圖形驗證碼
* @param request
*/
private void validateCode(HttpServletRequest request, String reqKey, String sessionKey) throws AuthenticationException {
HttpSession session = request.getSession();
String sessionValidateCode = null;
String reqCode = null;
try {
reqCode = ServletRequestUtils.getStringParameter(request, reqKey);
} catch (ServletRequestBindingException e) {
throw new ValidateException("the error is what can't get code");
}
if (StringUtils.isEmpty(reqCode)){
throw new ValidateException("can't get code");
}
try {
sessionValidateCode = (String) session.getAttribute(sessionKey);
} catch (Exception e) {
throw new ValidateException("the code is expired");
}
if (StringUtils.isEmpty(sessionValidateCode)){
throw new ValidateException("the code is expired");
}
if (!reqCode.equalsIgnoreCase(sessionValidateCode)){
throw new ValidateException("the code doesn't right");
}
// 驗證通過後,刪除 session 中的驗證碼
session.removeAttribute(sessionKey);
}
}
權限控制
權限表達式
參考:https://docs.spring.io/spring-security/site/docs/5.2.2.BUILD-SNAPSHOT/reference/htmlsingle/#el-access
RBAC(Role Based-Access-Control)
定義權限校驗的接口 RbacService
package top.simba1949.config.security.authorization;
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
/**
* 權限校驗接口
*
* @AUTHOR Theodore
* @DATE 2019/12/31 10:06
*/
public interface RbacService {
/**
* 權限控制
* @param request
* @param authentication
* @return
*/
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
定義實現權限校驗接口的實現類
package top.simba1949.config.security.authorization;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;
/**
* @AUTHOR Theodore
* @DATE 2019/12/31 10:07
*/
@Component("rbacService")
public class RbacServiceImpl implements RbacService {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 判斷結果
boolean hasPermission = false;
Object principal = authentication.getPrincipal();
// 獲取用戶是否登錄
if (principal instanceof UserDetails || principal instanceof WebAuthenticationDetails){
// 獲取用戶名
String username = ((UserDetails) principal).getUsername();
// 從數據庫中讀取用戶所擁有權權限的url
Set<String> urls = new HashSet<>();
urls.add("/rbac/0");
urls.add("/rbac/1");
urls.add("/rbac/2");
urls.add("/rbac/3");
// 匹配 url
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())){
hasPermission = true;
break;
}
}
}
return hasPermission;
}
}
在 SpringSecurtiy
中配置權限表達式
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import top.simba1949.config.security.account.AccountAuthenticationSecurityConfig;
import top.simba1949.config.security.authorization.MyAccessDeniedHandler;
import top.simba1949.filter.ValidateCodeFilter;
import javax.sql.DataSource;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
@Qualifier("myLogoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Autowired
private AccountAuthenticationSecurityConfig accountAuthenticationSecurityConfig;
/**
* 注入自定義認證流程
*/
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
/**
* 定義 token 存儲方式,示例使用功能 <b>數據庫存儲</b>
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動時候自動創建 tokenRepository 需要的表結構,第一次啓動時可以配置爲true,以後不能配置爲true
// 或者進入 JdbcTokenRepositoryImpl 類,執行類中對應的SQL,下面一行設置爲false或者刪除即可
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 圖形驗證碼過濾器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
http
// 圖形驗證碼過濾器要在 UsernamePasswordAuthenticationFilter 之前執行
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 記住我配置:其實remember-me功能是將token存儲起來,
// 在未過期時,用戶打開瀏覽器 remember-me 的 cookie還存在,後臺自動捕捉到並登錄,只是用戶無感知而已
.rememberMe()
// 設置 token 存儲位置
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
.logout()
// 自定義退出的url
.logoutUrl("/user/logout")
// 退出成功後,訪問的地址
// .logoutSuccessUrl("/logout.html")
// 配置成功退出後的業務邏輯
.logoutSuccessHandler(logoutSuccessHandler)
// 瀏覽器退出後清空指定cookie
.deleteCookies("SESSION")
.and()
// session 管理配置
.sessionManagement()
// 會話失效時,跳轉url和業務邏輯不能工共存
// session 失效的時候跳轉url路徑
.invalidSessionUrl("/session-expired.html")
// 設置同一用戶 session 最大數量,控制session併發數
.maximumSessions(1)
// 當會話過期的業務處理邏輯
.expiredSessionStrategy(sessionInformationExpiredStrategy)
// 當同一用戶的 session 數量達到最大數時,阻止後面的用戶登錄
//.maxSessionsPreventsLogin(true)
.and()
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login", "/user/logout", "/logout.html",
"/logout-redirect.html", "/session-expired.html","/img/validate/create",
"/account/send-code"
).permitAll()
.anyRequest()
// 權限表達式:rbacService.hasPermission 類.方法名,括號裏面是請求參數
.access("@rbacService.hasPermission(request, authentication)")
.and()
// 異常處理
.exceptionHandling()
// 無權限處理
.accessDeniedHandler(new MyAccessDeniedHandler())
.and()
// 禁用跨站請求防護
.csrf()
.disable()
// 添加自定義短信郵箱認證的配置
.apply(accountAuthenticationSecurityConfig)
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
自定義無權限異常處理邏輯
- 實現
org.springframework.security.web.access.AccessDeniedHandler
接口,重寫handle
方法 - 在
SpringSecurityCoreConfig
中進行配置
自定義無權限異常處理邏輯
package top.simba1949.config.security.authorization;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定義異常處理邏輯
*
* @AUTHOR Theodore
* @DATE 2019/12/31 10:15
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 返回json形式的錯誤信息
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println("無權限(應該將CommonResponse返回給前端)");
response.getWriter().flush();
}
}
在 SpringSecurityCoreConfig
中配置
package top.simba1949.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import top.simba1949.config.security.account.AccountAuthenticationSecurityConfig;
import top.simba1949.config.security.authorization.MyAccessDeniedHandler;
import top.simba1949.filter.ValidateCodeFilter;
import javax.sql.DataSource;
/**
* @EnableWebSecurity 開啓使用 SpringSecurity
*
* @AUTHOR Theodore
* @DATE 2019/12/30 9:49
*/
@EnableWebSecurity
public class SpringSecurityCoreConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
@Qualifier("myLogoutSuccessHandler")
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Autowired
private AccountAuthenticationSecurityConfig accountAuthenticationSecurityConfig;
/**
* 注入自定義認證流程
*/
@Autowired
@Qualifier("myUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
/**
* 定義 token 存儲方式,示例使用功能 <b>數據庫存儲</b>
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動時候自動創建 tokenRepository 需要的表結構,第一次啓動時可以配置爲true,以後不能配置爲true
// 或者進入 JdbcTokenRepositoryImpl 類,執行類中對應的SQL,下面一行設置爲false或者刪除即可
tokenRepository.setCreateTableOnStartup(false);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 圖形驗證碼過濾器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
http
// 圖形驗證碼過濾器要在 UsernamePasswordAuthenticationFilter 之前執行
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// form 表單登錄
.formLogin()
// 自定義登錄頁面
.loginPage("/login.html")
// 自定義處理登錄的uri,但是處理還是由SpringSecurity的UsernamePasswordAuthenticationFilter 進行獲取用戶名和密碼,
// 交給 MyUserDetailsService處理,這裏只是個性化定製
.loginProcessingUrl("/user/login")
// 配置自定義登錄成功後的業務處理邏輯
// .successHandler(new MyAuthenticationSuccessHandler())
// 和上面直接 new 同樣效果,不推薦使用 new,充分利用 spring 容器
.successHandler(authenticationSuccessHandler)
// 配置自定義認證失敗後的業務處理邏輯
.failureHandler(authenticationFailureHandler)
.and()
// 記住我配置:其實remember-me功能是將token存儲起來,
// 在未過期時,用戶打開瀏覽器 remember-me 的 cookie還存在,後臺自動捕捉到並登錄,只是用戶無感知而已
.rememberMe()
// 設置 token 存儲位置
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
.logout()
// 自定義退出的url
.logoutUrl("/user/logout")
// 退出成功後,訪問的地址
// .logoutSuccessUrl("/logout.html")
// 配置成功退出後的業務邏輯
.logoutSuccessHandler(logoutSuccessHandler)
// 瀏覽器退出後清空指定cookie
.deleteCookies("SESSION")
.and()
// session 管理配置
.sessionManagement()
// 會話失效時,跳轉url和業務邏輯不能工共存
// session 失效的時候跳轉url路徑
.invalidSessionUrl("/session-expired.html")
// 設置同一用戶 session 最大數量,控制session併發數
.maximumSessions(1)
// 當會話過期的業務處理邏輯
.expiredSessionStrategy(sessionInformationExpiredStrategy)
// 當同一用戶的 session 數量達到最大數時,阻止後面的用戶登錄
//.maxSessionsPreventsLogin(true)
.and()
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login", "/user/logout", "/logout.html",
"/logout-redirect.html", "/session-expired.html","/img/validate/create",
"/account/send-code"
).permitAll()
.anyRequest()
// 權限表達式:rbacService.hasPermission 類.方法名,括號裏面是請求參數
.access("@rbacService.hasPermission(request, authentication)")
.and()
// 異常處理
.exceptionHandling()
// 無權限處理
.accessDeniedHandler(new MyAccessDeniedHandler())
.and()
// 禁用跨站請求防護
.csrf()
.disable()
// 添加自定義短信郵箱認證的配置
.apply(accountAuthenticationSecurityConfig)
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}
.tokenRepository(persistentTokenRepository())
// 設置 token 過期時間
.tokenValiditySeconds(60)
// 設置自定義認證流程,記住我時最終拿到對應token去userDetailsService做認證
.userDetailsService(userDetailsService)
.and()
.logout()
// 自定義退出的url
.logoutUrl("/user/logout")
// 退出成功後,訪問的地址
// .logoutSuccessUrl("/logout.html")
// 配置成功退出後的業務邏輯
.logoutSuccessHandler(logoutSuccessHandler)
// 瀏覽器退出後清空指定cookie
.deleteCookies("SESSION")
.and()
// session 管理配置
.sessionManagement()
// 會話失效時,跳轉url和業務邏輯不能工共存
// session 失效的時候跳轉url路徑
.invalidSessionUrl("/session-expired.html")
// 設置同一用戶 session 最大數量,控制session併發數
.maximumSessions(1)
// 當會話過期的業務處理邏輯
.expiredSessionStrategy(sessionInformationExpiredStrategy)
// 當同一用戶的 session 數量達到最大數時,阻止後面的用戶登錄
//.maxSessionsPreventsLogin(true)
.and()
.and()
// 設置基於 HttpServletRequest 請求配置
.authorizeRequests()
// 匹配 antMatchers 裏的對應 uri,直接放行,防止進入登錄頁面不斷重定向
.antMatchers("/login.html", "/user/login", "/user/logout", "/logout.html",
"/logout-redirect.html", "/session-expired.html","/img/validate/create",
"/account/send-code"
).permitAll()
.anyRequest()
// 權限表達式:rbacService.hasPermission 類.方法名,括號裏面是請求參數
.access("@rbacService.hasPermission(request, authentication)")
.and()
// 異常處理
.exceptionHandling()
// 無權限處理
.accessDeniedHandler(new MyAccessDeniedHandler())
.and()
// 禁用跨站請求防護
.csrf()
.disable()
// 添加自定義短信郵箱認證的配置
.apply(accountAuthenticationSecurityConfig)
;
}
/**
* 配置加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(16);
}
}