用户每次访问后台的时候,如果是一些需要认证的链接,都需要识别用户身份,比如用户抢单、用户中心等,在传统项目中用的是Session,但在微服务中不建议使用Session,使用JWT令牌。
1. 初识JWT
JWT简称JSON Web Token ,也就是通过json形式作为web应用的令牌,用在各方之间安全的将信息作为json对象传输,在数据传输过程中还可以完成数据加密,签名相关处理 。
JWT令牌作用:
-
身份授权:这是使用jwt的最常见方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该领牌允许的路由,服务和资源,单点登录是的当今广泛使用JWT的一项功能,因为他的开销很小,并且可以在不同的域中使用。
-
信息交换:JWT令牌是在各方面之间安全地传输信息的好方法,因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人,此外由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。
JWT令牌认证流程:
JWT令牌鉴权流程如上图:
1:用户携带账号密码登录
2:登录通过后,后端服务会封装账号信息,并且采用指定算法进行加密,并将加密后的密文(令牌)返回客户端
3:客户端拿到令牌后,将令牌保存到本地,可以使Cookie,也可以是localStoreage
4:客户端每次发起请求的时候,会将本地令牌写到到请求头中,一起传到后台
5:后台在微服务网关中校验令牌是否正确,如果正确再执行权限校验
6:令牌校验通过、权限校验通过,则执行用户要操作的业务流程
7:令牌校验失败或者权限校验失败,则提示错误信息
JWT令牌校验优势:
1:简洁(Compact):可以通过URL,POST参数或者在HTTP header中发送,因为数据量小,所以传输的速度也快
2:自包含(Self-contained): 负载中包含了所有用户所需的信息,避免了多次查询数据库
3:因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何Web形式都支持
4:不要再服务端保存信息,特别适用于分布式微服务
2. JWT令牌结构
JWT令牌组成有3部分,分别为Header、Payload、Signature,将三部分组合就是标准的JWT令牌了,三部分组合通常以"."链接,如下:
HJLDISNDSSDYIREWREWRDFDSDSFBH.DSFTIHGNDSFSDREWREWRDFDSFDSFRDNSJDJ.DSVDSFEWEREREWREWRDFDS
Header:
1:通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC,SHA265或RSA,它会使用Base64编码组成JWT结构的一部分
2:注意: Base64是一种编码,也就是说,他是可以被翻译回原来的样子的,他并不是一种加密的过程
3:{"alg":"HS365", "typ":"JWT"}
Payload:
1:令牌的第二部分是有效负载,其中包含声明,声明有关实体(通常是指用户)和其他数据的声明。同样的他会使用Base64编码组成JWT结构的第二部分
2:{"sub":"12334798", "name":"John Doe", "admin":true}
Signature:
1:前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息,Signature需要使用编码后的Header和Payload以及我们提供的一个密钥,然后使用Header中指定的签名算法(HS256)进行签名,没有被篡改过:
2:如:HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payLoad).secret)
为什么要签名?
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合成新的JWT的话,那么服务器会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的,如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
JWT令牌存在数据安全隐患?
JWT令牌采用了Base64加密,Base64加密数据可以直接解密,因此我们在JWT令牌中尽量不要传输敏感信息,如果非要传输敏感信息,可以把敏感信息进行加密操作,比如AES加密。
3. JWT令牌实现
JWT令牌使用参考地址:https://github.com/auth0/java-jwt
在创建JWT令牌的时候,会有很多属性需要填写,关于JWT令牌中一些属性,我们说明一下:
iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
首先在我们的项目中引入相关依赖:
<!--JWT令牌-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.1</version>
</dependency>
然后编写相关工具类用来创建Token令牌和校验。
public class JwtToken {
//默认秘钥
private static final String DEFAULT_SECRET = "springcloudalibaba";
/***
* 生成令牌
* @param dataMap
* @return
*/
public static String createToken(Map<String, Object> dataMap) {
return createToken(dataMap, null);
}
/***
* 生成令牌
* @return
*/
public static String createToken(Map<String, Object> dataMap, String secret) {
//秘钥为空就采用默认秘钥
if (StringUtils.isEmpty(secret)) {
secret = DEFAULT_SECRET;
}
//创建令牌操作算法
Algorithm algorithm = Algorithm.HMAC256(secret);
//创建令牌
return JWT.create()
.withClaim("body", dataMap)
.withIssuer("black jack") //JWT签发者
.withSubject("JWT令牌") //主题
.withAudience("member") //接收JWT的一方
.withExpiresAt(new Date(System.currentTimeMillis() + 3600000)) //过期时间
.withNotBefore(new Date(System.currentTimeMillis())) //指定时间之前JWT令牌是不可用的
.withIssuedAt(new Date()) //JWT签发时间
.withJWTId(UUID.randomUUID().toString().replace("-", "")) // jwt唯一标识
.sign(algorithm);
}
/***
* 解析令牌
* @param token
* @return
*/
public static Map<String, Object> parseToken(String token) {
return parseToken(token, null);
}
/***
* 令牌校验并解析
* @param token
* @return
*/
public static Map<String, Object> parseToken(String token, String secret) {
//秘钥为空就采用默认秘钥
if (StringUtils.isEmpty(secret)) {
secret = DEFAULT_SECRET;
}
//确认签名算法
Algorithm algorithm = Algorithm.HMAC256(secret);
//创建令牌校验对象
JWTVerifier verifier = JWT.require(algorithm).build(); //Reusable verifier instance
//校验解析
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaim("body").as(Map.class);
}
/***
* 令牌校验解析
* @param args
*/
public static void main(String[] args) throws Exception {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("name", "black jack");
dataMap.put("age", "26");
dataMap.put("address", "南京市");
//创建令牌
String token = createToken(dataMap);
System.out.println(token);
//休眠1秒
Thread.sleep(1000);
//解析令牌
Map<String, Object> resultMap = parseToken(token);
System.out.println(resultMap);
}
}
测试结果如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKV1Tku6TniYwiLCJhdWQiOiJtZW1iZXIiLCJuYmYiOjE2Mzk4MDcwNDUsImlzcyI6ImJsYWNrIGphY2siLCJib2R5Ijp7ImFkZHJlc3MiOiLljZfkuqzluIIiLCJuYW1lIjoiYmxhY2sgamFjayIsImFnZSI6IjI2In0sImV4cCI6MTYzOTgxMDY0NSwiaWF0IjoxNjM5ODA3MDQ1LCJqdGkiOiI0MjI2NjgwMTQzNmI0NTQ4OTc0YmUzZTFjZWM3NWYwOSJ9.IO6jV89x8y5sn8mQRE8SZLwwnWyrUiA_Jsl1mzCss2w
{address=南京市, name=black jack, age=26}
4. 实战
4.1 令牌颁发
该部分代码直接通过mybatis plus 框架生成相关代码:
整体流程就是用户登陆后,后台根据用户信息封装token返回给客户端。
- 用户实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "user_info")
public class UserInfo {
@TableId(type = IdType.ASSIGN_ID)
private String username;
private String password;
private String phone;
private String name;
private Integer points;
private String roles;
}
- Dao
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
- Service
public interface UserInfoService extends IService<UserInfo> {
}
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
}
- controller
@RestController
@RequestMapping(value = "/user/info")
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
/****
* 登录
*/
@PostMapping(value = "/login")
public RespResult<String> login(@RequestParam String username,@RequestParam String pwd){
//登录
UserInfo userInfo = userInfoService.getById(username);
if(userInfo!=null){
//匹配密码是否一致
if(userInfo.getPassword().equals(pwd)){
//封装用户信息实现加密
Map<String,Object> dataMap = new HashMap<String,Object>();
dataMap.put("username",userInfo.getUsername());
dataMap.put("name",userInfo.getName());
dataMap.put("roles",userInfo.getRoles());
//创建令牌
String token = JwtToken.createToken(dataMap);
return RespResult.ok(token);
}
//账号密码不匹配
return RespResult.error("账号或者密码错误");
}
return RespResult.error("账号不存在");
}
}
4.2 令牌安全
我们前面说过,令牌数据不安全,其实除了令牌数据不安全之外,令牌还存在被盗用的风险,例如:
1:张三登录获得令牌 zzz
2:李四盗取了张三的令牌 zzz,并用令牌zzz直接访问后台
上面例子执行,后台只要能识别令牌,是不会拒绝zzz令牌的,这时候就存在盗用风险。
令牌盗用该如何解决?
如果令牌被盗,我们可以通过IP识别令牌是否安全,如上图:
1:每次生成令牌的时候,把用户的IP作为令牌的一部分进行MD5加密,并将密文存入到令牌中
2:用户每次访问API接口的时候,都先获取客户端IP,再将IP进行MD5加密,并和令牌中的IP密文比对
3:如果密文一致,则证明IP没有发生变化,如果密文不一致,则证明IP发生变化,提示重新登录
这种操作在很多大厂中都有应用,平时登录QQ、微信的时候会提示设备终端发生变化,其实和上图操作是一个道理。
4.2.1 令牌封装
在登录接口中对IP地址进行封装:
@PostMapping(value = "/login")
public RespResult<String> login(@RequestParam String username, @RequestParam String pwd, HttpServletRequest request) throws Exception {
//登录
UserInfo userInfo = userInfoService.getById(username);
if (userInfo != null) {
//匹配密码是否一致
if (userInfo.getPassword().equals(pwd)) {
//封装用户信息实现加密
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("username", userInfo.getUsername());
dataMap.put("name", userInfo.getName());
dataMap.put("roles", userInfo.getRoles());
//获取IP
String ip = IPUtils.getIpAddr(request);
dataMap.put("ip", MD5.md5(ip));
//创建令牌
String token = JwtToken.createToken(dataMap);
return RespResult.ok(token);
}
//账号密码不匹配
return RespResult.error("账号或者密码错误");
}
return RespResult.error("账号不存在");
}
工具类如下:
public class IPUtils {
/**
* 获取用户真实IP地址,不使用request.getRemoteAddr()的原因是有可能用户使用了代理软件方式避免真实IP地址,
* 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
*
* @return ip
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
4.2.2 令牌安全校验
public class AuthorizationInterceptor {
/***
* 令牌解析
*/
public static Map<String, Object> jwtVerify(String token, String clientIp) {
try {
//token解析
Map<String, Object> resultMap = JwtToken.parseToken(token);
//令牌中的IP
String jwtip = resultMap.get("ip").toString();
//IP校验
clientIp = MD5.md5(clientIp);
if (clientIp.equals(jwtip)) {
return resultMap;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
如下是关键代码:
首先获取客户端IP地址,然后获取客户端传过来的token信息,并进行比较。
//校验其他地址
String clientIp = IPUtil.getIp(request);
//获取令牌
String token = request.getHeaders().getFirst("authorization");
//令牌校验
Map<String, Object> resultMap = AuthorizationInterceptor.jwtVerify(token, clientIp);
if (resultMap == null) {
endProcess(exchange, 401, "no token");
}








还没有评论,来说两句吧...