Spring?Security全新版本使用方式
前言
前不久Spring Boot 2.7.0 剛剛發(fā)布,Spring Security 也升級到了5.7.1 。升級后發(fā)現,原來一直在用的Spring Security配置方法,居然已經被棄用了。不禁感慨技術更新真快,用著用著就被棄用了!今天帶大家體驗下Spring Security的最新用法,看看是不是夠優(yōu)雅!
SpringBoot實戰(zhàn)電商項目mall(50k+star)地址:github.com/macrozheng/…
基本使用
我們先對比下Spring Security提供的基本功能登錄認證,來看看新版用法是不是更好。
升級版本
首先修改項目的pom.xml
文件,把Spring Boot版本升級至2.7.0
版本。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.0</version> <relativePath/> <!-- lookup parent from repository --> </parent>
舊用法
在Spring Boot 2.7.0 之前的版本中,我們需要寫個配置類繼承WebSecurityConfigurerAdapter
,然后重寫Adapter中的三個方法進行配置;
/** * SpringSecurity的配置 * Created by macro on 2018/4/26. */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class OldSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UmsAdminService adminService; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { //省略HttpSecurity的配置 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
如果你在SpringBoot 2.7.0版本中進行使用的話,你就會發(fā)現WebSecurityConfigurerAdapter
已經被棄用了,看樣子Spring Security要堅決放棄這種用法了!
新用法
新用法非常簡單,無需再繼承WebSecurityConfigurerAdapter
,只需直接聲明配置類,再配置一個生成SecurityFilterChain
Bean的方法,把原來的HttpSecurity配置移動到該方法中即可。
/** * SpringSecurity 5.4.x以上新用法配置 * 為避免循環(huán)依賴,僅用于配置HttpSecurity * Created by macro on 2022/5/19. */ @Configuration public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { //省略HttpSecurity的配置 return httpSecurity.build(); } }
新用法感覺非常簡潔干脆,避免了繼承WebSecurityConfigurerAdapter
并重寫方法的操作,強烈建議大家更新一波!
高級使用
升級 Spring Boot 2.7.0版本后,Spring Security對于配置方法有了大的更改,那么其他使用有沒有影響呢?其實是沒啥影響的,這里再聊聊如何使用Spring Security實現動態(tài)權限控制!
基于方法的動態(tài)權限
首先來聊聊基于方法的動態(tài)權限控制,這種方式雖然實現簡單,但卻有一定的弊端。
在配置類上使用@EnableGlobalMethodSecurity
來開啟它;
/** * SpringSecurity的配置 * Created by macro on 2018/4/26. */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class OldSecurityConfig extends WebSecurityConfigurerAdapter { }
然后在方法中使用@PreAuthorize
配置訪問接口需要的權限;
/** * 商品管理Controller * Created by macro on 2018/4/26. */ @Controller @Api(tags = "PmsProductController", description = "商品管理") @RequestMapping("/product") public class PmsProductController { @Autowired private PmsProductService productService; @ApiOperation("創(chuàng)建商品") @RequestMapping(value = "/create", method = RequestMethod.POST) @ResponseBody @PreAuthorize("hasAuthority('pms:product:create')") public CommonResult create(@RequestBody PmsProductParam productParam, BindingResult bindingResult) { int count = productService.create(productParam); if (count > 0) { return CommonResult.success(count); } else { return CommonResult.failed(); } } }
再從數據庫中查詢出用戶所擁有的權限值設置到UserDetails
對象中去,這種做法雖然實現方便,但是把權限值寫死在了方法上,并不是一種優(yōu)雅的做法。
/** * UmsAdminService實現類 * Created by macro on 2018/4/26. */ @Service public class UmsAdminServiceImpl implements UmsAdminService { @Override public UserDetails loadUserByUsername(String username){ //獲取用戶信息 UmsAdmin admin = getAdminByUsername(username); if (admin != null) { List<UmsPermission> permissionList = getPermissionList(admin.getId()); return new AdminUserDetails(admin,permissionList); } throw new UsernameNotFoundException("用戶名或密碼錯誤"); } }
基于路徑的動態(tài)權限
其實每個接口對應的路徑都是唯一的,通過路徑來進行接口的權限控制才是更優(yōu)雅的方式。
首先我們需要創(chuàng)建一個動態(tài)權限的過濾器,這里注意下doFilter
方法,用于配置放行OPTIONS
和白名單
請求,它會調用super.beforeInvocation(fi)
方法,此方法將調用AccessDecisionManager
中的decide
方法來進行鑒權操作;
/** * 動態(tài)權限過濾器,用于實現基于路徑的動態(tài)權限過濾 * Created by macro on 2020/2/7. */ public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter { @Autowired private DynamicSecurityMetadataSource dynamicSecurityMetadataSource; @Autowired private IgnoreUrlsConfig ignoreUrlsConfig; @Autowired public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) { super.setAccessDecisionManager(dynamicAccessDecisionManager); } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain); //OPTIONS請求直接放行 if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } //白名單請求直接放行 PathMatcher pathMatcher = new AntPathMatcher(); for (String path : ignoreUrlsConfig.getUrls()) { if(pathMatcher.match(path,request.getRequestURI())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } } //此處會調用AccessDecisionManager中的decide方法進行鑒權操作 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } @Override public void destroy() { } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return dynamicSecurityMetadataSource; } }
接下來我們就需要創(chuàng)建一個類來繼承AccessDecisionManager
,通過decide
方法對訪問接口所需權限和用戶擁有的權限進行匹配,匹配則放行;
/** * 動態(tài)權限決策管理器,用于判斷用戶是否有訪問權限 * Created by macro on 2020/2/7. */ public class DynamicAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { // 當接口未被配置資源時直接放行 if (CollUtil.isEmpty(configAttributes)) { return; } Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while (iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //將訪問所需資源或用戶擁有資源進行比對 String needAuthority = configAttribute.getAttribute(); for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { if (needAuthority.trim().equals(grantedAuthority.getAuthority())) { return; } } } throw new AccessDeniedException("抱歉,您沒有訪問權限"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
由于上面的decide
方法中的configAttributes
屬性是從FilterInvocationSecurityMetadataSource
的getAttributes
方法中獲取的,我們還需創(chuàng)建一個類繼承它,getAttributes
方法可用于獲取訪問當前路徑所需權限值;
/** * 動態(tài)權限數據源,用于獲取動態(tài)權限規(guī)則 * Created by macro on 2020/2/7. */ public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private static Map<String, ConfigAttribute> configAttributeMap = null; @Autowired private DynamicSecurityService dynamicSecurityService; @PostConstruct public void loadDataSource() { configAttributeMap = dynamicSecurityService.loadDataSource(); } public void clearDataSource() { configAttributeMap.clear(); configAttributeMap = null; } @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { if (configAttributeMap == null) this.loadDataSource(); List<ConfigAttribute> configAttributes = new ArrayList<>(); //獲取當前訪問的路徑 String url = ((FilterInvocation) o).getRequestUrl(); String path = URLUtil.getPath(url); PathMatcher pathMatcher = new AntPathMatcher(); Iterator<String> iterator = configAttributeMap.keySet().iterator(); //獲取訪問該路徑所需資源 while (iterator.hasNext()) { String pattern = iterator.next(); if (pathMatcher.match(pattern, path)) { configAttributes.add(configAttributeMap.get(pattern)); } } // 未設置操作請求權限,返回空集合 return configAttributes; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
這里需要注意的是,所有路徑對應的權限值數據來自于自定義的DynamicSecurityService
;
/** * 動態(tài)權限相關業(yè)務類 * Created by macro on 2020/2/7. */ public interface DynamicSecurityService { /** * 加載資源ANT通配符和資源對應MAP */ Map<String, ConfigAttribute> loadDataSource(); }
一切準備就緒,把動態(tài)權限過濾器添加到FilterSecurityInterceptor
之前;
/** * SpringSecurity 5.4.x以上新用法配置 * 為避免循環(huán)依賴,僅用于配置HttpSecurity * Created by macro on 2022/5/19. */ @Configuration public class SecurityConfig { @Autowired private DynamicSecurityService dynamicSecurityService; @Autowired private DynamicSecurityFilter dynamicSecurityFilter; @Bean SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { //省略若干配置... //有動態(tài)權限配置時添加動態(tài)權限校驗過濾器 if(dynamicSecurityService!=null){ registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class); } return httpSecurity.build(); } }
如果你看過這篇僅需四步,整合SpringSecurity+JWT實現登錄認證 ! 的話,就知道應該要配置這兩個Bean了,一個負責獲取登錄用戶信息,另一個負責獲取存儲的動態(tài)權限規(guī)則,為了適應Spring Security的新用法,我們不再繼承SecurityConfig
,簡潔了不少!
/** * mall-security模塊相關配置 * 自定義配置,用于配置如何獲取用戶信息及動態(tài)權限 * Created by macro on 2022/5/20. */ @Configuration public class MallSecurityConfig { @Autowired private UmsAdminService adminService; @Bean public UserDetailsService userDetailsService() { //獲取登錄用戶信息 return username -> { AdminUserDetails admin = adminService.getAdminByUsername(username); if (admin != null) { return admin; } throw new UsernameNotFoundException("用戶名或密碼錯誤"); }; } @Bean public DynamicSecurityService dynamicSecurityService() { return new DynamicSecurityService() { @Override public Map<String, ConfigAttribute> loadDataSource() { Map<String, ConfigAttribute> map = new ConcurrentHashMap<>(); List<UmsResource> resourceList = adminService.getResourceList(); for (UmsResource resource : resourceList) { map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName())); } return map; } }; } }
效果測試
接下來啟動我們的示例項目mall-tiny-security
,使用如下賬號密碼登錄,該賬號只配置了訪問/brand/listAll
的權限,訪問地址:http://localhost:8088/swagger-ui/
然后把返回的token放入到Swagger的認證頭中;
當我們訪問有權限的接口時可以正常獲取到數據;
>
當我們訪問沒有權限的接口時,返回沒有訪問權限的接口提示。
總結
Spring Security的升級用法確實夠優(yōu)雅,夠簡單,而且對之前用法的兼容性也比較好!個人感覺一個成熟的框架不太會在升級過程中大改用法,即使改了也會對之前的用法做兼容,所以對于絕大多數框架來說舊版本會用,新版本照樣會用!
參考資料
- mall整合SpringSecurity和JWT實現認證和授權(一)
- mall整合SpringSecurity和JWT實現認證和授權(二)
- 僅需四步,整合SpringSecurity+JWT實現登錄認證 !
- 手把手教你搞定權限管理,結合Spring Security實現接口的動態(tài)權限控制!
項目源碼地址
本文僅僅是對Spring Security新用法的總結,如果你想了解Spring Security更多用法,可以參考下之前的文章,希望大家以后多多支持腳本之家!
相關文章
SpringBoot集成RabbitMQ實現用戶注冊的示例代碼
這篇文章主要介紹了SpringBoot集成RabbitMQ實現用戶注冊的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12spring中@RestController和@Controller的區(qū)別小結
@RestController和@Controller這兩個注解用于創(chuàng)建Web應用程序的控制器類,那么這兩個注解有哪些區(qū)別,本文就來介紹一下,并用示例代碼說明,感興趣的可以了解一下2023-09-09基于Eclipse 的JSP/Servlet的開發(fā)環(huán)境的搭建(圖文)
本文將會詳細地展示如何搭建JSP的開發(fā)環(huán)境。本次教程使用的是最新版的Eclipse 2018-09編輯器和最新版的Apache Tomcat v9.0,步驟詳細,內容詳盡,適合零基礎學者作為學習參考2018-12-12maven多個plugin相同phase的執(zhí)行順序
這篇文章主要介紹了maven多個plugin相同phase的執(zhí)行順序,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12