spring security是基於spring的安全框架.它提供全面的安全性解決方案,同時在Web請求級別和調用級別確認和受權.在Spring Framework基礎上,spring security充分利用了依賴注入(DI)和麪向切面編程(AOP)功能,爲應用系統提供聲明式的安全訪問控制功能,建曬了爲企業安全控制編寫大量重複代碼的工做,是一個輕量級的安全框架,而且很好集成Spring MVChtml
1 認證 :認證用戶web
2 驗證: 驗證用戶是否有哪些權限,能夠作哪些事情算法
Filter,Servlet,AOP實現spring
IDEA 2017.3 ,MAVEN 3+ ,springboot 2.2.6 spring security 5.2.2, JDK 8+數據庫
建立一個基於Maven的spring boot項目,引入必需依賴編程
父級依賴json
<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項目的主類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})
這樣咱們再次訪問接口,就不會要求咱們登錄就能夠直接訪問了.
去除上述全部配置,咱們從新配置一個配置類去繼承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又是如何幫咱們去完成的呢?
當咱們把用戶信息加入到數據庫,須要實現框架提供的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模型來實現的,根據框架的接口去作自定義實現來完成權限控制.