常见加密方式及JWT学习
1 常见的加密方式回顾
由于在学习JWT的时候会涉及使用很多加密算法, 所以在这里做下扫盲, 简单了解就可以
加密算法种类有:
1.1.可逆加密算法
解释: 加密后, 密文可以反向解密得到密码原文.
1.1.1. 对称加密
【文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥】

解释: 在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊的加密算法处理后,使其变成复杂的加密密文发送出去,收信方收到密文后,若想解读出原文,则需要使用加密时用的密钥以及相同加密算法的逆算法对密文进行解密,才能使其回复成可读明文。在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。
优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
缺点: 没有非对称加密安全.
用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。
常见的对称加密算法有: AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256
1.1.2. 非对称加密
【两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密】

解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端.
加密与解密:
- 私钥加密,持有公钥才可以解密
- 公钥加密,持有私钥才可解密
签名:
优点: 非对称加密与对称加密相比,其安全性更好;
缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
用途: 一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.
常见的非对称加密算法有: 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密码加密

1 2 3
| String md5Str = DigestUtils.md5DigestAsHex("abc".getBytes()); System.out.println(md5Str);
|
md5相同的密码每次加密都一样,不太安全
1.5 混淆加密(md5+随机字符串)
在md5的基础上手动加盐(salt)处理
hello + 314h1u2h3uh1 ad_user salt(314h1u2h3uh1 )
aa123214ji314h1u2h3uh1
1 2 3 4 5 6 7 8
| String salt = RandomStringUtils.randomAlphanumeric(10);
System.out.println(salt); 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
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC、SHA256或RSA)。
一个例子:
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
一个例子:
1 2 3 4 5 6
| { "sub": "1234567890", "name": "456", "admin": true, "id": 123 }
|
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明
签名算法进行签名。
一个例子:
1 2 3 4
| HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
|
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
下图中包含一个生成的jwt令牌:

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 { private static final int TOKEN_TIME_OUT = 3_600; private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY"; private static final int REFRESH_TIME = 300; 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) .compact(); }
private static Jws<Claims> getJws(String token) { return Jwts.parser() .setSigningKey(generalKey()) .parseClaimsJws(token); }
public static Claims getClaimsBody(String token) { try { return getJws(token).getBody(); }catch (ExpiredJwtException e){ return null; } }
public static JwsHeader getHeaderBody(String token) { return getJws(token).getHeader(); }
public static int verifyToken(Claims claims) { if(claims==null){ return 1; } try { claims.getExpiration() .before(new Date()); if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){ return -1; }else { return 0; } } catch (ExpiredJwtException ex) { return 1; }catch (Exception e){ return 2; } }
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); try { Claims claimsBody = getClaimsBody(token); int i = verifyToken(claimsBody); if(i<1){ Object id = claimsBody.get("id"); System.out.println("解析token成功 ==> 用户的id值 == "+ id); } } catch (Exception e) { e.printStackTrace(); System.out.println("解析token失败"); } }
|
3 admin端-登录实现
思路分析:
检查用户是否存在
检查密码是否正确
检查用户状态是否有效
修改最近登录时间
颁发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 {
@Override public ResponseResult login(AdUserDTO dto) { if (StringUtils.isBlank(dto.getName()) || StringUtils.isBlank(dto.getPassword())) { CustException.cust(AppHttpCodeEnum.PARAM_INVALID,"参数错误"); } 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,"用户状态异常,请联系管理员"); } 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,"用户名或密码错误"); } adUser.setLoginTime(new Date()); updateById(adUser); String token = AppJwtUtil.getToken(adUser.getId().longValue()); AdUserVO userVO = new AdUserVO(); BeanUtils.copyProperties(adUser, userVO); 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);
|
生成密码后的结果为:
salt:123456
password: 34e20b52f5bd120db806e57e27f47ed0
username:guest

接口工具测试,或者页面直接登录测试
注意:
当前ad_user表的name并没有添加唯一索引,那么就有可能出现相同用户名的脏数据
解决添加唯一索引:

其它字段调整
将id设置为自增字段
nickname字段过短,修改为长度36

4 网关校验jwt
4.1 全局过滤器实现jwt校验

思路分析:
- 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
- 用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
- 用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
- 网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误
在网关微服务中新建全局过滤器:
第一步,准备工具类
把heima-leadnews-utils模块中的AppJwtUtil类拷贝到网关模块下,如下图:

第二步,编写全局过滤器
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.*;
@Component @Slf4j @Order(0) public class AuthorizeFilter implements GlobalFilter { private static List<String> urlList = new ArrayList<>(); static { urlList.add("/login/in"); urlList.add("/v2/api-docs"); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String reqUrl = request.getURI().getPath(); for (String url : urlList) { if (reqUrl.contains(url)) { return chain.filter(exchange); } } String jwtToken = request.getHeaders().getFirst("token"); if(StringUtils.isBlank(jwtToken)){ return writeMessage(exchange, "需要登录"); } try { Claims claims = AppJwtUtil.getClaimsBody(jwtToken); int verifyToken = AppJwtUtil.verifyToken(claims); if (verifyToken > 0) { return writeMessage(exchange, "认证失效,请重新登录"); } Integer id = claims.get("id", Integer.class); log.info("token网关校验成功 id:{}, URL:{}", id, request.getURI().getPath()); request.mutate().header("userId", String.valueOf(id)); return chain.filter(exchange); } catch (Exception e) { log.error("token 校验失败 :{}", e); return writeMessage(exchange, "认证失效,请重新登录"); } }
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.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技术比较新,目前使用还没有那么普及 所以上面代码不要求掌握, 作用理解即可