乐优商城--服务(九) :授权微服务(LyAuthApplication)--后半部分

:紧接着上篇cookie没有写入的问题进行分析,地址: 授权微服务–前半部分

2.4 ****解决cookie写入问题

2.4.1 问题分析

我们在之前测试时,清晰的看到了响应头中,有Set-Cookie属性,为什么在这里却什么都没有?

我们之前在讲cors跨域时,讲到过跨域请求cookie生效的条件:

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

看看我们的服务端cors配置:
在这里插入图片描述
没有任何问题。

再看客户端浏览器的ajax配置,我们在js/common.js中对axios进行了统一配置:
在这里插入图片描述
一切OK。

那说明,问题一定出在响应的set-cookie头中。我们再次仔细看看刚才的响应头:
在这里插入图片描述
我们发现cookie的 domain属性似乎不太对。

cookie也是有域 的限制,一个网页,只能操作当前域名下的cookie,但是现在我们看到的地址是0.0.1,而页面是 www.leyou.com,域名不匹配,cookie设置肯定失败了!

总结:我们通过http://localhost:8087/login可以看到token信息,但是通过浏览器访问http://www.leyou.com/http://api.leyou.com/api/auth/login却没有写入到cookie中,可以保证我们代码没错,问题出在了路径上。由于路径过了网关和nginx,因此应逐一分析。

Set-cookie中有一个非常关键的点:domain(域),这的domain=localhost,代表的是本机,则证明是可以写的。由于cookie不能跨域,因此localhost只能在localhost下看得见。

2.4.2 跟踪CookieUtils

我们去Debug跟踪CookieUtils,看看到底是怎么回事:

通过http://api.leyou.com/api/auth/login进行访问,我们发现内部有一个方法(request.getRequestURL()获取请求路径 ),用来获取Domain:
在这里插入图片描述
它获取domain是通过服务器的host来计算的,然而我们的地址竟然是:127.0.0.1:8087,因此后续的截取运算,最终得到的domain就变成了:
在这里插入图片描述
问题找到了:

  • 我们请求时的serverName明明是 api.leyou.com,现在却被变成了:127.0.0.1,如果是api.leyou.com的话,最终就变成了 leyou.com,而leyou.com是所有相关网站的共同后缀,可以供leyou所有网站来访问,现在变成0.0.1,不是方法有问题,而是ServerName有问题,如果ServerName没有问题,最后截取的也自然没问题。因此计算domain是错误的,从而导致cookie设置失败!

2.4.3 解决host地址的变化

那么问题来了:为什么我们这里的请求serverName变成了:127.0.0.1:8087呢?

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

  • 我们使用了nginx反向代理,当监听到api.leyou.com的时候,会自动将请求转发至192.168.1.109:10010,即Zuul,已经从域名变成了ip,域名已经发生了改变,因此取到的域名也就不对。

    要解决这个问题,我们得先知道request.getRequestURL获取URL路径,tomcat是怎么拿到路径的。

    我们可以在前台随便打开一个js请求:
    在这里插入图片描述
    上图的js路径在request中被分成了几段:
    在这里插入图片描述
    影响我们得到域名的原因就是Host ! — 于是在nginx反向代理时多设置一个Host头。
    我们首先去更改nginx配置,让它不要修改我们的host:
    在这里插入图片描述
    把nginx进行重启:nginx-s reload
    这样就解决了nginx这里的问题。

  • 但是Zuul还会有一次转发:而后请求到达我们的网关Zuul,Zuul就会根据路径匹配,我们的请求是/api/auth,根据规则被转发到了 127.0.0.1:8087 ,即我们的授权中心,所以要去修改网关的配置。

    在网关中有很多过滤器,这些过滤器默认继承自ZuulFilter
    在这里插入图片描述
    我们在run方法打个断点,查询host,调用方法ctx.getRequest().getHeader(“host”),
    发现此时的host是 api.leyou.com 是正确的在这里插入图片描述
    再结合RequestURI,路径完全正确,说明我们的nginx配置生效了。
    在这里插入图片描述
    理论上此时放行,是不会出现错误的,如果此时出现错误,那就是网关没有将host写进去, 事实上,到此为止并没有将host写进去,原因是要进行一次if判断 :porperties.isAddHostHeader(),成立才将host写入,
    在这里插入图片描述
    点进方法发现AddHostHeader是一个boolean值,值为false,属于ZuulProperties,前缀是zuul,修改这个值很简单
    在这里插入图片描述
    我们在网关配置中将add-host-header的值设置成true
    在这里插入图片描述
    重启后,我们再次测试
    在这里插入图片描述
    最后计算得到的domain:
    在这里插入图片描述
    到这为止都没错。

2.4.4 再次测试

我们再次登录,发现依然没有cookie!!

我们通过RestClient访问下看看:
在这里插入图片描述
发现,响应头中根本没有set-cookie了。

这是怎么回事??

2.4.5 Zuul的敏感头过滤

Zuul内部有默认的过滤器,会对请求和响应头信息进行重组,过滤掉敏感的头信息:
在这里插入图片描述
会发现,这里会通过一个属性为SensitiveHeaders的属性,来获取敏感头列表,然后添加到IgnoredHeaders中,这些头信息就会被忽略。

而这个SensitiveHeaders的默认值就包含了set-cookie,此时host可以传过去,但是Set-Cookie会被过滤掉
在这里插入图片描述
同时,ZuulFilter下还有一个叫RibbonRoutingFilter的过滤器,是做负载均衡路由
在这里插入图片描述
它在构建上下文时会获取header,对头进行处理,先拿到原有头信息,对其进行判断,
如果是被允许的头信息,才会将其添加到Headers中去
在这里插入图片描述
判断依据是根据头名称进行判断,如果头是被忽略的,则不会被添加。但是下面的switch语句,其中就包含host,因此如果name是host,则会被忽略掉,无论怎样,我们都没有办法把host头添加进去
在这里插入图片描述
其实,buildZuulRequestHeaders对头做了两次判断,第一次是从request中拿到所有头信息,一个个判断要不要添加,第二次是从ZuulRequestHeaders中取出信息进行判断。
由于我们刚刚配置了add-host-header=true,PreDecorationFilter过滤器就会把host添加到ZuulRequestHeader中去,因此ZuulRequestHeaders是有host信息的,又通过if判断要不要添加,但是if判断又是刚才那个,因此即便有也会被忽略,host永远添加不进去BUG–原因是新版本的网关中多加了一个if判断。
在这里插入图片描述

在这里插入图片描述

解决方案

  • 降低网关版本,改成2.0.0(之前是2.0.1),同时忽略掉2.0.1的版本
    在这里插入图片描述
  • 同时,把敏感头设置为null,表示所有的头都放行
    在这里插入图片描述
    到此为止,所有问题都已解决!

2.4.6 最后的测试

再次重启后测试:

在这里插入图片描述
其中,domain=leyoucom。

浏览器访问cookie值也存在,
在这里插入图片描述
cookie写入问题到此解决,一切OK!

总结:

  • 首先我们认为是nginx问题,它把域名改成了ip地址,改完之后发现还不行;
  • 发现网关也有问题,网关又有一次反向代理,又把域名改成了ip;
  • 改了网关之后还是不行,发现是版本bug,版本降低OK;
  • 之后domain对了,但是cookie没有写回去,是因为它的敏感头把cookie过滤了,把敏感头去掉OK。

3. 首页判断登录状态

虽然cookie已经成功写入,但是我们首页的顶部,登录状态依然没能判断出用户信息:
在这里插入图片描述
这里需要向后台发起请求,获取根据cookie获取当前用户的信息。

我们先看页面实现

3.1 页面JS代码

页面的顶部已经被我们封装为一个独立的Vue组件,在/js/pages/shortcut.js中,并且shortcut.js在很多页面中都会有。
在这里插入图片描述
打开js,发现里面已经定义好了Vue组件,并且在created函数中,查询用户信息:
在这里插入图片描述
查看网络控制台,发现发起了请求:
在这里插入图片描述
因为token在cookie中,因此本次请求肯定会携带token信息在头中。

3.2 后台实现校验用户接口

我们在ly-auth-service中定义用户的校验接口,通过cookie获取token,然后校验通过返回用户信息。

  • 请求方式:GET
  • 请求路径:/verify
  • 请求参数:无,不过我们需要从cookie中获取token信息
  • 返回结果:UserInfo,校验成功返回用户信息;校验失败,则返回401

代码:
| AuthController:

// 校验用户登录状态
@GetMapping("verify")
public ResponseEntity<UserInfo> verifyUser(@CookieValue("LY_TOKEN") String token) {
    try {
        // 获取token信息
        UserInfo userInfo = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
        // 成功后直接返回
        return ResponseEntity.ok(userInfo);
    } catch (Exception e) {
        // 抛出异常,证明token无效,直接返回401
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
    }
}

注: 获取Cookie不需要request,直接@CookieValue注解即可。

3.3 测试

在这里插入图片描述
页面效果:
在这里插入图片描述

3.4 刷新token

每当用户在页面进行新的操作,都应该刷新token的过期时间,否则30分钟后用户的登录信息就无效了。而刷新其实就是重新生成一份token,然后写入cookie即可。

那么问题来了:我们怎么知道用户有操作呢?

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

 // 校验用户登录状态
@GetMapping("verify")
public ResponseEntity<UserInfo> verify(
        @CookieValue("LY_TOKEN") String token,
        HttpServletResponse response,
        HttpServletRequest request
){

    try {
        // 解析token
        UserInfo info = JwtUtils.getInfoFromToken(token, prop.getPublicKey());

        // 刷新token,重新生成
        String newToken = JwtUtils.generateToken(info, prop.getPrivateKey(), prop.getExpire());
        //写入cookie
        CookieUtils.newBuilder(response).httpOnly().request(request).build(cookieName, newToken);

        //已登录,返回用户信息
        return ResponseEntity.ok(info);
    } catch (Exception e){

        // 没有token token已过期 token被篡改
        throw new LyException(ExceptionEnum.NO_AUTHORIZED);
    }

}

4. 网关的登录拦截器

接下来,我们在Zuul编写拦截器,对用户的token进行校验,如果发现未登录,则进行拦截。

4.1 引入jwt相关配置

既然是登录拦截,一定是前置拦截器,我们在ly-gateway中定义。

首先引入所需要的依赖:

<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>ly-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

然后编写属性文件:

ly:
  jwt:
    pubKeyPath: E:/course/JavaProject/javacode/idea/rsa/rsa.pub # 公钥地址
    cookieName: LY_TOKEN # cookie的名称

注:解析token需要公钥,因此只提供公钥地址即可。

编写属性类,读取公钥:
在这里插入图片描述

4.2 编写过滤器逻辑

基本逻辑:

  • 获取cookie中的token
  • 通过JWT对token进行校验
  • 通过:则放行;不通过:则重定向到登录页
    在这里插入图片描述
@Component
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
public class AuthFilter extends ZuulFilter{

    @Autowired
    private JwtProperties prop;

    @Autowired
    private FilterProperties filterProperties;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;//前置过滤器
    }

    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;//官方前置过滤器-1,可以把自己定义的过滤器放在官方过滤器之前
    }

    @Override
    public boolean shouldFilter() {
        return true}

    @Override
    public Object run() throws ZuulException {
        // 获取上下文 获取request
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        // 获取token
        String token = CookieUtils.getCookieValue(request, prop.getCookieName());

        // 解析token
        try {
            UserInfo user = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
            //TODO 权限管理

        } catch (Exception e) {
            // 解析失败 未登录
            ctx.setSendZuulResponse(false);// 拦截功能
            ctx.setResponseStatusCode(403);// 返回状态码
        }

        return null;
    }
}

刷新页面,发现请求校验的接口也被拦截了:
在这里插入图片描述
证明我们的拦截器生效了,但是,似乎有什么不对的。这个路径似乎不应该被拦截啊!

4.3 白名单

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

  • 登录校验接口:/auth/**
  • 注册接口:/user/register
  • 数据校验接口:/user/check/**
  • 发送验证码接口:/user/code
  • 搜索接口:/search/**

另外,跟后台管理相关的接口,因为我们没有做登录和权限,因此暂时都放行,但是生产环境中要做登录校验:

  • 后台商品服务:/item/**

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

在application.yaml中添加规则:

filter:
  allowPaths:
    - /api/auth
    - /api/search
    - /api/user/register
    - /api/user/check
    - /api/user/code
    - /api/item
    - /api/cart

然后读取这些属性:
在这里插入图片描述

@Data
@ConfigurationProperties(prefix = "ly.filter")
public class FilterProperties {
    // 并不是所有的路径都拦截,比如:不需要登录也可以浏览商品
    private List<String> allowPaths;
}

在过滤器中的shouldFilter方法中添加判断逻辑:
在这里插入图片描述
代码:

@Component
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
public class AuthFilter extends ZuulFilter{

    @Autowired
    private JwtProperties prop;

    @Autowired
    private FilterProperties filterProperties;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;//前置过滤器
    }

    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;//官方前置过滤器-1,可以把自己定义的过滤器放在官方过滤器之前
    }

    @Override
    public boolean shouldFilter() {

        // 获取上下文以及request
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        // 获取请求的URL路径
        String path = request.getRequestURI();

        // 判断是否在白名单内 如果在 则放行
        return !isAllowPath(path);//是否过滤
    }

    private boolean isAllowPath(String path) {

        List<String> allowPaths = filterProperties.getAllowPaths();
        for (String allowPath : allowPaths) {
            if(path.startsWith(allowPath)){
                return true;
            }
        }
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        // 获取上下文 获取request
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        // 获取token
        String token = CookieUtils.getCookieValue(request, prop.getCookieName());

        // 解析token
        try {
            UserInfo user = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
            //TODO 权限管理

        } catch (Exception e) {
            // 解析失败 未登录
            ctx.setSendZuulResponse(false);// 拦截功能
            ctx.setResponseStatusCode(403);// 返回状态码
        }

        return null;
    }
}

再次测试:
在这里插入图片描述

4.4 可优化的点

授权登录还需要完善:

  • 需要引入权限控制系统
  • 在AuthFilter中,应该判断权限
  • 授权中心还可以做服务鉴权
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值