SpringSecurity學習筆記

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 核心功能

  1. 認證
  2. 授權
  3. 攻擊防護

SpringSecurity 基本原理

在這裏插入圖片描述

自定義 SpringSecurity

SpringSecurity 核心配置

  1. 定義 SpringSecurityCoreConfig 需要繼承 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
  2. SpringSecurityCoreConfig 添加註解 @EnableWebSecurity ,該類可以使用 SpringSecurity 配置和擴展 SpringSecurity
  3. 簡單配置後(使用 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

自定義用戶認證流程邏輯

  1. 創建一個類(示例:MyUserDetailsService),實現 org.springframework.security.core.userdetails.UserDetailsService 接口,重寫 loadUserByUsername 方法
  2. 自定義用戶名和密碼
  3. 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"));
    }
}

自定義用戶登錄頁面

  1. 創建一個自定義登陸頁面 login.html
  2. 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

  1. 首先需要實現 org.springframework.security.web.authentication.AuthenticationSuccessHandler 接口或者繼承 org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler 類,重寫 onAuthenticationSuccess 方法,根據業務需求處理
  2. 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);
    }
}

自定義用戶登錄失敗後處理邏輯

  1. 首先需要實現 org.springframework.security.web.authentication.AuthenticationFailureHandler 接口或者繼承org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler 類,重寫 onAuthenticationFailure 方法
  2. 配置自定義登錄失敗處理的類

定義認證失敗處理邏輯

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 **

  1. 配置 PersistentTokenRepository 類的 bean
  2. 注入 UserDetailsService ,並設置
  3. 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 登出之後會做如下:

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

實現自定義退出成功後業務邏輯

  1. 自定義退出成功類 MyLogoutSuccessHandler,實現 org.springframework.security.web.authentication.logout.LogoutSuccessHandler 接口或者基礎 org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler,重寫 onLogoutSuccess 方法
  2. 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 項目最少默認過期時間爲一分鐘 (配置過低則配置不生效,實際生效爲一分鐘)

  1. application.yml 中配置 SpringSession
  2. 自定義會話過期業務處理邏輯類 MyExpiredSessionStrategy ,實現 org.springframework.security.web.session.SessionInformationExpiredStrategy 接口,重寫 onExpiredSessionDetected
  3. SpringSecurityCoreConfig 中配置

SpringSessionapplication.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);
    }
}

實現圖形驗證碼功能

  1. 生成圖形驗證碼(生成圖形驗證碼,並將驗證碼存到 session 中,最後將生成的圖片寫到接口的響應中)
  2. 自定義圖形驗證碼校驗的過濾器
  3. 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);
    }
}

實現郵件/短信登錄

  1. 根據 org.springframework.security.authentication.UsernamePasswordAuthenticationToken 提供 AccountAuthenticationToke
  2. 根據 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 提供 AccountAuthenticationFilter ,拿到請求的數據,用於組裝成 Token
  3. 根據 org.springframework.security.authentication.dao.DaoAuthenticationProvider 提供 AccountAuthenticationProvider ,用於認證處理
  4. 自定義配置類 AccountAuthenticationSecurityConfig ,將上面AccountAuthenticationFilterAccountAuthenticationProvider 配置到 SpringSecurity 過濾器鏈中
  5. SpringSecurityCoreConfig 中將自定義配置加入到 SecurityBuilder
  6. 發送短信/Email驗證碼接口
  7. 和配置圖像驗證碼過濾器一樣,配置校驗短信驗證碼過濾器(示例中使用同一)

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

自定義無權限異常處理邏輯

  1. 實現 org.springframework.security.web.access.AccessDeniedHandler 接口,重寫 handle 方法
  2. 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);
}

}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章