ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-01-14 16:00:04  阅读:163  来源: 互联网

标签:网关 Spring JWT 令牌 用户 token session 认证


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. 感谢大家的支持!

 

标签:网关,Spring,JWT,令牌,用户,token,session,认证
来源: https://blog.csdn.net/m0_63437643/article/details/122496217

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有