基於spring的安全管理框架-Spring Security
什麼是spring security?
spring security是基於spring的安全框架.它提供全面的安全性解決方案,同時在Web請求級別和呼叫級別確認和授權.在Spring Framework基礎上,spring security充分利用了依賴注入(DI)和麵向切面程式設計(AOP)功能,為應用系統提供宣告式的安全訪問控制功能,建晒了為企業安全控制編寫大量重複程式碼的工作,是一個輕量級的安全框架,並且很好整合Spring MVC
spring security的核心功能有哪些?
1 認證 :認證使用者
2 驗證: 驗證使用者是否有哪些許可權,可以做哪些事情
spring security基於哪些技術實現?
Filter,Servlet,AOP實現
框架技術準備:
IDEA 2017.3 ,MAVEN 3+ ,springboot 2.2.6 spring security 5.2.2, JDK 8+
spring security初步整合使用
建立一個基於Maven的spring boot專案,引入必需依賴
父級依賴
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-parent</artifactId> <version>2.2.6.RELEASE</version> </parent>
springboot專案整合spring security的起步依賴
springboot web專案的起步依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
我們啟動springboot專案的主類
大家可以看到,此刻我們已經實現了spring security最簡單的功能,上面截圖的最下方就是spring sceurity給我們隨機生成的密碼
我們此刻可以建立一個最簡單的controller層來測試訪問安全控制
@RestController public class HelloController { @RequestMapping("/sayHello") public String sayHello() { System.out.println("Hello,spring security"); return "hello,spring security"; } }
接下來我們通過呼叫這個sayHello介面,我們會得到一個登入介面
此刻我們輸入預設的使用者名稱user ,密碼就是控制檯隨機生成的一串字元 2dddf218-48c7-454c-875d-f7283e8457c1
我們就可以以成功訪問: hello,spring security
當然,我們也可以在spring的配置檔案中去配置自定義的使用者名稱和密碼,這樣也可以實現同樣的效果,配置如下圖所示.
如果我們不想使用spring security的訪問控制功能,我們可以在Springboot的啟動類註解上排除spring security的自動配置
@SpringBootApplication(exclude ={SecurityAutoConfiguration.class})
這樣我們再次訪問介面,就不會要求我們登陸就可以直接訪問了.
Spring Security 基於記憶體配置:
去除上述所有配置,我們重新配置一個配置類去繼承WebSecurityConfigurerAdapter,這個介面卡類有很多方法,我們需要重寫configure(AuthenticationManagerBuilder auth)方法
@Configuration //配置類 @EnableWebSecurity //啟用spring security安全框架功能 public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder passwordEncoder = passwordEncoder(); auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456")) .roles(); } /** * spring security自帶的加密演算法PasswordEncoder,我們使用其中一種演算法來對密碼加密 BCryptPasswordEncoder方法採用SHA-256 * +隨機鹽+金鑰對密碼進行加密,過程不可逆 不加密高版本會報錯 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
這樣我們就在記憶體配置了使用者admin,密碼採用加密演算法去實現記憶體中的使用者登入認證.
在實際的場景中一個使用者可能有多個角色,接下來看一下基於記憶體角色的使用者認證
首先我們在配置類上需要添加註解啟用方法級別的使用者角色認證@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration //配置類 @EnableWebSecurity //啟用spring security安全框架功能 @EnableGlobalMethodSecurity(prePostEnabled = true) //啟用方法級別的認證 prePostEnabled boolean預設false,true表示可以使用 @PreAuthorize註解 和 @PostAuthorize註解 public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder passwordEncoder = passwordEncoder(); auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456")) .roles("super", "normal"); auth.inMemoryAuthentication().withUser("normal").password(passwordEncoder.encode("123456")) .roles("normal"); } /** * spring security自帶的加密演算法PasswordEncoder,我們使用其中一種演算法來對密碼加密 BCryptPasswordEncoder方法採用SHA-256 * +隨機鹽+金鑰對密碼進行加密,過程不可逆 不加密高版本會報錯 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
此刻我們在記憶體中建立了兩個使用者,一個normal使用者,只有normal許可權,一個admin使用者,擁有super許可權和normal許可權.
我們建立三個訪問路徑,分別對應super,normal和 super,normal都可以訪問
@RequestMapping("/super") @PreAuthorize(value = "hasRole('super')") public String saySuper() { System.out.println("Hello,super!"); return "Hello,super"; } @RequestMapping("/normal") @PreAuthorize(value = "hasRole('normal')") public String sayNormal() { System.out.println("Hello,normal!"); return "hello,normal"; } @RequestMapping("/all") @PreAuthorize(value = "hasAnyRole('normal','super')") public String sayAll() { System.out.println("Hello,super,normal!"); return "Hello,super,normal"; }
我們會發現,normal使用者可以訪問2,3 admin可以訪問 1,2,3,由此可以看出,此刻許可權控制是OK的
這樣簡單地基於記憶體的使用者許可權認證就完成了,但是記憶體中的使用者資訊是不穩定不可靠的,我們需要從資料庫讀取,那麼spring security又是如何幫我們去完成的呢?
spring security基於資料庫使用者資訊的安全訪問控制
當我們把使用者資訊加入到資料庫,需要實現框架提供的UserDetailsService介面,去通過呼叫資料庫去獲取我們需要的使用者和角色資訊
@Configuration //配置類 @EnableWebSecurity //啟用spring security安全框架功能 @EnableGlobalMethodSecurity(prePostEnabled = true) //啟用方法級別的認證 prePostEnabled boolean預設false,true表示可以使用 @PreAuthorize註解 和 @PostAuthorize註解 public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailService userDetailService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder passwordEncoder = passwordEncoder(); // auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456")) // .roles("super", "normal"); auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder()); } /** * spring security自帶的加密演算法PasswordEncoder,我們使用其中一種演算法來對密碼加密 BCryptPasswordEncoder方法採用SHA-256 * +隨機鹽+金鑰對密碼進行加密,過程不可逆 不加密高版本會報錯 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
自定義實現的介面,去通過資料庫查詢使用者資訊,此處需要注意兩個地方,
1:我們資料庫的密碼是通過new BCryptPasswordEncoder().encode("123456")生成的,明文密碼是不可以的,因為我們已經指定了密碼加密規則BCryptPasswordEncoder,
2:我們若有多個角色怎麼辦?迴圈遍歷放入list中,注意:角色必須以ROLE_開頭
@Component public class MyUserDetailService implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { org.springframework.security.core.userdetails.User user = null; User userInfo = null; if (!StringUtils.isEmpty(userName)) { userInfo = userMapper.getUserInfoByName(userName); if (userInfo != null) { List<GrantedAuthority> list = new ArrayList<>(); String role = userInfo.getRole(); GrantedAuthority authority = new SimpleGrantedAuthority( "ROLE_" + userInfo.getRole()); list.add(authority); //建立User物件返回 user = new org.springframework.security.core.userdetails.User(userInfo.getName(), userInfo.getPassword(), list); } } return user; } }
這裡的介面給予了使用者極大的擴充套件空間,我們最終建立User物件返回,User物件有兩個構造方法,根據需要選取,引數含義參考原始碼對照就行
這樣我們就通過查詢資料庫獲取使用者的登入使用者名稱和密碼以及角色資訊是否匹配和具有訪問許可權.
基於角色的許可權
認證和授權:
認證(authentication):認證訪問者是誰?是否是當前系統的有限使用者
授權(authorization):當前使用者可以做什麼?
我們就以RBAC(Role-Based Access controll),這樣我們就需要設計出最少五張表去完成許可權控制
user 表(儲存使用者資訊)
user_role(使用者角色資訊關係表)
role表(角色資訊)
role_permission(角色許可權資訊關係表)
permission(授權資訊,可以儲存訪問url路徑等)
這樣的許可權設計模型,許可權授予角色,角色授予使用者,管理起來清晰明瞭
接下來我們需要再次重寫MyWebSecurityConfig中的兩個configure方法
我們如果想忽略控制某些資源,不加訪問攔截,我們就可以在WebSecurity方法配置忽略請求的url,一般會設定登入路徑,獲取圖形驗證碼路徑,靜態資源等
@Override public void configure(WebSecurity web) throws Exception { //設定忽略攔截的路徑匹配,這些請求無需攔截,直接放行 web.ignoring().antMatchers("/index.html", "/static/**", "/login_p", "/getPicture"); }
接下來我們就重點講一下重新的下一個方法HttpSecurity,這個方法裡面配置了我們對於許可權的處理
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() //authorizeRequests() 允許基於使用HttpServletRequest限制訪問 .withObjectPostProcessor(postProcessor()) //請求都會經過此方法配置的過濾器*****重點******,出了WebSecurity配置的忽略請求 .and() //返回HttpSecurity物件----------------------------------- .formLogin() //指定基於表單的身份驗證沒指定,則將生成預設登入頁面 .loginPage("/login_p") //指定跳轉登入頁 .loginProcessingUrl("/login") //登入路徑 .usernameParameter("username") //使用者名稱引數名 .passwordParameter("password")//密碼引數名 .failureHandler(customAuthenticationFailureHandler()) //自定義失敗處理 .successHandler(customAuthenticationSuccessHandler()) //自定義成功處理 .permitAll().and() //返回HttpSecurity物件---------------------------------------- .logout()// .logoutUrl("/logout").logoutSuccessHandler(customLogoutSuccessHandler()) .permitAll()// .and() //返回HttpSecurity物件---------------------------------------- .csrf().disable() //預設會開啟CSRF處理,判斷請求是否攜帶了token,如果沒有就拒絕訪問 我們此處設定禁用 .exceptionHandling()// .authenticationEntryPoint(customAuthenticationEntryPoint()) //認證入口 .accessDeniedHandler(customAccessDeniedHandler()); //訪問拒絕處理 }
public ObjectPostProcessor<FilterSecurityInterceptor> postProcessor() { ObjectPostProcessor<FilterSecurityInterceptor> obj = new ObjectPostProcessor<FilterSecurityInterceptor>() { //此方法 @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(metadataSource); //通過請求地址獲取改地址需要的使用者角色 object.setAccessDecisionManager( accessDecisionManager); //判斷是否登入,是否當前使用者是否具有訪問當前url的角色 return object; } }; return obj; }
在這裡我們需要實現兩個介面FilterInvocationSecurityMetadataSource ,AccessDecisionManager
首先是FilterInvocationSecurityMetadataSource,我們在這個介面實現類裡面getAttributes()方法主要做的就是獲取請求路徑url,然後去資料庫查詢哪些角色具有此路徑的訪問許可權,然後把角色資訊返回List<ConfigAttribute>,很巧,SecurityConfig已經提供了一個方法createList,我們直接呼叫此方法返回就可以
@Component public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource { @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation)o).getRequestUrl(); List<String> list = new ArrayList(); if (list.size() > 0) {
//虛擬碼 匹配到具有該url的角色放入集合 String[] values = new String[list.size()]; return SecurityConfig.createList(values); } //沒有匹配上的資源,都是登入訪問 return SecurityConfig.createList("ROLE_LOGIN"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return FilterInvocation.class.isAssignableFrom(aClass); } }
下面我們需要通過使用者所擁有的角色和url所需角色作比對,匹配可以訪問,不匹配丟擲異常AccessDeniedException,這裡更巧的一點是
我們可以通過Authentication獲取使用者所擁有的的角色,我們在上面實現類放入的角色集合也通過引數形式再次傳了進來,我們可以迴圈比對當前使用者是否有足夠許可權
@Component public class UrlAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication auth, Object o, Collection<ConfigAttribute> cas){ Iterator<ConfigAttribute> iterator = cas.iterator(); while (iterator.hasNext()) { ConfigAttribute ca = iterator.next(); //當前請求需要的許可權 String needRole = ca.getAttribute(); if ("ROLE_LOGIN".equals(needRole)) { if (auth instanceof AnonymousAuthenticationToken) { throw new BadCredentialsException("未登入"); } else return; } //當前使用者所具有的許可權 Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return; } } } throw new AccessDeniedException("許可權不足!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
當我們把這兩個介面自定義實現了方法之後,後面每一步的自定義處理資訊,我們都可以根據業務需要去處理,比如
自定義身份驗證處理器: 根據異常去響應會不同資訊或者跳轉url,其他自定義處理器同理
下面給大家一個處理器demo,下面自定義處理器custom**的都可以參考做不同情況處理返回值等來完成處理,前後端分離可以響應資料,不分離的可以跳轉頁面
public AuthenticationFailureHandler customAuthenticationFailureHandler() { AuthenticationFailureHandler failureHandler = new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); RespBean respBean = null; if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) { respBean = RespBean.error("賬戶名或者密碼輸入錯誤!"); } else if (e instanceof LockedException) { respBean = RespBean.error("賬戶被鎖定,請聯絡管理員!"); } else if (e instanceof CredentialsExpiredException) { respBean = RespBean.error("密碼過期,請聯絡管理員!"); } else if (e instanceof AccountExpiredException) { respBean = RespBean.error("賬戶過期,請聯絡管理員!"); } else if (e instanceof DisabledException) { respBean = RespBean.error("賬戶被禁用,請聯絡管理員!"); } else { respBean = RespBean.error("登入失敗!"); } resp.setStatus(401); ObjectMapper om = new ObjectMapper(); PrintWriter out = resp.getWriter(); out.write(om.writeValueAsString(respBean)); out.flush(); out.close(); } }; return failureHandler; }
當我們把表建立好,實現上面的不同介面處理器,完成上述配置,我們就可以實現安全訪問控制,至於spring security更深層級的用法,歡迎大家一起探討!有時間我會分享一下另一個主流的安全訪問控制框架 Apache shiro.其實我們會發現,所有的安全框架都是基於RBAC模型來實現的,根據框架的介面去做自定義實現來完成許可權控