spring-cloud oauth2与前端跨域问题
问题描述
架构是前后端分离的,也就是说前端可能是在不同的服务器,甚至在不同域名下的。
那这样的话,势必有跨域问题,而正常的跨域问题,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工具能正常返回,并且还有状态码:
但是通过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的回调方法,并在控制台打印:
打印的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]
而这次是在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,说这个问题的之前,先来说下我的架构。
系统架构
目前我的服务请求流程大致是这样:
所有的资源请求都经过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;
}
再次访问,完美解决。。。。。