阅读此文前请先阅读上一篇SpringBoot整合JWT实现用户认证了解JWT

背景介绍:

因项目需求,有PC端 APP端和小程序端,但登陆接口是同一个,然而微服务也无法使用传统的session解决用户登录问题(注意这里是传统的session不是spring session),使用户信息在其他服务共享。

如此一来就想到了token安全认证,而JWT生成token可以包含用户信息,也就果断选择了JWT作为SpringCloud gateway网关的token校验工具,这样,我们便可以直接解析token获取用户信息了。

具体实现思路:

  1. 让JWT在其他所有服务可以共同使用,父工程需要引入JWT jar。避免在其他服务重复引入。

  2. 如何使用JWT生成token。

  3. 如何解析token。

  4. 如何让网关拦截用户请求校验token。

  5. 如何避免首次登录被网关拦截。

代码实现:

1.创建SpringCloud项目

SpringCloud子项目包含 eureka,gateway,auth三个工程,父工程maven依赖如下。

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>6.0</version>
</dependency>

2.Auth和gateway编写TOKEN工具类

 public class Token {
private static final Logger log = LoggerFactory.getLogger(Token.class);
/**
* 1.创建一个32-byte的密匙JWT生成TOKEN
*/
private static final byte[] secret = "geiwodiangasfdjsikolkjikolkijswe".getBytes();
//生成一个token
public static String creatToken(Map<String,Object> payloadMap) throws JOSEException {
//3.先建立一个头部Header
/**
* JWSHeader参数:1.加密算法法则,2.类型,3.。。。。。。。
* 一般只需要传入加密算法法则就可以。
* 这里则采用HS256
* JWSAlgorithm类里面有所有的加密算法法则,直接调用。
*/
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
//建立一个载荷Payload
Payload payload = new Payload(new JSONObject(payloadMap));
//将头部和载荷结合在一起
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
//建立一个密匙
JWSSigner jwsSigner = new MACSigner(secret);
//签名
jwsObject.sign(jwsSigner);
//生成token
return jwsObject.serialize();
}
/**
* 解析一个token
* @param token
* @return
* @throws ParseException
* @throws JOSEException
*/
public static Map<String,Object> valid(String token) throws ParseException, JOSEException {
//解析token
JWSObject jwsObject = JWSObject.parse(token);
//获取到载荷
Payload payload=jwsObject.getPayload();
//建立一个解锁密匙
JWSVerifier jwsVerifier = new MACVerifier(secret);
Map<String, Object> resultMap = new HashMap<>();
//判断token
if (jwsObject.verify(jwsVerifier)) {
resultMap.put("Result", 0);
//载荷的数据解析成json对象。
JSONObject jsonObject = payload.toJSONObject();
resultMap.put("data", jsonObject);
//判断token是否过期
if (jsonObject.containsKey("exp")) {
Long expTime = Long.valueOf(jsonObject.get("exp").toString());
Long nowTime = new Date().getTime();
//判断是否过期
if (nowTime > expTime) {
//已经过期
resultMap.clear();
resultMap.put("Result", 2);
}
}
}else {
resultMap.put("Result", 1);
}
return resultMap;
}
/**
* 生成token的业务逻辑 登录接口调用次业务
* @param uid
* @return
*/
public static String TokenTest(Long uid,Long deptId,String userType,int companyId) {
//获取生成token
Map<String, Object> map = new HashMap<>();
//建立载荷,这些数据根据业务,自己定义。
map.put("uid", uid);
map.put("deptId", deptId);
map.put("userType", userType);
map.put("companyId", companyId);
//生成时间
map.put("sta", new Date().getTime());
//过期时间
map.put("exp", new Date().getTime()+1000*3600*24*15);
try {
String token = Token.creatToken(map);
System.out.println("token="+token);
return token;
} catch (JOSEException e) {
System.out.println("生成token失败");
e.printStackTrace();
}
return null; } /**
* 处理解析的业务逻辑 gateway JWT认证过滤器解析
* @param token
*/
public static Map<String,Object> ValidToken(String token) {
Map<String, Object> userMsg = new HashMap<String, Object>();
//解析token
try {
if (token != null) {
Map<String, Object> validMap = Token.valid(token);
int i = (int) validMap.get("Result");
if (i == 0) {
log.info("token解析成功");
JSONObject jsonObject = (JSONObject) validMap.get("data");
log.info("uid是:" + jsonObject.get("uid"));
log.info("deptId是:" + jsonObject.get("deptId"));
log.info("userType是:" + jsonObject.get("userType"));
log.info("companyId是:" + jsonObject.get("companyId"));
log.info("生成时间是:"+jsonObject.get("sta"));
log.info("过期时间是:"+jsonObject.get("exp"));
userMsg.put("token",token);
userMsg.put("uid",jsonObject.get("uid"));
userMsg.put("deptId",jsonObject.get("deptId"));
userMsg.put("companyId",jsonObject.get("companyId"));
userMsg.put("userType",jsonObject.get("userType"));
return userMsg;
} else if (i == 2) {
log.info("token已经过期");
return userMsg;
}
}
} catch (ParseException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
return userMsg;
} public static void main(String[] ages) {
//获取token
Long uid = 1L;
Long deptId = 2L;
String userType = "3";
int companyId = 4;
String token = TokenTest(uid,deptId,userType,companyId);
//解析token
log.info(ValidToken(token).toString());
}
}

特别提示:以上工具类可以在用户登录授权接口中调用,用以生成token,示例代码如下(可以借鉴不可复制哦,请根据自己业务逻辑在合适的地方调用TOKEN工具)

@RestController
@RequestMapping("/currency")
public class CurrencyLoginController {
//密钥 (需要前端和后端保持一致)
private static final String KEY = "abcdefgabcdefg12";
//redis初始KEY值
private static final String LOGIN_USER = "login_user";
@Autowired
private RedisUtil ru;
@PostMapping("/login")
public Map<String, Object> ajaxLogin(String username, String password, Boolean rememberMe) throws Exception{
password = AESUtil.aesDecrypt(password,KEY);//双向加密规则
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
User user = ShiroUtils.getUser();
String access_token = Token.generateToken(user.getUserId(), user.getDeptId(),user.getLoginUserType(), user.getCompanyId());
UserMsg resultUser = new UserMsg();
resultUser.setCompanyId(user.getCompanyId());
resultUser.setUserType(user.getLoginUserType());
resultUser.setDeptId(user.getDeptId());
resultUser.setUid(user.getUserId());
resultUser.setToken(access_token);
ru.set(LOGIN_USER+user.getUserId(), resultUser, 3600*24*15);
return ResultMap.ok("登录成功", resultUser);//改造——》》获取用户信息保存到redis中实现用户信息在微服务中共享,生成token
}catch (AuthenticationException e){
String msg = "用户或密码错误";
if (StringUtils.isNotEmpty(e.getMessage())){
msg = e.getMessage();
}
return ResultMap.error(msg);
}
}
}

好了,此时呢,我们已经通过auth工程完成了用户登录授权,并且生成了token。那么如何在gateway网关中进行token认证呢?

3.gateway网关中编写JwtCheckGatewayFilterFactory过滤器。

此类需要继承gateway的AbstractGatewayFilterFactory。

代码实现如下:

首先gateway网关yml文件中需要代理auth路由。

spring:
cloud:
gateway:
routes:
- id: neo_route
uri: lb://YUNXI-AUTH
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- JwtCheck

自定义 JwtCheckGatewayFilterFactory 继承 AbstractGatewayFilterFactory 抽象类,代码如下:

public class JwtCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<JwtCheckGatewayFilterFactory.Config> {
private static final Logger log = LoggerFactory.getLogger(JwtCheckGatewayFilterFactory .class);
//定义用户认证登录接口
private static final String CURRENCY_URL="/currency/login";
//redis初始KEY值
private static final String LOGIN_USER = "login_user";
@Autowired
private RedisUtil ru;
public JwtCheckGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String jwtToken = exchange.getRequest().getHeaders().getFirst("Authorization");
log.info(exchange.getRequest().getURI().toString());
//校验jwtToken的合法性,如果当前请求url和认证url相同跳过认证,表示用户首次登录认证
if(exchange.getRequest().getURI().toString().contains(CURRENCY_URL)){
return chain.filter(exchange);
}
if(jwtToken != null){
log.info(Token.ValidToken(jwtToken).toString());
//解析TOKEN
Map<String, Object> userMsg = Token.ValidToken(jwtToken);
Long uid = (Long) userMsg.get("uid");
if(ru.hasKey(LOGIN_USER+uid)){
Object obj = ru.get(LOGIN_USER+uid);
UserMsg userModel = (UserMsg) obj;
//解析客户端传过来的TOKEN是否和缓存中的TOKEN相同,并且判断TOKEN过期时间是否大于当前时间
if(userModel.getToken().equals(jwtToken)){
return chain.filter(exchange);
}else{
ServerHttpResponse response = exchange.getResponse();
String warningStr = "不合法的请求";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
}
}else{
ServerHttpResponse response = exchange.getResponse();
String warningStr = "登录超时";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
//不合法(响应未登录的异常)
ServerHttpResponse response = exchange.getResponse();
//设置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//设置body
String warningStr = "未授权的请求,请登录";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
};
} public static class Config {
//Put the configuration properties for your filter here
}
}

编写config文件将JWT认证过滤器添加到Spring bean中。

@Configuration
public class AppConfig {
@Bean
public JwtCheckGatewayFilterFactory jwtCheckGatewayFilterFactory(){
return new JwtCheckGatewayFilterFactory();
}
}

此时我们就完成了整个token认证过程,其实简单的来说就是:

  • 第一步:Auth工程配合用户登录生成token,并将token和用户信息存储在redis中。

  • 第二步:在gayeway中编写JWT认证过滤器,用以校验用户请求中携带的token。

有图有真相

特别提示:我的auth工程端口是8766,登录认证接口路由是/currency/login。而此时我请求的认证接口是/main/currency/login,端口是8765,我们在文章开头就已说明,gateway网关在yml文件中配置auth代理为auth/,和这里的main是同一个道理。

如果此时我们再去请求项目中其他端口携带过期的token试试看效果:

我们登陆认证返回的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;

携带的其他过期的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjQ1NjUwNzIwMiwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1Nzc1MjUwNzIwMn0._yF2TeaR4MTmF-Re9QciMZOeRKBOQmfvi3o4hWeGSMU

再携带错误的token试试看:

登陆认证返回的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;

携带错误的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlD

携带正确的token:

到这里我么你的整个SpringCloud gateway网关+JWT安全认证就结束啦,非常抱歉,由于项目保密性不能为大家提供项目源码。但是整个过程我已经写的非常详细,也不希望大家做伸手党,如果有各种疑问欢迎留言,我可以帮大家一一解决。

记一次token安全认证的实践的更多相关文章

  1. NodeJS 实现基于 token 的认证应用

    此段摘自 http://zhuanlan.zhihu.com/FrontendMagazine/19920223 英文原文 http://code.tutsplus.com/tutorials/tok ...

  2. 理解JWT(JSON Web Token)认证

    理解JWT(JSON Web Token)认证 最近想做个小程序,需要用到授权认证流程.以前项目都是用的 OAuth2 认证,但是Sanic 使用OAuth2 不太方便,就想试一下 JWT 的认证方式 ...

  3. 使用 AngularJS & NodeJS 实现基于token 的认证应用(转)

    认证是任何 web 应用中不可或缺的一部分.在这个教程中,我们会讨论基于 token 的认证系统以及它和传统的登录系统的不同.这篇教程的末尾,你会看到一个使用 AngularJS 和 NodeJS 构 ...

  4. ASP.NET WebApi 基于OAuth2.0实现Token签名认证

    一.课程介绍 明人不说暗话,跟着阿笨一起玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将是我们需要思考的问题.为了保护我们的WebApi数 ...

  5. ASP.NET WebApi 基于JWT实现Token签名认证

    一.前言 明人不说暗话,跟着阿笨一起玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将会是需要思考的问题.在ASP.NET WebServi ...

  6. ASP.NET WebApi 基于分布式Session方式实现Token签名认证

    一.课程介绍 明人不说暗话,跟着阿笨一起学玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将会是需要思考的问题.在ASP.NETWebSer ...

  7. ASP.NET WebApi 基于分布式Session方式实现Token签名认证(发布版)

    一.课程介绍 明人不说暗话,跟着阿笨一起学玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将会是需要思考的问题.在ASP.NETWebSer ...

  8. 使用 AngularJS & NodeJS 实现基于 token 的认证应用

      认证是任何Web应用中不可或缺的一部分.在这个教程中,我们会讨论基于token的认证系统以及它和传统的登录系统的不同.这篇教程的末尾,你会看到一个使用 AngularJS 和 NodeJS 构建的 ...

  9. JWT(Json web token)认证详解

    JWT(Json web token)认证详解 什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该to ...

随机推荐

  1. getOutputStream() has already been called for this response 从了解到解决

    一.背景说明        在tomcat的localhost.log日志中时长见到 getOutputStream() has already been called for this respon ...

  2. Python Weekly 419

    文章,教程或讲座 如何用 Dropbox Security 构建用于日志系统的威胁检测和事件响应的工具 https://blogs.dropbox.com/tech/2019/10/how-dropb ...

  3. dict继承关系

    from collections.abc import Mapping, MutableMapping#dict属于mapping类型, MutalbelMapping继承Mapping a = {} ...

  4. 页面QQ临时对话的实现

    一.开通QQ服务 点我进入QQ推广官网>然后点击推广工具即可后面自己看中文 二.页面a标签 <a target="_blank" href="http://w ...

  5. Vue Cli3 中别名的配置问题

    Vue Cli3 中别名的配置问题 vue-cli3中是没有config.build等目录的,这是因为vue-cli3中将这些配置隐藏起来了,如果想要修改,可以在vue.config.js文件中进行修 ...

  6. vue $emit $on 从子组件传递数据给父组件

    原理是: 子组件使用$emit发送数据,父组件使用$on,或者v-on绑定, 来监听子组件发送的数据. 子组件: <button @click="sendChildData" ...

  7. 大话Git系列之初识版本控制系统(2)

    本次接着上次的来介绍版本控制系统,这个讲到的将是Git与Github,说一下什么是Git,什么是GitHub?他们之间的关系是什么? 1.Git 是代码管理的工具 2.GitHub 是基于Git实现的 ...

  8. arcgis api 4.x for js 地图加载多个气泡窗口展示(附源码下载)

    前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 4.x for js:esri 官网 api,里面详细的介绍 arcgis api 4.x 各个类 ...

  9. apk系统签名小技巧

    前言 对于经常和android系统打交道的攻城狮来说,给app打系统签名一定是日常操作啦.由于最近使用的比较多,特此总结一下,减少复制粘贴的操作,通过命令行来搞定. 简化前的操作 1.Android ...

  10. 模拟超市付款 (if 多分支结构)

    """ 模拟超市付款: 商品单价 商品数量 键盘上输入商品单价,以及商品数量, 然后计算应付总额 计算总额 float 提示用户可以有4种付款方式 不同的付款方式有不同的 ...