JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io
GitHub上jwt的java客户端:https://github.com/jwtk/jjwt
为了保证客户端cookie的安全性,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
即使使用redis保存用户的信息,也会损耗服务器资源。
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
带来的好处是什么呢?
无状态登录的流程:
token的安全性
token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。
采用何种方式加密才是安全可靠的呢?
我们将采用JWT + RSA非对称加密
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io
GitHub上jwt的java客户端:https://github.com/jwtk/jjwt
JWT包含三部分数据:
Header:头部,通常头部有两部分信息:
Payload:载荷,就是有效数据,一般包含下面信息:
这部分也会采用base64编码,得到第二部分数据
Signature:签名,是整个数据的认证信息。根据前两步的数据,再加上指定的密钥(secret)(不要泄漏,最好周期性更换),通过base64编码生成。用于验证整个数据完整和可靠性
因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。
加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
RSA算法历史:
1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA
本次示例采用SpringBoot工程搭建
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.16.18version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.10.6version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.10.6version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.10.6version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
<version>2.10.3version>
dependency>
如果需要使用JWT单点登录,RAS工具类和JWT工具类是必不可少的
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Map;
public class JwtUtils {
/**
* 私钥加密token
*
* @param map 载荷中的数据
* @param expireMinutes 过期时间,单位秒
* @return
* @throws Exception
*/
public static String generateToken(Map<String, Object> map, PrivateKey key, int expireMinutes) throws Exception {
return Jwts.builder()
.setClaims(map)
.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
.signWith(key, SignatureAlgorithm.RS256)
.compact();
}
/**
* 公钥解析token
*
* @param token 用户请求中的token
* @return
* @throws Exception
*/
private static Jws<Claims> parserToken(String token, PublicKey key) {
return Jwts.parser().setSigningKey(key).parseClaimsJws(token);
}
/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @return 用户信息
* @throws Exception
*/
public static Map<String, Object> getInfoFromToken(String token, PublicKey key) throws Exception {
Jws<Claims> claimsJws = parserToken(token, key);
return claimsJws.getBody();
}
}
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class RsaUtils {
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
public static PublicKey getPublicKey(byte[] bytes) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(2048, secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
writeFile(privateKeyFilename, privateKeyBytes);
}
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
公钥和私钥的区别,其实不用太过于纠结,我们可以理解为:
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = JwtDemoApplication.class)
public class JwtDemoApplicationTests {
// 公钥文件生成地址
private static final String PUB_KEY_PATH = "D:\\IT notes\\jwt\\tool\\rsa.pub";
// 私钥文件生成地址
private static final String PRI_KEY_PATH = "D:\\IT notes\\jwt\\tool\\rsa.pri";
private PublicKey publicKey;
private PrivateKey privateKey;
// 生成公钥和私钥
@Test
public void testRsa() throws Exception {
RsaUtils.generateKey(PUB_KEY_PATH, PRI_KEY_PATH, "234");
}
// 先生成 再获取 生成之前把@Before注释掉!
// 获取公钥和私钥
@Before
public void testGetRsa() throws Exception {
this.publicKey = RsaUtils.getPublicKey(PUB_KEY_PATH);
this.privateKey = RsaUtils.getPrivateKey(PRI_KEY_PATH);
}
// 生成token
@Test
public void testGenerateToken() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "11");
map.put("username", "liuyan");
// 生成token
String token = JwtUtils.generateToken(map, privateKey, 1);
System.out.println("token = " + token);
}
// 解析token
@Test
public void testParseToken() throws Exception {
String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjExIiwidXNlcm5hbWUiOiJsaXV5YW4iLCJleHAiOjE1ODc2MjYwODZ9.kO22BSWogDJPD6oG92dfFukothGMJej3FKLIfDJRVjGiF_O7kLPSmycmuyByy8wd7X_nOVDCPoMvvhoUzviDsgzIC0xiILoMobwyUDtbYdStCfiLVikqHmnf0Our5tuxwVaPOK2igoWW3zRRI7HG5RLh0p2pUAQe1C-is_8zczn2T5CQ-7vEwPS6U5FLn7_1y8rHNVsKlHqNBdSDxQn7jLOkHkKnRiShZ2_iBuXTzo6uZt2461IV8qk6Lmn35fyX7JHwHIVvuQyniFEsdYNW5t8P3Eo1UEbL3ZD5ZbhcIsK5gnvpXdsne6uK1jHQzClQi-hcGONuHXpS2IkueWEizg";
// 解析token
Map<String, Object> map = JwtUtils.getInfoFromToken(token, publicKey);
System.out.println("id: " + map.get("id"));
System.out.println("userName: " + map.get("username"));
}
}