SpringBoot淺析安全管理之Spring Security配置
在 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內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Spring boot整合security詳解
- 一文詳解Spring Security的基本用法
- java SpringSecurity使用詳解
- springSecurity實現簡單的登錄功能
- Spring Security系列教程之會話管理處理會話過期問題