在上一篇中,我们建立了Auth服务,这个服务就是专门用来处理用户认证,授权的服务,这里我们集成JWT来作为认证的票据。
什么JWT
什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON
的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON
对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC
算法或者是RSA
的公私秘钥对进行签名。
JWT请求流程
1. 用户使用账号和面发出post请求;
2. 服务器使用私钥创建一个jwt;
3. 服务器返回这个jwt给浏览器;
4. 浏览器将该jwt串在请求头中像服务器发送请求;
5. 服务器验证该jwt;
6. 返回响应的资源给浏览器。
JWT的主要应用场景
身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
优点
1.简洁(Compact): 可以通过URL
,POST
参数或者在HTTP header
发送,因为数据量小,传输速度也很快
2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
3.因为Token
是以JSON
加密的形式保存在客户端的,所以JWT
是跨语言的,原则上任何web形式都支持。
4.不需要在服务端保存会话信息,特别适用于分布式微服务。
`
JWT的结构
JWT是由三段信息构成的,将这三段信息文本用.
连接一起就构成了JWT字符串。
就像这样:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品)
Signature 签名/签证
Header
JWT的头部承载两部分信息:token类型和采用的加密算法。
{
"alg": "HS256",
"typ": "JWT"
}
声明类型:这里是jwt
声明加密的算法:通常直接使用 HMAC SHA256
加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。
MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值
SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证
Payload
载荷就是存放有效信息的地方。
有效信息包含三个部分
1.标准中注册的声明
2.公共的声明
3.私有的声明
标准中注册的声明 (建议但不强制使用) :
iss
: jwt签发者sub
: 面向的用户(jwt所面向的用户)aud
: 接收jwt的一方exp
: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)nbf
: 定义在什么时间之前,该jwt都是不可用的.iat
: jwt的签发时间jti
: jwt的唯一身份标识,主要用来作为一次性token
,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64
是对称解密的,意味着该部分信息可以归类为明文信息。
Signature
jwt的第三部分是一个签证信息
这个部分需要base64
加密后的header
和base64
加密后的payload
使用.
连接组成的字符串,然后通过header
中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt
的第三部分。
密钥secret
是保存在服务端的,服务端会根据这个密钥进行生成token
和进行验证,所以需要保护好。
参考资料:https://www.jianshu.com/p/e88d3f8151db
新建服务springcloud_common,存放公共依赖
和以前一样建立maven项目:
BaseApiService:获取用户信息,后面用到
BaseResult:封装通用的返回接口信息
AuthApiConstant类封装一些通用的常量值
BaseApiConstant类封装的是通用返回的接口常量:
包redis下是封装的关于redis的一些配置信息,在每个引用到Common依赖的服务中,都要在配置文件中配置redis
RedisConfig:redis配置信息
RedisParameter: redis参数信息
RedisUtil: 工具类,用于操作redis
代码的话我就不贴出来了,你们对照着源码拷贝
然后我们在Auth服务配置Redis的相关信息
server:
port: 8003
spring:
application:
name: api-auth
redis:
hostserver: 192.168.0.132:6379 #该节点配置redis单机,配置后下面哨兵模式失效
database: 4 # Redis数据库索引(默认为0)
timeout: 10000 # 连接超时时间(毫秒)
password: # Redis服务器连接密码(默认为空)
perKey: general_redis #redis前缀
sentinel:
master: mymaster
nodes:
lettuce:
pool:
max-active: 200 # 连接池最大连接数(使用负值表示没有限制
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8001/eureka/
Auth集成JWT
pom引入JWT依赖,这里使用jsonwebtoken
<!-- 引入jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
封装JWT工具类:
package com.springcloud.jwt;
import cn.hutool.core.date.DateUtil;
import com.springcloud.common.constant.AuthApiConstant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
/**
* JWT工具
*/
@Component
@Slf4j
public class JwtUtil {
/**
* 密钥
*/
private static final String SECRET = AuthApiConstant.AUTH_SECRET;
/**
* 生成用户token,设置token超时时间
*/
public String createToken(Map map) {
Claims claims = Jwts.claims();
claims.put(AuthApiConstant.AUTH_USER_INFO_NAME, map);
return Jwts.builder().setClaims(claims).setExpiration(DateUtil.offsetSecond(new Date(), Integer.parseInt(map.get(AuthApiConstant.AUTH_USER_REFRESH_NAME).toString())))
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
/**
* 通过Token获取用户信息
*
* @param token
* @return
*/
public Claims getClaimByToken(String token) {
try {
Jws<Claims> jws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return jws.getBody();
} catch (Exception e) {
return null;
}
}
/**
* 校验token是否过期
*
* @param expiration
* @return
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
}
注意:
我们这里Auth服务的方法全部抽取到一个集中管理的服务中:
在 springcloud_parent下新建服务springcloud_api,springcloud_api下又有很多子模块,统一管理每个服务的实体类和接口,不明白的直接看荡源码看一下
AuthService:
package com.springcloud.service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
public interface AuthService {
@GetMapping("/auth/getToken")
Map<String, Object> getToken(@RequestParam("userName") String userName, @RequestParam("password") String password);
@GetMapping("/auth/verifyToken")
Map<String, Object> verifyToken(@RequestParam(value = "token") String token);
@GetMapping("/auth/refreshToken")
Map<String, Object> refreshToken(@RequestParam(value = "refreshToken") String refreshToken);
}
服务Auth新建类TokenManage,并且实现AuthService
TokenManage:
package com.springcloud.manage;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.springcloud.common.constant.AuthApiConstant;
import com.springcloud.common.redis.RedisUtil;
import com.springcloud.jwt.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class TokenManage {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisUtil redisUtil;
/**
* 生成token
*
* @param userId 用户id
* @param userName 名字
* @param expire token 过期实际时间
* @param refreshTokenExpire refreshToken过期时间
* @return
*/
public Map<String, Object> getTokenInfo(String userId, String userName,
int expire, int refreshTokenExpire) {
Map<String, Object> map = new HashMap();
map.put(AuthApiConstant.AUTH_USER_USERNAME_NAME, userName); //用户名
map.put(AuthApiConstant.AUTH_USER_ID, userId); //用户编号
map.put(AuthApiConstant.AUTH_USER_REFRESH_NAME, expire);// Token过期时间
//产生认证token
String token = jwtUtil.createToken(map);
//生成refreshToken
String refreshToken = redisUtil.getKeys(AuthApiConstant.AUTH_REDIS_TOKENKEY + userId + ":*").stream().findFirst().orElse(null);
if (StrUtil.isBlank(refreshToken)) {
refreshToken = IdUtil.simpleUUID();
} else {
refreshTokenExpire = Convert.toInt(redisUtil.getExpire(refreshToken));
refreshToken = refreshToken.substring(refreshToken.lastIndexOf(":") + 1);
}
//将用户信息map放到redis
redisUtil.set(AuthApiConstant.AUTH_REDIS_TOKENKEY + userId + ":" + refreshToken, map, refreshTokenExpire);
map.put(AuthApiConstant.TOKEN_NAME, token);
map.put(AuthApiConstant.AUTH_USER_REFRESHTOKEN_NAME, refreshToken);
map.put(AuthApiConstant.AUTH_USER_REFRESHTOKENEXPIRE_NAME, refreshTokenExpire);
return map;
}
}
token过期刷新方案
1、单点登录
用户登录,后端验证用户成功之后生成两个token,这两个token分别是access_token(访问接口使用的token)、refresh_token(access_token过期后用于刷续期的token,注意设置refresh_token的过期时间需比access_token的过期时间长),后端将用户信息和这两个token存放到redis中并返回给前端。
前端在获取到登录成功返回的两个token之后,将之存放到localStorage本地存储中。
2、接口请求
前端封装统一接口请求函数、token刷新函数,在请求成功之后对返回结果进行校验,如果token过期,则调用token刷新函数请求新的token.
后端在接收到token刷新请求之后通过结合redis中存放的用户信息、token和refresh_token对请求参数进行验证,验证通过之后生成新的token和refresh_token存放到redis中并返回给前端。至此完成token刷新。
启动项目后:
然后这里的界面我是集成的Swagger2,有个配置文件
代码:
package com.springcloud.config;
import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
import com.springcloud.common.constant.AuthApiConstant;
import io.swagger.annotations.Api;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfig {
@Bean
public Docket defaultApi2() {
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.paths(PathSelectors.any())
.build();
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(AuthApiConstant.SWAGGER_TITLE)
.version(AuthApiConstant.SWAGGER_VERSION)
.build();
}
}
东西很多,用博客写出来难免会漏掉一些东西,遗漏的地方请看源码,先写到这吧,累了。
不足之处,多加指教。
项目地址:
https://gitee.com/bosszddquan/springcloudframe