首頁 > 軟體

SpringBoot淺析安全管理之Spring Security設定

2022-08-12 18:00:50

在 Java 開發領域常見的安全框架有 Shiro 和 Spring Security。Shiro 是一個輕量級的安全管理框架,提供了認證、授權、對談管理、密碼管理、快取管理等功能。Spring Security 是一個相對複雜的安全管理框架,功能比 Shiro 更加強大,許可權控制細粒度更高,對 OAuth 2 的支援也很友好,又因為 Spring Security 源自 Spring 家族,因此可以和 Spring 框架無縫整合,特別是 Spring Boot 中提供的自動化設定方案,可以讓 Spring Security 的使用更加便捷。

Spring Security 的基本設定

基本用法

1. 建立專案新增依賴

建立一個 Spring Boot 專案,然後新增 spring-boot-starter-security 依賴即可

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

2. 新增 hello 介面

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

3. 啟動專案測試

啟動成功後,存取 /hello 介面就會自動跳轉到登入頁面,這個登入頁面是由 Spring Security 提供的

預設的使用者名稱是 user ,預設的登入密碼則在每次啟動專案時隨機生成,檢視專案啟動紀錄檔

Using generated security password: 4f845a17-7b09-479c-8701-48000e89d364

登入成功後,使用者就可以存取 /hello 介面了

設定使用者名稱和密碼

如果開發者對預設的使用者名稱和密碼不滿意,可以在 application.properties 中設定預設的使用者名稱、密碼以及使用者角色

spring.security.user.name=tangsan
spring.security.user.password=tangsan
spring.security.user.roles=admin

基於記憶體的認證

開發者也可以自定義類繼承自 WebSecurityConfigurer,進而實現對 Spring Security 更多的自定義設定,例如基於記憶體的認證,設定方式如下:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123123").roles("ADMIN", "USER")
                .and()
                .withUser("tangsan").password("123123").roles("USER");
    }
}

程式碼解釋:

  • 自定義 MyWebSecurityConfig 繼承自 WebSecurityConfigurerAdapter ,並重寫 configure(AuthenticationManagerBuilder auth) 方法,在該方法中設定兩個使用者,一個使用者是 admin ,具備兩個角色 ADMIN、USER;另一個使用者是 tangsan ,具備一個角色 USER
  • 此處使用的 Spring Security 版本是 5.0.6 ,在 Spring Security 5.x 中引入了多種密碼加密方式,開發者必須指定一種,此處使用 NoOpPasswordEncoder ,即不對密碼進行加密

注意:基於記憶體的使用者設定,在設定角色時不需要新增 “ROLE_” 字首,這點和後面 10.2 節中基於資料庫的認證有差別。

設定完成後,重啟專案,就可以使用這裡設定的兩個使用者進行登入了。

HttpSecurity

雖然現在可以實現認證功能,但是受保護的資源都是預設的,而且不能根據實際情況進行角色管理,如果要實現這些功能,就需要重寫 WebSecurityConfigurerAdapter 中的另一個方法

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("root").password("123123").roles("ADMIN", "DBA")
                .and()
                .withUser("admin").password("123123").roles("ADMIN", "USER")
                .and()
                .withUser("tangsan").password("123123").roles("USER");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**")
                .hasRole("ADMIN")
                .antMatchers("/user/**")
                .access("hasAnyRole('ADMIN','USER')")
                .antMatchers("/db/**")
                .access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .csrf()
                .disable();
    }
}

程式碼解釋:

  • 首先設定了三個使用者,root 使用者具備 ADMIN 和 DBA 的角色,admin 使用者具備 ADMIN 和 USER 角色,tangsan 用於具備 USER 角色
  • 呼叫 authorizeRequests() 方法開啟 HttpSecurity 的設定,antMatchers() ,hasRole() ,access() 方法設定存取不同的路徑需要不同的使用者及角色
  • anyRequest(),authenticated() 表示出了前面定義的之外,使用者存取其他的 URL 都必須認證後存取
  • formLogin(),loginProcessingUrl(“/login”),permitAll(),表示開啟表單登入,前面看到的登入頁面,同時設定了登入介面為 /login 即可以直接呼叫 /login 介面,發起一個 POST 請求進行登入,登入引數中使用者名稱必須命名為 username ,密碼必須命名為 password,設定 loginProcessingUrl 介面主要是方便 Ajax 或者行動端呼叫登入介面。最後還設定了 permitAll,表示和登入相關的介面都不需要認證即可存取。

設定完成後,在 Controller 中新增如下介面進行測試:

@RestController
public class HelloController {
    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }
    @GetMapping("/user/hello")
    public String user() {
        return "hello user";
    }
    @GetMapping("/db/hello")
    public String dba() {
        return "hello dba";
    }
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

根據上文設定,“/admin/hello” 介面 root 和 admin 使用者具有存取許可權;“/user/hello” 介面 admin 和 tangsan 使用者具有存取許可權;“/db/hello” 只有 root 使用者有存取許可權。瀏覽器中的測試很容易,這裡不再贅述。

登入表單詳細設定

目前為止,登入表單一直使用 Spring Security 提供的頁面,登入成功後也是預設的頁面跳轉,但是,前後端分離已經成為企業級應用開發的主流,在前後端分離的開發方式中,前後端的資料互動通過 JSON 進行,這時,登入成功後就不是頁面跳轉了,而是一段 JSON 提示。要實現這些功能,只需要繼續完善上文的設定

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/**")
        .hasRole("ADMIN")
        .antMatchers("/user/**")
        .access("hasAnyRole('ADMIN','USER')")
        .antMatchers("/db/**")
        .access("hasRole('ADMIN') and hasRole('DBA')")
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .loginPage("/login_page")
        .loginProcessingUrl("/login")
        .usernameParameter("name")
        .passwordParameter("passwd")
        .successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest req,
                                                HttpServletResponse resp,
                                                Authentication auth)
                throws IOException {
                Object principal = auth.getPrincipal();
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                resp.setStatus(200);
                Map<String, Object> map = new HashMap<>();
                map.put("status", 200);
                map.put("msg", principal);
                ObjectMapper om = new ObjectMapper();
                out.write(om.writeValueAsString(map));
                out.flush();
                out.close();
            }
        })
        .failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest req,
                                                HttpServletResponse resp,
                                                AuthenticationException e)
                throws IOException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                resp.setStatus(401);
                Map<String, Object> map = new HashMap<>();
                map.put("status", 401);
                if (e instanceof LockedException) {
                    map.put("msg", "賬戶被鎖定,登入失敗!");
                } else if (e instanceof BadCredentialsException) {
                    map.put("msg", "賬戶名或密碼輸入錯誤,登入失敗!");
                } else if (e instanceof DisabledException) {
                    map.put("msg", "賬戶被禁用,登入失敗!");
                } else if (e instanceof AccountExpiredException) {
                    map.put("msg", "賬戶已過期,登入失敗!");
                } else if (e instanceof CredentialsExpiredException) {
                    map.put("msg", "密碼已過期,登入失敗!");
                } else {
                    map.put("msg", "登入失敗!");
                }
                ObjectMapper om = new ObjectMapper();
                out.write(om.writeValueAsString(map));
                out.flush();
                out.close();
            }
        })
        .permitAll()
        .and()
        .csrf()
        .disable();
}

程式碼解釋:

  • loginPage(“/login_page”) 表示如果使用者未獲授權就存取一個需要授權才能存取的介面,就會自動跳轉到 login_page 頁面讓使用者登入,這個 login_page 就是開發者自定義的登入頁面,而不再是 Spring Security 提供的預設登入頁
  • loginProcessingUrl(“/login”) 表示登入請求處理介面,無論是自定義登入頁面還是行動端登入,都需要使用該介面
  • usernameParameter(“name”),passwordParameter(“passwd”) 定義了認證所需要的使用者名稱和密碼的引數,預設使用者名稱引數是 username,密碼引數是 password,可以在這裡定義
  • successHandler() 方法定義了登入成功的處理邏輯。使用者登入成功後可以跳轉到某一個頁面,也可以返回一段 JSON ,這個要看具體業務邏輯,此處假設是第二種,使用者登入成功後,返回一段登入成功的 JSON 。onAuthenticationSuccess 方法的第三個引數一般用來獲取當前登入使用者的資訊,在登入後,可以獲取當前登入使用者的資訊一起返回給使用者端
  • failureHandler 方法定義了登入失敗的處理邏輯,和登入成功類似,不同的是,登入失敗的回撥方法裡有一個 AuthenticationException 引數,通過這個異常引數可以獲取登入失敗的原因,進而給使用者一個明確的提示

設定完成後,使用 Postman 進行測試

如果登入失敗也會有相應的提示

登出登入設定

如果想要登出登入,也只需要提供簡單的設定即可

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/**")
        .hasRole("ADMIN")
        .antMatchers("/user/**")
        .access("hasAnyRole('ADMIN','USER')")
        .antMatchers("/db/**")
        .access("hasRole('ADMIN') and hasRole('DBA')")
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .loginPage("/login_page")
        .loginProcessingUrl("/login")
        .usernameParameter("name")
        .passwordParameter("passwd")
        .successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest req,
                                                HttpServletResponse resp,
                                                Authentication auth)
                throws IOException {
                Object principal = auth.getPrincipal();
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                resp.setStatus(200);
                Map<String, Object> map = new HashMap<>();
                map.put("status", 200);
                map.put("msg", principal);
                ObjectMapper om = new ObjectMapper();
                out.write(om.writeValueAsString(map));
                out.flush();
                out.close();
            }
        })
        .failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest req,
                                                HttpServletResponse resp,
                                                AuthenticationException e)
                throws IOException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                resp.setStatus(401);
                Map<String, Object> map = new HashMap<>();
                map.put("status", 401);
                if (e instanceof LockedException) {
                    map.put("msg", "賬戶被鎖定,登入失敗!");
                } else if (e instanceof BadCredentialsException) {
                    map.put("msg", "賬戶名或密碼輸入錯誤,登入失敗!");
                } else if (e instanceof DisabledException) {
                    map.put("msg", "賬戶被禁用,登入失敗!");
                } else if (e instanceof AccountExpiredException) {
                    map.put("msg", "賬戶已過期,登入失敗!");
                } else if (e instanceof CredentialsExpiredException) {
                    map.put("msg", "密碼已過期,登入失敗!");
                } else {
                    map.put("msg", "登入失敗!");
                }
                ObjectMapper om = new ObjectMapper();
                out.write(om.writeValueAsString(map));
                out.flush();
                out.close();
            }
        })
        .permitAll()
        .and()
        .logout()
        .logoutUrl("/logout")
        .clearAuthentication(true)
        .invalidateHttpSession(true)
        .addLogoutHandler(new LogoutHandler() {
            @Override
            public void logout(HttpServletRequest req,
                               HttpServletResponse resp,
                               Authentication auth) {
            }
        })
        .logoutSuccessHandler(new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest req,
                                        HttpServletResponse resp,
                                        Authentication auth)
                throws IOException {
                resp.sendRedirect("/login_page");
            }
        })
        .and()
        .csrf()
        .disable();
}

程式碼解釋:

  • logout() 表示開啟登出登入的設定
  • logoutUrl(“/logout”) 表示登出登入請求 URL 為 /logout ,預設也是 /logout
  • clearAuthentication(true) 表示是否清楚身份認證資訊,預設為 true
  • invalidateHttpSession(true) 表示是否使 Session 失效,預設為 true
  • addLogoutHandler 方法中完成一些資料清楚工作,例如 Cookie 的清楚
  • logoutSuccessHandler 方法用於處理登出成功後的業務邏輯,例如返回一段 JSON 提示或者跳轉到登入頁面等

多個 HttpSecurity

如果業務比較複雜,也可以設定多個 HttpSecurity ,實現對 WebSecurityConfigurerAdapter 的多次擴充套件

@Configuration
public class MultiHttpSecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123123").roles("ADMIN", "USER")
                .and()
                .withUser("tangsan").password("123123").roles("USER");
    }
    @Configuration
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/admin/**").authorizeRequests()
                    .anyRequest().hasRole("ADMIN");
        }
    }
    @Configuration
    public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/login")
                    .permitAll()
                    .and()
                    .csrf()
                    .disable();
        }
    }
}

程式碼解釋:

  • 設定多個 HttpSecurity 時,MultiHttpSecurityConfig 不需要繼承 WebSecurityConfigurerAdapter ,在 MultiHttpSecurityConfig 中建立靜態內部類繼承 WebSecurityConfigurerAdapter 即可,靜態內部類上新增 @Configuration 註解和 @Order註解,數位越大優先順序越高,未加 @Order 註解的設定優先順序最低
  • AdminSecurityConfig 類表示該類主要用來處理 “/admin/**” 模式的 URL ,其它 URL 將在 OtherSecurityConfig 類中處理

密碼加密

1. 為什麼要加密

2. 加密方案

Spring Security 提供了多種密碼加密方案,官方推薦使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 強雜湊函數,開發者在使用時可以選擇提供 strength 和 SecureRandom 範例。strength 越大,密碼的迭代次數越多,金鑰迭代次數為 2^strength 。strength 取值在 4~31 之間,預設為 10 。

3. 實踐

只需要修改上文設定的 PasswordEncoder 這個 Bean 的實現即可

 @Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(10);
}

引數 10 就是 strength ,即金鑰的迭代次數(也可以不設定,預設為 10)。

使用以下方式獲取加密後的密碼。

public static void main(String[] args) {
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);
    String encode = bCryptPasswordEncoder.encode("123123");
    System.out.println(encode);
}

修改設定的記憶體使用者的密碼

auth.inMemoryAuthentication()
    .withUser("admin")
    .password("$2a$10$.hZESNfpLSDUnuqnbnVaF..Xb2KsAqwvzN7hN65Gd9K0VADuUbUzy")
    .roles("ADMIN", "USER")
    .and()
    .withUser("tangsan")
    .password("$2a$10$4LJ/xgqxSnBqyuRjoB8QJeqxmUeL2ynD7Q.r8uWtzOGs8oFMyLZn2")
    .roles("USER");

雖然 admin 和 tangsan 加密後的密碼不一樣,但是明文都是 123123 設定完成後,使用 admin/123123,或 tangsan/123123 就可以實現登入,一般情況下,使用者資訊是儲存在資料庫中的,因此需要使用者註冊時對密碼進行加密處理

@Service
public class RegService {
    public int reg(String username, String password) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
        String encodePasswod = encoder.encode(password);
        return saveToDb(username, encodePasswod);
    }
    private int saveToDb(String username, String encodePasswod) {
        // 業務處理
        return 0;
    }
}

使用者將密碼從前端傳來之後,通過 BCryptPasswordEncoder 範例中的 encode 方法對密碼進行加密處理,加密完成後將密文存入資料庫。

方法安全

上文介紹的認證和授權都是基於 URL 的,開發者也可通過註解來靈活設定方法安全,使用相關注解,首先要通過 @EnableGlobalMethodSecurity 註解開啟基於註解的安全設定

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MultiHttpSecurityConfig{
}

程式碼解釋:

  • prePostEnabled = true 會解鎖 @PreAuthorize 和 @PostAuthorize 兩個註解, @PreAuthorize 註解會在方法執行前進行驗證,而 @PostAuthorize 註解在方法執行後進行驗證
  • securedEnabled = true 會解鎖 @Secured 註解

開啟註解安全後,建立一個 MethodService 進行測試

@Service
public class MethodService {
    @Secured("ROLE_ADMIN")
    public String admin() {
        return "hello admin";
    }
    @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")
    public String dba() {
        return "hello dba";
    }
    @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')")
    public String user() {
        return "user";
    }
}

程式碼解釋:

  • @Secured(“ROLE_ADMIN”) 註解表示存取該方法需要 ADMIN 角色,注意這裡需要在角色前加一個字首 ROLE_
  • @PreAuthorize(“hasRole(‘ADMIN’) and hasRole(‘DBA’)”) 註解表示存取該方法既需要 ADMIN 角色又需要 DBA 角色
  • @PreAuthorize(“hasAnyRole(‘ADMIN’,‘DBA’,‘USER’)”) 表示存取該方法需要 ADMIN 、DBA 或 USER 角色中至少一個
  • @PostAuthorize 和 @PreAuthorize 中都可以使用基於表示式的語法

最後在 Controller 中注入 Service 並呼叫 Service 中的方法進行測試

@RestController
public class HelloController {
    @Autowired
    MethodService methodService;
    @GetMapping("/hello")
    public String hello() {
        String user = methodService.user();
        return user;
    }
    @GetMapping("/hello2")
    public String hello2() {
        String admin = methodService.admin();
        return admin;
    }
    @GetMapping("/hello3")
    public String hello3() {
        String dba = methodService.dba();
        return dba;
    }
}

admin 存取 hello

admin 存取 hello2

admin 存取 hello3

到此這篇關於SpringBoot淺析安全管理之Spring Security設定的文章就介紹到這了,更多相關SpringBoot Spring Security內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com