Spring gateway配置Spring Security實(shí)現(xiàn)統(tǒng)一權(quán)限驗(yàn)證與授權(quán)示例源碼
在使用Spring Cloud 進(jìn)行微服務(wù),分布式開發(fā)時(shí),網(wǎng)關(guān)是請求的第一入口,所以一般把客戶端請求的權(quán)限驗(yàn)證統(tǒng)一放在網(wǎng)關(guān)進(jìn)行認(rèn)證與鑒權(quán)。因?yàn)镾pring Cloud Gateway使用是基于WebFlux與Netty開發(fā)的,所以與傳統(tǒng)的Servlet方式不同。而且網(wǎng)關(guān)一般不會(huì)直接請求數(shù)據(jù)庫,不提供用戶管理服務(wù),所以如果想在網(wǎng)關(guān)處進(jìn)行登陸驗(yàn)證與授權(quán)就需要做一些額外的開發(fā)了。
需求設(shè)求
眾所周知,一切架構(gòu)都必須按需求來設(shè)計(jì),萬能構(gòu)架基本上是不存在的,即使是像Spring Security安全架構(gòu)也只是實(shí)現(xiàn)了一個(gè)能用方式,并不是放之四海而皆準(zhǔn)的,但是一個(gè)構(gòu)架的良好擴(kuò)展性是必須的,可以讓使用者按照自己的需要進(jìn)行擴(kuò)展使用。所以為了說明本示例的實(shí)現(xiàn),先假定這樣一個(gè)需求
1,需要有一個(gè)Web網(wǎng)關(guān)服務(wù)進(jìn)行權(quán)限統(tǒng)一認(rèn)證
2,網(wǎng)關(guān)后面有一個(gè)用戶管理服務(wù),負(fù)責(zé)用戶賬號的管理
3,網(wǎng)關(guān)后面還存在其它的服務(wù),但是這些服務(wù)需要認(rèn)證成功之后才能訪問
4,需要支持同一個(gè)請求可以被多個(gè)角色訪問
服務(wù)搭建請參考源碼 https://gitee.com/wgslucky/Spring-Gateway-Security
主要技能點(diǎn)說明
修改默認(rèn)登陸頁面
在項(xiàng)目中添加完spring security依賴之后,如果不添加任何額外的配置,這時(shí)不管發(fā)送任何請求,都會(huì)跳到spring security提供的默認(rèn)登陸頁面。這顯然不是我們想要的,那么第一步就是要顯示自定義的登陸頁面。在Spring Gateway 網(wǎng)關(guān)項(xiàng)目中添加Security的配置,如下面代碼所示:
@EnableWebFluxSecurity public class WebSecurityConfig { @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { RedirectServerAuthenticationEntryPoint loginPoint = new RedirectServerAuthenticationEntryPoint("/xinyue-server-a/account/index"); http.authorizeExchange().pathMatchers("/xinyue-server-a/easyui/**","/xinyue-server-a/js/**","/xinyue-server-a/account/index","/xinyue-server-a/account/login").permitAll() .and().formLogin().loginPage("/xinyue-server-a/account/authen").authenticationEntryPoint(loginPoint) .and().authorizeExchange().anyExchange().authenticated() .and().csrf().disable(); SecurityWebFilterChain chain = http.build(); return chain; } }
這里有一個(gè)容易出現(xiàn)理解錯(cuò)誤的地址,網(wǎng)上有好多示例說是直接只配置loginPage("/my/login")即可,這樣配置的話,需要你的登陸頁面,和提交登陸信息的form的action都必須是一致的,只不過,一個(gè)是get方式請求/my/login,一個(gè)是post方式請求/my/login,但是大多數(shù)據(jù)情況下,我們的登陸頁面地址,和登陸form的action地址是分離的,所以需要按我上面的方式進(jìn)行配置才可以。
http.authorizeExchange().pathMatchers("/xinyue-server-a/easyui/**","/xinyue-server-a/js/**","/xinyue-server-a/account/index","/xinyue-server-a/account/login").permitAll()
這個(gè)配置表示這些請求都不做驗(yàn)證,直接放過。
.and().formLogin().loginPage("/xinyue-server-a/account/authen").authenticationEntryPoint(loginPoint)
這段配置表示需要認(rèn)證的請求是/xinyue-server-a/account/authen(對手正常的Springmvc 服務(wù)來說,這個(gè)應(yīng)該是登陸時(shí)form的action請求地址),如果沒有認(rèn)證,跳轉(zhuǎn)到loginPoint設(shè)置的地址:/xinyue-server-a/account/index,即登陸頁面。
.and().authorizeExchange().anyExchange().authenticated()
這段配置表示其它請求都必須是認(rèn)證(登陸成功)之后才可以訪問。
Spring Cloud Gateway 認(rèn)證方式
如果是微服務(wù)模式,在Spring cloud gateway網(wǎng)關(guān)處進(jìn)行用戶認(rèn)證與授權(quán)有兩種方式:
1,在Spring Cloud Gateway服務(wù)這里添加數(shù)據(jù)庫訪問,直接檢測登陸信息是否正確,如果正確,再給此用戶授權(quán)。
2,在網(wǎng)關(guān)后面專門的認(rèn)證服務(wù)進(jìn)行登陸信息認(rèn)證,如果登陸成功,在返回的Header中添加用戶認(rèn)證與授權(quán)需要的信息,然后在網(wǎng)關(guān)處理再完成認(rèn)證與授權(quán)
Ajax Post登陸與認(rèn)證
本示例采用第二種方式,首先是客戶端向xinyue-server-a服務(wù)發(fā)送登陸請求,如下面代碼所示:
<script type="text/javascript"> function postAjax(url, json, success) { $.ajax({ type : "POST", url : url, data : JSON.stringify(json), dataType : "json", contentType : "application/json", success : function(data) { if (data.code == 0) { success(data); } else { alert("服務(wù)器異常,請聯(lián)系開發(fā)者"); } }, error : function(data) { alert(url + "請求錯(cuò)誤:" + JSON.stringify(data)); } }); } function submitForm() { $("#errorTips").html(""); var username = $("#username").val(); var password = $("#password").val(); var url = "/xinyue-server-a/account/login"; var json = { "username" : username, "password" : password }; postAjax( url, json, function(data) { if (data.code == 0) { //如果登陸成功,發(fā)送認(rèn)證請求 var authUrl = "/xinyue-server-a/account/authen"; var param = {}; postAjax( authUrl, param, function(data) { if (data.code == 0) {//認(rèn)證成功之后,跳轉(zhuǎn)請求 window.location.href = "/xinyue-server-a/account/main"; } else { $("#errorTips").html(data.msg); } }); } else { $("#errorTips").html(data.msg); } }); } </script>
這里使用ajax post方式向服務(wù)端發(fā)送登陸請求,如果登陸成功,然后再發(fā)送認(rèn)證請求,在網(wǎng)關(guān)處完成認(rèn)證。
登陸成功之后,返回用戶信息,緩存在網(wǎng)關(guān)session中
在本示例的源碼中,在xinyue-server-a服務(wù)中模擬用戶登陸成功,并返回此登陸用戶的信息,主要是權(quán)限信息,如下面代碼所示:
@RequestMapping("login") @ResponseBody public Object login(HttpServletResponse response) { JSONObject userInfo = new JSONObject(); userInfo.put("username", "xinyues"); List<String> roles = new ArrayList<>(); roles.add("Admin"); roles.add("Dev"); userInfo.put("roles", roles);//添加角色信息 response.addHeader("AccountInfo", userInfo.toJSONString());//將信息放入響應(yīng)的包頭 JSONObject result = new JSONObject(); result.put("code", 0); return result; }
然后在網(wǎng)關(guān)處添加過濾器,攔截登陸請求的響應(yīng)信息,如下面代碼所示:
@Service public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> { private Logger logger = LoggerFactory.getLogger(AuthenticationGatewayFilterFactory.class); @Override public GatewayFilter apply(Object config) { return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> { List<String> gmAccountInfoJsons = exchange.getResponse().getHeaders().get("AccountInfo"); exchange.getResponse().getHeaders().remove("AccountInfo");//移除包頭中的用戶信息不需要返回給客戶端 if(gmAccountInfoJsons != null && gmAccountInfoJsons.size() > 0) { String gmAccountInfoJson = gmAccountInfoJsons.get(0);//獲取header中的用戶信息 //將信息放在session中,在后面認(rèn)證的請求中使用 exchange.getSession().block().getAttributes().put("AccountInfo", gmAccountInfoJson); } logger.debug("登陸返回信息:{}",gmAccountInfoJsons); })); } }
請求認(rèn)證過濾器,AuthenticationWebFilter
當(dāng)有請求過來時(shí),AuthenticationWebFilter用來攔截認(rèn)證請求,如果客戶端是認(rèn)證請求的話,在這里實(shí)現(xiàn)對此客戶端的認(rèn)證,一般來說攔截的是登陸form中的action地址,可以從form提交的數(shù)據(jù)中獲取用戶名和密碼,然后使用用戶和密碼進(jìn)行用戶驗(yàn)證。但是本示例中并沒有使用form提交登陸,而是使用Ajax Post方式在網(wǎng)關(guān)后面的xinyue-server-a服務(wù)中進(jìn)行的登陸驗(yàn)證。在AuthenticationWebFilter中可以看到,如果是認(rèn)證請求的話,需要使用.flatMap( matchResult -> this.authenticationConverter.convert(exchange))
方式從認(rèn)證請求獲取認(rèn)證需要的信息,默認(rèn)是獲取登陸的用戶名和密碼。但是我們在上面已經(jīng)將登陸信息存在session中了,所示需要重新提供一個(gè)authenticationConverter類,如下面代碼所示:
public class XinyueAuthenticationConverter extends ServerFormLoginAuthenticationConverter{ @Override public Mono<Authentication> convert(ServerWebExchange exchange) { //從session中獲取登陸用戶信息 String value = exchange.getSession().block().getAttribute("AccountInfo"); if(value == null) { return Mono.empty(); } else { List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); //獲取權(quán)限信息 List<String> roels = JSON.parseObject(value).getJSONArray("roles").toJavaList(String.class); roels.forEach(role->{ //這里必須添加前綴,參考:AuthorityReactiveAuthorizationManager.hasRole(role) SimpleGrantedAuthority auth = new SimpleGrantedAuthority("ROLE_" + role); simpleGrantedAuthorities.add(auth); }); //添加用戶信息到spring security之中。 XinyueAccountAuthentication xinyueAccountAuthentication = new XinyueAccountAuthentication(null, value, simpleGrantedAuthorities); return Mono.just(xinyueAccountAuthentication); } } }
然后將XinyueAuthenticationConverter添加到WebSecurityConfig配置中(完成代碼請參考源碼)
SecurityWebFilterChain chain = http.build(); Iterator<WebFilter> weIterable = chain.getWebFilters().toIterable().iterator(); while(weIterable.hasNext()) { WebFilter f = weIterable.next(); if(f instanceof AuthenticationWebFilter) { AuthenticationWebFilter webFilter = (AuthenticationWebFilter) f; //將自定義的AuthenticationConverter添加到過濾器中 webFilter.setServerAuthenticationConverter(new XinyueAuthenticationConverter()); } }
然后添加認(rèn)證成功操作,如下面代碼所示:
@Bean public ReactiveAuthenticationManager reactiveAuthenticationManager() { return new ReactiveAuthenticationManagerAdapter((authentication)->{ if(authentication instanceof XinyueAccountAuthentication) { XinyueAccountAuthentication gmAccountAuthentication = (XinyueAccountAuthentication) authentication; if(gmAccountAuthentication.getPrincipal() != null) { authentication.setAuthenticated(true); return authentication; } else { return authentication; } } else { return authentication; } }); }
到此,就可以算是認(rèn)證成功了,登陸成功之后,就會(huì)跳到轉(zhuǎn)到主頁面了。
請求權(quán)限驗(yàn)證
一般來說,在管理系統(tǒng)中,用戶擁有不同的角色,不同的角色擁有不同的權(quán)限,那么在收到請求的時(shí)候,就需要在網(wǎng)關(guān)驗(yàn)證當(dāng)前用戶是否擁有訪問這個(gè)請求的權(quán)限,或是否是某一個(gè)角色,如果是才能進(jìn)行訪問,否則返回用戶權(quán)限不足,拒絕訪問?,F(xiàn)在給下面這個(gè)請求配置必須擁有Manager權(quán)限才可以訪問
.and().authorizeExchange().pathMatchers("/xinyue-server-a/account/main").hasRole("Manager")
如果這個(gè)時(shí)候再登陸,會(huì)發(fā)現(xiàn)服務(wù)器返回Access Denied
,如果配置為Dev權(quán)限
.and().authorizeExchange().pathMatchers("/xinyue-server-a/account/main").hasRole("Dev")
因?yàn)榇擞脩魮碛蠨ev權(quán)限(模擬賬號),所以可以正常訪問。
多個(gè)角色判斷
目前Spring Security提供的模式是一個(gè)請求配置一個(gè)角色,有些復(fù)雜的系統(tǒng),要求一個(gè)請求的訪問權(quán)限可以被多個(gè)角色共同擁有。這就需要我們自定義一個(gè)權(quán)限的驗(yàn)證了。比如添加如下配置:
.and().authorizeExchange().pathMatchers("/xinyue-server-a/account/main").access(new XinyueReactiveAuthorizationManager("Manager", "Dev"))
表示這個(gè)請求需要Manager或Dev其中一個(gè)角色就可以訪問。然后在XinyueReactiveAuthorizationManager中實(shí)現(xiàn)權(quán)限驗(yàn)證判斷,詳細(xì)請考參源碼
@Override public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) { return authentication .filter(a -> a.isAuthenticated()) .flatMapIterable( a -> a.getAuthorities()) .map( g-> g.getAuthority()) .any(c->{ //檢測權(quán)限是否匹配 String[] roles = c.split(","); for(String role:roles) { if(authorities.contains(role)) { return true; } } return false; }) .map( hasAuthority -> new AuthorizationDecision(hasAuthority)) .defaultIfEmpty(new AuthorizationDecision(false)); }
到此,Spring Cloud Gateway + Spring Security配置完畢,在實(shí)際應(yīng)用中,可以根據(jù)自己的需求再進(jìn)行適當(dāng)?shù)姆庋b。
源碼地址:https://gitee.com/wgslucky/Spring-Gateway-Security
到此這篇關(guān)于Spring gateway配置Spring Security實(shí)現(xiàn)統(tǒng)一權(quán)限驗(yàn)證與授權(quán)的文章就介紹到這了,更多相關(guān)Spring gateway配置Spring Security內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot攔截器無法注入redisTemplate的解決方法
在工作中我們經(jīng)常需要做登錄攔截驗(yàn)證或者其他攔截認(rèn)證功能,但是在寫攔截器的時(shí)候發(fā)現(xiàn)redisTemplate一直無法注入進(jìn)來,本文就詳細(xì)的介紹了解決方法,感興趣的可以了解一下2021-06-06SpringMVC接收與響應(yīng)json數(shù)據(jù)的幾種方式
這篇文章主要給大家介紹了關(guān)于SpringMVC接收與響應(yīng)json數(shù)據(jù)的幾種方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者使用springmvc具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03springboot使用Thymeleaf報(bào)錯(cuò)常見的幾種解決方案
這篇文章主要介紹了springboot使用Thymeleaf報(bào)錯(cuò)常見的幾種解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11詳解如何通過Java實(shí)現(xiàn)壓縮PDF文檔
PDF文檔是我們?nèi)粘^k公中使用最頻繁的文檔格式。但因?yàn)榇蠖鄶?shù)PDF文檔都包含很多頁面圖像或大量圖片,這就導(dǎo)致PDF文檔過大,處理起來較為麻煩。本文將介紹如何通過Java應(yīng)用程序壓縮PDF文檔,需要的可以了解一下2022-12-12java調(diào)用遠(yuǎn)程服務(wù)器的shell腳本以及停止的方法實(shí)現(xiàn)
這篇文章主要介紹了java調(diào)遠(yuǎn)程服務(wù)器的shell腳本以及停止的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Shiro中session超時(shí)頁面跳轉(zhuǎn)的處理方式
這篇文章主要介紹了Shiro中session超時(shí)頁面跳轉(zhuǎn)的處理方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06SpringBoot發(fā)送html郵箱驗(yàn)證碼功能
這篇文章主要介紹了SpringBoot發(fā)送html郵箱驗(yàn)證碼,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12SpringBoot項(xiàng)目使用yml文件鏈接數(shù)據(jù)庫異常問題解決方案
在使用SpringBoot時(shí),利用yml進(jìn)行數(shù)據(jù)庫連接配置需小心數(shù)據(jù)類型區(qū)分,如果用戶名或密碼是數(shù)字,必須用雙引號包裹以識別為字符串,避免連接錯(cuò)誤,特殊字符密碼也應(yīng)用引號包裹2024-10-10SpringBoot整合Redis實(shí)現(xiàn)訪問量統(tǒng)計(jì)的示例代碼
本文主要介紹了SpringBoot整合Redis實(shí)現(xiàn)訪問量統(tǒng)計(jì)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02