JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
未加密前的jwt就是一个json
头部:头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象
{"typ":"JWT","alg":"HS256"}
然后用base64进行编码
载荷:载荷就是存放有效信息的地方。
{"sub":"1234567890","name":"John Doe","admin":true}
然后用base64进行编码
签证:jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
1 是头部经base64编码后的字符
2 是载荷经base64编码后的字符
3 是盐(密钥),通常存于服务器
将1和2用.连接,通过头部中声明的加密算法进行加盐(3)计算,得到第三部分
将头的base64和载荷的base64和签证三部分用.分割,得到的最终字符串就是jwt
java中使用jwt
引入坐标
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
编写测试类
public class JwtTest {
public static void main(String[] args) {
//获取系统的当前时间,以便设置过期时间
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
//生成jwt令牌
JwtBuilder jwtBuilder = Jwts.builder()
.setId("66")//设置jwt编码 这是载荷
.setSubject("黑马程序员")//设置jwt主题 这是载荷
.setIssuedAt(new Date())//设置jwt签发日期 这是载荷
.setExpiration(date)//设置jwt的过期时间 目前是创建就会过期
.claim("roles","admin") //自定义的信息 这是载荷 多条自定义信息就多写几个claim
.claim("company","itheima")//自定义的信息 这是载荷
.signWith(SignatureAlgorithm.HS256, "itheima");
//生成jwt令牌
String jwtToken = jwtBuilder.compact();
System.out.println(jwtToken);
//解析jwt令牌
Claims claims = Jwts.parser().setSigningKey("itheima")//签名必须和生成时的一样 签名是signwith方法参数
.parseClaimsJws(jwtToken).getBody();
System.out.println(claims);
}
}
在项目中的jwt
工具类
package com.changgou.system.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; /** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "itcast"; /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); SecretKey secretKey = generalKey(); JwtBuilder builder = Jwts.builder() .setId(id) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("admin") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate);// 设置过期时间 return builder.compact(); } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
用户提交时由服务器发放令牌,随后会再次提交用来验证
//用户登录 @PostMapping("/login") public Result login(@RequestBody Admin admin){ boolean result = adminService.login(admin); if(result){ //密码是正确的 //生成jwt令牌,返回到客户端 Map<String,String> info = new HashMap<>(); info.put("username",admin.getLoginName()); //基于工具类生成jwt令牌 String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null); info.put("token",jwt); return new Result(true,StatusCode.OK,"登录成功",info); } else { return new Result(false,StatusCode.ERROR,"登录失败",result); } }
服务器验证用户提交的令牌,使用拦截器,要实现两个接口
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //获取请求对象 ServerHttpRequest request = exchange.getRequest(); //获取响应对象 ServerHttpResponse response = exchange.getResponse(); //判断当前的请求是否为登录请求,如果是登录请求,直接放行 if(request.getURI().getPath().contains("/admin/login")){ //放行 return chain.filter(exchange); } //获取所有的请求头信息 HttpHeaders headers = request.getHeaders(); //获取jwt令牌信息 String jwtToken = headers.getFirst("token"); //判断当前令牌是否存在 if(StringUtils.isEmpty(jwtToken)){ //令牌为空或不存在,返回错误信息 response.setStatusCode(HttpStatus.UNAUTHORIZED);//返回一个状态,状态为未认证 return response.setComplete();//设置完成,类似与break } //如果当前令牌存在,解析令牌,判断是否合法,如果不合法,则向客户端返回错误提示 try { //解析令牌 通过密钥解析令牌,如果能正常解析,说明令牌是正确的 JwtUtil.parseJWT(jwtToken); }catch (Exception e){ e.printStackTrace(); //令牌解析失败 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //如果当前令牌合法,则放行 return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
生成私钥和公钥
RAS算法生成私钥和公钥,桌面新建一个jwt文件夹,在文件夹里打开cmd,执行
生成私钥
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
keytool是java提供的证书管理工具
-genkeypair 要生成密钥 -alias 证书的别名
-keyalg RSA 指定加密算法 -keypass密钥的访问密码
-keystore 生成的文件的名字 -storepass 密钥库的访问密码
生成过程中会询问一些信息,最后确定时输入 y
查看密钥,在生成的密钥文件所在的文件里,cmd
keytool -list -keystore changgou.jks
导出公钥
使用openssl导出公钥,安装好后要配置一下环境变量
然后在生成的密钥文件所在的文件里,重启cmd
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
即可看到公钥
复制,复制时要带着-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----,在私钥处新建一个public.key,用np++打开,粘贴,注意所有文字都要在同一行
借助私钥和公钥,使用代码完成jwt验证,非项目,非网络请求,仅为演示,如有疑问,查看畅购项目的changgou-user-oauth的test
私钥和公钥都在当前模块的resources下
引入坐标
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-data</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency>
编写测试类创建jwt,CreateJwtTest.class
public class CreateJwtTest { @Test public void createJWT(){ //基于私钥生成JWT //创建一个密钥工厂 //私钥的位置 本模块中resources中的changgou.jks就是私钥,public.key就是公钥 ClassPathResource classPathResource = new ClassPathResource("changgou.jks"); //密钥库的密码 String keyPass = "changgou"; /** * 参数1 私钥的位置 * 参数2 密钥库的密码 */ KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,keyPass.toCharArray()); //基于工厂获取私钥 //密钥的别名 String alias = "changgou"; //密钥的密码 String password ="changgou"; /** * 参数1 密钥的别名 * 参数2 密钥的密码 */ KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray()); //将当前的私钥转为RSA的私钥 RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); //生成jwt Map<String,String > map = new HashMap<>(); map.put("company","heima"); map.put("address","beijing"); /** * 参数1 当前的令牌的内容 * 参数2 签名(用RSA的私钥来做签名) */ Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey)); String jwtEncoded = jwt.getEncoded(); System.out.println(jwtEncoded); } }
编写测试类解析jwt,ParseJwtTest.class
public class ParseJwtTest { @Test public void parseJwt(){ //基于公钥解析jwt 这是直接复制的CreateJwtTest.class的打印结果 String jwt ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiYmVpamluZyIsImNvbXBhbnkiOiJoZWltYSJ9.cjZNz8G0m4noNYN2VM1SH3ujAtbHElW5Vtbadb0NDI0cjM1DaAXzMA53Qbj4pmVQPl_IfSKqUEXbLxowdRa5NHR43laFsR0kzGbJiTINfSVSroSslYpDdEVwCeAF_a7I-R819YTj4p6sjuYKXbzXpeZQErczFbWWWGR2_U44xH6u1ejRNv8PikFiuzNw-muL7zUJkvqeSJzbEMnQdZMbfvZp4LtSI6B4G_PqpdNXkv19-juxAh99VgJInH_ItF0y5IBOxofA7gRebCZmU8L57gO9ohf2L00D95kis_Ji8lmA1ptLIfXqO_qLVvLBUNH-VtgjGAF0-0pyB-5jlbHP7w"; //公钥,直接复制的Public.key里的公钥 String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----"; //解析和验签jwt,获取令牌 Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey)); //解析令牌,获取令牌中的载荷 String claims = token.getClaims(); System.out.println(claims);//打印结果为{"address":"beijing","company":"heima"} } }
1