Spring Security 最佳實踐,看了必懂!

来源:https://www.cnblogs.com/javastack/archive/2022/08/09/16565959.html
-Advertisement-
Play Games

作者:清茶淡粥醬 鏈接:https://juejin.cn/post/7026734817853210661 Spring Security簡介 Spring Security 是一種高度自定義的安全框架,利用(基於)SpringIOC/DI和AOP功能,為系統提供了聲明式安全訪問控制功能,減少了為 ...


作者:清茶淡粥醬
鏈接:https://juejin.cn/post/7026734817853210661

Spring Security簡介

Spring Security 是一種高度自定義的安全框架,利用(基於)SpringIOC/DI和AOP功能,為系統提供了聲明式安全訪問控制功能,減少了為系統安全而編寫大量重覆代碼的工作

核心功能:認證和授權

Spring Security 認證流程

Spring Security 項目搭建

導入依賴

Spring Security已經被Spring boot進行集成,使用時直接引入啟動器即可

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Boot 基礎就不介紹了,推薦下這個實戰教程:

https://github.com/javastacks/spring-boot-best-practice

訪問頁面

導入spring-boot-starter-security啟動器後,Spring Security已經生效,預設攔截全部請求,如果用戶沒有登錄,跳轉到內置登錄頁面。

在瀏覽器輸入:http://localhost:8080/ 進入Spring Security內置登錄頁面

用戶名: user

密碼:項目啟動,列印在控制臺中

自定義用戶名和密碼

修改application.yml 文件

# 靜態用戶,一般只在內部網路認證中使用,如:內部伺服器1,訪問伺服器2
spring:
  security:
    user:
      name: test  # 通過配置文件,設置靜態用戶名
      password: test # 配置文件,設置靜態登錄密碼

UserDetailsService詳解

什麼也沒有配置的時候,賬號和密碼是由Spring Security定義生成的。而在實際項目中賬號和密碼都是從資料庫中查詢出來的。 所以我們要通過自定義邏輯控制認證邏輯。如果需要自定義邏輯時,只需要實現UserDetailsService介面

@Component
public class UserSecurity implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {

        User user = userService.login(userName);
        System.out.println(user);
        if (null==user){
            throw new UsernameNotFoundException("用戶名錯誤");
        }
        org.springframework.security.core.userdetails.User result =
                new org.springframework.security.core.userdetails.User(
                        userName,user.getPassword(), AuthorityUtils.createAuthorityList()
                );
        return result;
    }

}

推薦一個 Spring Boot 基礎教程:

https://github.com/javastacks/spring-boot-best-practice

PasswordEncoder密碼解析器詳解

PasswordEncoder

PasswordEncoder 是SpringSecurity 的密碼解析器,用戶密碼校驗、加密 。 自定義登錄邏輯時要求必須給容器註入PaswordEncoder的bean對象

SpringSecurity 定義了很多實現介面PasswordEncoder 滿足我們密碼加密、密碼校驗 使用需求

自定義密碼解析器

  1. 編寫類,實現PasswordEncoder 介面
/**
 * 憑證匹配器,用於做認證流程的憑證校驗使用的類型
 * 其中有2個核心方法
 * 1. encode - 把明文密碼,加密成密文密碼
 * 2. matches - 校驗明文和密文是否匹配
 * */
public class MyMD5PasswordEncoder implements PasswordEncoder {

    /**
     * 加密
     * @param charSequence  明文字元串
     * @return
     */
    @Override
    public String encode(CharSequence charSequence) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            return toHexString(digest.digest(charSequence.toString().getBytes()));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return "";
        }
    }

    /**
     * 密碼校驗
     * @param charSequence 明文,頁面收集密碼
     * @param s 密文 ,資料庫中存放密碼
     * @return
     */
    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(encode(charSequence));
    }

     /**
     * @param tmp 轉16進位位元組數組
     * @return 飯回16進位字元串
     */
    private String toHexString(byte [] tmp){
        StringBuilder builder = new StringBuilder();
        for (byte b :tmp){
            String s = Integer.toHexString(b & 0xFF);
            if (s.length()==1){
                builder.append("0");
            }
            builder.append(s);
        }

        return builder.toString();

    }
}

2.在配置類中指定自定義密碼憑證匹配器

/**
  * 加密
  * @return 加密對象
  * 如需使用自定義密碼憑證匹配器 返回自定義加密對象
  * 例如: return new MD5PasswordEncoder(); 
  */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(); //Spring Security 自帶
}

登錄配置

方式一 轉發

http.formLogin()
    .usernameParameter("name") // 設置請求參數中,用戶名參數名稱。 預設username
    .passwordParameter("pswd") // 設置請求參數中,密碼參數名稱。 預設password
    .loginPage("/toLogin") // 當用戶未登錄的時候,跳轉的登錄頁面地址是什麼? 預設 /login
    .loginProcessingUrl("/login") // 用戶登錄邏輯請求地址是什麼。 預設是 /login
    .failureForwardUrl("/failure"); // 登錄失敗後,請求轉發的位置。Security請求轉發使用Post請求。預設轉發到: loginPage?error
    .successForwardUrl("/toMain"); // 用戶登錄成功後,請求轉發到的位置。Security請求轉發使用POST請求。

方式二 :重定向

http.formLogin()
    .usernameParameter("name") // 設置請求參數中,用戶名參數名稱。 預設username
    .passwordParameter("pswd") // 設置請求參數中,密碼參數名稱。 預設password
    .loginPage("/toLogin") // 當用戶未登錄的時候,跳轉的登錄頁面地址是什麼? 預設 /login
    .loginProcessingUrl("/login") // 用戶登錄邏輯請求地址是什麼。 預設是 /login
	.defaultSuccessUrl("/toMain",true); //用戶登錄成功後,響應重定向到的位置。 GET請求。必須配置絕對地址。
	 .failureUrl("/failure"); // 登錄失敗後,重定向的位置。

方式三:自定義登錄處理器

自定義登錄失敗邏輯處理器

/*自定義登錄失敗處理器*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private  String url;
    private boolean isRedirect;


    public MyAuthenticationFailureHandler(String url, boolean isRedirect) {
        this.url = url;
        this.isRedirect = isRedirect;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        if (isRedirect){
            httpServletResponse.sendRedirect(url);
        }else {
            httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);
        }
    }

//get set 方法 省略

自定義登錄成功邏輯處理器

/**
 * 自定義登錄成功後處理器
 * 轉發重定向,有代碼邏輯實現
 * */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private String url;
    private boolean isRedirect;

    public MyAuthenticationSuccessHandler(String url, boolean isRedirect) {
        this.url = url;
        this.isRedirect = isRedirect;
    }

    /**
     * @param request 請求對象 request.getRequestDispatcher.forward()
     * @param response 響應對象 response.sendRedirect()
     * @param authentication 用戶認證成功後的對象。其中報換用戶名許可權結合,內容是
     *                       自定義UserDetailsService
     * */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if (isRedirect){
            response.sendRedirect(url);
        }else {
            request.getRequestDispatcher(url).forward(request,response);
        }
    }

//get set 方法 省略   
http.formLogin()
    .usernameParameter("name") // 設置請求參數中,用戶名參數名稱。 預設username
    .passwordParameter("pswd") // 設置請求參數中,密碼參數名稱。 預設password
    .loginPage("/toLogin") // 當用戶未登錄的時候,跳轉的登錄頁面地址是什麼? 預設 /login
    .loginProcessingUrl("/login") // 用戶登錄邏輯請求地址是什麼。 預設是 /login

登錄相關配置類

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private  UserSecurity userSecurity;
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;


    /**
     * 加密
     * @return 加密對象
     * 如需使用自定義加密邏輯 返回自定義加密對象
     * return new MD5PasswordEncoder(); return new SimplePasswordEncoder();
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); //Spring Security 自帶
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置登錄請求相關內容。
        http.formLogin()
            .loginPage("/toLogin") // 當用戶未登錄的時候,跳轉的登錄頁面地址是什麼? 預設 /login
            .usernameParameter("name") // 設置請求參數中,用戶名參數名稱。 預設username
            .passwordParameter("pswd") // 設置請求參數中,密碼參數名稱。 預設password
            .loginProcessingUrl("/login") //設置登錄 提交表單數據訪問請求地址
            .defaultSuccessUrl("/toMain")   
            .failureUrl("/toLogin");
        	//.successForwardUrl("/toMain")
       		//.failureForwardUrl("/toLogin");
            //.successHandler(new LoginSuccessHandler("/toMain", true)) //自定義登錄成功處理器
                //.failureHandler(new LoginErrorHandler("/toLogin", true));

        http.authorizeRequests()
            //.antMatchers("/toLogin").anonymous() //只能匿名用戶訪問
            .antMatchers("/toLogin", "/register", "/login", "/favicon.ico").permitAll() // /toLogin請求地址,可以隨便訪問。
            .antMatchers("/**/*.js").permitAll() // 授予所有目錄下的所有.js文件可訪問許可權
            .regexMatchers(".*[.]css").permitAll() // 授予所有目錄下的所有.css文件可訪問許可權
            .anyRequest().authenticated(); // 任意的請求,都必須認證後才能訪問。


        // 配置退出登錄
        http.logout()
                .invalidateHttpSession(true) // 回收HttpSession對象。退出之前調用HttpSession.invalidate() 預設 true
                .clearAuthentication(true) // 退出之前,清空Security記錄的用戶登錄標記。 預設 true
                // .addLogoutHandler() // 增加退出處理器。
                .logoutSuccessUrl("/") // 配置退出後,進入的請求地址。 預設是loginPage?logout
                .logoutUrl("/logout"); // 配置退出登錄的路徑地址。和頁面請求地址一致即可。

        // 關閉CSRF安全協議。
        // 關閉是為了保證完整流程的可用。
        http.csrf().disable();
    }


   @Bean
   public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

}

角色許可權

hasAuthority(String) 判斷角色是否具有特定許可權

http.authorizeRequests().antMatchers("/main1.html").hasAuthority("admin")

hasAnyAuthority(String ...) 如果用戶具備給定許可權中某一個,就允許訪問

http.authorizeRequests().antMatchers("/admin/read").hasAnyAuthority("xxx","xxx") 

hasRole(String) 如果用戶具備給定角色就允許訪問。否則出現403

//請求地址為/admin/read的請求,必須登錄用戶擁有'管理員'角色才可訪問
http.authorizeRequests().antMatchers("/admin/read").hasRole("管理員") 

hasAnyRole(String ...) 如果用戶具備給定角色的任意一個,就允許被訪問

//用戶擁有角色是管理員 或 訪客 可以訪問 /guest/read
http.authorizeRequests().antMatchers("/guest/read").hasAnyRole("管理員", "訪客")

hasIpAddress(String) 請求是指定的IP就運行訪問

//ip 是127.0.0.1 的請求 可以訪問/ip
http.authorizeRequests().antMatchers("/ip").hasIpAddress("127.0.0.1")

403 許可權不足頁面處理

1.編寫類實現介面AccessDeniedHandler

/**
 * @describe  403 許可權不足
 * @author: AnyWhere
 * @date 2021/4/18 20:57
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) 
            throws IOException, ServletException {

        response.setStatus(HttpServletResponse.SC_OK);

        response.setContentType("text/html;charset=UTF-8");

        response.getWriter().write(
                "<html>" +
                        "<body>" +
                        "<div style='width:800px;text-align:center;margin:auto;font-size:24px'>" +
                        "許可權不足,請聯繫管理員" +
                        "</div>" +
                        "</body>" +
                        "</html>"

        );

        response.getWriter().flush();//刷新緩衝區
    }
}

2.配置類中配置exceptionHandling

// 配置403訪問錯誤處理器。
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);/

RememberMe(記住我)

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    //配置記住密碼
    http.rememberMe()
        .rememberMeParameter("remember-me") // 修改請求參數名。 預設是remember-me
        .tokenValiditySeconds(14*24*60*60) // 設置記住我有效時間。單位是秒。預設是14天
        .rememberMeCookieName("remember-me") // 修改remember me的cookie名稱。預設是remember-me
        .tokenRepository(persistentTokenRepository) // 配置用戶登錄標記的持久化工具對象。
        .userDetailsService(userSecurity); // 配置自定義的UserDetailsService介面實現類對象

  }
  @Bean
  public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
     JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
     jdbcTokenRepository.setDataSource(dataSource);
     //jdbcTokenRepository.setCreateTableOnStartup(true);
     return jdbcTokenRepository;
  }
}   

Spring Security 註解

@Secured

角色校驗 ,請求到來訪問控制單元方法時必須包含XX角色才能訪問

角色必須添加ROLE_首碼

  @Secured({"ROLE_管理員","ROLE_訪客"})
  @RequestMapping("/toMain")
  public String toMain(){
      return "main";
  }

使用註解@Secured需要在配置類中添加註解 使@Secured註解生效

@EnableGlobalMethodSecurity(securedEnabled = true)

@PreAuthorize

許可權檢驗,請求到來訪問控制單元之前必須包含xx許可權才能訪問,控制單元方法執行前進行角色校驗

   /**
     * [ROLE_管理員, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
     * @PreAuthorize   角色 、許可權 校驗 方法執行前進行角色校驗
     *
     *  hasAnyAuthority() 
     *  hasAuthority()
     *
     *  hasPermission()
     *
     *
     *  hasRole()   
     *  hasAnyRole()
     * */

    @PreAuthorize("hasAnyRole('ROLE_管理員','ROLE_訪客')")
    @RequestMapping("/toMain")
    @PreAuthorize("hasAuthority('admin:write')")
    public String toMain(){
        return "main";
    }

使用@PreAuthorize@PostAuthorize 需要在配置類中配置註解@EnableGlobalMethodSecurity 才能生效

@EnableGlobalMethodSecurity(prePostEnabled = true)

@PostAuthorize

許可權檢驗,請求到來訪問控制單元之後必須包含xx許可權才能訪問 ,控制單元方法執行完後進行角色校驗

   /**
     * [ROLE_管理員, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
     * @PostAuthorize  角色 、許可權 校驗 方法執行後進行角色校驗
     *
     *  hasAnyAuthority()
     *  hasAuthority()
     *  hasPermission()
     *  hasRole()
     *  hasAnyRole()
     * */

    @PostAuthorize("hasRole('ROLE_管理員')")
    @RequestMapping("/toMain")
    @PreAuthorize("hasAuthority('admin:write')")
    public String toMain(){
        return "main";
    }

Spring Security 整合Thymeleaf 進行許可權校驗

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
     <groupId>org.thymeleaf.extras</groupId>
     <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

Spring Security中CSRF

什麼是CSRF?

CSRF(Cross-site request forgery)跨站請求偽造,也被稱為“One Click Attack” 或者Session Riding。通過偽造用戶請求訪問受信任站點的非法請求訪問。

跨域:只要網路協議,ip地址,埠中任何一個不相同就是跨域請求。

客戶端與服務進行交互時,由於http協議本身是無狀態協議,所以引入了cookie進行記錄客戶端身份。在cookie中會存放session id用來識別客戶端身份的。在跨域的情況下,session id可能被第三方惡意劫持,通過這個session id向服務端發起請求時,服務端會認為這個請求是合法的,可能發生很多意想不到的事情。

通俗解釋:

CSRF就是別的網站非法獲取我們網站Cookie值,我們項目伺服器是無法區分到底是不是我們的客戶端,只有請求中有Cookie,認為是自己的客戶端,所以這個時候就出現了CSRF。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • @Autowired註解是spring用來支持依賴註入的核心利器之一,但是我們或多或少都會遇到required a single bean, but 2 were found(2可能是其他數字)的問題,接下來我們從源碼的角度去看為什麼會出現這個問題,以及這個問題的解法是什麼? 首先我們寫一個demo ...
  • 2、ElasticSearch高級搜索 Elasticsearch提供了基於JSON的DSL(Domain Specific Language)來定義查詢。常見的查詢類型如下所示 ①、查詢所有 查詢出所有數據,一般測試用;例如 match_all 如下圖所示 ②、全文檢索(full text)查詢 ...
  • Java多線程基礎入門 參考:b站-狂神-多線程詳解 練習與演示代碼見gitee:https://gitee.com/yuhaozhee/java-learning-record ...
  • 1.ObjectPostProcessor 使用 前面介紹了 ObjectPostProcessor的基本概念。相信讀者已經明白,所有的過濾器都由對應的配置類來負責創建,配置類在將過濾器創建成功之後,會調用父類的postProcess方法,該 方法最終會調用到CompositeObjectPostP ...
  • 0. 前言 寫完這篇文章後發現自己對於 DP 的優化一竅不通,所以補了補 DP 的一些優化,寫篇 blog 總結一下。 1. 單調隊列/單調棧優化 1.2 演算法介紹 這應該算是最基礎的 DP 優化方法了。 顧名思義,單調隊列/單調棧優化 DP 就是保持容器內元素的單調性,以達成減少冗餘狀態的目的。 ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • 前言 在Python中 in 操作符可以用於判斷某個元素是否存在於當前對象中,而對於不同的Python對象,使用 in 操作符的處理效率是不一樣的。 今天我們主要針對 4 種不同的Python數據類型進行學習:list列表、tuple元組、set集合、dict字典。 測試過程 我們用於測試的 4 種 ...
  • 多商戶商城系統,也稱為B2B2C(BBC)平臺電商模式多商家商城系統。可以快速幫助企業搭建類似拼多多/京東/天貓/淘寶的綜合商城。 多商戶商城系統支持商家入駐加盟,同時滿足平臺自營、旗艦店等多種經營方式。平臺可以通過收取商家入駐費,訂單交易服務費,提現手續費,簡訊通道費等多手段方式,實現整體盈利。 ...
一周排行
    -Advertisement-
    Play Games
  • public static void GetRegistData() { string name = "huishuangzhu"; //搜索到註冊表根目錄 RegistryKey hkml = Registry.ClassesRoot; //搜索到註冊表根目錄下的XXX文件夾。 RegistryK ...
  • 用acme.sh自動部署功能變數名稱證書 安裝ACME 目前使用量最大的免費SSL證書就是Let’s Encrypt,自2018-03開始,Let’s Encrypt官方發佈上線了免費的SSL泛功能變數名稱證書,目前通過DNS方式獲取比較快,國內可以通過鵝雲的DNSPod功能變數名稱API或者貓雲功能變數名稱API自動簽發Let’ ...
  • 經常看到有群友調侃“為什麼搞Java的總在學習JVM調優?那是因為Java爛!我們.NET就不需要搞這些!”真的是這樣嗎?今天我就用一個案例來分析一下。 昨天,一位學生問了我一個問題:他建了一個預設的ASP.NET Core Web API的項目,也就是那個WeatherForecast的預設項目模 ...
  • 1、環境搭建 1.1 依賴 <!-- nacos註冊中心 註解 @EnableDiscoveryClient --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba- ...
  • ULID:Universally Unique Lexicographically Sortable Identifier(通用唯一詞典分類標識符) UUID:Universally Unique Identifier(通用唯一標識符) 為什麼不選擇UUID UUID 目前有 5 個版本: 版本1: ...
  • 虛基類/抽象類 抽象類:有純虛函數的類 虛繼承 通過修飾繼承方式, 如代碼2是虛繼承,被虛繼承的類稱為虛基類 虛繼承派生類的記憶體佈局方式 先是vbptr => 派生類的數據 =>基類的數據 , 對比代碼1和代碼2,發現原本基類數據在前面,派生類數據在後面,但是在虛繼承的時候 基類數據方式放到了後面, ...
  • 下麵給出 Kafka 一些重要概念,讓大家對 Kafka 有個整體的認識和感知,後面還會詳細的解析每一個概念的作用以及更深入的原理 • Producer:消息生產者,向 Kafka Broker 發消息的客戶端。 • Consumer:消息消費者,從 Kafka Broker 取消息的客戶端。 • ...
  • 前面介紹了對稱加密演算法,本文將介紹密碼學中另一類重要應用:消息摘要(Digest),什麼是消息摘要?簡單的定義是:對一份數據,進行一個單向的Hash函數,生成一個固定長度的Hash值,這個值就是這份數據的摘要,也稱為指紋。 ...
  • 弟弟最近要考試,臨時抱佛腳在網上找了一堆學習資料複習,這不剛就來找我了,說PDF上有水印,影響閱讀效果,到時候考不好就怪資料不行,氣的我差點當場想把他揍一頓! 算了,弟弟長大了,看在打不過他的份上,就不打他了~ 稍加思索,我想起了Python不是可以去水印?說搞就搞! 去除水印原理 去除方法: 用 ...
  • 作者:陳昌浩 1 導讀 if…else…在代碼中經常使用,聽說可以通過Java 8的Function介面來消滅if…else…!Function介面是什麼?如果通過Function介面介面消滅if…else…呢?讓我們一起來探索一下吧。 2 Function介面 Function介面就是一個有且僅有 ...