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";@Testpublic void testRas() throws Exception {//生产公钥和私钥RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");}@Testpublic 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 将内容封装到对象JavaBeanT 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";@Testpublic 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);}@Testpublic 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);@PostConstructpublic 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配置类实例@Resourceprivate JwtProperties jwtProperties;@Resourceprivate FilterProperties filterProperties;@Overridepublic String filterType() {return FilterConstants.PRE_TYPE;}@Overridepublic int filterOrder() {return 5;}@Overridepublic boolean shouldFilter() { //03.当前过滤器是否执行,true执行,false不执行//3.1 获得用户请求路径// 3.1.1 获得上下文RequestContext currentContext = RequestContext.getCurrentContext();// 3.1.2 获得requestHttpServletRequest request = currentContext.getRequest();// 3.1.2 获得请求路径 , /v3/cgwebservice/loginString requestURI = request.getRequestURI();//3.2 如果路径是 /auth-service/login ,当前拦截不执行for (String path : filterProperties.getAllowPaths()) {//判断结尾if(requestURI.contains(path)){return false;}}//3.3 其他都执行 run()方法return true;}@Overridepublic 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 ); //响应的状态码:403currentContext.setResponseBody("token失效或无效");currentContext.setSendZuulResponse( false ); //没有响应内容}// 2.5 如果没有异常,登录了--放行return null;}
}
1.6 下游服务获得token
zuul默认配置
zuul:sensitive-headers: Cookie,Set-Cookie,Authorization