JWT + zuul
JWT 生成磁盘公钥秘钥文件
(代码有点多,直接复制)
JwtUtils
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;
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;
}
}
RasUtils
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 RasUtils {
/**
* 从文件中读取公钥
*
* @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);
}
}
TestRas测试生成
import org.junit.Test;
public class TestRas {
private static final String pubKeyPath = "D:\\ras\\ras.pub";
private static final String priKeyPath = "D:\\ras\\ras.pri";
@Test
public void testGen() throws Exception {
//生产公钥和私钥
RasUtils.generateKey(pubKeyPath,priKeyPath,"234");
}
}
成功结果
jwt 可以自己用,进行用户名密码判断。(尽量和cloud的zuul一起用,zuul可以拦截所有请求,对请求进行操作,是否进服务。)
可以和SpringCloud的zuul进行用,由网关进行判断前端的token是否存在或者成功与否
Zuul 网关
加入网关后访问服务的路径
http://localhost:网关端口号/网关前缀/对应的服务名/路径
http://localhost:9090/v3/loginservice/login
简介
通过前面的学习,使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fszzfF0Z-1598846307581)(C:\Users\20297\Desktop\播客创建\创建数据\(未完成)cloud中的网关,feign,集群,熔断,负载均衡\z1.gif)]
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;
而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;
通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。
为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,这样的实现是否合理,或者是否有更好的实现方式呢?
先来说说这样架构需要做的一些事儿以及存在的不足:
• 首先,破坏了服务无状态特点。
– 为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。
– 从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外考虑对接口访问的控制处理。
• 其次,无法直接复用既有接口。
– 当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
面对类似上面的问题,我们要如何解决呢?答案是:服务网关!
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器的 服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
Zuul:维基百科:
电影《捉鬼敢死队》中的怪兽,Zuul,在纽约引发了巨大骚乱。
事实上,在微服务架构中,Zuul就是守门的大Boss!一夫当关,万夫莫开!
Zuul加入后的架构
• 不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
pom
基础,整合的是时候在添加pom文件
<!--网关zuul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
application.yml 配置文件
#端口号
server:
port: 10010
#服务名
spring:
application:
name: cgzuul
servlet:
multipart:
max-file-size: 2MB #上传文件的大小
#将网关服务添加到注册中心
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
#网关统一配置 -- 访问路径 http://localhost:10010/v3/服务名/路径
zuul:
prefix: /v3 #网关前缀
sensitive-headers: Cookie,Set-Cookie
ribbon:
ReadTimeout: 120000 #请求处理的超时时间
ConnectTimeout: 30000 #请求连接的超时时间
#自定义内容
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
- /specifications
- /search
- /goods
- /comments
- /sku/esData
- /carts
- /pay/callback
- /pay
CGZuulApplication 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* @author The Phoenix(不死人)@itcast.cn
* @version 1.0
* @date 2020/3/20
*/
@SpringBootApplication
@EnableDiscoveryClient //注册中心客户端(通用写法), 等效 @EnableEurekaClient
@EnableZuulProxy //zuul配置
public class CGZuulApplication {
public static void main(String[] args) {
SpringApplication.run( CGZuulApplication.class ,args );
}
}
FilterProperties
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Data
@ConfigurationProperties(prefix="sc.filter")
public class FilterProperties {
//允许访问路径集合
private List<String> allowPaths;
}
GlobalCorsConfig 全局跨域配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/** 全局跨域配置类
* Created by liangtong.
*/
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//放行哪些原始域
config.addAllowedOrigin("*");
//是否发送Cookie信息
config.setAllowCredentials(true);
//放行哪些原始域(请求方式)
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET"); //get
config.addAllowedMethod("PUT"); //put
config.addAllowedMethod("POST"); //post
config.addAllowedMethod("DELETE"); //delete
config.addAllowedMethod("PATCH");
config.addAllowedHeader("*");
//2.添加映射路径
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
JwtProperties
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;
/**
* @author The Phoenix(不死人)@itcast.cn
* @version 1.0
* @date 2020/3/27
*/
@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();
}
}
}
LoginFilter 网关的灵魂 拦截器
拦截所有请求,继承ZuulFilter 整合 JWT
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;
@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执行(进run方法),false不执行(根据请求路径进对应的服务)
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String requestURI = request.getRequestURI();
// 从FilterProperties类中 获取 application.yml 文件中的白名单(就是不过网关的路径)
for (String allowPath : filterProperties.getAllowPaths()) {
// 请求路径包含集合中的就是白名单路径,不过zuul过滤器
if (requestURI.contains(allowPath)){
return false;
}
}
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 {
//用户名密码
User fromToken = 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;
}
}
JWT token生成
上面说token校验,这里说token生成
主要介绍zuul所以JWT介绍的比较少
这里边的方法不做过多介绍,思路一样,详细请看JWT详解
里边的代码和下边(相同功能的代码)的不一样,但是思路一样。
@PostMapping("/login")
public BaseResult login(@RequestBody User user ){
System.out.println("用户登录:"+user);
//校验验证码--使用后删除
String redisCode = stringRedisTemplate.opsForValue().get( "login" + user.getUsername() );
stringRedisTemplate.delete( "login" + user.getUsername() );
if(redisCode == null) {
return BaseResult.error("验证码无效");
}
if(! redisCode.equalsIgnoreCase(user.getCode())) {
return BaseResult.error("验证码错误");
}
//登录
User loginUser = authService.login(user);
if(loginUser != null ) {
System.out.println("登录成功");
//生成Token
String token = JwtUtils.generateToken(loginUser, jwtProperties.getExpire(), jwtProperties.getPrivateKey());
return BaseResult.ok("登录成功").append("loginUser",loginUser).append("token",token);
} else {
return BaseResult.error("用户名或密码不匹配");
}
}