微服务网关与用户身份识别,JWT+Spring Security进行网关安全认证

2022-10-28 11:39:55 浏览数 (1)

JWT Spring Security进行网关安全认证

JWT和Spring Security相结合进行系统安全认证是目前使用比较多的一种安全认证组合。疯狂创客圈crazy-springcloud微服务开发脚手架使用JWT身份令牌结合Spring Security的安全认证机制完成用户请求的安全权限认证。整个用户认证的过程大致如下:

(1)前台(如网页富客户端)通过REST接口将用户名和密码发送到UAA用户账号与认证微服务进行登录。

(2)UAA服务在完成登录流程后,将Session ID作为JWT的负载(payload),生成JWT身份令牌后发送给前台。

(3)前台可以将JWT令牌存到localStorage或者sessionStorage中,当然,退出登录时,前端必须删除保存的JWT令牌。

(4)前台每次在请求微服务提供者的REST资源时,将JWT令牌放到请求头中。crazy-springcloud脚手架做了管理端和用户端的前台区分,管理端前台的令牌头为Authorization,用户端前台的令牌头为token。

(5)在请求到达Zuul网关时,Zuul会结合Spring Security进行拦截,从而验证JWT的有效性。

(6)Zuul验证通过后才可以访问微服务所提供的REST资源。

需要说明的是,在crazy-springcloud微服务开发脚手架中,Provider微服务提供者自身不需要进行单独的安全认证,Provider之间的内部远程调用也是不需要安全认证的,安全认证全部由网关负责。严格来说,这套安全机制是能够满足一般的生产场景安全认证要求的。如果觉得这个安全级别不是太高,单个的Provider微服务也需要进行独立的安全认证,那么实现起来也是很容易的,只需要导入公共的安全认证模块base-auth即可。实际上早期的crazy-springcloud脚手架也是这样做的,后期发现这样做纯属多虑,而且大大降低了Provider服务提供者模块的可复用性和可移植性(这是微服务架构的巨大优势之一)。所以,crazy-springcloud后来将整体架构调整为由网关(如Zuul或者Nginx)负责安全认证,去掉了Provider服务提供者的安全认证能力。

JWT安全令牌规范详解

JWT(JSON Web Token)是一种用户凭证的编码规范,是一种网络环境下编码用户凭证的JSON格式的开放标准(RFC 7519)。JWT令牌的格式被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)、用户身份认证等场景。

一个编码之后的JWT令牌字符串分为三部分:header payload signature。这三部分通过点号“.”连接,第一部分常被称为头部(header),第二部分常被称为负载(payload),第三部分常被称为签名(signature)。

1.JWT的header

编码之前的JWT的header部分采用JSON格式,一个完整的头部就像如下的JSON内容:

代码语言:javascript复制
{ "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内容:

代码语言:javascript复制
{ "sub":"session id", "exp":, "iat":}

采用JWT令牌编码时,playload的JSON字符串将进行Base64编码,编码之后的字符串构成了JWT令牌的第二部分。

3.JWT的signature

JWT的第三部分是一个签名字符串,这一部分是将header的Base64编码和payload的Base64编码使用点号(.)连接起来之后,通过header声明的加密算法进行加密所得到的密文。为了保证安全,加密时需要加入盐(salt)。

下面是一个演示用例:用Java代码生成JWT令牌,然后对令牌的header部分字符串和payload部分字符串进行Base64解码,并输出解码后的JSON。

代码语言:javascript复制
package com.crazymaker.demo.auth;//省略import@Slf4jpublic class JwtDemo{ @Test public void testBaseJWT() { try { /** *JWT的演示内容 */ String subject = "session id"; /** *签名的加密盐 */ String salt = "user password"; /** *签名的加密算法 */ Algorithm algorithm = Algorithm.HMAC256(salt); //签发时间 long start = System.currentTimeMillis() - ; //过期时间,在签发时间的基础上加上一个有效时长 Date end = new Date(start   SessionConstants.SESSION_TIME_OUT *); /** *获取编码后的JWT令牌 */ String token = JWT.create() .withSubject(subject) .withIssuedAt(new Date(start)) .withExpiresAt(end) .sign(algorithm); log.info("token="   token); //编码后输出demo为://token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZXNzaW9uIGlkIiwiZXhwIjoxNTc5MzE1NzE3LCJpYXQiOjE1Nzg0NTE3MTd9.iANh9Fa0B_6H5TQ11bLCWcEpmWxuCwa2Rt6rnzBWteI //以.分隔令牌 String[] parts = token.split("\." ); /** *对第一部分和第二部分进行解码 *解码后的第一部分:header */ String headerJson =StringUtils.newStringUtf8(Base64.decodeBase64(parts[])); log.info("parts[0]="   headerJson); //解码后的第一部分输出的示例为://parts[]={"typ":"JWT","alg":"HS256"} /** *解码后的第二部分:payload */ String payloadJson; payloadJson = StringUtils.newStringUtf8(Base64.decodeBase64(parts[])); log.info("parts[1]="   payloadJson); //输出的示例为://解码后的第二部分:parts[]={"sub":"session id","exp":,"iat":} } catch (Exception e) { e.printStackTrace(); } } ...}

在编码前的JWT中,payload部分JSON中的属性被称为JWT的声明。JWT的声明分为两类:

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

(2)私有的声明(自定义的JSON属性)。

公有的声明也就是JWT标准中注册的声明,主要为以下JSON属性:

(1)iss:签发人。

(2)sub:主题。

(3)aud:用户。

(4)iat:JWT的签发时间。

(5)exp:JWT的过期时间,这个过期时间必须要大于签发时间。

(6)nbf:定义在什么时间之前该JWT是不可用的。

私有的声明是除了公有声明之外的自定义JSON字段,私有的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息。下面的JSON例子中的uid、user_name、nick_name等都是私有声明。

代码语言:javascript复制
{ "uid": "123...", "sub": "session id", "user_name": "admin", "nick_name": "管理员", "exp": , "iat": }

下面是一个向JWT令牌添加私有声明的实例,代码如下:

代码语言:javascript复制
package com.crazymaker.demo.auth;//省略import@Slf4jpublic class JwtDemo{ /** *测试私有声明 */ @Test public void testJWTWithClaim() { try { String subject = "session id"; String salt = "user password"; /** *签名的加密算法 */ Algorithm algorithm = Algorithm.HMAC256(salt); //签发时间 long start = System.currentTimeMillis() - ; //过期时间,在签发时间的基础上加上一个有效时长 Date end = new Date(start   SessionConstants.SESSION_TIME_OUT *); /** *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[])); log.info("parts[1]="   payloadJson); //输出demo为:parts[]= //{"uid":"123...","sub":"session id","user_name":"admin","nick_name":"管理员","exp":,"iat":} } catch (Exception e) { e.printStackTrace(); } }}

由于JWT的payload声明(JSON属性)是可以解码的,属于明文信息,因此不建议添加敏感信息。

JWT Spring Security认证处理流程

实际开发中如何使用JWT进行用户认证呢?疯狂创客圈的crazy-springcloud开发脚手架将JWT令牌和Spring Security相结合,设计了一个公共的、比较方便复用的用户认证模块base-auth。一般来说,在Zuul网关或者微服务提供者进行用户认证时导入这个公共的base-auth模块即可。

这里还是按照6.4.2节中请求认证处理流程的5个步骤介绍base-auth模块中JWT令牌的认证处理流程。

首先看第一步:定制一个凭证/令牌类,封装用户信息和JWT认证信息。

代码语言:javascript复制
package com.crazymaker.springcloud.base.security.token;//省略importpublic class JwtAuthenticationToken extends AbstractAuthenticationToken{ private static final long serialVersionUID = 3981518947978158945L; //封装用户信息:用户id、密码 private UserDetails userDetails; //封装的JWT认证信息 private DecodedJWT decodedJWT; ...}

再看第二步:定制一个认证提供者类和凭证/令牌类进行配套,并完成对自制凭证/令牌实例的验证。

代码语言:javascript复制
package com.crazymaker.springcloud.base.security.provider;//省略importpublic 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("还没有登录,请登录系统!"); } 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("认证有误,请重新登录"); }判断是否在其他地方已经登录 //判断是否在其他地方已经登录 if (null == newToken || !newToken.equals(userDTO.getToken())) { throw new NonceExpiredException("您已经在其他的地方登录!"); } 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); //创建验证器 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)放入令牌并予以返回,供后续操作使用。

第三步:定制一个过滤器类,从请求中获取用户信息组装成JwtAuthenticationToken凭证/令牌,交给认证管理者。在crazy-springcloud脚手架中,前台有用户端和管理端的两套界面,所以,将认证头部信息区分成管理端和用户端两类:管理端的头部字段为Authorization;用户端的认证信息头部字段为token。

过滤器从请求中获取认证的头部字段,解析之后组装成JwtAuthenticationToken令牌实例,提交给AuthenticationManager进行验证。

代码语言:javascript复制
package com.crazymaker.springcloud.base.security.filter;//省略importpublic 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将调用注册在内部的JwtAuthenticationProvider认证提供者,对JwtAuthenticationToken进行验证。

为了使得过滤器能够生效,必须将过滤器加入HTTP请求的过滤处理责任链,这一步可以通过实现一个AbstractHttpConfigurer配置类来完成。

第四步:定制一个HTTP的安全认证配置类(AbstractHttpConfigurer子类),将上一步定制的过滤器加入请求的过滤处理责任链。

代码语言:javascript复制
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() { //创建认证过滤器 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适配器来完成。

代码语言:javascript复制
package com.crazymaker.springcloud.cloud.center.zuul.config;...@ConditionalOnWebApplication@EnableWebSecurity()public class ZuulWebSecurityConfig extends WebSecurityConfigurerAdapter{ //注入session存储实例,用于查找session(根据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()); } //创建一个JwtAuthenticationProvider提供者实例 @DependsOn({"sessionRepository"}) @Bean("jwtAuthenticationProvider") protected AuthenticationProvider jwtAuthenticationProvider() { return new JwtAuthenticationProvider(sessionRepository); } ...}

至此,一个基于JWT Spring Security的用户认证处理流程就定义完了。但是,此流程仅仅涉及JWT令牌的认证,没有涉及JWT令牌的生成。一般来说,JWT令牌的生成需要由系统的UAA(用户账号与认证)服务(或者模块)负责完成。

Zuul网关与UAA微服务的配合

crazy-springcloud脚手架通过Zuul网关和UAA微服务相互结合来完成整个用户的登录与认证闭环流程。二者的关系大致为:

(1)登录时,UAA微服务负责用户名称和密码的验证并且将用户信息(包括令牌加密盐)放在分布式Session中,然后返回JWT令牌(含Session ID)给前台。

(2)认证时,前台请求带上JWT令牌,Zuul网关能根据令牌中的Session ID取出分布式Session中的加密盐,对JWT令牌进行验证。在crazy-springcloud脚手架的会话架构中,Zuul网关必须能和UAA微服务进行会话的共享,如图6-7所示。

图6-7 Zuul网关和UAA微服务进行会话的共享

在crazy-springcloud的UAA微服务提供者crazymaker-uaa实现模块中,controller(控制层)的REST登录接口的定义如下:

代码语言:javascript复制
@Api(value = "用户端登录与退出", tags = {"用户信息、基础学习DEMO"})@RestController@RequestMapping("/api/session" )public class SessionController{ //用户端会话服务 @Resource private FrontUserEndSessionServiceImpl userService; //用户端的登录REST接口 @PostMapping("/login/v1" ) @ApiOperation(value = "用户端登录" ) public RestOut<LoginOutDTO> login(@RequestBody LoginInfoDTO loginInfoDTO, HttpServlet { //调用服务层登录方法获取令牌 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负责从用户数据库中获取用户,然后进行密码验证。

代码语言:javascript复制
package com.crazymaker.springcloud.user.info.service.impl;//省略import@Slf4j@Servicepublic class FrontUserEndSessionServiceImpl{ //Dao Bean,用于查询数据库用户 @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(); //从数据库获取用户 List<UserPO> 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); } /** *:将userid -> session id作为键-值对(Key-Value Pair)缓存起来,防止频繁创建session *:将用户信息保存到分布式Session *:创建JWT token,提供给Spring Security进行权限验证 *@param userPO用户信息 *@return登录的输出信息 */ private LoginOutDTO setSession(UserPO userPO) { if (null == userPO) { throw BusinessException.builder().errMsg("用户不存在或者密码错误" ).build(); } /** *根据用户id查询之前保存的session id *防止频繁登录的时候session被大量创建 */ String uid = String.valueOf(userPO.getUserId()); String sid = redisRepository.getSessionId(uid); Session session = null; try { /** *查找现有的session */ session = sessionRepository.findById(sid); } catch (Exception e) { //e.printStackTrace(); log.info("查找现有的session失败,将创建一个新的session" ); } if (null == session) { session = sessionRepository.createSession(); //新的session id和用户id一起作为键-值对进行保存 //用户访问的时候可以根据用户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方法中创建Redis分布式Session(如果不存在旧Session),然后将用户信息(密码为令牌的salt)缓存起来。如果用户存在旧Session,那么旧Session的ID将通过用户的uid查找到,然后通过sessionRepository找到旧Session,做到在频繁登录的场景下不会导致Session被大量创建。

最终,uaa-provider微服务将返回JWT令牌(subject设置为Session ID)给前台。由于Zuul网关和uaa-provider微服务共享分布式Session,在进行请求认证时,Zuul网关能通过JWT令牌中的Session ID取出分布式Session中的用户信息和加密盐,对JWT令牌进行验证。

使用Zuul过滤器添加代理请求的用户标识

完成用户认证后,Zuul网关的代理请求将转发给上游的微服务Provider实例。此时,代理请求仍然需要带上用户的身份标识,而此时身份标识不一定是Session ID,而是和上游的Provider强相关:

(1)如果Provider是将JWT令牌作为用户身份标识(和Zuul一

样),那么Zuul网关将JWT令牌传给Provider微服务提供者。(2)如果Provider是将Session ID作为用户身份标识,那么Zuul需要将JWT令牌的subject中的Session ID解析出来,然后传给Provider微服务提供者。

(3)如果Provider是将用户ID作为用户身份标识,那么Zuul既不能将JWT令牌传给Provider,又不能将Session ID传给Provider,而是要将会话中缓存的用户ID传给Provider。

前两种用户身份标识的传递方案都要求Provider微服务和网关共享会话,而实际场景中,这种可能性不是100%。另外,负责安全认证的网关可能不是Zuul,而是性能更高的OpenResty(甚至是Kong),如果这样,共享Session技术难度就会更大。总之,为了使程序的可扩展性和可移植性更好,建议使用第三种用户身份标识的代理传递方案。

crazy-springcloud脚手架采用的是第三种用户标识传递方案。

JWT令牌被验证成功后,网关的代理请求被加上"USER-ID"头,将用户ID作为用户身份标识添加到请求头部,传递给上游Provider。这个功能使用了一个Zuul过滤器实现,代码如下:

代码语言:javascript复制
package com.crazymaker.springcloud.cloud.center.zuul.filter;//省略import@Component@Slf4jpublic class ModifyRequestHeaderFilter extends ZuulFilter{ /** *根据条件判断是否需要路由,是否需要执行该过滤器 */ @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; } /** *调用上游微服务之前修改请求头,加上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 ; }}

本文给大家讲解的内容是 微服务网关与用户身份识别,JWT Spring Security进行网关安全认证

  1. 下篇文章给大家讲解的是微服务网关与用户身份识别,服务提供者之间的会话共享关系;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

0 人点赞