<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          微服務網(wǎng)關與用戶身份識別,JWT+Spring Security進行網(wǎng)關安全認證

          共 18066字,需瀏覽 37分鐘

           ·

          2022-03-06 13:06


          JWT+Spring Security進行網(wǎng)關安全認證

          JWT和Spring Security相結合進行系統(tǒng)安全認證是目前使用比較多的一種安全認證組合。瘋狂創(chuàng)客圈crazy-springcloud微服務開發(fā)腳手架使用JWT身份令牌結合Spring Security的安全認證機制完成用戶請求的安全權限認證。整個用戶認證的過程大致如下:

          (1)前臺(如網(wǎng)頁富客戶端)通過REST接口將用戶名和密碼發(fā)送到UAA用戶賬號與認證微服務進行登錄。

          (2)UAA服務在完成登錄流程后,將Session ID作為JWT的負載(payload),生成JWT身份令牌后發(fā)送給前臺。

          (3)前臺可以將JWT令牌存到localStorage或者sessionStorage中,當然,退出登錄時,前端必須刪除保存的JWT令牌。

          (4)前臺每次在請求微服務提供者的REST資源時,將JWT令牌放到請求頭中。crazy-springcloud腳手架做了管理端和用戶端的前臺區(qū)分,管理端前臺的令牌頭為Authorization,用戶端前臺的令牌頭為token。

          (5)在請求到達Zuul網(wǎng)關時,Zuul會結合Spring Security進行攔截,從而驗證JWT的有效性。

          (6)Zuul驗證通過后才可以訪問微服務所提供的REST資源。

          需要說明的是,在crazy-springcloud微服務開發(fā)腳手架中,Provider微服務提供者自身不需要進行單獨的安全認證,Provider之間的內(nèi)部遠程調(diào)用也是不需要安全認證的,安全認證全部由網(wǎng)關負責。嚴格來說,這套安全機制是能夠滿足一般的生產(chǎn)場景安全認證要求的。如果覺得這個安全級別不是太高,單個的Provider微服務也需要進行獨立的安全認證,那么實現(xiàn)起來也是很容易的,只需要導入公共的安全認證模塊base-auth即可。實際上早期的crazy-springcloud腳手架也是這樣做的,后期發(fā)現(xiàn)這樣做純屬多慮,而且大大降低了Provider服務提供者模塊的可復用性和可移植性(這是微服務架構的巨大優(yōu)勢之一)。所以,crazy-springcloud后來將整體架構調(diào)整為由網(wǎng)關(如Zuul或者Nginx)負責安全認證,去掉了Provider服務提供者的安全認證能力。

          JWT安全令牌規(guī)范詳解

          JWT(JSON Web Token)是一種用戶憑證的編碼規(guī)范,是一種網(wǎng)絡環(huán)境下編碼用戶憑證的JSON格式的開放標準(RFC 7519)。JWT令牌的格式被設計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)、用戶身份認證等場景。

          一個編碼之后的JWT令牌字符串分為三部分:header+payload+signature。這三部分通過點號“.”連接,第一部分常被稱為頭部(header),第二部分常被稱為負載(payload),第三部分常被稱為簽名(signature)。

          1.JWT的header

          編碼之前的JWT的header部分采用JSON格式,一個完整的頭部就像如下的JSON內(nèi)容:

          {
          "typ":"JWT",
          "alg":"HS256"
          }

          其中,"typ"是type(類型)的簡寫,值為"JWT"代表JWT類型;"alg"是加密算法的簡寫,值為"HS256"代表加密方式為HS256。

          采用JWT令牌編碼時,header的JSON字符串將進行Base64編碼,編碼之后的字符串構成了JWT令牌的第一部分。

          2.JWT的playload

          編碼之前的JWT的playload部分也是采用JSON格式,playload是存放有效信息的部分,一個簡單的playload就像如下的JSON內(nèi)容:

          {
          "sub":"session id",
          "exp":1579315717,
          "iat":1578451717
          }

          采用JWT令牌編碼時,playload的JSON字符串將進行Base64編碼,編碼之后的字符串構成了JWT令牌的第二部分。

          3.JWT的signature

          JWT的第三部分是一個簽名字符串,這一部分是將header的Base64編碼和payload的Base64編碼使用點號(.)連接起來之后,通過header聲明的加密算法進行加密所得到的密文。為了保證安全,加密時需要加入鹽(salt)。

          下面是一個演示用例:用Java代碼生成JWT令牌,然后對令牌的header部分字符串和payload部分字符串進行Base64解碼,并輸出解碼后的JSON。

          package com.crazymaker.demo.auth;
          //省略import
          @Slf4j
          public class JwtDemo
          {
          @Test
          public void testBaseJWT()
          {
          try
          {
          /**
          *JWT的演示內(nèi)容
          */

          String subject = "session id";
          /**
          *簽名的加密鹽
          */

          String salt = "user password";
          /**
          *簽名的加密算法
          */

          Algorithm algorithm = Algorithm.HMAC256(salt);
          //簽發(fā)時間
          long start = System.currentTimeMillis() - 60000;
          //過期時間,在簽發(fā)時間的基礎上加上一個有效時長
          Date end = new Date(start + SessionConstants.SESSION_TIME_OUT *1000);
          /**
          *獲取編碼后的JWT令牌
          */

          String token = JWT.create()
          .withSubject(subject)
          .withIssuedAt(new Date(start))
          .withExpiresAt(end)
          .sign(algorithm);
          log.info("token=" + token);
          //編碼后輸出demo為:
          //token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZXNza
          W9uIGlkIiwiZXhwIjoxNTc5MzE1NzE3LCJpYXQiOjE1Nzg0NTE3MTd9.iANh9Fa0B_6H5TQ11bLCWcEpmWxuCwa2Rt6rnzBWteI
          //以.分隔令牌
          String[] parts = token.split("\\." );
          /**
          *對第一部分和第二部分進行解碼
          *解碼后的第一部分:header
          */

          String headerJson =
          StringUtils.newStringUtf8(Base64.decodeBase64(parts[0]));
          log.info("parts[0]=" + headerJson);
          //解碼后的第一部分輸出的示例為://parts[0]={"typ":"JWT","alg":"HS256"}
          /**
          *解碼后的第二部分:payload
          */

          String payloadJson;
          payloadJson = StringUtils.newStringUtf8
          (Base64.decodeBase64(parts[1]));
          log.info("parts[1]=" + payloadJson);
          //輸出的示例為:
          //解碼后的第二部分:parts[1]={"sub":"session id","exp":1579315535,"iat":
          1578451535}
          } catch (Exception e)
          {
          e.printStackTrace();
          }
          }
          ...
          }

          在編碼前的JWT中,payload部分JSON中的屬性被稱為JWT的聲明。JWT的聲明分為兩類:

          (1)公有的聲明(如iat)。

          (2)私有的聲明(自定義的JSON屬性)。

          公有的聲明也就是JWT標準中注冊的聲明,主要為以下JSON屬性:

          (1)iss:簽發(fā)人。

          (2)sub:主題。

          (3)aud:用戶。

          (4)iat:JWT的簽發(fā)時間。

          (5)exp:JWT的過期時間,這個過期時間必須要大于簽發(fā)時間。

          (6)nbf:定義在什么時間之前該JWT是不可用的。

          私有的聲明是除了公有聲明之外的自定義JSON字段,私有的聲明可以添加任何信息,一般添加用戶的相關信息或其他業(yè)務需要的必要信息。下面的JSON例子中的uid、user_name、nick_name等都是私有聲明。

          {
          "uid": "123...",
          "sub": "session id",
          "user_name": "admin",
          "nick_name": "管理員",
          "exp": 1579317358,
          "iat": 1578453358
          }

          下面是一個向JWT令牌添加私有聲明的實例,代碼如下:

          package com.crazymaker.demo.auth;
          //省略import
          @Slf4j
          public class JwtDemo
          {
          /**
          *測試私有聲明
          */

          @Test
          public void testJWTWithClaim()
          {
          try
          {
          String subject = "session id";
          String salt = "user password";
          /**
          *簽名的加密算法
          */

          Algorithm algorithm = Algorithm.HMAC256(salt);
          //簽發(fā)時間
          long start = System.currentTimeMillis() - 60000;
          //過期時間,在簽發(fā)時間的基礎上加上一個有效時長
          Date end = new Date(start + SessionConstants.SESSION_TIME_OUT *1000);
          /**
          *JWT建造者
          */

          JWTCreator.Builder builder = JWT.create();
          /**
          *增加私有聲明
          */

          builder.withClaim("uid", "123...");
          builder.withClaim("user_name", "admin");
          builder.withClaim("nick_name","管理員");
          /**
          *獲取編碼后的JWT令牌
          */

          String token =builder
          .withSubject(subject)
          .withIssuedAt(new Date(start))
          .withExpiresAt(end)
          .sign(algorithm);
          log.info("token=" + token);
          //以.分隔,這里需要轉義
          String[] parts = token.split("\\." );
          String payloadJson;
          /**
          *解碼payload
          */

          payloadJson = StringUtils.newStringUtf8
          (Base64.decodeBase64(parts[1]));
          log.info("parts[1]=" + payloadJson);
          //輸出demo為:parts[1]=
          //{"uid":"123...","sub":"session id","user_name":"admin",
          "nick_name":"管理員","exp":1579317358,"iat":1578453358}
          } catch (Exception e)
          {
          e.printStackTrace();
          }
          }
          }

          由于JWT的payload聲明(JSON屬性)是可以解碼的,屬于明文信息,因此不建議添加敏感信息。

          ?JWT+Spring Security認證處理流程

          實際開發(fā)中如何使用JWT進行用戶認證呢?瘋狂創(chuàng)客圈的crazy-springcloud開發(fā)腳手架將JWT令牌和Spring Security相結合,設計了一個公共的、比較方便復用的用戶認證模塊base-auth。一般來說,在Zuul網(wǎng)關或者微服務提供者進行用戶認證時導入這個公共的base-auth模塊即可。

          這里還是按照6.4.2節(jié)中請求認證處理流程的5個步驟介紹base-auth模塊中JWT令牌的認證處理流程。

          首先看第一步:定制一個憑證/令牌類,封裝用戶信息和JWT認證信息。

          package com.crazymaker.springcloud.base.security.token;
          //省略import
          public class JwtAuthenticationToken extends AbstractAuthenticationToken
          {
          private static final long serialVersionUID = 3981518947978158945L;
          //封裝用戶信息:用戶id、密碼
          private UserDetails userDetails;
          //封裝的JWT認證信息
          private DecodedJWT decodedJWT;
          ...
          }

          再看第二步:定制一個認證提供者類和憑證/令牌類進行配套,并完成對自制憑證/令牌實例的驗證。

          package com.crazymaker.springcloud.base.security.provider;
          //省略import
          public class JwtAuthenticationProvider implements AuthenticationProvider
          {
          //用于通過session id查找用戶信息
          private RedisOperationsSessionRepository sessionRepository;
          public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository)
          {
          this.sessionRepository = sessionRepository;
          }
          @Override
          public Authentication authenticate(Authentication authentication) throws AuthenticationException
          {
          //判斷JWT令牌是否過期
          JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
          DecodedJWT jwt =jwtToken.getDecodedJWT();
          if (jwt.getExpiresAt().before(Calendar.getInstance().getTime()))
          {
          throw new NonceExpiredException("認證過期");
          }
          //取得session id
          String sid = jwt.getSubject();
          //取得令牌字符串,此變量將用于驗證是否重復登錄
          String newToken = jwt.getToken();
          //獲取session
          Session session = null;
          try
          {
          session = sessionRepository.findById(sid);
          } catch (Exception e)
          {
          e.printStackTrace();
          }
          if (null == session)
          {
          throw new NonceExpiredException("還沒有登錄,請登錄系統(tǒng)!");
          }
          String json = session.getAttribute(G_USER);
          if (StringUtils.isBlank(json))
          {
          throw new NonceExpiredException("認證有誤,請重新登錄");
          }
          //取得session中的用戶信息
          UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class);
          if (null == userDTO)
          {
          throw new NonceExpiredException("認證有誤,請重新登錄");
          }
          判斷是否在其他地方已經(jīng)登錄 //判斷是否在其他地方已經(jīng)登錄
          if (null == newToken || !newToken.equals(userDTO.getToken()))
          {
          throw new NonceExpiredException("您已經(jīng)在其他的地方登錄!");
          }
          String userID = null;
          if (null == userDTO.getUserId())
          {
          userID = String.valueOf(userDTO.getId());
          } else
          {
          userID = String.valueOf(userDTO.getUserId());
          }
          UserDetails userDetails = User.builder()
          .username(userID)
          .password(userDTO.getPassword())
          .authorities(SessionConstants.USER_INFO)
          .build();
          try
          {
          //用戶密碼的密文作為JWT的加密鹽
          String encryptSalt = userDTO.getPassword();
          Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
          //創(chuàng)建驗證器
          JWTVerifier verifier = JWT.require(algorithm)
          .withSubject(sid)
          .build();
          //進行JWTtoken驗證
          verifier.verify(newToken);
          } catch (Exception e)
          {
          throw new BadCredentialsException("認證有誤:令牌校驗失敗,請重新登錄", e);
          }
          //返回認證通過的token,包含用戶信息,如user id等
          JwtAuthenticationToken passedToken =
          new JwtAuthenticationToken(userDetails, jwt, userDetails.getAuthorities());
          passedToken.setAuthenticated(true);
          return passedToken;
          }
          //支持自定義的令牌JwtAuthenticationToken
          @Override
          public boolean supports(Class authentication)
          {
          return authentication.isAssignableFrom(JwtAuthenticationToken.class);
          }
          }

          JwtAuthenticationProvider負責對傳入的JwtAuthenticationToken憑證/令牌實例進行多方面的驗證:(1)驗證解碼后的DecodedJWT實例是否過期;(2)由于本演示中JWT的subject(主題)信息存放的是用戶的Session ID,因此還要判斷會話是否存在;(3)使用會話中的用戶密碼作為鹽,對JWT令牌進行安全性校驗。

          如果以上驗證都順利通過,就構建一個新的JwtAuthenticationToken令牌,將重要的用戶信息(UserID)放入令牌并予以返回,供后續(xù)操作使用。

          第三步:定制一個過濾器類,從請求中獲取用戶信息組裝成JwtAuthenticationToken憑證/令牌,交給認證管理者。在crazy-springcloud腳手架中,前臺有用戶端和管理端的兩套界面,所以,將認證頭部信息區(qū)分成管理端和用戶端兩類:管理端的頭部字段為Authorization;用戶端的認證信息頭部字段為token。

          過濾器從請求中獲取認證的頭部字段,解析之后組裝成JwtAuthenticationToken令牌實例,提交給AuthenticationManager進行驗證。

          package com.crazymaker.springcloud.base.security.filter;
          //省略import
          public class JwtAuthenticationFilter extends OncePerRequestFilter
          {
          ...
          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
          {
          ...
          Authentication passedToken = null;
          AuthenticationException failed = null;
          //從HTTP請求取得JWT令牌的頭部字段 String token = null;
          //用戶端存放的JWT的HTTP頭部字段為token
          String sessionIDStore = SessionHolder.getSessionIDStore();
          if (sessionIDStore.equals(SessionConstants.SESSION_STORE))
          {
          token = request.getHeader(SessionConstants.AUTHORIZATION_HEAD);
          }
          //管理端存放的JWT的HTTP頭部字段為Authorization
          else if (sessionIDStore.equals
          (SessionConstants.ADMIN_SESSION_STORE))
          {
          token = request.getHeader
          (SessionConstants.ADMIN_AUTHORIZATION_HEAD);
          }
          //沒有取得頭部,報異常
          else
          {
          failed = new InsufficientAuthenticationException("請求頭認證消息為空" );
          unsuccessfulAuthentication(request, response, failed);
          return;
          }
          token = StringUtils.removeStart(token, "Bearer " );
          try
          {
          if (StringUtils.isNotBlank(token))
          {
          //組裝令牌
          JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
          //提交給AuthenticationManager進行令牌驗證,獲取認證后的令牌
          passedToken = this.getAuthenticationManager()
          .authenticate(authToken);
          //取得認證后的用戶信息,主要是用戶id
          UserDetails details = (UserDetails) passedToken.getDetails();
          //通過details.getUsername()獲取用戶id,并作為請求屬性進行緩存
          request.setAttribute(SessionConstants.USER_IDENTIFIER, details.getUsername());
          } else
          {
          failed = new InsufficientAuthenticationException("請求頭認證消息為空" );
          }
          } catch (JWTDecodeException e)
          {
          ...
          }
          ...
          filterChain.doFilter(request, response);
          }
          ...
          }

          AuthenticationManager將調(diào)用注冊在內(nèi)部的JwtAuthenticationProvider認證提供者,對JwtAuthenticationToken進行驗證。

          為了使得過濾器能夠生效,必須將過濾器加入HTTP請求的過濾處理責任鏈,這一步可以通過實現(xiàn)一個AbstractHttpConfigurer配置類來完成。

          第四步:定制一個HTTP的安全認證配置類(AbstractHttpConfigurer子類),將上一步定制的過濾器加入請求的過濾處理責任鏈。

          package com.crazymaker.springcloud.base.security.configurer;
          ...
          public class JwtAuthConfigurer<T extends JwtAuthConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigu
          {
          private JwtAuthenticationFilter jwtAuthenticationFilter;
          public JwtAuthConfigurer()
          {
          //創(chuàng)建認證過濾器
          this.jwtAuthenticationFilter = new JwtAuthenticationFilter();
          }
          //將過濾器加入http過濾處理責任鏈
          @Override
          public void configure(B http) throws Exception
          {
          //獲取Spring Security共享的AuthenticationManager實例
          //將其設置到jwtAuthenticationFilter認證過濾器 jwtAuthenticationFilter.setAuthenticationManager(http.getSharedObject
          jwtAuthenticationFilter.setAuthenticationFailureHandler(new AuthFailureHandler());
          JwtAuthenticationFilter filter = postProcess(jwtAuthenticationFilter);
          //將過濾器加入http過濾處理責任鏈
          http.addFilterBefore(filter, LogoutFilter.class);
          }
          ...
          }

          第五步:定義一個Spring Security安全配置類(
          WebSecurityConfigurerAdapter子類),對Web容器的HTTP安全認證機制進行配置。這是最后一步,有兩項工作:一是在HTTP安全策略上應用JwtAuthConfigurer配置實例;二是構造AuthenticationManagerBuilder認證管理者實例。這一步可以通過繼承WebSecurityConfigurerAdapter適配器來完成。

          package com.crazymaker.springcloud.cloud.center.zuul.config;
          ...
          @ConditionalOnWebApplication
          @EnableWebSecurity()
          public class ZuulWebSecurityConfig extends WebSecurityConfigurerAdapter
          {
          //注入session存儲實例,用于查找session(根據(jù)session id)
          @Resource
          RedisOperationsSessionRepository sessionRepository;
          //配置HTTP請求的安全策略,應用DemoAuthConfigurer配置類實例
          @Override
          protected void configure(HttpSecurity http) throws Exception
          {
          http.csrf().disable()
          ...
          .authorizeRequests()
          .and()
          .authorizeRequests().anyRequest().authenticated()
          .and()
          .formLogin().disable()
          .sessionManagement().disable()
          .cors()
          .and()
          //在HTTP安全策略上應用JwtAuthConfigurer配置類實例
          .apply(new JwtAuthConfigurer<>()) .tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissi
          .and()
          .logout().disable()
          .sessionManagement().disable();
          }
          //配置認證Builder,由其負責構造AuthenticationManager實例
          //Builder所構造的AuthenticationManager實例將作為HTTP請求的共享對象
          //可以通過http.getSharedObject(AuthenticationManager.class)來獲取
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception
          {
          //在Builder實例中加入自定義的Provider認證提供者實例
          auth.authenticationProvider(jwtAuthenticationProvider());
          }
          //創(chuàng)建一個JwtAuthenticationProvider提供者實例
          @DependsOn({"sessionRepository"})
          @Bean("jwtAuthenticationProvider")
          protected AuthenticationProvider jwtAuthenticationProvider()
          {
          return new JwtAuthenticationProvider(sessionRepository);
          }
          ...
          }

          至此,一個基于JWT+Spring Security的用戶認證處理流程就定義完了。但是,此流程僅僅涉及JWT令牌的認證,沒有涉及JWT令牌的生成。一般來說,JWT令牌的生成需要由系統(tǒng)的UAA(用戶賬號與認證)服務(或者模塊)負責完成。

          Zuul網(wǎng)關與UAA微服務的配合

          crazy-springcloud腳手架通過Zuul網(wǎng)關和UAA微服務相互結合來完成整個用戶的登錄與認證閉環(huán)流程。二者的關系大致為:

          (1)登錄時,UAA微服務負責用戶名稱和密碼的驗證并且將用戶信息(包括令牌加密鹽)放在分布式Session中,然后返回JWT令牌(含Session ID)給前臺。

          (2)認證時,前臺請求帶上JWT令牌,Zuul網(wǎng)關能根據(jù)令牌中的Session ID取出分布式Session中的加密鹽,對JWT令牌進行驗證。在crazy-springcloud腳手架的會話架構中,Zuul網(wǎng)關必須能和UAA微服務進行會話的共享,如圖6-7所示。

          圖6-7 Zuul網(wǎng)關和UAA微服務進行會話的共享

          在crazy-springcloud的UAA微服務提供者crazymaker-uaa實現(xiàn)模塊中,controller(控制層)的REST登錄接口的定義如下:

          @Api(value = "用戶端登錄與退出", tags = {"用戶信息、基礎學習DEMO"})
          @RestController
          @RequestMapping("/api/session" )
          public class SessionController
          {
          //用戶端會話服務
          @Resource
          private FrontUserEndSessionServiceImpl userService;
          //用戶端的登錄REST接口
          @PostMapping("/login/v1" )
          @ApiOperation(value = "用戶端登錄" )
          public RestOut login(@RequestBody LoginInfoDTO loginInfoDTO, HttpServlet
          {
          //調(diào)用服務層登錄方法獲取令牌
          LoginOutDTO dto = userService.login(loginInfoDTO);
          response.setHeader("Content-Type", "text/html;charset=utf-8" );
          response.setHeader(SessionConstants.AUTHORIZATION_HEAD, dto.getToken());
          return RestOut.success(dto);
          }
          ...
          }

          用戶登錄時,在服務層,客戶端會話服務
          FrontUserEndSessionServiceImpl負責從用戶數(shù)據(jù)庫中獲取用戶,然后進行密碼驗證。

          package com.crazymaker.springcloud.user.info.service.impl;
          //省略import
          @Slf4j
          @Service
          public class FrontUserEndSessionServiceImpl
          {
          //Dao Bean,用于查詢數(shù)據(jù)庫用戶
          @Resource
          UserDao userDao;
          //加密器
          @Resource
          private PasswordEncoder passwordEncoder;
          //緩存操作服務
          @Resource
          RedisRepository redisRepository;
          //Redis會話存儲服務
          @Resource
          private RedisOperationsSessionRepository sessionRepository;
          /**
          *登錄處理
          *@param dto用戶名、密碼
          *@return登錄成功的dto
          */

          public LoginOutDTO login(LoginInfoDTO dto)
          {
          String username = dto.getUsername();
          //從數(shù)據(jù)庫獲取用戶
          List list = userDao.findAllByUsername(username);
          if (null == list || list.size() <= 0)
          {
          throw BusinessException.builder().errMsg("用戶名或者密碼錯誤" );
          }
          UserPO userPO = list.get(0);
          //進行密碼的驗證
          //String encode = passwordEncoder.encode(dto.getPassword());
          String encoded = userPO.getPassword();
          String raw = dto.getPassword();
          boolean matched = passwordEncoder.matches(raw, encoded);
          if (!matched)
          {
          throw BusinessException.builder().errMsg("用戶名或者密碼錯誤" );
          }
          //設置session,方便Spring Security進行權限驗證
          return setSession(userPO);
          }
          /**
          *1:將userid -> session id作為鍵-值對(Key-Value Pair)緩存起來,防止頻繁創(chuàng)建session
          *2:將用戶信息保存到分布式Session
          *3:創(chuàng)建JWT token,提供給Spring Security進行權限驗證
          *@param userPO用戶信息
          *@return登錄的輸出信息
          */

          private LoginOutDTO setSession(UserPO userPO)
          {
          if (null == userPO)
          {
          throw BusinessException.builder().errMsg("用戶不存在或者密碼錯誤" ).build();
          }
          /**
          *根據(jù)用戶id查詢之前保存的session id
          *防止頻繁登錄的時候session被大量創(chuàng)建
          */

          String uid = String.valueOf(userPO.getUserId());
          String sid = redisRepository.getSessionId(uid);
          Session session = null;
          try
          {
          /**
          *查找現(xiàn)有的session
          */

          session = sessionRepository.findById(sid);
          } catch (Exception e)
          {
          //e.printStackTrace();
          log.info("查找現(xiàn)有的session失敗,將創(chuàng)建一個新的session" );
          }
          if (null == session)
          {
          session = sessionRepository.createSession();
          //新的session id和用戶id一起作為鍵-值對進行保存
          //用戶訪問的時候可以根據(jù)用戶id查找session id
          sid = session.getId();
          redisRepository.setSessionId(uid, sid);
          }
          String salt = userPO.getPassword();
          構建 //構建JWT token
          String token = AuthUtils.buildToken(sid, salt);
          /**
          *將用戶信息緩存到分布式Session
          */

          UserDTO cacheDto = new UserDTO();
          BeanUtils.copyProperties(userPO, cacheDto);
          cacheDto.setToken(token);
          session.setAttribute(G_USER, JsonUtil.pojoToJson(cacheDto));
          LoginOutDTO outDTO = new LoginOutDTO();
          BeanUtils.copyProperties(cacheDto, outDTO);
          return outDTO;
          }
          }

          如果用戶驗證通過,那么前端會話服務
          FrontUserEndSessionServiceImpl在setSession方法中創(chuàng)建Redis分布式Session(如果不存在舊Session),然后將用戶信息(密碼為令牌的salt)緩存起來。如果用戶存在舊Session,那么舊Session的ID將通過用戶的uid查找到,然后通過sessionRepository找到舊Session,做到在頻繁登錄的場景下不會導致Session被大量創(chuàng)建。

          最終,uaa-provider微服務將返回JWT令牌(subject設置為Session ID)給前臺。由于Zuul網(wǎng)關和uaa-provider微服務共享分布式Session,在進行請求認證時,Zuul網(wǎng)關能通過JWT令牌中的Session ID取出分布式Session中的用戶信息和加密鹽,對JWT令牌進行驗證。

          使用Zuul過濾器添加代理請求的用戶標識

          完成用戶認證后,Zuul網(wǎng)關的代理請求將轉發(fā)給上游的微服務Provider實例。此時,代理請求仍然需要帶上用戶的身份標識,而此時身份標識不一定是Session ID,而是和上游的Provider強相關:

          (1)如果Provider是將JWT令牌作為用戶身份標識(和Zuul一

          樣),那么Zuul網(wǎng)關將JWT令牌傳給Provider微服務提供者。(2)如果Provider是將Session ID作為用戶身份標識,那么Zuul需要將JWT令牌的subject中的Session ID解析出來,然后傳給Provider微服務提供者。

          (3)如果Provider是將用戶ID作為用戶身份標識,那么Zuul既不能將JWT令牌傳給Provider,又不能將Session ID傳給Provider,而是要將會話中緩存的用戶ID傳給Provider。

          前兩種用戶身份標識的傳遞方案都要求Provider微服務和網(wǎng)關共享會話,而實際場景中,這種可能性不是100%。另外,負責安全認證的網(wǎng)關可能不是Zuul,而是性能更高的OpenResty(甚至是Kong),如果這樣,共享Session技術難度就會更大??傊瑸榱耸钩绦虻目蓴U展性和可移植性更好,建議使用第三種用戶身份標識的代理傳遞方案。

          crazy-springcloud腳手架采用的是第三種用戶標識傳遞方案。

          JWT令牌被驗證成功后,網(wǎng)關的代理請求被加上"USER-ID"頭,將用戶ID作為用戶身份標識添加到請求頭部,傳遞給上游Provider。這個功能使用了一個Zuul過濾器實現(xiàn),代碼如下:

          package com.crazymaker.springcloud.cloud.center.zuul.filter;
          //省略import@Component
          @Slf4j
          public class ModifyRequestHeaderFilter extends ZuulFilter
          {
          /**
          *根據(jù)條件判斷是否需要路由,是否需要執(zhí)行該過濾器
          */

          @Override
          public boolean shouldFilter()
          {
          RequestContext ctx = RequestContext.getCurrentContext();
          HttpServletRequest request = ctx.getRequest();
          /**
          *存在用戶端認證token
          */

          String token = request.getHeader(SessionConstants
          .AUTHORIZATION_HEAD);
          if (!StringUtils.isEmpty(token))
          {
          return true;
          }
          /**
          *存在管理端認證token
          */

          token = request.getHeader(SessionConstants
          .ADMIN_AUTHORIZATION_HEAD);
          if (!StringUtils.isEmpty(token))
          {
          return true;
          }
          return false;
          }
          /**
          *調(diào)用上游微服務之前修改請求頭,加上USER-ID頭
          *
          *@return
          *@throws ZuulException
          */

          @Override
          public Object run() throws ZuulException
          {
          RequestContext ctx = RequestContext.getCurrentContext();
          HttpServletRequest request = ctx.getRequest();
          //認證成功,請求的"USER-ID"(USER_IDENTIFIER)屬性被設置
          String identifier = (String) request.getAttribute
          (SessionConstants.USER_IDENTIFIER);
          //代理請求加上 "USER-ID" 頭
          if (StringUtils.isNotBlank(identifier))
          {
          ctx.addZuulRequestHeader(SessionConstants.USER_IDENTIFIER, identifier);
          }
          return null;
          }
          @Override
          public String filterType()
          {
          return FilterConstants.PRE_TYPE;
          }
          @Override
          public int filterOrder()
          {
          return 1;
          }
          }

          本文給大家講解的內(nèi)容是 微服務網(wǎng)關與用戶身份識別,JWT+Spring Security進行網(wǎng)關安全認證

          1. 下篇文章給大家講解的是微服務網(wǎng)關與用戶身份識別,服務提供者之間的會話共享關系;

          2. 覺得文章不錯的朋友可以轉發(fā)此文關注小編;

          3. 感謝大家的支持!

          本文就是愿天堂沒有BUG給大家分享的內(nèi)容,大家有收獲的話可以分享下,想學習更多的話可以到微信公眾號里找我,我等你哦。


          瀏覽 67
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本A黄色大片在线看免费在线看 | 欧美啪啪视频 | 蜜臀视频网站狠狠操b | 一级A片色情大片视频我和少妇 | 国产 在线 | 日韩 |