常见加密方式及JWT学习

1 常见的加密方式回顾

由于在学习JWT的时候会涉及使用很多加密算法, 所以在这里做下扫盲, 简单了解就可以

加密算法种类有:

1.1.可逆加密算法

解释: 加密后, 密文可以反向解密得到密码原文.

1.1.1. 对称加密

文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥

1599808731133

解释: 在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊的加密算法处理后,使其变成复杂的加密密文发送出去,收信方收到密文后,若想解读出原文,则需要使用加密时用的密钥以及相同加密算法的逆算法对密文进行解密,才能使其回复成可读明文。在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。

优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。

缺点: 没有非对称加密安全.

用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。

常见的对称加密算法有: AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256

1.1.2. 非对称加密

两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密

1599809661084

解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端.

加密与解密:

  • 私钥加密,持有公钥才可以解密
  • 公钥加密,持有私钥才可解密

签名:

  • 私钥签名, 持有公钥进行验证是否被篡改过.

优点: 非对称加密与对称加密相比,其安全性更好;

缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。

用途: 一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.

常见的非对称加密算法有: RSA、DSA(数字签名用)、ECC(移动设备用)、RS256 (采用SHA-256 的 RSA 签名)

1.2.不可逆加密算法

解释: 一旦加密就不能反向解密得到密码原文.

种类: Hash加密算法, 散列算法, 摘要算法等

用途:一般用于校验下载文件正确性,一般在网站上下载文件都能见到;存储用户敏感信息,如密码、 卡号等不可解密的信息。

常见的不可逆加密算法有: MD5、SHA、HMAC

1.3.Base64编码

Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一。Base64编码可用于在HTTP环境下传递较长的标识信息。采用Base64Base64编码解码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。注意:Base64只是一种编码方式,不算加密方法。

在线编码工具:

https://www.bejson.com

1.4 MD5密码加密

1583919949361

1
2
3
//md5加密  DegestUtils:spring框架提供的工具类  
String md5Str = DigestUtils.md5DigestAsHex("abc".getBytes());
System.out.println(md5Str);//900150983cd24fb0d6963f7d28e17f72

md5相同的密码每次加密都一样,不太安全

1.5 混淆加密(md5+随机字符串)

在md5的基础上手动加盐(salt)处理

hello + 314h1u2h3uh1 ad_user salt(314h1u2h3uh1 )

aa123214ji314h1u2h3uh1

1
2
3
4
5
6
7
8
//uername:zhangsan  password:123   salt:随时字符串
String salt = RandomStringUtils.randomAlphanumeric(10);//获取一个10位的随机字符串


System.out.println(salt); // 124ewfdsfdsd
String pswd = "123"+salt;
String saltPswd = DigestUtils.md5DigestAsHex(pswd.getBytes());
System.out.println(saltPswd);

这样同样的密码,加密多次值是不相同的,因为加入了随机字符串

2 登录认证jwt介绍

2.1 token认证

随着 Restful API、微服务的兴起,基于 Token 的认证现在已经越来越普遍。基于token的用户认证是一种服务端无状态的认证方式,所谓服务端无状态指的token本身包含登录用户所有的相关数据,而客户端在认证后的每次请求都会携带token,因此服务器端无需存放token数据。

当用户认证后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。

2.2 什么是JWT?

我们现在了解了基于token认证的交互机制,但令牌里面究竟是什么内容?什么格式呢?市面上基于token的认证方式大都采用的是JWT(Json Web Token)。

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。

JWT令牌结构:

JWT令牌由Header、Payload、Signature三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

  • Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC、SHA256或RSA)。

一个例子:

1
2
3
4
{
"alg": "HS256"
"typ": "JWT"
}

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

  • Payload

第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
一个例子:

1
2
3
4
5
6
{
"sub": "1234567890"
"name": "456"
"admin": true,
"id": 123
}

最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

  • Signature

第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明
签名算法进行签名。
一个例子:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。

下图中包含一个生成的jwt令牌:

1581919573856

2.3 生成token

需要引入jwt相关依赖

1
2
3
4
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.heima.utils.common;

import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

public class AppJwtUtil {
// TOKEN的有效期一天(S)
private static final int TOKEN_TIME_OUT = 3_600;
// 加密KEY
private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
// 最小刷新间隔(S)
private static final int REFRESH_TIME = 300;
// 生产ID
public static String getToken(Long id){
Map<String, Object> claimMaps = new HashMap<>();
claimMaps.put("id",id);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(currentTime)) //签发时间
.setSubject("system") //说明
.setIssuer("heima") //签发者信息
.setAudience("app") //接收用户
.compressWith(CompressionCodecs.GZIP) //数据压缩方式
.signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
.addClaims(claimMaps) //cla信息
.compact();
}
/**
* 获取token中的claims信息
* @param token
* @return
*/
private static Jws<Claims> getJws(String token) {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(token);
}
/**
* 获取payload body信息
* @param token
* @return
*/
public static Claims getClaimsBody(String token) {
try {
return getJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
}
/**
* 获取hearder body信息
* @param token
* @return
*/
public static JwsHeader getHeaderBody(String token) {
return getJws(token).getHeader();
}
/**
* 是否过期
* @param claims
* @return -1:有效,0:有效,1:过期,2:过期
*/
public static int verifyToken(Claims claims) {
if(claims==null){
return 1;
}
try {
claims.getExpiration()
.before(new Date());
// 需要自动刷新TOKEN
if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
return -1;
}else {
return 0;
}
} catch (ExpiredJwtException ex) {
return 1;
}catch (Exception e){
return 2;
}
}
/**
* 由字符串生成加密key
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
}

测试token生成与解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
String token = AppJwtUtil.getToken(1L);
System.out.println(token); // 生成token
try {
Claims claimsBody = getClaimsBody(token); // 解析token 载荷信息
int i = verifyToken(claimsBody); // 校验是否过期
if(i<1){ // -1:有效,0:有效,1:过期,2:过期
Object id = claimsBody.get("id"); // 获取载荷中存储的用户id
System.out.println("解析token成功 ==> 用户的id值 == "+ id);
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("解析token失败");
}
}

3 admin端-登录实现

思路分析:

  1. 检查用户是否存在

  2. 检查密码是否正确

  3. 检查用户状态是否有效

  4. 修改最近登录时间

  5. 颁发token

3.1 接口定义

接口地址:/login/in

请求方式:POST

请求数据类型:application/json

响应数据类型:application/json

接口描述: Admin端登录接口

请求示例:

1
2
3
4
{
"name": "",
"password": ""
}

请求参数:

参数名称 参数说明 in 是否必须 数据类型 schema
dto dto body true AdUserDto AdUserDto
  name 用户名 true string
  password 密码 true string

响应结果:

1
2
3
4
5
6
7
8
9
10
{
"code":"状态码",
"errorMessage":"提示信息",
"data": {
"token":"颁发访问凭证",
"user": { // 登录用户信息

}
}
}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Service
public class AdUserServiceImpl extends ServiceImpl<AdUserMapper, AdUser> implements AdUserService {
/**
* admin 登录
* @param dto
* @return
*/
@Override
public ResponseResult login(AdUserDTO dto) {
//1 参数校验
if (StringUtils.isBlank(dto.getName()) || StringUtils.isBlank(dto.getPassword())) {
CustException.cust(AppHttpCodeEnum.PARAM_INVALID,"参数错误");
}
//2 根据用户名查询用户信息
AdUser adUser = getOne(Wrappers.<AdUser>lambdaQuery()
.eq(AdUser::getName, dto.getName() )
);
if (adUser == null) {
CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST,"用户名或密码错误");
}
if(9 != adUser.getStatus().intValue()){
CustException.cust(AppHttpCodeEnum.LOGIN_STATUS_ERROR,"用户状态异常,请联系管理员");
}
//3 获取数据库密码和盐, 匹配密码
String dbPwd = adUser.getPassword(); // 数据库密码(加密)
String salt = adUser.getSalt();
// 用户输入密码(加密后)
String newPwd = DigestUtils.md5DigestAsHex((dto.getPassword() + salt).getBytes());
if (!dbPwd.equals(newPwd)) {
CustException.cust(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR,"用户名或密码错误");
}
//4 修改登录时间
adUser.setLoginTime(new Date());
updateById(adUser);
//5 颁发token jwt 令牌
String token = AppJwtUtil.getToken(adUser.getId().longValue());
// 用户信息返回 VO
AdUserVO userVO = new AdUserVO();
BeanUtils.copyProperties(adUser, userVO);
//6 返回结果(jwt)
Map map = new HashMap();
map.put("token", token);
map.put("user", userVO);
return ResponseResult.okResult(map);
}
}

封装vo对象,返回登录用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.heima.model.admin.vo;

import lombok.Data;
import java.util.Date;

@Data
public class AdUserVO {
private Integer id;
private String name;
private String nickname;
private String image;
private String email;
private Date loginTime;
private Date createdTime;
}

3.4 控制层代码

1
2
3
4
5
6
7
8
9
10
11
12
@Api(value = "运营平台登录API",tags = "运营平台登录API")
@RestController
@RequestMapping("/login")
public class LoginController{
@Autowired
AdUserService userService;
@ApiOperation("登录")
@PostMapping("/in")
public ResponseResult login(@RequestBody AdUserDTO DTO) {
return userService.login(DTO);
}
}

3.5 测试

在表中创建一个用户guest,使用以下代码生成密码后修改表中的密码

1
2
3
4
5
String salt = "123456";
String pswd = "guest"+salt;
String saltPswd = DigestUtils.md5DigestAsHex(pswd.getBytes());
System.out.println(saltPswd);
//34e20b52f5bd120db806e57e27f47ed0

生成密码后的结果为:

salt:123456

password: 34e20b52f5bd120db806e57e27f47ed0

username:guest

1596092694393

接口工具测试,或者页面直接登录测试

注意:

当前ad_user表的name并没有添加唯一索引,那么就有可能出现相同用户名的脏数据

解决添加唯一索引:

image-20210522214333752

其它字段调整

将id设置为自增字段

nickname字段过短,修改为长度36

image-20210522214602173

4 网关校验jwt

4.1 全局过滤器实现jwt校验

思路分析:

  1. 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
  2. 用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
  3. 用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
  4. 网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误

网关微服务中新建全局过滤器:

第一步,准备工具类

把heima-leadnews-utils模块中的AppJwtUtil类拷贝到网关模块下,如下图:

image-20210409211233730

第二步,编写全局过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.heima.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.heima.gateway.util.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.*;
/**
* @Description: 认证过滤器
* @Version: V1.0
*/
@Component
@Slf4j
@Order(0) // 值越小越优先执行
public class AuthorizeFilter implements GlobalFilter {
private static List<String> urlList = new ArrayList<>();
// 初始化白名单 url路径
static {
urlList.add("/login/in");
urlList.add("/v2/api-docs");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1 判断当前是否是登录请求,如果是登录则放行
ServerHttpRequest request = exchange.getRequest();
String reqUrl = request.getURI().getPath();
for (String url : urlList) {
if (reqUrl.contains(url)) {
return chain.filter(exchange);
}
}
//2 获取请求头jwt token信息
String jwtToken = request.getHeaders().getFirst("token");
if(StringUtils.isBlank(jwtToken)){
//如果不存在,向客户端返回错误提示信息
return writeMessage(exchange, "需要登录");
}
//3 判断令牌信息是否正确
try {
Claims claims = AppJwtUtil.getClaimsBody(jwtToken);
// -1:有效,0:有效,1:过期,2:过期
int verifyToken = AppJwtUtil.verifyToken(claims);
//3.1 如果不存在或失效,则拦截
if (verifyToken > 0) {
return writeMessage(exchange, "认证失效,请重新登录");
}
//3.2 解析JWT令牌信息
Integer id = claims.get("id", Integer.class);
log.info("token网关校验成功 id:{}, URL:{}", id, request.getURI().getPath());
//***4 将令牌信息传递到对应的微服务
request.mutate().header("userId", String.valueOf(id));
//5 返回结果
return chain.filter(exchange);
} catch (Exception e) {
log.error("token 校验失败 :{}", e);
return writeMessage(exchange, "认证失效,请重新登录");
}
}
/**
* 返回错误提示信息
* @return
*/
private Mono<Void> writeMessage(ServerWebExchange exchange, String message) {
Map<String, Object> map = new HashMap<>();
map.put("code", HttpStatus.UNAUTHORIZED.value());
map.put("errorMessage", message);
//获取响应对象
ServerHttpResponse response = exchange.getResponse();
//设置状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//response.setStatusCode(HttpStatus.OK);
//设置返回类型
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
//设置返回数据
DataBuffer buffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
//响应数据回浏览器
return response.writeWith(Flux.just(buffer));
}
}

上面api中语法大家可能会感觉陌生,这是因为Gateway 采用的是基于webFlux异步非阻塞的网络处理框架,api的设计和传统SpringMVC的api大不相同, WebFlux技术比较新,目前使用还没有那么普及 所以上面代码不要求掌握, 作用理解即可