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

文章目錄

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

  1.1 CustomAuthenticationSuccessHandlergit

  1.2 CustomAuthenticationFailureHandler
  1.3 修改 WebSecurityConfig
  1.4 運行程序
2、Session 超時
3、限制最大登陸數
4、踢出用戶
5、退出登陸
6、Session 共享
  6.1 配置 Redis
  6.2 配置 Session 共享
  6.3 運行程序
在本篇中,主要關注登陸的管理,所以代碼使用最原始版本的便可,即《SpringBoot集成Spring Security(1)——入門程序》源碼便可。github

源碼地址:https://github.com/jitwxs/blog_sample

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

有些時候咱們想要在認證成功後作一些業務處理,例如添加積分;有些時候咱們想要在認證失敗後也作一些業務處理,例如記錄日誌。redis

在以前的文章中,關於認證成功、失敗後的處理都是以下配置的:spring

http.authorizeRequests()
    // 若是有容許匿名的url,填在下面
//    .antMatchers().permitAll()
    .anyRequest().authenticated().and()
    // 設置登錄頁
    .formLogin().loginPage("/login")
    .failureUrl("/login/error")
    .defaultSuccessUrl("/")
    .permitAll()
    ...;

即 failureUrl() 指定認證失敗後Url,defaultSuccessUrl() 指定認證成功後Url。咱們能夠經過設置 successHandler()和 failureHandler() 來實現自定義認證成功、失敗處理。docker

PS:當咱們設置了這兩個後,須要去除 failureUrl() 和 defaultSuccessUrl() 的設置,不然沒法生效。這兩套配置同時只能存在一套。

 

 

1.1 CustomAuthenticationSuccessHandler

 自定義 CustomAuthenticationSuccessHandler 類來實現 AuthenticationSuccessHandler 接口,用來處理認證成功後邏輯:json

 

@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 運行程序

運行程序,當咱們成功登錄後,發現日誌信息被打印出來,頁面被重定向到了首頁:

當咱們認證失敗後,發現日誌中「登錄失敗」被打印出來,頁面展現了認證失敗的異常消息:

 

2、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 過時

3、限制最大登陸數

接下來實現限制最大登錄數,原理就是限制單個用戶可以存在的最大 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

4、踢出用戶

下面來看下如何主動踢出一個用戶。

首先須要在容器中注入名爲 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 刷新頁面,發現被踢出。

5、退出登陸

補充一下退出登陸的內容,在以前,咱們直接在 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;
退出登陸的比較簡單,我就直接貼代碼,不截圖了。

6、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

相關文章
相關標籤/搜索