什么是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-starter-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模型來實現的,根據框架的接口去做自定義實現來完成權限控制.