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所示。
图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进行网关安全认证
- 下篇文章给大家讲解的是微服务网关与用户身份识别,服务提供者之间的会话共享关系;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持!