1. 无状态登录
1.1 微服务的状态
微服务集群中的每个服务,对外提供的都是Rest风格的接口,而Rest风格的一个最重要的规范就是:服务的无状态性。
什么是无状态?
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
无状态,在微服务开放中,优势是?
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
- 服务端的是否集群对客户端透明
- 服务端可以任意的迁移和伸缩
- 减小服务端存储压力
1.2 无状态登录实现原理
服务器端生产唯一标识(注意:最终需要进行校验)
方案1:UUID,数据单一,不能包含种类过多的信息。
方案2:JWT 生成唯一标识,数据可以自定义。【使用】
为了保证JWT生成数据安全性,采用RSA加密。
浏览器存储和自动携带数据
方案1:使用cookie,有很多局限性(大小,个数)。
方案2:请求参数,get请求URL有长度限制,每一个路径都需要处理比较麻烦。
方案3:浏览器localStorage/sessionStorage存储,通过ajax的请求头携带。【使用】
1.3 RSA加密
1.3.1 概述
RSA公开密钥密码体制是一种使用不同的加密密钥与解密密钥,“由已知加密密钥推导出解密密钥在计算上是不可逆的”密码体制。
在公开密钥密码体制中,加密密钥(即公开密钥)PK是公开信息,而解密密钥(即秘密密钥)SK是需要保密的。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的,但却不能根据PK计算出SK [2]。
RSA加密:非对称加密。
同时生产一对秘钥:公钥和私钥。
公钥秘钥:用于加密
私钥秘钥:用于解密
既然是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可得出公钥负责加密,私钥负责解密;同理,既然是签名,那肯定是不希望有人冒充我发消息,只有我才能发布这个签名,所以可得出私钥负责签名,公钥负责验证。
使用RSA加密保证token数据在传输过程中不会被篡改。
1.3.2 工具类
package com.czxy.utils;
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;
/**
* Created by liangtong.
*/
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 Exception
*/
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(1024, 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.getParentFile().exists()){
dest.getParentFile().mkdirs();
}
//创建需要的文件
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
1.3.3 生产公钥和私钥
package com.czxy.utils;
import org.junit.Test;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* Created by liangtong.
*/
public class TestRsa {
private static final String pubKeyPath = "D:\\rsa\\rsa.pub";
private static final String priKeyPath = "D:\\rsa\\rsa.pri";
@Test
public void testRas() throws Exception {
//生产公钥和私钥
RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");
}
@Test
public void testGetRas() throws Exception {
//获得公钥和私钥
PublicKey publicKey = RasUtils.getPublicKey(pubKeyPath);
PrivateKey privateKey = RasUtils.getPrivateKey(priKeyPath);
System.out.println(publicKey.toString());
System.out.println(privateKey.toString());
}
1.4 JWT
1.4.1 概述
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io
1.4.2 添加坐标
<properties>
<jwt.jjwt.version>0.9.0</jwt.jjwt.version>
<jwt.joda.version>2.9.7</jwt.joda.version>
<lombok.version>1.16.20</lombok.version>
<beanutils.version>1.9.3</beanutils.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${beanutils.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.jjwt.version}</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${jwt.joda.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
1.4.3 时间处理工具:DateTime
//当前时间
DateTime.now().toDate().toLocaleString()
//当前时间加5分钟
DateTime.now().plusMinutes(5).toDate().toLocaleString()
//当前时间减5分钟
DateTime.now().minusMinutes(5).toDate().toLocaleString()
1.4.4 测试JWT
生成Token
@Test
public void testGenerateToken() throws Exception {
String str = Jwts.builder()
.claim("test","测试数据")
.setExpiration(DateTime.now().plusMinutes(60).toDate())
.signWith(SignatureAlgorithm.RS256,RsaUtils.getPrivateKey(priKeyPath))
.compact();
System.out.println(str);
}
解析Token
@Test
public void testParseToken() throws Exception {
String token = "";
Claims claims = Jwts.parser().setSigningKey(RsaUtils.getPublicKey(pubKeyPath)).
parseClaimsJws(token).getBody();
String text = claims.get("test",String.class);
System.out.println(text);
}
1.4.5 工具类:JwtUtils
package com.czxy.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.beanutils.BeanUtils;
import org.joda.time.DateTime;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* Created by liangtong.
*/
public class JwtUtils {
/**
* 私钥加密token
* @param data 需要加密的数据(载荷内容)
* @param expireMinutes 过期时间,单位:分钟
* @param privateKey 私钥
* @return
*/
public static String generateToken(Object data, int expireMinutes, PrivateKey privateKey) throws Exception {
//1 获得jwt构建对象
JwtBuilder jwtBuilder = Jwts.builder();
//2 设置数据
if( data == null ) {
throw new RuntimeException("数据不能为空");
}
BeanInfo beanInfo = Introspector.getBeanInfo(data.getClass());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
// 获得属性名
String name = propertyDescriptor.getName();
// 获得属性值
Object value = propertyDescriptor.getReadMethod().invoke(data);
if(value != null) {
jwtBuilder.claim(name,value);
}
}
//3 设置过期时间
jwtBuilder.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate());
//4 设置加密
jwtBuilder.signWith(SignatureAlgorithm.RS256, privateKey);
//5 构建
return jwtBuilder.compact();
}
/**
* 通过公钥解析token
* @param token 需要解析的数据
* @param publicKey 公钥
* @param beanClass 封装的JavaBean
* @return
* @throws Exception
*/
public static <T> T getObjectFromToken(String token, PublicKey publicKey,Class<T> beanClass) throws Exception {
//1 获得解析后内容
Claims body = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token).getBody();
//2 将内容封装到对象JavaBean
T bean = beanClass.newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
// 获得属性名
String name = propertyDescriptor.getName();
// 通过属性名,获得对应解析的数据
Object value = body.get(name);
if(value != null) {
// 将获得的数据封装到对应的JavaBean中
BeanUtils.setProperty(bean,name,value);
}
}
return bean;
}
}
1.4.6 生产token和校验token
编写测试对象UserInfo
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private Long id;
private String username;
}
编写测试类
package com.czxy.utils;
import com.czxy.entity.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import org.junit.Test;
/**
* Created by liangtong.
*/
public class TestJWT {
private static final String pubKeyPath = "D:\\rsa\\rsa.pub";
private static final String priKeyPath = "D:\\rsa\\rsa.pri";
@Test
public void testToken() throws Exception {
UserInfo userInfo = new UserInfo();
userInfo.setId(10L);
userInfo.setUsername("用户名");
String token = JwtUtils.generateToken(userInfo, 30, RsaUtils.getPrivateKey(priKeyPath));
System.out.println(token);
}
@Test
public void testParserToken() throws Exception {
String token = "eyJhbGciOiJSUzI1NiJ9.eyJjbGFzcyI6ImNvbS5jenh5LmVudGl0eS5Vc2VySW5mbyIsImlkIjoxMCwidXNlcm5hbWUiOiLnlKjmiLflkI0iLCJleHAiOjE1NzU5MTYyMDl9.W3Q3Iz1vGq1nf7RQW3eAzkMvkME9P5_5zoDcFQXX0eke07lA2PLuZGCYcB6-DI0i7UrahFOmB0OFQodrK_3CZkrh-sI_802twkGatRaI0ifetRLV_1XHVl_Lymh6SaDdBB1OT3-EQCAppjoHFb9Tyq1EGyQZ5xoU-vLp7fzNQLQ";
UserInfo userInfo = JwtUtils.getObjectFromToken(token, RsaUtils.getPublicKey(pubKeyPath), UserInfo.class);
System.out.println(userInfo);
}
}
1.4.7 扩展:JWT token组成
JWT的token包含三部分数据:头部、载荷、签名。
名称 | 描述 | 组成部分 |
---|---|---|
头部(Header) | 通常头部有两部分信息 | 1. 声明类型,这里是JW2. 加密算法,自定义 |
载荷(Payload) | 就是有效数据 | 1. 用户身份信息2. 注册声明 |
签名(Signature) | 整个数据的认证信息 | 一般根据前两步的数据,再加上服务的的密钥(secret),通过加密算法生成。用于验证整个数据完整和可靠性 |
生成的数据格式:
1.5 Zuul整合JWT
1.5.1 自定义配置内容
修改application.yml 添加内容
#自定义内容
sc:
jwt:
secret: sc@Login(Auth}*^31)&czxy% # 登录校验的密钥(自定义内容)
pubKeyPath: D:/rsa/rsa.pub # 公钥地址
priKeyPath: D:/rsa/rsa.pri # 私钥地址
expire: 360 # 过期时间,单位分钟
filter:
allowPaths:
- /checkusername
- /checkmobile
- /sms
- /register
- /login
- /verifycode
- /categorys
- /news
- /brands
- /sku/esData
- /specifications
- /search
- /goods
- /comments
- /pay/callback
1.5.2 JWT配置类
package com.czxy.changgou3.config;
import com.czxy.utils.RasUtils;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* Created by liangtong.
*/
@Data
@ConfigurationProperties(prefix = "sc.jwt")
@Component
public class JwtProperties {
private String secret; // 密钥
private String pubKeyPath;// 公钥
private String priKeyPath;// 私钥
private int expire;// token过期时间
private PublicKey publicKey; // 公钥
private PrivateKey privateKey; // 私钥
private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
@PostConstruct
public void init(){
try {
File pubFile = new File(this.pubKeyPath);
File priFile = new File(this.priKeyPath);
if( !pubFile.exists() || !priFile.exists()){
RasUtils.generateKey( this.pubKeyPath ,this.priKeyPath , this.secret);
}
this.publicKey = RasUtils.getPublicKey( this.pubKeyPath );
this.privateKey = RasUtils.getPrivateKey( this.priKeyPath );
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.5.3 过滤路径配置类
package com.czxy.changgou3.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* Created by liangtong.
*/
@Data
@ConfigurationProperties(prefix="sc.filter")
public class FilterProperties {
//允许访问路径集合
private List<String> allowPaths;
}
1.5.4 过滤器
package com.czxy.changgou3.filter;
import com.czxy.changgou3.config.FilterProperties;
import com.czxy.changgou3.config.JwtProperties;
import com.czxy.changgou3.pojo.User;
import com.czxy.utils.JwtUtils;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* Created by liangtong.
*/
@Component
//2.1 加载JWT配置类
@EnableConfigurationProperties({JwtProperties.class , FilterProperties.class} )
public class LoginFilter extends ZuulFilter {
//2.2 注入jwt配置类实例
@Resource
private JwtProperties jwtProperties;
@Resource
private FilterProperties filterProperties;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 5;
}
@Override
public boolean shouldFilter() { //03.当前过滤器是否执行,true执行,false不执行
//3.1 获得用户请求路径
// 3.1.1 获得上下文
RequestContext currentContext = RequestContext.getCurrentContext();
// 3.1.2 获得request
HttpServletRequest request = currentContext.getRequest();
// 3.1.2 获得请求路径 , /v3/cgwebservice/login
String requestURI = request.getRequestURI();
//3.2 如果路径是 /auth-service/login ,当前拦截不执行
for (String path : filterProperties.getAllowPaths()) {
//判断结尾
if(requestURI.contains(path)){
return false;
}
}
//3.3 其他都执行 run()方法
return true;
}
@Override
public Object run() throws ZuulException {
//1 获得token
//1.1 获得上下文
RequestContext currentContext = RequestContext.getCurrentContext();
//1.2 获得request对象
HttpServletRequest request = currentContext.getRequest();
//1.3 获得指定请求头的值
String token = request.getHeader("Authorization");
//2 校验token -- 使用JWT工具类进行解析
// 2.3 使用工具类,通过公钥获得对应信息
try {
JwtUtils.getObjectFromToken(token , jwtProperties.getPublicKey() , User.class);
} catch (Exception e) {
// 2.4 如果有异常--没有登录(没有权限)
currentContext.addOriginResponseHeader("content-type","text/html;charset=UTF-8");
currentContext.addZuulResponseHeader("content-type","text/html;charset=UTF-8");
currentContext.setResponseStatusCode( 403 ); //响应的状态码:403
currentContext.setResponseBody("token失效或无效");
currentContext.setSendZuulResponse( false ); //没有响应内容
}
// 2.5 如果没有异常,登录了--放行
return null;
}
}
1.6 下游服务获得token
zuul默认配置
zuul:
sensitive-headers: Cookie,Set-Cookie,Authorization