日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

Spring Security实现分布式系统授权

發布時間:2024/1/18 windows 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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的認證方式更適合分布式,它的優點是:

  • 適合統一認證的機制,客戶端、一方應用、三方應用都遵循一致的認證機制。
  • token認證方式對第三方應用接入更適合,因為它更開放,可使用當前有流行的開放協議Oauth2.0、JWT等。
  • 一般情況服務端無需存儲會話信息,減輕了服務端的壓力。
  • 分布式系統認證技術方案見下圖:

    流程描述:

  • 用戶通過接入方(應用)登錄,接入方采取OAuth2.0方式在統一認證服務(UAA)中認證。
  • 認證服務(UAA)調用驗證該用戶的身份是否合法,并獲取用戶權限信息。
  • 認證服務(UAA)獲取接入方權限信息,并驗證接入方是否合法。
  • 若登錄用戶以及接入方都合法,認證服務生成jwt令牌返回給接入方,其中jwt中包含了用戶權限及接入方權限。
  • 后續,接入方攜帶jwt令牌對API網關內的微服務資源進行訪問。
  • API網關對令牌解析、并驗證接入方的權限是否能夠訪問本次請求的微服務。
  • 如果接入方的權限沒問題,API網關將原請求header中附加解析后的明文Token,并將請求轉發至微服務。
  • 微服務收到請求,明文token中包含登錄用戶的身份和權限信息。因此后續微服務自己可以干兩件事:1.用戶授權攔截(看當前用戶是否有權訪問該資源);2.將用戶信息存儲進當前線程上下文(有利于后續業務邏輯隨時獲取當前用戶信息)
  • 流程所涉及到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網關在認證授權體系里主要負責兩件事:

  • 作為OAuth2.0的資源服務器角色,實現接入方權限攔截。
  • 令牌解析并轉發當前登錄用戶信息(明文token)給微服務
  • 微服務拿到明文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:

    @PreAuthorize("hasAuthority('p1')")@GetMapping(value = "/r1")public String r1() {UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return user.getUsername() + "訪問資源1";}@PreAuthorize("hasAuthority('p2')")@GetMapping(value = "/r2")public String r2() {//通過Spring Security API獲取當前登錄用戶UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return user.getUsername() + "訪問資源2";}@GetMapping(value = "/r3")public String r3() {//通過Spring Security API獲取當前登錄用戶UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return user.getUsername() + "訪問資源3";}

    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對象:

    @Component public class TokenAuthenticationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {//1.解析出頭中的tokenString token = httpServletRequest.getHeader("json-token");if (token != null) {String json = EncryptUtil.decodeUTF8StringBase64(token);//將token轉成json對象JSONObject jsonObject = JSON.parseObject(json);//用戶身份信息UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);//用戶權限JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);//2.新建并填充authenticationUsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken(userDTO, null, AuthorityUtils.createAuthorityList(authorities));authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));//3.將authenticationToken填充到安全上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);}filterChain.doFilter(httpServletRequest, httpServletResponse);} }

    經過上邊的過濾器,資源服務中就可以方便到的獲取用戶的身份信息:

    UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

    總結

  • 解析token
  • 新建并填充authentication
  • 將authentication保存進安全上下文
  • 認證服務


    在認證服務UAA中,要注意loadUserByUsername這個方法,我們將整個數據庫查出來的用戶信息存放到UserDto對象中,并將這個對象序列化成json字符串,然后賦值給了UserDetails的username字段:

    @Service public class SpringDataUserDetailsService implements UserDetailsService {@AutowiredUserDao userDao;//根據賬號查詢用戶信息@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//連接數據庫根據賬號查詢用戶信息UserDto userDto = userDao.getUserByUsername(username);if(userDto == null){//如果用戶查不到,返回null,由provider來拋出異常return null;}//根據用戶的id查詢用戶的權限List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());//將permissions轉成數組String[] permissionArray = new String[permissions.size()];permissions.toArray(permissionArray);//將userDto轉成jsonString principal = JSON.toJSONString(userDto);UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionArray).build();return userDetails;} }

    因為只有這樣,我們才能在網關中通過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实现分布式系统授权的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。