在上一篇中,我们介绍了微服务架构中,token的多种实现方式。
https://blog.csdn.net/u011177064/article/details/104822700
本篇我们继续探究微服务架构中token的验证环节,在token微服务搭建好之后,我们将它接入 注册中心 (nacos)。
这样其他微服务就可以通过 注册中心 结合 RPC(远程过程调用)来轻松调用 token微服务中的 验证接口了。
如下图所示,我们可以在每个需要验证token的工程中,借助AOP编程的方式实现token验证。
首先,我们回顾一下之前编写的 token 验证接口,以 simple 方式为例。
@ApiOperation("简单token模式:step2.鉴定token有效性,并返回认证数据")
@GetMapping(path = "/simple/validateAndGet")
@ResponseBody
public RespData validateAndGet(@RequestParam String token) {
try {
Object obj = simpleTokenService.validateAndGet(token);
return new RespData(obj);
}catch (Exception e) {
LOGGER.error(e.getMessage(),e);
return new RespData(e);
}
}
接下来,我们新建一个工程,充当“其他微服务”。(我这里工程名为:jfcloud-mall-order)
主要的maven依赖如下:(springboot,openfeign,nacos-discovery)
org.springframework.boot
spring-boot-starter-aop
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
同样的,我们这两个工程都需要接入 nacos注册中心 (各自的application.yml 配置)
然后,在新工程(jfcloud-mall-order)中,使用openfeign 去调用 jfcloud-mall-user 中的token验证接口
@FeignClient中的 name 对应 nacos中token服务的注册名(也就是 jfcloud-mall-user工程里配置项 spring.application.name)
@FeignClient(name ="jfcloud-mall-user")
public interface UserServiceClient{
@GetMapping(path = "/token/simple/validateAndGet")
public RespData validateAndGet(@RequestParam("token") String token);
}
由于feign主要是通过http+json作为传输媒介,收发均使用json进行正反序列化,所以可以直接使用对象来接收远程接口的返回数据。
到此处,就已经实现了“其他微服务”通过nacos注册中心,调用“token微服务”中的token验证接口。
"其他微服务"中如果需要获取用户相关信息,大概代码实现如下:
RespData ru = userServiceClient.validateAndGet(token);
if(null == ru || RespData.Success!=ru.getStatus()){
//token验证失败的逻辑处理
return null;
}else{
//token 验证成功
Map ruMap = (Map) ru.getDatas();
Long userId = MapUtil.getLong("id", ruMap);
String userName = MapUtil.getString("username", ruMap);
TokenUser user = new TokenUser();
user.setUserId(userId);
user.setUserName(userName);
//从 token微服务 获取验证后的用户信息
//业务逻辑处理
}
是不是稍显麻烦啊!
结合springboot的 @Aspect AOP切面编程方式,只需要在需要鉴权的接口中进行注解标识,就可以在接口中自动获取 用户鉴权数据了。
在 jfcloud-mall-order中的某个控制器方法,使用了 @CheckToken 注解,然后里面就可以直接用 TokenUser.load(request) 来获取到上面的“TokenUser -- 验证token后的用户数据”了。
@ApiOperation("创建订单")
@PostMapping(path = "/create")
@ResponseBody
@CheckToken
public RespDataT create(@RequestBody POrder orderInfo,HttpServletRequest request,HttpServletResponse response) {
try {
TokenUser tokenUser=TokenUser.load(request);
VOrder token = orderService.createOrder(orderInfo,tokenUser);
return new RespDataT(token);
}catch (Exception e) {
LOGGER.error(e.getMessage(),e);
return new RespDataT(e);
}
}
@CheckToken 的声明, 自定义的注解,可以在java方法上使用该注解
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.METHOD })
@Documented
public @interface CheckToken {
}
TokenUser 实体封装,实现一个load的静态方法,从request中拿数据。
@Data
public class TokenUser {
private Long userId;
private String userName;
public static TokenUser load(HttpServletRequest request) {
TokenUser tokenUser = (TokenUser) request.getAttribute("tokenUser");
return tokenUser;
}
}
最后再做一个 @Aspect拦截器,对所有标注了 @CheckToken 的方法进行拦截,然后调用token微服务的远程接口,并且收到 token微服务的验证结果(以及用户数据)后,将 数据 放入到request中
(即 request.setAttribute("tokenUser", user); 存入的数据仅对本次接口调用生命周期内有效)。
package com.jfcloud.mall.order.aspect;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSON;
@Aspect
@Component
public class TokenAspect extends BaseAspect{
private static final Logger logger = LoggerFactory.getLogger(TokenAspect.class);
@Autowired
private UserServiceClient userService;
@Around("@annotation(com.jfcloud.common.annotation.CheckToken)")
public Object aroundMethod(ProceedingJoinPoint point ) {
logger.info("token aspect start");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
MethodSignature ms=(MethodSignature) point.getSignature();
Method method=ms.getMethod();
CheckToken check = method.getAnnotation(CheckToken.class);
if(check!=null){
Object[] args = point.getArgs();
//解析参数
Map argMap = analysisArgs(args,method);
//判断访问类型
String requestMethod = request.getMethod();
String token =null;
if("get".equalsIgnoreCase(requestMethod)){
//get请求直接从request获取token
token = request.getParameter("token");
}else if("post".equalsIgnoreCase(requestMethod)){
//post请求从requestBody里获取对象
Object o = argMap.get(requestBody);
if(o instanceof Map){
Map p = (Map)o;
if(StringUtil.isNotEmpty(p.get("token"))){
token = p.get("token").toString();
}else{
logger.error("参数中必须要有 token");
return null;
}
}else{
//通过反射获取token
try {
Method getToken = o.getClass().getDeclaredMethod("getToken");
if(getToken==null){
logger.error(o.getClass().getName()+"必须有 getToken 方法");
return null;
}
token = (String) getToken.invoke(o);
} catch (Exception e) {
logger.error(e.getMessage(),e);
return null;
}
}
}
if(null == token || "".equals(token.trim())){
try {
writeErrorJson((HttpServletResponse) argMap.get(response),"token不能为空", 2);
} catch (IOException e) {
logger.error(e.getMessage(),e);
}
logger.info("token aspect fail :" + token);
return null;
}
// 校验token
RespData ru = userService.validateAndGet(token);
if(null == ru || RespData.Success!=ru.getStatus()){
try {
writeErrorJson((HttpServletResponse) argMap.get(response),"token验证失败" , 2);
} catch (IOException e) {
logger.error(e.getMessage(),e);
}
logger.info("token aspect fail :" + token);
return null;
}else{//token 验证成功
Map ruMap = (Map) ru.getDatas();
Long userId = MapUtil.getLong("id", ruMap);
String userName = MapUtil.getString("username", ruMap);
TokenUser user = new TokenUser();
user.setUserId(userId);
user.setUserName(userName);
request.setAttribute("tokenUser", user);
}
}
Object retVal;
try {
retVal = point.proceed();
return retVal;
} catch (Throwable e) {
logger.error(e.getMessage(),e);
}
return null;
}
private void writeErrorJson(HttpServletResponse response, String errorMsg , int status) throws IOException{
response.setContentType("application/json; charset=utf-8");
response.setCharacterEncoding("utf-8");
Writer writer = response.getWriter();
RespData resp = new RespData(status,errorMsg);
writer.write(JSON.toJSONString(resp));
writer.flush();
writer.close();
}
}
至此,我们就完成了一个简洁的 微服务架构下 跨服务的token验证 功能了。