微服务实战(十七)微服务Token的实现: MD5简易Token、Jwt、Jwt结合AES加密

关于Token (令牌)

说起令牌,仿佛回到了封建时代,一掏出令牌,其他人都知道你是什么身份了。

而在web系统中,前后端的业务交互也往往需要附带身份数据,先姑且把这个数据统称为 Token (令牌) 。

面向有状态HTTP请求:

Token实际上是由cookies+session 实现

服务端session存储了每个客户端的状态(比如用户登录后,服务端为这个客户端存储的session中就有了用户标识数据)。

session存储时为每一个客户端分配了sessionId

前端cookies存储了sessionId,保证同一个客户端一段时间内请求后端,使用同一个sessionId

以上实现了后端在一定时间内都"认识“这个客户端

面向无状态HTTP请求:

服务端不再存储每个请求客户端的状态,客户端也不再依赖cookies机制

客户端每次请求中自带身份标识

服务端根据客户端请求中的token参数来验证或者提取用户数据

可以有很多种实现,系统中定义一个token,或者使用JWT等

微服务架构下token授权与验证流程

我画了个微服务架构下tokenn授权与验证的流程。

其中我这里拟支持3种Token形式:

1、MD5 Token :  属于系统内部自定义一种Token规则,我这里只是简单地将用户的几个字段进行了拼接MD5,用户调用接口登录成功后,就把token生成并颁发给客户端。

验证时需要在数据库中查询token颁发记录,从而获取对应的用户信息

2、Jwt : JSON Web Tokens,一种便于分布式架构中传输Token的一种规范,跟上述的Token串不同的是,它可以直接把用户信息也打包进去。所以这里面常常存的不只是一个令牌,而是直接包含了业务接口所需的用户信息、以及业务数据。Jwt默认不加密,只是进行签名验证,防止篡改,但是里面存放的数据是可见的(通过Base64URL 解码)。

验证时直接从Jwt中提取登录用户信息,不需要查询数据库

3、Jwt+ AES : 是基于第二种方案进行了AES对称加密,这样在传输过程中,里面的数据是不可见的。

验证时进行AES解密出原Jwt,直接从Jwt中提取登录用户信息,不需要查询数据库

主要代码

完整代码:https://gitee.com/zhaojunfu2014/springcloud-learn-tokens

项目基于:springboot,mybatis plus

数据库:mysql

目的:实现  流程图的 2,3   6,7  节点 ,接口定义如下:

package com.jfcloud.all.user.service;

import com.jfcloud.all.user.domain.param.GrantParams;

/**
 * 微服务鉴权服务
 * @author zhaojunfu
 *
 */
public interface TokenService {
	
	/**
	 * 认证接口<br>
	 * 认证通过后返回token对象
	 * @param params 参数列表
	 * @return
	 * @throws Exception 
	 */
	Object grant(GrantParams params) throws Exception;
	
	
	/**
	 * 验证token有效性,并提取用户信息
	 * @param token
	 * @return
	 */
	Object validateAndGet(Object token);
}

自定义MD5 Token 实现

数据库表设计

jfcloud_user:用户表,模拟业务系统中需要登录的用户,登录后可用token验证后换取此用户信息

jfcloud_user_token: token表,为用户颁发token的记录

CREATE TABLE `jfcloud_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
  `realname` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '昵称',
  `password` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
  `account` decimal(20,2) DEFAULT NULL COMMENT '账户余额',
  `order_num` bigint(20) DEFAULT NULL COMMENT '下单数量',
  `buy_num` bigint(20) DEFAULT NULL COMMENT '购买产品数',
  `buy_amount` decimal(20,2) DEFAULT NULL COMMENT '购买金额',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `is_delete` int(1) DEFAULT '0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE `jfcloud_user_token` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `token` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户登录token',
  `status` int(1) DEFAULT '1' COMMENT '状态 0:无效  1:有效',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `is_delete` int(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

接口实现代码

package com.jfcloud.all.user.service.impl;

import java.time.LocalDateTime;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jfcloud.all.user.domain.JfcloudUser;
import com.jfcloud.all.user.domain.JfcloudUserToken;
import com.jfcloud.all.user.domain.param.GrantParams;
import com.jfcloud.all.user.persistent.JfcloudUserService;
import com.jfcloud.all.user.persistent.JfcloudUserTokenService;
import com.jfcloud.all.user.service.TokenService;
import com.jfcloud.common.exception.BussinessException;
import com.jfcloud.common.utils.MD5;

/**
 * 简易token授权模式<br>
 * 认证通过后颁发token,token内容为:指定字段组拼接后的MD5<br>
 * 被授权者后续通过该token调用 信息获取 接口获取所需数据
 * @author zhaojunfu
 *
 */
@Service(value = "SimpleTokenServiceImpl")
@Transactional
public class SimpleTokenServiceImpl implements TokenService {

	private static final Logger LOGGER = LoggerFactory.getLogger(SimpleTokenServiceImpl.class);
	
	@Autowired
	private JfcloudUserService jfcloudUserService;
	@Autowired
	private JfcloudUserTokenService jfcloudUserTokenService;
	
	@Override
	public Object grant(GrantParams params) {
		//1.参数校验
		String username = params.getUsername();
		String password = params.getPassword();
		Assert.notNull(username, "用户名不能为空");
		Assert.notNull(password, "密码不能为空");
		//2.认证
		JfcloudUser user = null;
		try{
			user=jfcloudUserService.getOne(new QueryWrapper<JfcloudUser>().eq("username", username) );
		}catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			throw new BussinessException("账号异常");
		}
		Assert.notNull(user, "账号不存在");
		String dbPwd = user.getPassword();
		if(password.equals(dbPwd)) {
			//验证通过
			//3.判断是否已有token
			JfcloudUserToken oldToken = jfcloudUserTokenService.getOne(
					new QueryWrapper<JfcloudUserToken>().eq("user_id", user.getId())
					.eq("status", 1)
					.eq("is_delete", 0));
			if(oldToken!=null) return oldToken.getToken();
			//4.颁发token
			JfcloudUserToken token = new JfcloudUserToken();
			token.setUserId(user.getId());
			token.setCreateTime(LocalDateTime.now());
			token.setStatus(1);
			//可指定任意数量的字段进行拼接MD5
			String md5Token = MD5.generate(user.getId(),user.getUsername(),user.getPassword(),token.getCreateTime());
			token.setToken(md5Token);
			token.setIsDelete(0);
			jfcloudUserTokenService.save(token);
			return md5Token;
		}else {
			//验证不通过
			throw new BussinessException("密码不正确");
		}
	}
	

	@Override
	public Object validateAndGet(Object token) {
		JfcloudUserToken oldToken = jfcloudUserTokenService.getOne(
				new QueryWrapper<JfcloudUserToken>().eq("token", token)
				.eq("status", 1)
				.eq("is_delete", 0));
		Assert.notNull(oldToken, "token已失效");
		
		JfcloudUser user = jfcloudUserService.getById(oldToken.getUserId());
		
		Assert.isTrue(user.getIsDelete().equals(0), "用户已被删除");
		
		return user;
	}
}

 控制器代码

/**
	 * 简单token模式:step1.授权
	 * @param params
	 * @return
	 */
	@PostMapping(path = "/simple/grant")
	@ResponseBody
	public RespData grant(@RequestBody GrantParams params) {
		try {
			Object token = simpleTokenService.grant(params);
			return new RespData(token);
		}catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			return new RespData(e);
		}
	}
	/**
	 * 简单token模式:step2.鉴定token有效性,并返回认证数据
	 * @param token
	 * @return
	 */
	@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);
		}
	}

效果验证

用户表数据:

步骤1:调用授权接口,提交用户名密码

颁发token 

步骤2:调用验证接口去验证token,并且提取用户信息

此方案会根据token再去查一遍数据库

错误情况下:(此时前端应该废弃使用该token,并重新进行登录授权)

Jwt 方式实现

数据库表设计

为了便于区分,使用了两套表来承载不同的token方式。

jfcloud_user_jwt  :记录颁发过的jwt

CREATE TABLE `jfcloud_user_jwt` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `jwt_token` text COLLATE utf8mb4_bin COMMENT 'jwt 信息',
  `status` int(1) DEFAULT '1' COMMENT '状态 0:无效  1:有效',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `is_delete` int(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

接口实现代码

package com.jfcloud.all.user.service.impl;

import java.time.LocalDateTime;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jfcloud.all.user.domain.JfcloudUser;
import com.jfcloud.all.user.domain.JfcloudUserJwt;
import com.jfcloud.all.user.domain.param.GrantParams;
import com.jfcloud.all.user.persistent.JfcloudUserJwtService;
import com.jfcloud.all.user.persistent.JfcloudUserService;
import com.jfcloud.all.user.service.TokenService;
import com.jfcloud.common.exception.BussinessException;

/**
 * jwt 非加密模式
 * @author zhaojunfu
 *
 */
@Service(value = "JwtTokenServiceImpl")
@Transactional
public class JwtTokenServiceImpl implements TokenService {
	private static final Logger LOGGER = LoggerFactory.getLogger(SimpleTokenServiceImpl.class);

	/*
	 * jwt 签名密钥
	 * 可在本地配置文件或者配置中心进行配置
	 */
	@Value("${jfcloud.jwt.sign:zjf2020}")
	private String sign;

	@Autowired
	private JfcloudUserService jfcloudUserService;
	@Autowired
	private JfcloudUserJwtService jfcloudUserJwtService;

	@Override
	public Object grant(GrantParams params) throws Exception {
		// 1.参数校验
		String username = params.getUsername();
		String password = params.getPassword();
		Assert.notNull(username, "用户名不能为空");
		Assert.notNull(password, "密码不能为空");

		// 2.认证
		JfcloudUser user = null;
		try {
			user = jfcloudUserService.getOne(new QueryWrapper<JfcloudUser>().eq("username", username));
		} catch (Exception e) {
			LOGGER.error(e.getMessage(), e);
			throw new BussinessException("账号异常");
		}

		Assert.notNull(user, "账号不存在");
		String dbPwd = user.getPassword();
		if (password.equals(dbPwd)) {
			// 验证通过
			
			String token = "";
			user.setPassword("");
			token = JWT.create().withClaim("userInfo",fillNull( BeanUtils.describe(user) ))
					.sign(Algorithm.HMAC256(sign));

			JfcloudUserJwt jwt = new JfcloudUserJwt();
			jwt.setUserId(user.getId());
			jwt.setCreateTime(LocalDateTime.now());
			jwt.setStatus(1);
			jwt.setJwtToken(token);
			jfcloudUserJwtService.save(jwt);
			
			return token;
		} else {
			// 验证不通过
			throw new BussinessException("密码不正确");
		}

	}


	private Map<String,String> fillNull(Map<String, String> describe) {
		for(String k :describe.keySet()) {
			String v  = describe.get(k);
			if(v==null) {
				describe.put(k, "");
			}
		}
		return describe;
	}


	@Override
	public Object validateAndGet(Object token) {
		JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(sign)).build();
		try {
			jwtVerifier.verify(String.valueOf(token));
			Map<String,Object> userMap = JWT.decode(String.valueOf(token)).getClaim("userInfo").asMap();
			return userMap;
		} catch (JWTVerificationException e) {
			LOGGER.info("jwt 验证失败 "+e.getMessage());
			throw e;
		}
	}

}

控制器代码

/**
	 * jwt token模式: step1.授权
	 * @param params
	 * @return
	 */
	@PostMapping(path = "/jwt/grant")
	@ResponseBody
	public RespData jwtGrant(@RequestBody GrantParams params) {
		try {
			Object token = jwtTokenService.grant(params);
			return new RespData(token);
		}catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			return new RespData(e);
		}
	}
	
	/**
	 * jwt token模式:step2.鉴定token有效性,并返回认证数据
	 * @param token
	 * @return
	 */
	@RequestMapping(path = "/jwt/validateAndGet")
	@ResponseBody
	public RespData jwtValidateAndGet(@RequestParam(required = false) String token,HttpServletRequest request) {
		try {
			//支持以 POST GET URL参数方式校验token
			//【优先】支持将 token放入http的header里(参数名为:jwt_token)
			String headerToken = request.getHeader("jwt_token");
			if(StringUtils.isNotEmpty(headerToken)) {
				token = headerToken;
			}
			Assert.notNull(token,"token不能为空");
			Object obj = jwtTokenService.validateAndGet(token);
			return new RespData(obj);
		}catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			return new RespData(e);
		}
	}

效果验证

步骤1:发送用户名密码,调用授权接口

jwt 中 直接包含了登录用户信息,通过base64URL解码可以查看内容

步骤2:调用验证接口去验证token,并且提取用户信息

因为比较长,所以最好是放入header中

此方案直接从Jwt中提取登录用户信息,不需要查询数据库

Jwt + AES 加密方式Token

接口实现比较简单,直接调用Jwt模式的接口,只是在调用前进行加密和解码,并且配置一个对称密钥: aesKey。

这个配置后续接入 springcloud后,可以直接放入nacos等配置中心

接口实现代码

package com.jfcloud.all.user.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.jfcloud.all.user.domain.param.GrantParams;
import com.jfcloud.all.user.service.TokenService;
import com.jfcloud.common.utils.AESUtil;

/**
 * jwt + aes 加密
 * @author zhaojunfu
 *
 */
@Service(value = "JwtAesServiceImpl")
@Transactional
public class JwtAesServiceImpl implements TokenService {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(JwtAesServiceImpl.class);

	@Value(value = "${jfcloud.jwt.aes:zjf20200310}")
	private String aesKey;
	
	@Autowired
	@Qualifier(value = "JwtTokenServiceImpl")
	private TokenService jwtTokenService;
	
	
	@Override
	public Object grant(GrantParams params) throws Exception {
		Object jwt = jwtTokenService.grant(params);
		//加密后返回
		String jwtAES=AESUtil.encrypt(String.valueOf(jwt), aesKey);
		return jwtAES;
	}

	@Override
	public Object validateAndGet(Object token) {
		//先解密
		String tokenDecrypted = AESUtil.decrypt(String.valueOf(token), aesKey);
		return jwtTokenService.validateAndGet(tokenDecrypted);
	}

}

控制器代码

/**
	 * jwt aes模式: step1.授权
	 * @param params
	 * @return
	 */
	@PostMapping(path = "/sjwt/grant")
	@ResponseBody
	public RespData sjwtGrant(@RequestBody GrantParams params) {
		try {
			Object token = jwtAesService.grant(params);
			return new RespData(token);
		}catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			return new RespData(e);
		}
	}
	
	/**
	 * jwt aes模式:step2.鉴定token有效性,并返回认证数据
	 * @param token
	 * @return
	 */
	@RequestMapping(path = "/sjwt/validateAndGet")
	@ResponseBody
	public RespData sjwtValidateAndGet(@RequestParam(required = false) String token,HttpServletRequest request) {
		try {
			//支持以 POST GET URL参数方式校验token
			//【优先】支持将 token放入http的header里(参数名为:jwt_token)
			String headerToken = request.getHeader("jwt_token");
			if(StringUtils.isNotEmpty(headerToken)) {
				token = headerToken;
			}
			Assert.notNull(token,"token不能为空");
			Object obj = jwtAesService.validateAndGet(token);
			return new RespData(obj);
		}catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			return new RespData(e);
		}
	}

效果验证

步骤1:发送用户名密码,调用授权接口

颁发的jwt 已经被AES加密过了

步骤2:调用验证接口去验证token,并且提取用户信息

此方案直接从Jwt中提取登录用户信息,不需要查询数据库

 优化点:减少外部调用

其中,我们可以将 6,7 两个步骤省略掉,

采用JWT 以及 JWT+AES的情况下,可以直接把Token服务工程作为工程依赖,加入到要使用token的其他工程中。

在接收到前端的token后,直接在本地做验证即可。

发布了36 篇原创文章 · 获赞 134 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/u011177064/article/details/104822700
今日推荐