Spring?Security全新版本使用方式
前言
前不久Spring Boot 2.7.0 剛剛發(fā)布,Spring Security 也升級(jí)到了5.7.1 。升級(jí)后發(fā)現(xiàn),原來(lái)一直在用的Spring Security配置方法,居然已經(jīng)被棄用了。不禁感慨技術(shù)更新真快,用著用著就被棄用了!今天帶大家體驗(yàn)下Spring Security的最新用法,看看是不是夠優(yōu)雅!
SpringBoot實(shí)戰(zhàn)電商項(xiàng)目mall(50k+star)地址:github.com/macrozheng/…
基本使用
我們先對(duì)比下Spring Security提供的基本功能登錄認(rèn)證,來(lái)看看新版用法是不是更好。
升級(jí)版本
首先修改項(xiàng)目的pom.xml
文件,把Spring Boot版本升級(jí)至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 之前的版本中,我們需要寫個(gè)配置類繼承WebSecurityConfigurerAdapter
,然后重寫Adapter中的三個(gè)方法進(jìn)行配置;
/** * 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版本中進(jìn)行使用的話,你就會(huì)發(fā)現(xiàn)WebSecurityConfigurerAdapter
已經(jīng)被棄用了,看樣子Spring Security要堅(jiān)決放棄這種用法了!
新用法
新用法非常簡(jiǎn)單,無(wú)需再繼承WebSecurityConfigurerAdapter
,只需直接聲明配置類,再配置一個(gè)生成SecurityFilterChain
Bean的方法,把原來(lái)的HttpSecurity配置移動(dòng)到該方法中即可。
/** * 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(); } }
新用法感覺(jué)非常簡(jiǎn)潔干脆,避免了繼承WebSecurityConfigurerAdapter
并重寫方法的操作,強(qiáng)烈建議大家更新一波!
高級(jí)使用
升級(jí) Spring Boot 2.7.0版本后,Spring Security對(duì)于配置方法有了大的更改,那么其他使用有沒(méi)有影響呢?其實(shí)是沒(méi)啥影響的,這里再聊聊如何使用Spring Security實(shí)現(xiàn)動(dòng)態(tài)權(quán)限控制!
基于方法的動(dòng)態(tài)權(quán)限
首先來(lái)聊聊基于方法的動(dòng)態(tài)權(quán)限控制,這種方式雖然實(shí)現(xiàn)簡(jiǎn)單,但卻有一定的弊端。
在配置類上使用@EnableGlobalMethodSecurity
來(lái)開啟它;
/** * SpringSecurity的配置 * Created by macro on 2018/4/26. */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class OldSecurityConfig extends WebSecurityConfigurerAdapter { }
然后在方法中使用@PreAuthorize
配置訪問(wèn)接口需要的權(quán)限;
/** * 商品管理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(); } } }
再?gòu)臄?shù)據(jù)庫(kù)中查詢出用戶所擁有的權(quán)限值設(shè)置到UserDetails
對(duì)象中去,這種做法雖然實(shí)現(xiàn)方便,但是把權(quán)限值寫死在了方法上,并不是一種優(yōu)雅的做法。
/** * UmsAdminService實(shí)現(xiàn)類 * 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("用戶名或密碼錯(cuò)誤"); } }
基于路徑的動(dòng)態(tài)權(quán)限
其實(shí)每個(gè)接口對(duì)應(yīng)的路徑都是唯一的,通過(guò)路徑來(lái)進(jìn)行接口的權(quán)限控制才是更優(yōu)雅的方式。
首先我們需要?jiǎng)?chuàng)建一個(gè)動(dòng)態(tài)權(quán)限的過(guò)濾器,這里注意下doFilter
方法,用于配置放行OPTIONS
和白名單
請(qǐng)求,它會(huì)調(diào)用super.beforeInvocation(fi)
方法,此方法將調(diào)用AccessDecisionManager
中的decide
方法來(lái)進(jìn)行鑒權(quán)操作;
/** * 動(dòng)態(tài)權(quán)限過(guò)濾器,用于實(shí)現(xiàn)基于路徑的動(dòng)態(tài)權(quán)限過(guò)濾 * 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請(qǐng)求直接放行 if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } //白名單請(qǐng)求直接放行 PathMatcher pathMatcher = new AntPathMatcher(); for (String path : ignoreUrlsConfig.getUrls()) { if(pathMatcher.match(path,request.getRequestURI())){ fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } } //此處會(huì)調(diào)用AccessDecisionManager中的decide方法進(jìn)行鑒權(quán)操作 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; } }
接下來(lái)我們就需要?jiǎng)?chuàng)建一個(gè)類來(lái)繼承AccessDecisionManager
,通過(guò)decide
方法對(duì)訪問(wèn)接口所需權(quán)限和用戶擁有的權(quán)限進(jìn)行匹配,匹配則放行;
/** * 動(dòng)態(tài)權(quán)限決策管理器,用于判斷用戶是否有訪問(wèn)權(quán)限 * 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 { // 當(dāng)接口未被配置資源時(shí)直接放行 if (CollUtil.isEmpty(configAttributes)) { return; } Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while (iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //將訪問(wèn)所需資源或用戶擁有資源進(jìn)行比對(duì) String needAuthority = configAttribute.getAttribute(); for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { if (needAuthority.trim().equals(grantedAuthority.getAuthority())) { return; } } } throw new AccessDeniedException("抱歉,您沒(méi)有訪問(wèn)權(quán)限"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
由于上面的decide
方法中的configAttributes
屬性是從FilterInvocationSecurityMetadataSource
的getAttributes
方法中獲取的,我們還需創(chuàng)建一個(gè)類繼承它,getAttributes
方法可用于獲取訪問(wèn)當(dāng)前路徑所需權(quán)限值;
/** * 動(dòng)態(tài)權(quán)限數(shù)據(jù)源,用于獲取動(dòng)態(tài)權(quán)限規(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<>(); //獲取當(dāng)前訪問(wèn)的路徑 String url = ((FilterInvocation) o).getRequestUrl(); String path = URLUtil.getPath(url); PathMatcher pathMatcher = new AntPathMatcher(); Iterator<String> iterator = configAttributeMap.keySet().iterator(); //獲取訪問(wèn)該路徑所需資源 while (iterator.hasNext()) { String pattern = iterator.next(); if (pathMatcher.match(pattern, path)) { configAttributes.add(configAttributeMap.get(pattern)); } } // 未設(shè)置操作請(qǐng)求權(quán)限,返回空集合 return configAttributes; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
這里需要注意的是,所有路徑對(duì)應(yīng)的權(quán)限值數(shù)據(jù)來(lái)自于自定義的DynamicSecurityService
;
/** * 動(dòng)態(tài)權(quán)限相關(guān)業(yè)務(wù)類 * Created by macro on 2020/2/7. */ public interface DynamicSecurityService { /** * 加載資源ANT通配符和資源對(duì)應(yīng)MAP */ Map<String, ConfigAttribute> loadDataSource(); }
一切準(zhǔn)備就緒,把動(dòng)態(tài)權(quán)限過(guò)濾器添加到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 { //省略若干配置... //有動(dòng)態(tài)權(quán)限配置時(shí)添加動(dòng)態(tài)權(quán)限校驗(yàn)過(guò)濾器 if(dynamicSecurityService!=null){ registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class); } return httpSecurity.build(); } }
如果你看過(guò)這篇僅需四步,整合SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證 ! 的話,就知道應(yīng)該要配置這兩個(gè)Bean了,一個(gè)負(fù)責(zé)獲取登錄用戶信息,另一個(gè)負(fù)責(zé)獲取存儲(chǔ)的動(dòng)態(tài)權(quán)限規(guī)則,為了適應(yīng)Spring Security的新用法,我們不再繼承SecurityConfig
,簡(jiǎn)潔了不少!
/** * mall-security模塊相關(guān)配置 * 自定義配置,用于配置如何獲取用戶信息及動(dòng)態(tài)權(quán)限 * 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("用戶名或密碼錯(cuò)誤"); }; } @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; } }; } }
效果測(cè)試
接下來(lái)啟動(dòng)我們的示例項(xiàng)目mall-tiny-security
,使用如下賬號(hào)密碼登錄,該賬號(hào)只配置了訪問(wèn)/brand/listAll
的權(quán)限,訪問(wèn)地址:http://localhost:8088/swagger-ui/
然后把返回的token放入到Swagger的認(rèn)證頭中;
當(dāng)我們?cè)L問(wèn)有權(quán)限的接口時(shí)可以正常獲取到數(shù)據(jù);
>
當(dāng)我們?cè)L問(wèn)沒(méi)有權(quán)限的接口時(shí),返回沒(méi)有訪問(wèn)權(quán)限的接口提示。
總結(jié)
Spring Security的升級(jí)用法確實(shí)夠優(yōu)雅,夠簡(jiǎn)單,而且對(duì)之前用法的兼容性也比較好!個(gè)人感覺(jué)一個(gè)成熟的框架不太會(huì)在升級(jí)過(guò)程中大改用法,即使改了也會(huì)對(duì)之前的用法做兼容,所以對(duì)于絕大多數(shù)框架來(lái)說(shuō)舊版本會(huì)用,新版本照樣會(huì)用!
參考資料
- mall整合SpringSecurity和JWT實(shí)現(xiàn)認(rèn)證和授權(quán)(一)
- mall整合SpringSecurity和JWT實(shí)現(xiàn)認(rèn)證和授權(quán)(二)
- 僅需四步,整合SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證 !
- 手把手教你搞定權(quán)限管理,結(jié)合Spring Security實(shí)現(xiàn)接口的動(dòng)態(tài)權(quán)限控制!
項(xiàng)目源碼地址
本文僅僅是對(duì)Spring Security新用法的總結(jié),如果你想了解Spring Security更多用法,可以參考下之前的文章,希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot集成RabbitMQ實(shí)現(xiàn)用戶注冊(cè)的示例代碼
這篇文章主要介紹了SpringBoot集成RabbitMQ實(shí)現(xiàn)用戶注冊(cè)的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12spring中@RestController和@Controller的區(qū)別小結(jié)
@RestController和@Controller這兩個(gè)注解用于創(chuàng)建Web應(yīng)用程序的控制器類,那么這兩個(gè)注解有哪些區(qū)別,本文就來(lái)介紹一下,并用示例代碼說(shuō)明,感興趣的可以了解一下2023-09-09基于Eclipse 的JSP/Servlet的開發(fā)環(huán)境的搭建(圖文)
本文將會(huì)詳細(xì)地展示如何搭建JSP的開發(fā)環(huán)境。本次教程使用的是最新版的Eclipse 2018-09編輯器和最新版的Apache Tomcat v9.0,步驟詳細(xì),內(nèi)容詳盡,適合零基礎(chǔ)學(xué)者作為學(xué)習(xí)參考2018-12-12maven多個(gè)plugin相同phase的執(zhí)行順序
這篇文章主要介紹了maven多個(gè)plugin相同phase的執(zhí)行順序,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12詳解領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)之事件驅(qū)動(dòng)與CQRS
這篇文章分析了如何應(yīng)用事件來(lái)分離軟件核心復(fù)雜度。探究CQRS為什么廣泛應(yīng)用于DDD項(xiàng)目中,以及如何落地實(shí)現(xiàn)CQRS框架。當(dāng)然我們也要警惕一些失敗的教訓(xùn),利弊分析以后再去抉擇正確的應(yīng)對(duì)之道2021-06-06JAVA實(shí)現(xiàn)簡(jiǎn)單停車場(chǎng)系統(tǒng)代碼
JAVA項(xiàng)目中正號(hào)需要一個(gè)停車收費(fèi)系統(tǒng),就整理出來(lái)java實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的停車收費(fèi)系統(tǒng)給大家分享一下,希望對(duì)大家有所幫助2017-04-04Java詳解多線程協(xié)作作業(yè)之信號(hào)同步
信號(hào)量同步是指在不同線程之間,通過(guò)傳遞同步信號(hào)量來(lái)協(xié)調(diào)線程執(zhí)行的先后次序。CountDownLatch是基于時(shí)間維度的Semaphore則是基于信號(hào)維度的2022-05-05