目录
一、前言
1. Spring Cloud Gateway 简介
2. token 简介
二、登录认证流程
三、项目实战
1. auth 认证服务
2. gateway 网关服务
本文将演示,登录时,如何创建token,然后在网关校验token,并提取用户信息放到Header请求头中传给下游业务系统。
项目采用 Spring Cloud Gateway作为微服务的统一路由的网关,由auth认证模块负责生成token,gateway网关模块负责校验token,统一入口。
网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。
Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
一个jwt实际上就是一个字符串,它由三部分组成,头部、载荷与签名,这三个部分都是json格式。
详情可参考:https://blog.csdn.net/a1036645146/article/details/103726635
2.1 头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。
{
"typ": "JWT",
"alg": "HS256"
}
在这里,我们说明了这是一个JWT,并且我们所用的签名算法是HS256算法。
2.2 载荷(Payload)
载荷可以用来放一些不敏感的信息。
{
"iss": "John Wu JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "[email protected]",
"from_user": "B",
"target_user": "A"
}
这里面的前五个字段都是由JWT的标准所定义的。
iss
: 该JWT的签发者sub
: 该JWT所面向的用户aud
: 接收该JWT的一方exp
(expires): 什么时候过期,这里是一个Unix时间戳iat
(issued at): 在什么时候签发的把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号.
连接在一起(头部在前),形成新的字符串:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
2.3 签名(signature)
最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密后的内容也是一个字符串,最后这个字符串就是签名,把这个签名拼接在刚才的字符串后面就能得到完整的jwt。header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分,服务端也就无法通过,在jwt中,消息体是透明的,使用签名可以保证消息不被篡改。
当然, 如果一个人的token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJyb2xlcyI6InB1cmNoYXNlciIsInNvdXJjZSI6IlBDIiwidXNlck5hbWUiOiIxNTYyMjEzMzIzMSIsImV4cCI6MTYwNDgwMDM5NSwidXNlcklkIjoiMTE5MCJ9
.zpM6dyakS1N6kySMIEHuOZfN8l4WlybRbq6VK1cDqGc
用户登录成功以后,认证服务(auth模块)生成token,此后前端的所有请求都在请求头中携带此token。网关负责校验token,并将用户信息放入请求头Header,以便下游系统可以方便的获取用户信息。
基于 Token 认证的基本认证流程如下:
项目涉及模块:
每个模块应用都是基于SpringBoot 构建,使用Nacos作为 配置中心与注册中心,服务间的接口调用采用Feign,发布Http 接口( REST API ),使用gateway作为API统一入口,前端请求不能直接调用下游微服务的具体接口。
通过SpringBoot构建一个auth认证服务模块,注册与配置中心统一使用 Nacos。
该模块主要提供登录接口,生成token。
1.1 pom.xml
需要引入jwt工具包,使用最新稳定版本即可。
com.auth0
java-jwt
${jwt.version}
1.2 JWTUtils.java 工具类
/**
* jwt工具类
*
* @author stwen_gan
* @since
*/
public class JWTUtils {
// token 签名的秘钥,可设置到配置文件中
private static final String SECRET_KEY = "secretKey:123456";
// token过期时间
public static final long TOKEN_EXPIRE_TIME = 7200 * 1000;
/**
* 生成jwt
*/
public String createJwt(String userId){
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
//设置头信息
HashMap header = new HashMap<>(2);
header.put("typ", "JWT");
header.put("alg", "HS256");
// 生成 token:头部+载荷+签名
return JWT.create().withHeader(header)
.withClaim(RequestKeyConstants.USER_ID,userId)
.withExpiresAt(new Date(System.currentTimeMillis()+TOKEN_EXPIRE_TIME)).sign(algorithm);
}
/**
* 解析jwt
*/
public Map parseJwt(String token) {
Map claims = null;
try {
Algorithm algorithm = Algorithm.HMAC256(key);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
claims = jwt.getClaims();
return claims;
} catch (Exception e) {
return null;
}
}
}
注:该工具好类的签名秘钥、过期时间 建议统一配置到配置中心的文件,通过方法入参传入即可。
Token组成:
{
"typ": "JWT",
"alg": "HS256"
}
1.3 登录接口
LoginController
注:该接口为了方便演示,省略了部分代码。
@PostMapping("/auth/login")
public Result login(HttpServletRequest request, @Valid @RequestBody LoginDTO loginDto) {
String ip = IpAddressUtils.getIpAddr(request);
// 获取用户信息、比对密码
Result result = loginFeignApi.login(loginDto,ip);
if(ResultCode.SUCCESS.getCode()!=result.getCode()){
log.error(result.getMsg());
return result;
}
UserDTO user = result.getData();
String token = JWTUtils.createJwt(user.getId() + "");
data.put("token",token);
return Result.success(data);
}
其中,loginFeignApi.login(loginDto,ip) 是 通过feign 调用 user-service 模块的接口:根据用户名查询用户信息,并对密码进行MD5加密加盐值校验等,如下:
User user = userService.getUserByName(loginDTO.getUserName());
// 密码md5 校验
if (!user.getPassword().equals(MD5Utils.toMD5Password(user.getUserSalt(), loginDTO.getPassword()))){
userService.userLog(user,ip,UserLogTypeEnum.LOGIN_FAIL,loginDTO.getSource());
return Result.error(ResultCode.PASSWORD_ERROR);
}
同理,通过SpringBoot构建一个gateway 网关服务模块,注册与配置中心统一使用 Nacos,并配置其他服务接口的路由策略,作为其他服务API的统一入口。
网关负责校验token,并将用户相关信息放入请求头Header,以便下游系统可以方便的获取用户信息。
2.1 pom.xml
同理,需要需要引入jwt工具包
com.auth0
java-jwt
${jwt.version}
2.2 application.properties
server.port=1234
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# gateway 路由策略
spring.cloud.gateway.routes[0].id=user
spring.cloud.gateway.routes[0].uri=lb://user-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/userService/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[1].id=order
spring.cloud.gateway.routes[1].uri=lb://order-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/orderService/**
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1
### 。。。。。。。
2.3 校验Token
我们可以通过实现 GatewayFilter 或 GlobalFilter 过滤器接口,前端请求通过gateway时,将被过滤校验token的合法性,这里实现GlobalFilter 接口。
TokenFilter.java
/**
* @description: token过滤器
* @author: xianhao_gan
* @date:
**/
@Slf4j
@Component
public class TokenFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtils jwtUtils;
/**
* 校验token
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst(RequestKeyConstants.TOKEN);
//检查token是否为空
if (StringUtils.isEmpty(token)) {
return denyAccess(exchange, ResultCode.TOKEN_NULL);
}
Map claimMap1 = jwtUtils.parseJwt(token);
//token有误
if (claimMap.containsKey("exception")) {
log.error() (claimMap1.get("exception").toString());
return denyAccess(exchange, ResultCode.TOKEN_INVALID);
}
//token无误,将用户信息设置进header中,传递到下游服务
Map claimMap = claimMap1;
String userId = claimMap.get(RequestKeyConstants.USER_ID).asString();
Consumer headers = httpHeaders -> {
httpHeaders.add(RequestKeyConstants.USER_ID, userId);
};
request.mutate().headers(headers).build();
// todo 权限校验
return chain.filter(exchange);
}
/**
* 拦截并返回自定义的json字符串
*/
private Mono denyAccess(ServerWebExchange exchange, ResultCode resultCode) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
//这里在返回头添加编码,否则中文会乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] bytes = JSON.toJSONBytes(Result.error(resultCode), SerializerFeature.WriteMapNullValue);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -1;
}
}
注:
当然,实际项目中的代码会比这些复杂,这里为了方便演示,说明原理思路,省略了部分代码,实际设计与开发中可根据此思路来扩展。
对于登录后生成的token,后端设置了固定的有效期,在有效期内用户携带token访问没问题,当tokent过期后会失效,前端会跳转到登录页面让用户重新登录,但用户体验不友好。
改进:活跃的用户应该在无感知的情况下,token失效后自动获取新的token,携带这个新的token进行访问,而长时间不活跃的用户应该在jwt失效后需要重新的登录认证。后续有时间再补充 token超时刷新策略。
参考:https://www.cnblogs.com/cjsblog/p/12425912.html
●史上最强Tomcat8性能优化
●阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路
●B2B电商平台--ChinaPay银联电子支付功能
●学会Zookeeper分布式锁,让面试官对你刮目相看
●SpringCloud电商秒杀微服务-Redisson分布式锁方案
查看更多好文,进入公众号--撩我--往期精彩
一只 有深度 有灵魂 的公众号0.0