spring security是spring家族的一個安全框架,入門簡單。對比shiro,它自帶登錄頁面,自動完成登錄操作。權限過濾時支持http方法過濾。
在新手入門使用時,只需要簡單的配置,即可實現登錄以及權限的管理,無需自己寫功能邏輯代碼。
但是對於現在大部分前后端分離的web程序,尤其是前端普遍使用ajax請求時,spring security自帶的登錄系統就有一些不滿足需求了。
因為spring security有自己默認的登錄頁,自己默認的登錄控制器。而登錄成功或失敗,都會返回一個302跳轉。登錄成功跳轉到主頁,失敗跳轉到登錄頁。如果未認證直接訪問也會跳轉到登錄頁。但是如果前端使用ajax請求,ajax是無法處理302請求的。前后端分離web中,規范是使用json交互。我們希望登錄成功或者失敗都會返回一個json。況且spring security自帶的登錄頁太丑了,我們還是需要使用自己的。
spring security一般簡單使用:
web的安全控制一般分為兩個部分,一個是認證,一個是授權。
認證管理:
就是認證是否為合法用戶,簡單的說是登錄。一般為匹對用戶名和密碼,即認證成功。
在spring security認證中,我們需要注意的是:哪個類表示用戶?哪個屬性表示用戶名?哪個屬性表示密碼?怎么通過用戶名取到對應的用戶?密碼的驗證方式是什么?
只要告訴spring security這幾個東西,基本上就可以了。
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { }
事實上只要繼承WebSecurityConfigurerAdapter ,spring security就已經啟用了,當你訪問資源時,它就會跳轉到它自己默認的登錄頁。但是這還不行,
當用戶點擊登錄時,
1.它會拿到用戶輸入的用戶名密碼;
2.根據用戶名通過UserDetailsService 的 loadUserByUsername(username)方法獲得一個用戶對象;
3.獲得一個UserDetails 對象,獲得內部的成員屬性password;
4.通過PasswordEncoder 的 matchs(s1, s2) 方法對比用戶的輸入的密碼和第3步的密碼;
5.匹配成功;
所以我們要實現這三個接口的三個方法:
1.實現UserDetailsService ,可以選擇同時實現用戶的正常業務方法和UserDetailsService ;
例如:UserServiceImpl implement IUserService,UserDetailsService {}
2.實現UserDetails ,一般使用用戶的實體類實現此接口。
其中有getUsername(), getPassword(), getAuthorities()為獲取用戶名,密碼,權限。可根據個人情況實現。
3.實現PasswordEncoder ,spring security 提供了多個該接口的實現類,可百度和查看源碼理解,也可以自己寫。
三個實現類的配置如下:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.password.NoOpPasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(NoOpPasswordEncoder.getInstance()); } }
其中Userdetails 為UserDetailsService 中 loadUserByUsername() 方法的返回值類型。
到目前為止,就可以完成簡單認證了。而授權管理,到現在,是默認的:所有資源都只有‘認證’權限,所有用戶也只有‘認證’權限。即,經過認證就可以訪問所有資源。
以上,就是spring security的簡易應用。可以實現一個稍微完整的安全控制。非常簡單。
授權管理:
授權管理,是在已認證的前提下。用戶在認證后,根據用戶的不同權限,開放不同的資源。
根據RBAC設計,用戶有多個角色,角色有多個權限。(真正控制資源的是權限,角色只是一個權限列表,方便使用。)
每個用戶都有一個權限列表,授權管理,就是權限和資源的映射。在編程中,寫好對應關系。然后當用戶請求資源時,查詢用戶是否有資源對應的權限決定是否通過。
權限寫在數據庫,配置文件或其他任何地方。只要調用loadUserByUsername()時返回的UserDetails對象中的getAuthorities()方法能獲取到。
所以無論用戶的權限寫在哪里,只要getAuthorities()能得到就可以了。
舉例:
授權管理映射:add==/api/add,query==/api/query;
數據庫中存儲了用戶權限:query;
那么該用戶就只能訪問/api/query,而不能訪問/api/add。
授權管理配置如下:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.password.NoOpPasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(NoOpPasswordEncoder.getInstance()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(HttpMethod.POST, "/api/data").hasAuthority("add") .antMatchers(HttpMethod.GET, "/api/data").hasAuthority("query") .antMatchers("/home").hasAuthority("base"); } }
以上就是spring security的基本應用。下面是解決前后端分離下的無法302跳轉的情況。
需求是:前后端分離,需要自己的登錄頁面,使用ajax請求。
出現問題:自己的登錄頁面請求登錄后,后端返回302跳轉主頁,ajax無法處理;未認證請求資源時,后端返回302跳轉登錄頁,也無法處理。
解決思想:修改302狀態碼,修改為401,403或者200和json數據。
HttpSecurity 有很多方法,可以看一看
比如 設置登錄頁(formLogin().loginPage("/login.html")) 可以設置自己的登錄頁(該設置主要是針對使用302跳轉,且有自己的登錄頁,如果不使用302跳轉,前后端完全分離,無需設置)。
比如 設置認證成功處理
比如 設置認證失敗處理
比如 設置異常處理
比如 設置退出成功處理
可以繼承重寫其中的主要方法(里面有httpResponse對象,可以隨便返回任何東西)
例如:
import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setStatus(HttpStatus.OK.value()); } }
設置完成登錄成功和失敗處理后,還是不夠滿足需求,當用戶未通過登錄頁進入網站,我們需要在用戶直接訪問資源時,告訴前端此用戶未認證。(默認是302跳轉到登錄頁)。我們可以改成返回403狀態碼。
這里就需要實現一個特殊的方法:AuthenticationEntryPoint 接口的 commence()方法。
這個方法主要是,用戶未認證訪問資源時,所做的處理。
spring security給我們提供了很多現成的AuthenticationEntryPoint 實現類,
比如默認的302跳轉登錄頁,比如返回403狀態碼,還比如返回json數據等等。當然也可以自己寫。和上面的登錄處理一樣,實現接口方法,將實現類實例傳到配置方法(推薦spring注入)。
如下:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.password.NoOpPasswordEncoder; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Qualifier("userService") @Autowired private UserDetailsService userDetailsService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailureHandler loginFailureHandler; @Autowired private MyLogoutHandler logoutHandler; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(NoOpPasswordEncoder.getInstance()); } @Override protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginProcessingUrl("/login") // 登錄成功 .successHandler(loginSuccessHandler) // 登錄失敗 .failureHandler(loginFailureHandler).permitAll() .and() // 注銷成功 .logout().logoutSuccessHandler(logoutHandler) .and() // 未登錄請求資源 .exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint()) .and() .authorizeRequests() .antMatchers(HttpMethod.POST, "/api/data").hasAuthority("add") .antMatchers(HttpMethod.GET, "/api/data").hasAuthority("query") .antMatchers("/home").hasAuthority("base"); } }
以上就算是完了,前端發起ajax請求時,后端會返回200,401,403狀態碼,前端可根據狀態碼做相應的處理。
以下是我的全部代碼(后端,安全管理demo)
https://github.com/Question7/spring-security-demo