文章目錄
一、自定義認證成功、失敗處理
1.1 CustomAuthenticationSuccessHandler
1.2 CustomAuthenticationFailureHandler
1.3 修改 WebSecurityConfig
1.4 運行程序
二、Session 超時
三、限制最大登錄數
四、踢出用戶
五、退出登錄
六、Session 共享
6.1 配置 Redis
6.2 配置 Session 共享
6.3 運行程序
在本篇中,主要關注登錄的管理,因此代碼使用最原始版本的即可,即《SpringBoot集成Spring Security(1)——入門程序》源碼即可。
源碼地址:https://github.com/jitwxs/blog_sample
一、自定義認證成功、失敗處理
有些時候我們想要在認證成功后做一些業務處理,例如添加積分;有些時候我們想要在認證失敗后也做一些業務處理,例如記錄日志。
在之前的文章中,關於認證成功、失敗后的處理都是如下配置的:
http.authorizeRequests() // 如果有允許匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated().and() // 設置登陸頁 .formLogin().loginPage("/login") .failureUrl("/login/error") .defaultSuccessUrl("/") .permitAll() ...;
即 failureUrl()
指定認證失敗后Url,defaultSuccessUrl()
指定認證成功后Url。我們可以通過設置 successHandler()
和 failureHandler()
來實現自定義認證成功、失敗處理。
PS:當我們設置了這兩個后,需要去除 failureUrl() 和 defaultSuccessUrl() 的設置,否則無法生效。這兩套配置同時只能存在一套。
1.1 CustomAuthenticationSuccessHandler
自定義 CustomAuthenticationSuccessHandler 類來實現 AuthenticationSuccessHandler
接口,用來處理認證成功后邏輯:
@Component public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登錄成功,{}", authentication); response.sendRedirect("/"); } }
onAuthenticationSuccess()
方法的第三個參數 Authentication
為認證后該用戶的認證信息,這里打印日志后,重定向到了首頁。
1.2 CustomAuthenticationFailureHandler
自定義 CustomAuthenticationFailureHandler 類來實現 AuthenticationFailureHandler
接口,用來處理認證失敗后邏輯:
@Component public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; private Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登陸失敗"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage())); } }
onAuthenticationFailure()
方法的第三個參數 exception
為認證失敗所產生的異常,這里也是簡單的返回到前台。
1.3 修改 WebSecurityConfig
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; @Autowired private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; ... @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允許匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated().and() // 設置登陸頁 .formLogin().loginPage("/login") .successHandler(customAuthenticationSuccessHandler).permitAll() .failureHandler(customAuthenticationFailureHandler) // .failureUrl("/login/error") // .defaultSuccessUrl("/") .permitAll() ...; // 關閉CSRF跨域 http.csrf().disable(); } ... }
- 首先將 customAuthenticationSuccessHandler 和 customAuthenticationFailureHandler注入進來
- 配置 successHandler() 和 failureHandler()
- 注釋 failureUrl() 和 defaultSuccessUrl()
1.4 運行程序
運行程序,當我們成功登陸后,發現日志信息被打印出來,頁面被重定向到了首頁:
當我們認證失敗后,發現日志中“登陸失敗”被打印出來,頁面展示了認證失敗的異常消息:
二、Session 超時
當用戶登錄后,我們可以設置 session 的超時時間,當達到超時時間后,自動將用戶退出登錄。
Session 超時的配置是 SpringBoot 原生支持的,我們只需要在 application.properties
配置文件中配置:
# session 過期時間,單位:秒
server.servlet.session.timeout=60
Tip:
從用戶最后一次操作開始計算過期時間。
過期時間最小值為 60 秒,如果你設置的值小於 60 秒,也會被更改為 60 秒。
我們可以在 Spring Security 中配置處理邏輯,在 session 過期退出時調用。修改 WebSecurityConfig 的 configure()
方法,添加:
.sessionManagement() // 以下二選一 //.invalidSessionStrategy() //.invalidSessionUrl();
Spring Security 提供了兩種處理配置,一個是 invalidSessionStrategy(),另外一個是 invalidSessionUrl()。
這兩個的區別就是一個是前者是在一個類中進行處理,后者是直接跳轉到一個 Url。簡單起見,我就直接用 invalidSessionUrl()了,跳轉到 /login/invalid,我們需要把該 Url 設置為免授權訪問, 配置如下:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允許匿名的url,填在下面 .antMatchers("/login/invalid").permitAll() .anyRequest().authenticated().and() ... .sessionManagement() .invalidSessionUrl("/login/invalid"); // 關閉CSRF跨域 http.csrf().disable(); }
在 controller 中寫一個接口進行處理:
@RequestMapping("/login/invalid") @ResponseStatus(HttpStatus.UNAUTHORIZED) @ResponseBody public String invalid() { return "Session 已過期,請重新登錄"; }
運行程序,登陸成功后等待一分鍾(或者重啟服務器),刷新頁面:
三、限制最大登錄數
接下來實現限制最大登陸數,原理就是限制單個用戶能夠存在的最大 session 數。
在上一節的基礎上,修改 configure()
為:
.sessionManagement() .invalidSessionUrl("/login/invalid") .maximumSessions(1) // 當達到最大值時,是否保留已經登錄的用戶 .maxSessionsPreventsLogin(false) // 當達到最大值時,舊用戶被踢出后的操作 .expiredSessionStrategy(new CustomExpiredSessionStrategy())
增加了下面三行代碼,其中:
- maximumSessions(int):指定最大登錄數
- maxSessionsPreventsLogin(boolean):是否保留已經登錄的用戶;為true,新用戶無法登錄;為 false,舊用戶被踢出
- expiredSessionStrategy(SessionInformationExpiredStrategy):舊用戶被踢出后處理方法
maxSessionsPreventsLogin()可能不太好理解,這里我們先設為 false,效果和 QQ 登錄是一樣的,登陸后之前登錄的賬戶被踢出。
編寫 CustomExpiredSessionStrategy 類,來處理舊用戶登陸失敗的邏輯:
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy { private ObjectMapper objectMapper = new ObjectMapper(); // private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { Map<String, Object> map = new HashMap<>(16); map.put("code", 0); map.put("msg", "已經另一台機器登錄,您被迫下線。" + event.getSessionInformation().getLastRequest()); // Map -> Json String json = objectMapper.writeValueAsString(map); event.getResponse().setContentType("application/json;charset=UTF-8"); event.getResponse().getWriter().write(json); // 如果是跳轉html頁面,url代表跳轉的地址 // redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "url"); } }
在 onExpiredSessionDetected() 方法中,處理相關邏輯,我這里只是簡單的返回一句話。
執行程序,打開兩個瀏覽器,登錄同一個賬戶。因為我設置了 maximumSessions(1),也就是單個用戶只能存在一個 session,因此當你刷新先登錄的那個瀏覽器時,被提示踢出了。
下面我們來測試下 maxSessionsPreventsLogin(true)
時的情況,我們發現第一個瀏覽器登錄后,第二個瀏覽器無法登錄:
四、踢出用戶
下面來看下如何主動踢出一個用戶。
首先需要在容器中注入名為 SessionRegistry
的 Bean,這里我就簡單的寫在 WebSecurityConfig 中:
@Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); }
修改 WebSecurityConfig 的 configure() 方法,在最后添加一行 .sessionRegistry()
:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允許匿名的url,填在下面 .antMatchers("/login/invalid").permitAll() .anyRequest().authenticated().and() // 設置登陸頁 .formLogin().loginPage("/login") .successHandler(customAuthenticationSuccessHandler) .failureHandler(customAuthenticationFailureHandler) .permitAll().and() .logout().and() .sessionManagement() .invalidSessionUrl("/login/invalid") .maximumSessions(1) // 當達到最大值時,是否保留已經登錄的用戶 .maxSessionsPreventsLogin(false) // 當達到最大值時,舊用戶被踢出后的操作 .expiredSessionStrategy(new CustomExpiredSessionStrategy()) .sessionRegistry(sessionRegistry()); // 關閉CSRF跨域 http.csrf().disable(); } }
編寫一個接口用於測試踢出用戶:
@Controller public class LoginController { @Autowired private SessionRegistry sessionRegistry; ... @GetMapping("/kick") @ResponseBody public String removeUserSessionByUsername(@RequestParam String username) { int count = 0; // 獲取session中所有的用戶信息 List<Object> users = sessionRegistry.getAllPrincipals(); for (Object principal : users) { if (principal instanceof User) { String principalName = ((User)principal).getUsername(); if (principalName.equals(username)) { // 參數二:是否包含過期的Session List<SessionInformation> sessionsInfo = sessionRegistry.getAllSessions(principal, false); if (null != sessionsInfo && sessionsInfo.size() > 0) { for (SessionInformation sessionInformation : sessionsInfo) { sessionInformation.expireNow(); count++; } } } } } return "操作成功,清理session共" + count + "個"; } }
- sessionRegistry.getAllPrincipals(); 獲取所有 principal 信息
- 通過 principal.getUsername 是否等於輸入值,獲取到指定用戶的 principal
- sessionRegistry.getAllSessions(principal, false)獲取該 principal 上的所有 session
- 通過 sessionInformation.expireNow() 使得 session 過期
運行程序,分別使用 admin 和 jitwxs 賬戶登錄,admin 訪問 /kick?username=jitwxs
來踢出用戶 jitwxs,jitwxs 刷新頁面,發現被踢出。
五、退出登錄
補充一下退出登錄的內容,在之前,我們直接在 WebSecurityConfig 的 configure() 方法中,配置了:
http.logout();
這就是 Spring Security 的默認退出配置,Spring Security 在退出時候做了這樣幾件事:
- 使當前的 session 失效
- 清除與當前用戶有關的 remember-me 記錄
- 清空當前的 SecurityContext
- 重定向到登錄頁
Spring Security 默認的退出 Url 是 /logout,我們可以修改默認的退出 Url,例如修改為 /signout,那么在退出登錄的按鈕,地址也要改為/signout:
http.logout()
.logoutUrl("/signout");
我們也可以配置當退出時清除瀏覽器的 Cookie,例如清除 名為 JSESSIONID 的 cookie:
http.logout() .logoutUrl("/signout") .deleteCookies("JSESSIONID");
我們也可以配置退出后處理的邏輯,方便做一些別的操作:
http.logout() .logoutUrl("/signout") .deleteCookies("JSESSIONID") .logoutSuccessHandler(logoutSuccessHandler);
創建類 DefaultLogoutSuccessHandler
:
@Component public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { Logger log = LoggerFactory.getLogger(getClass()); @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String username = ((User) authentication.getPrincipal()).getUsername(); log.info("退出成功,用戶名:{}", username); // 重定向到登錄頁 response.sendRedirect("/login"); } }
最后把它注入到 WebSecurityConfig 即可:
@Autowired private CustomLogoutSuccessHandler logoutSuccessHandler;
退出登錄的比較簡單,我就直接貼代碼,不截圖了。
六、Session 共享
在最后補充下關於 Session 共享的知識點,一般情況下,一個程序為了保證穩定至少要部署兩個,構成集群。那么就牽扯到了 Session 共享的問題,不然用戶在 8080 登錄成功后,后續訪問了 8060 服務器,結果又提示沒有登錄。
這里就簡單實現下 Session 共享,采用 Redis 來存儲。
6.1 配置 Redis
為了方便起見,我直接使用 Docker 快速部署,如果你需要傳統方式安裝,可以參考文章《Redis初探(1)——Redis的安裝》。
docker pull redis docker run --name myredis -p 6379:6379 -d redis docker exec -it myredis redis-cli
這樣就啟動了 redis,並且進入到 redis 命令行中。
6.2 配置 Session 共享
首先需要導入依賴,因為我們采用 Redis 方式實現,因此導入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
在 application.xml
中新增配置指定 redis 地址以及 session 的存儲方式:
spring.redis.host=192.168.139.129 spring.redis.port=6379 spring.session.store-type=redis
然后為主類添加 @EnableRedisHttpSession
注解。
@EnableRedisHttpSession @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
如果在主類添加的@EnableRedisHttpSession
后,程序運行拋出異常,則取消上述注解,將@EnableRedisHttpSession
注解移交到RedisSessionConfig 類
@Configuration @EnableRedisHttpSession public class RedisSessionConfig { }
6.3 運行程序
這樣就完成了基於 Redis 的 Session 共享,下面來測試下。首先修改 IDEA 配置來允許項目在多端口運行,勾選 Allow running in parallel
:
運行程序,然后修改配置文件,將 server.port
更改為 8060,再次運行。這樣項目就會分別在默認的 8080 端口和 8060 端口運行。
先訪問 localhost:8080
,登錄成功后,再訪問 localhost:8060
,發現無需登錄。
然后我們進入 Redis 查看下 key:
最后再測試下之前配置的 session 設置是否還有效,使用其他瀏覽器登陸,登陸成功后發現原瀏覽器用戶的確被踢出。
---------------------
作者:Jitwxs
來源:CSDN
原文:https://blog.csdn.net/yuanlaijike/article/details/84638745