微服务中的授权中心(JWT如何加密传输、Cookie如何跨域写入请求)

JWT(JSON Web Token)

结合Zuul的鉴权流程

在微服务架构中,我们可以把服务的鉴权操作放到网关中,将未通过鉴权的的请求直接拦截;

在这里插入图片描述

  1. 用户请求登录;
  2. Zuul将请求转发到授权中心,请求授权;
  3. 授权中心校验完成,颁发JWT凭证;
  4. 客户端请求其他功能携带JWT;
  5. Zuul将JWT交给授权中心校验,通过后放行;
  6. 用户请求到达微服务;
  7. 微服务将JWT交给鉴权中心,鉴权同时解析用户信息;
  8. 鉴权中心返回用户数据给微服务;
  9. 微服务处理请求,返回响应;

出现的问题

每次鉴权都需要访问鉴权中心,系统间的网络请求频率过高,效率较差,鉴权中心压力较大;

  • 为什么需要这么多次鉴权?
  • 为什么每次鉴权都需要访问鉴权中心?

这是因为JWT在传输过程中是不安全的,所以每次使用之前都需要确认JWT的正确性,是否被别人篡改。而JWT中的签名只能检验JWT的正确性,却不能阻止JWT在传输过程中被篡改。可以想到对JWT进行加密传输,只有指定的微服务可以访问JWT。这样就可以避免在传输过程中被访问篡改,也就不用每个微服务都去授权中心鉴权了,可以自己解析使用JWT。

非对称加密

加密技术是对信息进行编码和解码的技术,编码是把原来可读的信息译成代码形式,其逆过程就是解码,加密技术的要点是加密算法,可分为三类:

对称加密

  • 基本原理:将明文分为N组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有分组密文进行合并,形成最终密文;
  • 优势:算法公开、计算量小、加密速度快、加密效率高;
  • 缺点:双方都使用同样的密钥,安全性得不到保证;

非对称加密 如RSA

  • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端;私钥加密,持有私钥或公钥才可以解开;公钥加密,持有私钥可解密。
  • 优点:安全,难以破解
  • 缺点:算法比较耗时;

不可逆加密 如MD5、SHA

基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

RSA算法历史:

1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA

结合RSA的鉴权

在这里插入图片描述

  • 首先利用RSA生成公钥和私钥,私钥保存在授权中心,公钥保存在Zuul和各个微服务
  • 用户请求登录;
  • 授权中心校验,通过后用私钥对JWT进行签名加密;
  • 返回JWT给用户;
  • 用户携带JWT访问;
  • Zuul直接通过公钥解密JWT,进行验证,验证通过则放行;
  • 请求到达微服务,微服务直接用公钥解析JWT,获取用户信息,无需访问授权中心;

实战

创建父工程ly-auth,两个子工程ly-auth-commonsly-auth-service,其中ly-auth-commons中是工具类,存放操作token、生成公钥私钥等类,ly-auth-service是微服务。

ly-auth-commons中引入依赖

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
    </dependency>
</dependencies>

ly-auth-service依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.auth</groupId>
            <artifactId>ly-auth-commons</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.leyou.common</groupId>
            <artifactId>ly-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.user</groupId>
            <artifactId>ly-user-interface</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
        </dependency>
    </dependencies>

目录结构
在这里插入图片描述

public class JwtConstants {
    public static final String JWT_KEY_ID = "id";
    public static final String JWT_KEY_USER_NAME = "username";
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
    private Long id;
    private String name;
}
public class JwtUtils {
    /**
     * 生成Token
     * @param userInfo
     * @param privateKey
     * @param expireMinutes
     * @return
     */
    public static String generateToken(UserInfo userInfo, PrivateKey privateKey, int expireMinutes) {
        return Jwts.builder()
                .claim(JwtConstants.JWT_KEY_ID, userInfo.getId())
                .claim(JwtConstants.JWT_KEY_USER_NAME, userInfo.getName())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 生成Token
     * @param userInfo
     * @param privateKey
     * @param expireMinutes
     * @return
     * @throws Exception
     */
    public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception {
        return Jwts.builder()
                .claim(JwtConstants.JWT_KEY_ID, userInfo.getId())
                .claim(JwtConstants.JWT_KEY_USER_NAME, userInfo.getName())
                .setExpiration(DateTime.now().plus(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.ES256, RsaUtils.getPrivateKey(privateKey))
                .compact();
    }

    /**
     * 公钥解析Token
     * @param publicKey
     * @param token
     * @return
     */
    public static Jws<Claims> parseToken(PublicKey publicKey, String token) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }


    /**
     * 公钥解析Token
     * @param publicKey
     * @param token
     * @return
     * @throws Exception
     */
    public static Jws<Claims> parseToken(byte[] publicKey, String token) throws Exception {
        return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey)).parseClaimsJws(token);
    }


    /**
     * 从Token中获取用户信息(使用公钥解析)
     * @param publicKey
     * @param token
     * @return
     */
    public static UserInfo getUserInfo(PublicKey publicKey, String token) {
        Jws<Claims> claimsJws = parseToken(publicKey, token);
        Claims body = claimsJws.getBody();
        return new UserInfo(
                ObjectUtils.toLong(body.get(JwtConstants.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstants.JWT_KEY_USER_NAME))
        );
    }

    /**
     * 从Token中获取用户信息(使用公钥解析)
     * @param publicKey
     * @param token
     * @return
     * @throws Exception
     */
    public static UserInfo getUserInfo(byte[] publicKey, String token) throws Exception {
        Jws<Claims> claimsJws = parseToken(publicKey, token);
        Claims body = claimsJws.getBody();
        return new UserInfo(
                ObjectUtils.toLong(body.get(JwtConstants.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstants.JWT_KEY_USER_NAME))
        );
    }
}

public class ObjectUtils {

    public static String toString(Object obj) {
        if (obj == null) {
            return null;
        }
        return obj.toString();
    }

    public static Long toLong(Object obj) {
        if (obj == null) {
            return 0L;
        }
        if (obj instanceof Double || obj instanceof Float) {
            return Long.valueOf(StringUtils.substringBefore(obj.toString(), "."));
        }
        if (obj instanceof Number) {
            return Long.valueOf(obj.toString());
        }
        if (obj instanceof String) {
            return Long.valueOf(obj.toString());
        } else {
            return 0L;
        }
    }

    public static Integer toInt(Object obj) {
        return toLong(obj).intValue();
    }
}
public class RsaUtils {

    /**
     * 从文件中读取公钥
     *
     * @param fileName
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(String fileName) throws Exception {
        byte[] bytes = readFile(fileName);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取私钥
     * @param fileName
     * @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);
    }

    /**
     * 读取文件
     *
     * @param fileName
     * @return
     * @throws IOException
     */
    public static byte[] readFile(String fileName) throws IOException {
        return Files.readAllBytes(new File(fileName).toPath());

    }

    /**
     * 根据密文,生存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(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    /**
     * 向目标路径写入文件
     *
     * @param destPath
     * @param bytes
     * @throws IOException
     */
    public static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

授权登录接口

对外提供登录授权服务

  • 客户端携带用户名和密码请求登录;
  • 授权中心调用客户中心接口,根据用户名和密码查询用户信息;
  • 如果用户名密码正确,则能够获得用户信息,否则登录失败;
  • 如果校验成功则生成JWT并返回。

生成公钥和私钥

server:
  port: 8087
spring:
  application:
    name: ly-auth-service
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
    registry-fetch-interval-seconds: 10
  instance:
    lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${spring.application.name}:${server.port}

ly:
  jwt:
    secret: leyou@Login(Auth}*^31)&heiMa% # 登录校验的密钥
    pubKeyPath: C:\\tmp\\rsa\\rsa.pub # 公钥地址
    priKeyPath: C:\\tmp\\rsa\\rsa.pri # 私钥地址
    expire: 30 # 过期时间,单位分钟
    cookieName: Set-Cookie #cookie名
    cookieMaxAge: 30 #cookie最长有效期

编写属性类,加载数据

@ConfigurationProperties("ly.jwt")
@Data
@Slf4j
@Component
public class JwtProperties {
    private String secret; //密钥
    private String pubKeyPath; //公钥文件路径
    private String priKeyPath; //私钥文件路径
    private int expire; //token过期时间

    private PublicKey publicKey; //公钥
    private PrivateKey privateKey; //私钥

    private String cookieName;
    private Integer cookieMaxAge;



    /**
     * 在该类初始化之后,将公钥、私钥从磁盘加载到内存中
     */
    @PostConstruct
    public void init(){
        try {
            File pubKey = new File(pubKeyPath);
            File priKey = new File(priKeyPath);
            //如果密钥文件不存在,则生成
            if (!pubKey.exists()||!priKey.exists()){
                RsaUtils.generateKey(pubKeyPath,priKeyPath,secret);
            }
            this.publicKey=RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey=RsaUtils.getPrivateKey(priKeyPath);
        }catch (Exception e){
            log.error("初始化公钥和私钥失败",e);
        }
    }

}

controller

@RestController
@EnableConfigurationProperties(JwtProperties.class)
public class AuthController {

    @Autowired
    private AuthService authService;

    @Autowired
    private JwtProperties jwtProperties;

    @PostMapping("login")
    public ResponseEntity<Void> login(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            HttpServletRequest request,
            HttpServletResponse response
    ){
        //登录校验
        String token = this.authService.login(username, password);
        if (token==null){
            return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
        }
        //将token写入cookie,并指定httpOnly为true,防止通过JS获取和修改
        CookieUtils.setCookie(request,response,jwtProperties.getCookieName(),token
                ,jwtProperties.getCookieMaxAge(),false);

        return ResponseEntity.ok().build();
    }
}

CookieUtils

/**
 * 
 * Cookie 工具类
 *
 */
public final class CookieUtils {

	protected static final Logger logger = LoggerFactory.getLogger(CookieUtils.class);

	/**
	 * 得到Cookie的值, 不编码
	 * 
	 * @param request
	 * @param cookieName
	 * @return
	 */
	public static String getCookieValue(HttpServletRequest request, String cookieName) {
		return getCookieValue(request, cookieName, false);
	}

	/**
	 * 得到Cookie的值,
	 * 
	 * @param request
	 * @param cookieName
	 * @return
	 */
	public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
		Cookie[] cookieList = request.getCookies();
		if (cookieList == null || cookieName == null){
			return null;			
		}
		String retValue = null;
		try {
			for (int i = 0; i < cookieList.length; i++) {
				if (cookieList[i].getName().equals(cookieName)) {
					if (isDecoder) {
						retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
					} else {
						retValue = cookieList[i].getValue();
					}
					break;
				}
			}
		} catch (UnsupportedEncodingException e) {
			logger.error("Cookie Decode Error.", e);
		}
		return retValue;
	}

	/**
	 * 得到Cookie的值,
	 * 
	 * @param request
	 * @param cookieName
	 * @return
	 */
	public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
		Cookie[] cookieList = request.getCookies();
		if (cookieList == null || cookieName == null){
			return null;			
		}
		String retValue = null;
		try {
			for (int i = 0; i < cookieList.length; i++) {
				if (cookieList[i].getName().equals(cookieName)) {
					retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
					break;
				}
			}
		} catch (UnsupportedEncodingException e) {
			logger.error("Cookie Decode Error.", e);
		}
		return retValue;
	}

	/**
	 * 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) {
		setCookie(request, response, cookieName, cookieValue, -1);
	}

	/**
	 * 设置Cookie的值 在指定时间内生效,但不编码
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage) {
		setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
	}

	/**
	 * 设置Cookie的值 不设置生效时间,但编码
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode) {
		setCookie(request, response, cookieName, cookieValue, -1, isEncode);
	}

	/**
	 * 设置Cookie的值 在指定时间内生效, 编码参数
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
		doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
	}

	/**
	 * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
		doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
	}

	/**
	 * 删除Cookie带cookie域名
	 */
	public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
		doSetCookie(request, response, cookieName, "", -1, false);
	}

	/**
	 * 设置Cookie的值,并使其在指定时间内生效
	 * 
	 * @param cookieMaxage
	 *            cookie生效的最大秒数
	 */
	private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
		try {
			if (cookieValue == null) {
				cookieValue = "";
			} else if (isEncode) {
				cookieValue = URLEncoder.encode(cookieValue, "utf-8");
			}
			Cookie cookie = new Cookie(cookieName, cookieValue);
			if (cookieMaxage > 0)
				cookie.setMaxAge(cookieMaxage);
			if (null != request)// 设置域名的cookie
				cookie.setDomain(getDomainName(request));
			cookie.setPath("/");
			response.addCookie(cookie);
		} catch (Exception e) {
			logger.error("Cookie Encode Error.", e);
		}
	}

	/**
	 * 设置Cookie的值,并使其在指定时间内生效
	 * 
	 * @param cookieMaxage
	 *            cookie生效的最大秒数
	 */
	private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
		try {
			if (cookieValue == null) {
				cookieValue = "";
			} else {
				cookieValue = URLEncoder.encode(cookieValue, encodeString);
			}
			Cookie cookie = new Cookie(cookieName, cookieValue);
			if (cookieMaxage > 0)
				cookie.setMaxAge(cookieMaxage);
			if (null != request)// 设置域名的cookie
				cookie.setDomain(getDomainName(request));
			cookie.setPath("/");
			response.addCookie(cookie);
		} catch (Exception e) {
			logger.error("Cookie Encode Error.", e);
		}
	}

	/**
	 * 得到cookie的域名
	 */
	private static final String getDomainName(HttpServletRequest request) {
		String domainName = null;

		String serverName = request.getRequestURL().toString();
		if (serverName == null || serverName.equals("")) {
			domainName = "";
		} else {
			serverName = serverName.toLowerCase();
			serverName = serverName.substring(7);
			final int end = serverName.indexOf("/");
			serverName = serverName.substring(0, end);
			final String[] domains = serverName.split("\\.");
			int len = domains.length;
			if (len > 3) {
				// www.xxx.com.cn
				domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
			} else if (len <= 3 && len > 1) {
				// xxx.com or xxx.cn
				domainName = domains[len - 2] + "." + domains[len - 1];
			} else {
				domainName = serverName;
			}
		}

		if (domainName != null && domainName.indexOf(":") > 0) {
			String[] ary = domainName.split("\\:");
			domainName = ary[0];
		}
		return domainName;
	}

}


UserClient

通过FeignClient去访问ly-user-service微服务

@FeignClient(value = "ly-user-service")
public interface UserClient extends UserApi {
}

ly-user-service中对外提供接口,如果用户名密码正确则返回User,否则返回400

@RequestMapping
public interface UserApi {

    @GetMapping("query")
    User queryUser(
            @RequestParam(value = "username",required = true) String username,
            @RequestParam(value = "password",required = true) String password
    );
}

Service

@Service
@EnableConfigurationProperties(JwtProperties.class)
public class AuthService {

    @Autowired
    private UserClient userClient;

    @Autowired
    private JwtProperties jwtProperties;


    public String login(String username, String password) {
        try {
            //认证用户身份
            User user = this.userClient.queryUser(username, password);
            if(user==null){
                throw new LyException(ExceptionEnums.INVALID_USERNAME_OR_PASSWORD);
            }
            //生成token
            UserInfo userInfo = new UserInfo();
            userInfo.setId(user.getId());
            userInfo.setName(user.getUsername());
            String token = JwtUtils.generateToken(userInfo, jwtProperties.getPrivateKey(), jwtProperties.getExpire());
            return token;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

测试
在这里插入图片描述

去登录页面测试

页面跳转成功,但是Cookie却没有写入。

在这里插入图片描述

问题分析

跨域请求cookie生效的条件:

  • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true;
  • 响应头中的Acess-Control-Allow-Origin一定不能为*,必须是指定的域名;
  • 浏览器发起ajax需要指定withCredentials为true;

服务端的cors配置:
在这里插入图片描述

没有任何问题。

再看客户端浏览器ajax配置:

在这里插入图片描述
服务端和客户端都OK!

那说明问题出在set-Cookie头中:

在这里插入图片描述
可以看到cookie的domain域属性不太对,Cookie是有域限制的,一个网页,只能操作当前域名下的cookie,但是现在看到的与www.leyou.com域名不匹配,cookie设置肯定失败。

跟踪CookieUtils

在CookieUtils中有一个获取域名的方法,我们来进行debug测试;

在这里插入图片描述

在这里插入图片描述
它获取的domain是通过服务器的host来计算的,我们的地址变成了127.0.0.1:8087,因此后续的运算,最终得到的domian就变成了0.0.1
在这里插入图片描述

我们请求的serverName是api.leyou.com,现在却变成了127.0.0.1,因此计算domain是错误的,从而导致cookie设置失败。

解决host地址的变化

这里的serverName其实就是请求时的主机名:Host,之所以改变有两个原因:

  • 我们使用了nginx反向代理,当监听到api.leyou.com时会自动将请求转发至127.0.0.1:10010即Zuul;
  • 而请求到达网关Zuul,Zuul就会根据路径匹配,我们的请求是/api/auth,根据规则被转发到127.0.0.1:8087,即我们的授权中心。

原来的nginx配置:

在这里插入图片描述

修改proxy_set_header Host $host;
在这里插入图片描述
修改后重启nginxnginx -s reload

但是Zuul发现请求为api.leyou.com还会做一次转发,所以要去修改网关配置:

在这里插入图片描述
在Zuul的PreDecorationFilter拦截器的run方法处打一个断点,继续测试

ctx.getRequest().getHeader(“host”),获取host,发现是api.leyou.com,说明nginx配置生效。
在这里插入图片描述
由于我们添加了add-host-header: true #携带请求本身的head头信息配置,所以下面的代码将request中的"host"添加到网关Request头中。
在这里插入图片描述
添加成功
在这里插入图片描述
继续测试,还是不行

在这里插入图片描述

这是因为另一个拦截器RibbonRoutingFilter路由分发过滤器的原因

在这里插入图片描述
在这个过滤器中,会过滤request中的头信息,包含下面的头信息是不会被添加到ZuulRequestHeader中的。

在这里插入图片描述
虽然在上一步已经将host头信息添加到了ZuulRequestHeaders中,但是这一步还是要过滤的,所以host永远不可能被添加到网关头信息中。
在这里插入图片描述
这是因为Zuul版本的问题,上面使用的是2.0.1版本,下面我们将版本降一级为2.0.0版本。重新下载源码,发现没有了上一步的if条件判断。
在这里插入图片描述
再对CookieUtils进行测试,发现host成功添加。
在这里插入图片描述
在这里插入图片描述

但是Cookie还是没有写进来。

在这里插入图片描述

这是因为在ZuulProperties里面包含敏感头信息,而我们的cookie名刚好为Set-Cookie,所以我们要将原始的敏感头信息覆盖掉。在这里插入图片描述

在网关配置文件中

  sensitive-headers:   

我们什么都不写,就表示敏感头信息无内容

在进行测试,发现Cookie信息已经被写入,而且域名也正确。
在这里插入图片描述

然后在登录页面测试

在这里插入图片描述
成功写入。

首页判断登录状态

虽然Cookie已经成功写入,但是首页的登录状态还是显示的请登录
在这里插入图片描述
这里需要向后台发起请求,根据Cookie获取当前用户信息,判断是否为有效登录
在这里插入图片描述

后台实现

controller

@GetMapping("verify")
    public ResponseEntity<UserInfo> verifyUser(@CookieValue("Set-Cookie") String token){
        try {
        //从token中将用户信息解析出来,
        UserInfo userInfo = JwtUtils.getUserInfo(jwtProperties.getPublicKey(), token);
        // 解析成功返回用户信息
        return ResponseEntity.ok(userInfo);
        }catch (Exception e){
            e.printStackTrace();
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }

在这里插入图片描述

刷新token

每当用户在页面进行新的操作,都应该刷新token的过期时间,否则30分钟后用户的登录信息就无效了,而token的过期时间也应该是在用户最后一次操作的基础上计算,而不是从登录开始计算。

那么我们怎么知道用户有操作呢?

事实上,每当用户来查询其个人信息的时候,就证明他正在浏览网页,此时刷新cookie是比较合适的。所以我们可以对刚才校验用户登录状态的接口进行改进,加入刷新token的逻辑。

 @GetMapping("verify")
    public ResponseEntity<UserInfo> verifyUser(@CookieValue("Set-Cookie") String token,
                                               HttpServletRequest request,
                                               HttpServletResponse response){
        try {
        //从token中将用户信息解析出来,
        UserInfo userInfo = JwtUtils.getUserInfo(jwtProperties.getPublicKey(), token);
        //解析成功要刷新token
        String newToken = JwtUtils.generateToken(userInfo, this.jwtProperties.getPrivateKey(),
                this.jwtProperties.getExpire());
        //更新cookie中的token
            CookieUtils.setCookie(request,response,this.jwtProperties.getCookieName(),newToken,
                    this.jwtProperties.getExpire());
        // 解析成功返回用户信息
        return ResponseEntity.ok(userInfo);
        }catch (Exception e){
            e.printStackTrace();
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }

网关登录拦截器

在Zuul编写拦截器,对用户的Token进行校验,如果发现未登录,则进行拦截;

引入jwt相关配置

 <dependency>
            <groupId>com.leyou.auth</groupId>
            <artifactId>ly-auth-commons</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.leyou.common</groupId>
            <artifactId>ly-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
ly: 
  jwt: 
    pubKeyPath: C:\\tmp\\rsa\\rsa.pub
    cookieName: Set-Cookie   

编写配置类

@ConfigurationProperties(prefix = "ly.jwt")
@Data
@Slf4j
public class JwtProperties {

    private String pubKeyPath;
    private PublicKey publicKey;
    private String cookieName;

    @PostConstruct
    public void init(){
        try {
            this.publicKey=RsaUtils.getPublicKey(pubKeyPath);
        }catch (Exception e){
            log.error("[网关服务]初始化公钥失败",e);
            throw new RuntimeException();
        }
    }

}

自定义一个前置拦截器

@Component
@EnableConfigurationProperties(JwtProperties.class)
public class LoginFilter extends ZuulFilter {

    @Autowired
    private JwtProperties properties;

    @Override
    public String filterType() {
        //前置过滤器
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 5;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //从请求上下文中获取请求,从请求中获取token
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        String token = CookieUtils.getCookieValue(request, this.properties.getCookieName());
        //校验
        try {
            JwtUtils.getUserInfo(this.properties.getPublicKey(),token);
        }catch (Exception e){
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
        }
        return null;
    }
}

删除cookie,重新登录,发现403,这是因为在前置拦截器中将所有请求都拦截了,包括登录请求。
在这里插入图片描述

白名单

要注意,并不是所有路径都需要被拦截的,例如:

  • 登录校验接口:/auth/**
  • 注册接口:/user/register
  • 数据校验接口:/user.check/**
  • 发送验证码接口:/user/code
  • 搜索接口:/search/**
    总之,在没登录时就能访问的接口,都需要加到白名单

所以我们要在拦截时,配置一个白名单,如果在名单内,则不进行拦截

ly:
  jwt:
    pubKeyPath: C:\\tmp\\rsa\\rsa.pub
    cookieName: Set-Cookie
  filter:
    allowPaths:
     - /api/auth
     - /api/search
     - /api/user/register
     - /api/user/check
     - /api/user/code
     - /api/item

配置类

@ConfigurationProperties(prefix = "ly.filter")
@Data
public class FilterProperties {

    private List<String> allowPaths;

}

在过滤器中

 @Override
    public boolean shouldFilter() {
        //获取上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //获取Request
        HttpServletRequest request = ctx.getRequest();
        //获取路径
        String url = request.getRequestURI();
        //判断白名单,如果在白名单里就不启用过滤器,否则启用过滤器
        return !isAllowPath(url);

    }

    private Boolean isAllowPath(String url) {
        List<String> allowPaths = this.filterProperties.getAllowPaths();
        for (String allowPath : allowPaths) {
            log.info("[网关服务]"+allowPath);
            if(url.startsWith(allowPath)){
                return true;
            }
        }
        return false;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值