转载请注明出处:https://blog.csdn.net/weixin_41133233/article/details/85070323
本文出自 程熙cjp 的博客
继上一篇讲述完Zuul的详解和实例之后,本篇小熙将会讲述Zuul的实例优化动态配置、jwt、rsa。
一. 案例代码
-
yml添加数据,便于修改和动态获取
sc: jwt: secret: sc@Login(Auth}*^31)&chengxi% # 登录校验的密钥 pubKeyPath: D:/rsa/rsa.pub # 公钥地址 priKeyPath: D:/rsa/rsa.pri # 私钥地址 expire: 30 # 过期时间,单位分钟
-
Config的LoginFilter类修改之后的
package com.chengxi.config; import com.chengxi.jwt.JWTUtil; import com.chengxi.jwt.JwtUtils; import com.chengxi.ras.RsaUtils; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import io.jsonwebtoken.Claims; import org.bouncycastle.jcajce.provider.asymmetric.rsa.RSAUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.security.PrivateKey; import java.security.PublicKey; /** * 编辑ZuulFilter自定义过滤器,用于校验登录 * 重写zuulFilter类,有四个重要的方法 * 1.- `shouldFilter`:返回一个`Boolean`值,判断该过滤器是否需要执行。返回true执行,返回false不执行。 * 2.- `run`:过滤器的具体业务逻辑。 * 3.- `filterType`:返回字符串,代表过滤器的类型。包含以下4种: * - `pre`:请求在被路由之前执行 * - `routing`:在路由请求时调用 * - `post`:在routing和errror过滤器之后调用 * - `error`:处理请求时发生错误调用 * 4.- `filterOrder`:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高 * * * @author chengxi * @date 2018/12/5 17:24 */ @Component public class LoginFilter extends ZuulFilter { // 从yml配置文件中获取配置的数据 @Value("${sc.jwt.secret}") private String secret; @Value("${sc.jwt.pubKeyPath}") private String pubKeyPath; @Value("${sc.jwt.priKeyPath}") private String priKeyPath; @Value("${sc.jwt.expire}") private Integer expire; private PublicKey publicKey; private PrivateKey privateKey; @Override public String filterType() { // 登录校验的过滤级别,肯定是第一层过滤 return "pre"; } @Override public int filterOrder() { // 执行顺序为1,值越小执行顺行越靠前 return 1; } /** * 默认此类过滤器时false,不开启的,需要改为true * @return */ @Override public boolean shouldFilter() { // 登录校验逻辑 // 1)获取zuul提供的请求上下文对象(即是请求全部内容) RequestContext currentContext = RequestContext.getCurrentContext(); // 2) 从上下文中获取request对象 HttpServletRequest request = currentContext.getRequest(); // 3) 从请求中获取url String url= request.getRequestURI(); // 4) 判断用户是否是注册请求(因为只是一个简单的案例,auth服务并没有抒写,所以这里简单判断下。这里本应该是由auth服务处理的) if(url.indexOf("register") != -1){ String token = setRegisterToken(); // 没有token,认为登录校验失败,进行拦截 currentContext.setSendZuulResponse(false); // 生成token返回 currentContext.setResponseBody(token); // 返回201状态码。表示生成token返回 currentContext.setResponseStatusCode(HttpStatus.CREATED.value()); } // 3) 从请求中获取token String token = request.getParameter("access-token"); // 4) 判断(如果没有token,认为用户还没有登录,返回401状态码) if(token == null || "".equals(token.trim())){ // 没有token,认为登录校验失败,进行拦截 currentContext.setSendZuulResponse(false); // 返回401状态码。也可以考虑重定向到登录页 currentContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); return true; }else{ getKey(); System.out.println("privateKey: "+privateKey); String claims = null; try { claims = JwtUtils.getInfoFromToken(token, publicKey); } catch (Exception e) { e.printStackTrace(); System.out.println("解析token出错"); } System.out.println("claims:"+claims); // 如果解析成功,则不再进入过滤器,否则进入 if(claims != null){ return false; }else{ // token错误,认为登录校验失败,进行拦截 currentContext.setSendZuulResponse(false); // 返回401状态码。也可以考虑重定向到登录页 currentContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); return true; } } } /** * 登录校验过滤器,执行逻辑的方法 * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { System.out.println("拦截器run方法"); // // 登录校验逻辑 // // 1)获取zuul提供的请求上下文对象(即是请求全部内容) // RequestContext currentContext = RequestContext.getCurrentContext(); // // 2) 从上下文中获取request对象 // HttpServletRequest request = currentContext.getRequest(); // // 3) 从请求中获取token // String token = request.getParameter("access-token"); // // 4) 判断(如果没有token,认为用户还没有登录,返回401状态码) // if(token == null || "".equals(token.trim())){ // // 没有token,认为登录校验失败,进行拦截 // currentContext.setSendZuulResponse(false); // // 返回401状态码。也可以考虑重定向到登录页 // currentContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); // } // 如果校验通过,可以考虑吧用户信息放入上下文,继续向后执行 return null; } public String setRegisterToken() { try { // 生成公私秘钥 RsaUtils.generateKey(pubKeyPath,priKeyPath,secret); getKey(); } catch (Exception e) { e.printStackTrace(); System.out.println("生成公私秘钥抛错"); } //生成token String token = null; try { token = JwtUtils.generateToken(1,"小熙",privateKey,30); } catch (Exception e) { e.printStackTrace(); System.out.println("生成token报错"); } System.out.println("生成的token:"+token); return token; } public void getKey(){ // 获取公私秘钥 try { publicKey = RsaUtils.getPublicKey(pubKeyPath); System.out.println("publicKey: "+publicKey); privateKey = RsaUtils.getPrivateKey(priKeyPath); System.out.println("privateKey: "+privateKey); } catch (Exception e) { e.printStackTrace(); System.out.println("获取公私钥出错"); } } }
-
jwt工具包(生成token和解析token)
package com.chengxi.jwt; import com.chengxi.ras.RsaUtils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.joda.time.DateTime; import org.springframework.util.ObjectUtils; import java.security.PrivateKey; import java.security.PublicKey; /** * @author: XinQi.Yuan * 修改: chengxi * @create: 2018-05-26 15:43 **/ public class JwtUtils { /** * 私钥加密token * * @param userId、userName 载荷中的数据 * @param privateKey 私钥 * @param expireMinutes 过期时间,单位秒 * @return * @throws Exception */ public static String generateToken(Integer userId, String userName, PrivateKey privateKey, int expireMinutes) throws Exception { return Jwts.builder() .claim("userId", userId) .claim("userName", userName) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, privateKey) .compact(); } /** * 私钥加密token * * @param userId、userName 载荷中的数据 * @param privateKey 私钥字节数组 * @param expireMinutes 过期时间,单位秒 * @return * @throws Exception */ public static String generateToken(Integer userId, String userName, byte[] privateKey, int expireMinutes) throws Exception { return Jwts.builder() .claim("userId", userId) .claim("userName", userName) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey)) .compact(); } /** * 公钥解析token * * @param token 用户请求中的token * @param publicKey 公钥 * @return * @throws Exception */ private static Jws<Claims> parserToken(String token, PublicKey publicKey) { return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token); } /** * 公钥解析token * * @param token 用户请求中的token * @param publicKey 公钥字节数组 * @return * @throws Exception */ private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception { return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey)) .parseClaimsJws(token); } /** * 获取token中的用户信息 * * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 * @throws Exception */ public static String getInfoFromToken(String token, PublicKey publicKey) throws Exception { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); return body+""; } /** * 获取token中的用户信息 * * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 * @throws Exception */ public static String getInfoFromToken(String token, byte[] publicKey) throws Exception { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); return body+""; } }
-
rsa工具包(用于生成公私秘钥)
package com.chengxi.ras; 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; /*** * 此类是利用RSA算法生成公私秘钥。(底层是欧拉函数) * 在非对称加密中利用私有秘钥进行加密,传输到另一端之后可以使用公有秘钥解密。反之亦然 * 举例如在: jwt中使用的密钥就可以是该算法生成的公私秘钥 * (因为该算法的特殊性,所以加密时使用公钥,解密时用私钥仍可以加解密无误。不用使用同一把钥匙,提高了安全性(非对称加密,消耗性能、降低效率、延缓时间)) * * Created by ace on 2018/5/10. * @author XinQi.Yuan * 修改:chengxi * */ 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.exists()) { dest.createNewFile(); } Files.write(dest.toPath(), bytes); } /** * 测试使用 * @param args */ public static void main(String[] args) throws Exception { // 调用generateKey方法,输入公私钥的地址,生成秘钥的明文(原料)。即可在指定的地方生成秘钥 RsaUtils.generateKey("D:/cjp/key.pub","D:/cjp/key.pri","程熙"); // 这是输入地址获取公有秘钥的,还有一个是输入字节对象获取的。下面获取私有秘钥同理 PublicKey publicKey = RsaUtils.getPublicKey("D:/cjp/key.pub"); System.out.println("这是公有的秘钥:"+publicKey); PrivateKey privateKey = RsaUtils.getPrivateKey("D:/cjp/key.pri"); System.out.println("这是私有的秘钥:"+privateKey); System.out.println("ok"); } }
二. 测试运行
-
没有去注册也没有token,请求出错返回自定的401未授权状态码。
如图:
-
去访问注册路径模拟注册,返回注册成功的token
如图:
-
根据返回的token,携带在请求头上继续请求访问用户(注意这里的token估计少些几个,演示错误信息)
如图(解析token出错):
返回的还是小熙自定义的未授权401,方便省事而已。
-
使用返回的正确token,携带在请求头上请求访问用户信息
如图(正确返回):
在这里小熙优化了动态配置和讲说了jwt、rsa,下面小熙将讲说简写zuul映射路径。
三. 优化zuul映射服务路径
上面所抒写的是正常抒写,下面介绍简写的方式。
-
修改yml文件
zuul: prefix: /api # 访问网关路径的前缀(在映射路径的前面,一般用于区别开发的版本) routes: user-service: /user-service-zuul/** #前面的是eureka服务注册列表中的名称,后面的是映射路径的名称
-
运行测试
如图:
嗯,至此小熙关于案例的优化就说完了,由于这是入门案例还不健全,诚然代码还有诸多粗糙的地方但重在使用。
下一篇小熙将会讲述SpringCloudConfig配置文件的讲解和实例。