文章目录
  1. 1. 问题描述
    1. 1.1. 关于readystate五个状态总结
      1. 1.1.1. readyState 状态 状态说明
  2. 2. 系统架构

问题描述

架构是前后端分离的,也就是说前端可能是在不同的服务器,甚至在不同域名下的。
那这样的话,势必有跨域问题,而正常的跨域问题,springboot都已经提供了解决方案,即在BackGatewayApplication启动类上添加如下filter:

@Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

或者还有更方便的:

@CrossOrigin(origins = "*")
public class BackGatewayApplication {
    .....
}

目前遇到的问题是,前端通过ajax请求,如果传了一个失效的token,则前端会直接报错,显示的是这样的:

Access to XMLHttpRequest at 'http://127.0.0.1:9002/rios-upms/authUser/test' from origin 'http://www.runoob.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

看着像是说跨域问题。

但是,我们之前遇到的跨域大部分是浏览器的行为,将请求拦截了,但是这个不同的是他的请求发出去了,服务端也接收到了请求,并返回了,返回的失效token的提示,通过postman或者curl工具能正常返回,并且还有状态码:

paste image

但是通过ajax请求浏览器访问,却总是出现error,以下是用jquery的ajax请求:

$(document).ready(function(){
    $("button").click(function(){
        $.ajax({
                url:"http://bgateway.riosclub.com/rios-upms/authUser/findMenu",
                headers: {
                    Accept: "application/json; charset=utf-8",
                    Authorization: "Bearer Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDY2MTYyMzAsInVzZXJfbmFtZSI6InhpYW9oZWkiLCJhdXRob3JpdGllcyI6WyJVU0VSIiwiQURNSU4iXSwianRpIjoiNTc5NzcwZjUtYjU5OC00ZGU2LWFjODktNGI2YzM3Mjg0ZTAzIiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsiYWxsIl19.b-E1XXWuUZuq61eIkJ07LWjDkUY4P5CpP8_kRNKz-7A"
                },
                type: "post",
                success:function(result){
                    alert('success');
                    console.log(JSON.stringify(result));
                    $("#div1").html(result);
                },
                fail: function(e){
                    alert('fail');
                    alert(e);
                },
             error: function(xhr,status,error){
                 alert('error');
                 console.log(JSON.stringify(xhr));
                 console.log(JSON.stringify(status));
                 console.log(JSON.stringify(error));

             },
            complete: function(xhr,status){
                alert('complete');
                alert(JSON.stringify(xhr));
                alert('complete' + status);
            }
               });
    });
});

调用ajax请求后,总是直接调用了error的回调方法,并在控制台打印:

paste image

打印的xhr是:

{"readyState":0,"responseText":"","status":0,"statusText":"error"}

关于readystate五个状态总结

readyState 状态 状态说明

  • (0)未初始化

此阶段确认XMLHttpRequest对象是否创建,并为调用open()方法进行未初始化作好准备。值为0表示对象已经存在,否则浏览器会报错--对象不存在。

  • (1)载入

此阶段对XMLHttpRequest对象进行初始化,即调用open()方法,根据参数(method,url,true)完成对象状态的设置。并调用send()方法开始向服务端发送请求。值为1表示正在向服务端发送请求。

  • (2)载入完成

此阶段接收服务器端的响应数据。但获得的还只是服务端响应的原始数据,并不能直接在客户端使用。值为2表示已经接收完全部响应数据。并为下一阶段对数据解析作好准备。

  • (3)交互

此阶段解析接收到的服务器端响应数据。即根据服务器端响应头部返回的MIME类型把数据转换成能通过responseBody、responseText或responseXML属性存取的格式,为在客户端调用作好准备。状态3表示正在解析数据。

  • (4)完成

此阶段确认全部数据都已经解析为客户端可用的格式,解析已经完成。值为4表示数据解析完毕,可以通过XMLHttpRequest对象的相应属性取得数据。
概而括之,整个XMLHttpRequest对象的生命周期应该包含如下阶段:
创建-初始化请求-发送请求-接收数据-解析数据-完成

readyState为0,了解了一下,出现这种情况,xhr中readyState为0的几种情况:

引用官方对XMLHttpRequest的说明:

http://www.w3.org/TR/XMLHttpRequest/

The status attribute must return the result of running these steps:

status的值一定会返回运行这些步骤的结果。

  • 1、If the state is UNSENT or OPENED, return 0.(如果状态是UNSENT或者OPENED,返回0)
  • 2、If the error flag is set, return 0.(如果错误标签被设置,返回0)
  • 3、Return the HTTP status code.(返回HTTP状态码)

而根据我得到的error错误,第一反应是跨域访问,被浏览器拒绝了,但是,关键是我服务端收到了请求。

还有一种情况,也是会导致state的状态为0的,就是:

If the cross-origin request status is network error

    This is a network error.

虽然去访问了,应该是浏览器跨域的返回头没有被允许,所以浏览器阻止返回的响应,Access-Control-Allow-Origin 浏览器没有发现这个属性的设置,所以就报跨域,也就是服务器在响应头里也要设置跨域处理,不然也会出现这种情况。

那么,我就在response的header设置一下:

httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");

再次访问,就得到了状态码:

paste image![paste image]

而这次是在complete方法里取得了xhr:

{"readyState":4,"responseText":"{\"msg\":\"无效的token\",\"code\":401}","responseJSON":{"msg":"无效的token","code":401},"status":403,"statusText":"error"}

得到的状态值是4,status值是403。

在这里还没完,因为我用的是security+oauth2,在gateway处理所有资源请求的权限认证的,而在oauth2这里,确有个头疼的问题,在token失效的时候,我就拿不到readyState,也一直是0,说这个问题的之前,先来说下我的架构。

系统架构

目前我的服务请求流程大致是这样:

paste image

所有的资源请求都经过gateway转发并在gateway认证(在oauth2里把gateway作为一个总的资源服务器配置来校验各资源服务器的权限),调用认证服务器去校验token的正确性。

而oauth2的工作流程大致是:

OAuth2AuthenticationProcessingFilter:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    .......

    Authentication authResult = this.authenticationManager.authenticate(authentication);
    ......
}

然后是OAuth2AuthenticationManager:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if(authentication == null) {
            throw new InvalidTokenException("Invalid token (token not found)");
        } else {
            String token = (String)authentication.getPrincipal();
            OAuth2Authentication auth = this.tokenServices.loadAuthentication(token);
            .......
                    return auth;
                }

再UserInfoTokenServices:

public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
        Map<String, Object> map = this.getMap(this.userInfoEndpointUrl, accessToken);
        if(map.containsKey("error")) {
            if(this.logger.isDebugEnabled()) {
                this.logger.debug("userinfo returned error: " + map.get("error"));
            }

            throw new InvalidTokenException(accessToken);
        } else {
            return this.extractAuthentication(map);
        }
    }

这段很关键,if(map.containsKey("error"))

有这个key就会抛出InvalidTokenException异常。

然后当中的this.getMap(…)这个方法很重要,我们看看:

private Map<String, Object> getMap(String path, String accessToken) {
        if(this.logger.isDebugEnabled()) {
            this.logger.debug("Getting user info from: " + path);
        }

        try {
            OAuth2RestOperations restTemplate = this.restTemplate;
            if(restTemplate == null) {
                BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
                resource.setClientId(this.clientId);
                restTemplate = new OAuth2RestTemplate(resource);
            }

            OAuth2AccessToken existingToken = ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().getAccessToken();
            if(existingToken == null || !accessToken.equals(existingToken.getValue())) {
                DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken);
                token.setTokenType(this.tokenType);
                ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().setAccessToken(token);
            }

            return (Map)((OAuth2RestOperations)restTemplate).getForEntity(path, Map.class, new Object[0]).getBody();
        } catch (Exception var6) {
            this.logger.warn("Could not fetch user details: " + var6.getClass() + ", " + var6.getMessage());
            return Collections.singletonMap("error", "Could not fetch user details");
        }
    }

这个方法是调用认证服务器获取用户信息的接口,也就是我写在认证服务器的一个接口:

@GetMapping("/userinfo")
    public Object user(Principal user){
        return user;
    }

很显然,如果我传的token是过期的token,那这个返回的user肯定是null,通过调试,果然如此,但是,按理说应该会被catch掉,但是,因为返回的是null,却没走这里,不知道为什么,可能是被其它的filter或者监听器给捕获了,那么,我只能让它不返回null,但又不能破坏oauth2原有的规则,既然是通过返回的map里面的key来判断是否有error来抛出InvalidTokenException异常的,那我这么改造一下:

@GetMapping("/userinfo")
    public Object user(Principal user){
        if (user == null){
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("error","未获取到用户信息");
            return jsonObject;
        }
        return user;
    }

再次访问,完美解决。。。。。

paste image

文章目录
  1. 1. 问题描述
    1. 1.1. 关于readystate五个状态总结
      1. 1.1.1. readyState 状态 状态说明
  2. 2. 系统架构