微服务实战(十八)通过AOP的方式自动完成微服务token验证

本章主要内容

在上一篇中,我们介绍了微服务架构中,token的多种实现方式。

https://blog.csdn.net/u011177064/article/details/104822700

本篇我们继续探究微服务架构中token的验证环节,在token微服务搭建好之后,我们将它接入 注册中心 (nacos)。

这样其他微服务就可以通过 注册中心  结合 RPC(远程过程调用)来轻松调用 token微服务中的 验证接口了。

如下图所示,我们可以在每个需要验证token的工程中,借助AOP编程的方式实现token验证。

微服务实战(十八)通过AOP的方式自动完成微服务token验证_第1张图片

通过feign完成远程接口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 配置)

微服务实战(十八)通过AOP的方式自动完成微服务token验证_第2张图片微服务实战(十八)通过AOP的方式自动完成微服务token验证_第3张图片

然后,在新工程(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微服务 获取验证后的用户信息
    //业务逻辑处理
}

是不是稍显麻烦啊!

使用AOP,更简洁一点

结合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验证 功能了。

你可能感兴趣的:(微服务架构实战)