Spring Security实现分布式系统授权
分布式系統認證方案
分布式系統
隨著軟件環境和需求的變化 ,軟件的架構由單體結構演變為分布式架構,具有分布式架構的系統叫分布式系統,分布式系統的運行通常依賴網絡,它將單體結構的系統分為若干服務,服務之間通過網絡交互來完成用戶的業務處理,當前流行的微服務架構就是分布式系統架構,如下圖:
分布式系統具體如下基本特點:
- 分布性:每個部分都可以獨立部署,服務之間交互通過網絡進行通信,比如:訂單服務、商品服務。
- 伸縮性:每個部分都可以集群方式部署,并可針對部分結點進行硬件及軟件擴容,具有一定的伸縮能力。
- 共享性:每個部分都可以作為共享資源對外提供服務,多個部分可能有操作共享資源的情況。
- 開放性:每個部分根據需求都可以對外發布共享資源的訪問接口,并可允許第三方系統訪問。
分布式認證需求
分布式系統的每個服務都會有認證、授權的需求,如果每個服務都實現一套認證授權邏輯會非常冗余,考慮分布式系統共享性的特點,需要由獨立的認證服務處理系統認證授權的請求;考慮分布式系統開放性的特點,不僅對系統內部服務提供認證,對第三方系統也要提供認證。分布式認證的需求總結如下:
統一認證授權
提供獨立的認證服務,統一處理認證授權。
無論是不同類型的用戶,還是不同種類的客戶端(web端,H5、APP),均采用一致的認證、權限、會話機制,實現統一認證授權。
要實現統一則認證方式必須可擴展,支持各種認證需求,比如:用戶名密碼認證、短信驗證碼、二維碼、人臉識別等認證方式,并可以非常靈活的切換。
應用接入認證
應提供擴展和開放能力,提供安全的系統對接機制,并可開放部分API給接入第三方使用,一方應用(內部系統服務)和第三方應用均采用統一機制接入。
分布式認證方案
基于session的認證方式
在分布式的環境下,基于session的認證會出現一個問題,每個應用服務都需要在session中存儲用戶身份信息,通過負載均衡將本地的請求分配到另一個應用服務需要將session信息帶過去,否則會重新認證。
這個時候,通常的做法有下面幾種:
- Session復制:多臺應用服務器之間同步session,使session保持一致,對外透明。
- Session黏貼:當用戶訪問集群中某臺服務器后,強制指定后續所有請求均落到此機器上。
- Session集中存儲:將Session存入分布式緩存中,所有服務器應用實例統一從分布式緩存中存取Session。
總體來講,基于session認證的認證方式,可以更好的在服務端對會話進行控制,且安全性較高。但是,session機制方式基于cookie,在復雜多樣的移動客戶端上不能有效的使用,并且無法跨域,另外隨著系統的擴展需提高session的復制、黏貼及存儲的容錯性。
基于token的認證方式
基于token的認證方式,服務端不用存儲認證數據,易維護擴展性強, 客戶端可以把token存在任意地方,并且可以實現web和app統一認證機制。其缺點也很明顯,token由于自包含信息,因此一般數據量較大,而且每次請求都需要傳遞,因此比較占帶寬。另外,token的簽名驗簽操作也會給cpu帶來額外的處理負擔。
通過比較2種方式,我們認為基于token的認證方式更適合分布式,它的優點是:
分布式系統認證技術方案見下圖:
流程描述:
流程所涉及到UAA服務、API網關這二個組件職責如下:
- 統一認證服務(UAA):它承載了OAuth2.0接入方認證、登入用戶的認證、授權以及生成令牌的職責,完成實際的用戶認證、授權功能。
- API網關:作為系統的唯一入口,API網關為接入方提供定制的API集合,它可能還具有其它職責,如身份驗證、監控、負載均衡、緩存等。API網關方式的核心要點是,所有的接入方和消費端都通過統一的網關接入微服務,在網關層處理所有的非業務功能。
具體實現
我們將模擬一個微服務架構的系統,創建四個SpringBoot模塊,其中將采用eureka作為微服務注冊中心,zuul作為微服務網關,以及基于spring security實現的認證服務和資源服務。項目結構如下:
注冊中心
創建distributed-security-discovery模塊作為注冊中心,由于本文重點關注SpringSecurity分布式,而非SpringCloud微服務架構,所以不作過多解釋,其中配置文件application.yml如下:
spring:application:name: distributed-discoveryserver:port: 53000 #啟動端口eureka:server:enable-self-preservation: false #關閉服務器自我保護,客戶端心跳檢測15分鐘內錯誤達到80%服務會保護,導致別人還認為是好用的服務eviction-interval-timer-in-ms: 10000 #清理間隔(單位毫秒,默認是60*1000)5秒將客戶端剔除的服務在服務注冊列表中剔除#shouldUseReadOnlyResponseCache: true #eureka是CAP理論種基于AP策略,為了保證強一致性關閉此切換CP 默認不關閉 false關閉client:register-with-eureka: false #false:不作為一個客戶端注冊到注冊中心fetch-registry: false #為true時,可以啟動,但報異常:Cannot execute request on any known serverinstance-info-replication-interval-seconds: 10serviceUrl:defaultZone: http://localhost:${server.port}/eureka/instance:hostname: ${spring.cloud.client.ip-address}prefer-ip-address: trueinstance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}網關
網關整合 OAuth2.0 有兩種思路,一種是認證服務器生成jwt令牌, 所有請求統一在網關層驗證,判斷權限等操作;另一種是由各資源服務處理,網關只做請求轉發。
我們選用第一種,把API網關作為OAuth2.0的資源服務器角色,實現接入客戶端權限攔截、令牌解析并轉發當前登錄用戶信息(jsonToken)給微服務,這樣下游微服務就不需要關心令牌格式解析以及OAuth2.0相關機制了。
API網關在認證授權體系里主要負責兩件事:
微服務拿到明文token(明文token中包含登錄用戶的身份和權限信息)后也需要做兩件事:
統一認證服務(UAA)與統一用戶服務(Order)都是網關下微服務,需要在網關上新增路由配置:
zuul.routes.uaa-service.stripPrefix = false zuul.routes.uaa-service.path = /uaa/**zuul.routes.order-service.stripPrefix = false zuul.routes.order-service.path = /order/**上面配置了網關接收的請求url若符合/order/**表達式,將被被轉發至order-service(統一用戶服務)。
完整目錄結構如下:
配置Token
資源服務器由于需要驗證并解析令牌,往往可以通過在授權服務器暴露check_token的Endpoint來完成,而我們在授權服務器使用的是對稱加密的jwt,因此知道密鑰即可,資源服務與授權服務本就是對稱設計,創建一個TokenConfig配置類:
@Configuration public class TokenConfig {private String SIGNING_KEY = "uaa123";@Beanpublic TokenStore tokenStore() {//JWT令牌存儲方案return new JwtTokenStore(accessTokenConverter());}@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey(SIGNING_KEY); //對稱秘鑰,資源服務器使用該秘鑰來驗證return converter;} }配置資源服務
創建ResouceServerConfig配置類,在其中定義資源服務配置,主要配置的內容就是定義一些匹配規則,描述某個接入客戶端需要什么樣的權限才能訪問某個微服務,如
@Configuration public class ResouceServerConfig {public static final String RESOURCE_ID = "res1";//uaa資源服務配置@Configuration@EnableResourceServerpublic class UAAServerConfig extends ResourceServerConfigurerAdapter {@Autowiredprivate TokenStore tokenStore;@Overridepublic void configure(ResourceServerSecurityConfigurer resources){resources.tokenStore(tokenStore).resourceId(RESOURCE_ID).stateless(true);}@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/uaa/**").permitAll();}}//order資源服務配置@Configuration@EnableResourceServerpublic class OrderServerConfig extends ResourceServerConfigurerAdapter {@Autowiredprivate TokenStore tokenStore;@Overridepublic void configure(ResourceServerSecurityConfigurer resources){resources.tokenStore(tokenStore).resourceId(RESOURCE_ID).stateless(true);}@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");}}//配置其它的資源服務..}上面定義了兩個微服務的資源,其中:UAAServerConfig指定了若請求匹配/uaa/**網關不進行攔截。 OrderServerConfig指定了若請求匹配/order/**,也就是訪問統一用戶服務,接入客戶端需要有scope中包含ROLE_API權限。
轉發明文token給微服務
通過Zuul過濾器的方式實現,目的是讓下游微服務能夠很方便的獲取到當前的登錄用戶信息(明文token)。實現Zuul前置過濾器,完成當前登錄用戶信息提取,并放入轉發微服務的request中:
public class AuthFilter extends ZuulFilter {@Overridepublic boolean shouldFilter() {return true;}@Overridepublic String filterType() {return "pre";}@Overridepublic int filterOrder() {return 0;}@Overridepublic Object run() throws ZuulException {/*** 1.獲取令牌內容 */RequestContext ctx = RequestContext.getCurrentContext();//從安全上下文中拿到用戶身份對象Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//無token訪問網關內資源的情況,目前僅有uua服務直接暴露if (!(authentication instanceof OAuth2Authentication)) {return null;}OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();//取出用戶身份信息String principal = userAuthentication.getName();/*** 2.組裝明文token,轉發給微服務,放入header,名稱為json‐token *///取出用戶權限List<String> authorities = new ArrayList<>();//從userAuthentication取出權限,放在authoritiesuserAuthentication.getAuthorities().stream().forEach(c -> authorities.add(((GrantedAuthority) c).getAuthority()));OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();Map<String, String> requestParameters = oAuth2Request.getRequestParameters();Map<String, Object> jsonToken = new HashMap<>(requestParameters);if (userAuthentication != null) {jsonToken.put("principal", principal);jsonToken.put("authorities", authorities);}//把身份信息和權限信息放在json中,加入http的header中,轉發給微服務ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));return null;} }將filter納入spring 容器,配置ZuulConfig:
@Configuration public class ZuulConfig {@Beanpublic AuthFilter preFilter() {return new AuthFilter();}@Beanpublic FilterRegistrationBean corsFilter() {final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();final CorsConfiguration config = new CorsConfiguration();config.setAllowCredentials(true);config.addAllowedOrigin("*");config.addAllowedHeader("*");config.addAllowedMethod("*");config.setMaxAge(18000L);source.registerCorsConfiguration("/**", config);CorsFilter corsFilter = new CorsFilter(source);FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);bean.setOrder(Ordered.HIGHEST_PRECEDENCE);return bean;} }資源服務
資源服務Order依然采用SpringSecurity的機制進行認證,不同的是資源服務并不需要解析token,因為已經在網關中解析了,并且將明文token放到了請求頭中。現在我們只需要取出請求頭中的json-token并封裝到authentication中即可,后續SpringSecurity會自動鑒權。所以我們要做的是增加微服務用戶鑒權攔截功能。
添加一些測試資源,OrderController增加以下endpoint:
SpringSecurity配置,開啟方法保護,并增加Spring配置策略,客戶端的scope需要有ROLE_ADMIN權限才能訪問資源res1。
@Configuration @EnableResourceServer public class ResouceServerConfig extends ResourceServerConfigurerAdapter {public static final String RESOURCE_ID = "res1";@AutowiredTokenStore tokenStore;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) {resources.resourceId(RESOURCE_ID)//資源 id//.tokenServices(tokenService())//驗證令牌的服務.tokenStore(tokenStore).stateless(true);resources.authenticationEntryPoint(new SimpleAuthenticationEntryPoint());resources.accessDeniedHandler(new SimpleAccessDeniedHandler());}@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')").and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);} }客戶端oauth_client_details表數據,c1客戶端擁有res1資源權限,同時它的scope范圍有ROLE_ADMIN,ROLE_USER,ROLE_API,如果采用c2客戶端獲取token,并用該token訪問Order方法將會提示拒絕訪問。
綜合上面的配置,咱們共定義了三個資源了,擁有p1權限可以訪問r1資源,擁有p2權限可以訪問r2資源,只要認證通過就能訪問r3資源。 接下來定義filter攔截token,并形成Spring Security的Authentication對象:
經過上邊的過濾器,資源服務中就可以方便到的獲取用戶的身份信息:
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();總結:
認證服務
在認證服務UAA中,要注意loadUserByUsername這個方法,我們將整個數據庫查出來的用戶信息存放到UserDto對象中,并將這個對象序列化成json字符串,然后賦值給了UserDetails的username字段:
因為只有這樣,我們才能在網關中通過Authentication的getName獲取到整個用戶身份信息,而非僅僅是登錄名username:
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication(); //取出用戶身份信息,UserDto的JSON字符串 String principal = userAuthentication.getName(); ... jsonToken.put("principal", principal);然后網關將該值封裝到明文token中,繼而資源服務可以獲取到整個用戶身份信息。
//用戶身份信息 UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class); //用戶權限 JSONArray authoritiesArray = jsonObject.getJSONArray("authorities"); String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]); //將用戶信息和權限填充 到用戶身份token對象中 UsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken(userDTO, null, AuthorityUtils.createAuthorityList(authorities)); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));源碼地址
https://github.com/Mcdull0921/distributed-security
鏈接: https://www.xdull.cn/spring-security-distributed.html
來源: 兜兜轉轉的博客
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的Spring Security实现分布式系统授权的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: KepServer问题解答
- 下一篇: java信息管理系统总结_java实现科