SpringBoot集成Spring Security(6)——登錄管理


文章目錄

一、自定義認證成功、失敗處理

  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();
    }

    ...
}

 

  1. 首先將 customAuthenticationSuccessHandler 和 customAuthenticationFailureHandler注入進來
  2. 配置 successHandler() 和 failureHandler()
  3. 注釋 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 過期

三、限制最大登錄數

接下來實現限制最大登陸數,原理就是限制單個用戶能夠存在的最大 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 為 false

下面我們來測試下 maxSessionsPreventsLogin(true) 時的情況,我們發現第一個瀏覽器登錄后,第二個瀏覽器無法登錄:

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 + "個";
    }
}
  1. sessionRegistry.getAllPrincipals(); 獲取所有 principal 信息
  2. 通過 principal.getUsername 是否等於輸入值,獲取到指定用戶的 principal
  3. sessionRegistry.getAllSessions(principal, false)獲取該 principal 上的所有 session
  4. 通過 sessionInformation.expireNow() 使得 session 過期

運行程序,分別使用 admin 和 jitwxs 賬戶登錄,admin 訪問 /kick?username=jitwxs 來踢出用戶 jitwxs,jitwxs 刷新頁面,發現被踢出。

五、退出登錄

補充一下退出登錄的內容,在之前,我們直接在 WebSecurityConfig 的 configure() 方法中,配置了:

http.logout();

 

這就是 Spring Security 的默認退出配置,Spring Security 在退出時候做了這樣幾件事:

  1. 使當前的 session 失效
  2. 清除與當前用戶有關的 remember-me 記錄
  3. 清空當前的 SecurityContext
  4. 重定向到登錄頁

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

Allow running in parallel

運行程序,然后修改配置文件,將 server.port 更改為 8060,再次運行。這樣項目就會分別在默認的 8080 端口和 8060 端口運行。

先訪問 localhost:8080,登錄成功后,再訪問 localhost:8060,發現無需登錄。

Session 共享運行結果

然后我們進入 Redis 查看下 key:

最后再測試下之前配置的 session 設置是否還有效,使用其他瀏覽器登陸,登陸成功后發現原瀏覽器用戶的確被踢出。


---------------------
作者:Jitwxs
來源:CSDN
原文:https://blog.csdn.net/yuanlaijike/article/details/84638745


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM