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

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内容:

{
 "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内容:

{
 "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的演示内容
 */
 String subject = "session id";
 /**
 *签名的加密盐
 */
 String salt = "user password";
 /**
 *签名的加密算法
 */
 Algorithm algorithm = Algorithm.HMAC256(salt);
 //签发时间
 long start = System.currentTimeMillis() - 60000;
 //过期时间,在签发时间的基础上加上一个有效时长
 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:签发人。

(2)sub:主题。

(3)aud:用户。

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

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

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

私有的声明是除了公有声明之外的自定义JSON字段,私有的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息。下面的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);
 //签发时间
 long start = System.currentTimeMillis() - 60000;
 //过期时间,在签发时间的基础上加上一个有效时长
 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认证处理流程

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

这里还是按照6.4.2节中请求认证处理流程的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("还没有登录,请登录系统!");
 }
 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进行验证。

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将调用注册在内部的JwtAuthenticationProvider认证提供者,对JwtAuthenticationToken进行验证。

为了使得过滤器能够生效,必须将过滤器加入HTTP请求的过滤处理责任链,这一步可以通过实现一个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()
 {
 //创建认证过滤器
 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(根据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所示。

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

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

在crazy-springcloud的UAA微服务提供者crazymaker-uaa实现模块中,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<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负责从用户数据库中获取用户,然后进行密码验证。

package com.crazymaker.springcloud.user.info.service.impl;
//省略import
@Slf4j
@Service
public 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);
 }
 /**
 *1:将userid -> session id作为键-值对(Key-Value Pair)缓存起来,防止频繁创建session
 *2:将用户信息保存到分布式Session
 *3:创建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过滤器实现,代码如下:

package com.crazymaker.springcloud.cloud.center.zuul.filter;
//省略import@Component
@Slf4j
public 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 1;
 }
}

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

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

 

上一篇:Appium自动化环境安装


下一篇:理解Cookie和Session机制,及其安全问题