解决ajax跨域访问 为了方便以后查看

来源:互联网 发布:linux常用命令有什么用 编辑:程序博客网 时间:2024/06/06 00:57

来源:http://www.chinastor.org/gdcc/8804.html?ref=myread

解决跨域问题

比如,前端应用为静态站点且部署在 http://web.xxx.com 域下,后端应用发布 REST API 并部署在 http://api.xxx.com 域下,如何使前端应用通过 AJAX 跨域访问后端应用呢?这需要使用到  CORS 技术来实现,这也是目前最好的解决方案了。

CORS 全称为 Cross Origin Resource Sharing(跨域资源共享),服务端只需添加相关响应头信息,即可实现客户端发出 AJAX 跨域请求。

CORS 技术非常简单,易于实现,目前绝大多数浏览器均已支持该技术(IE8 浏览器也支持了),服务端可通过任何编程语言来实现,只要能将 CORS 响应头写入 response 对象中即可。

下面我们继续扩展 REST 框架,通过 CORS 技术实现 AJAX 跨域访问。

首先,我们需要编写一个 Filter,用于过滤所有的 HTTP 请求,并将 CORS 响应头写入 response 对象中,代码如下:

public class CorsFilter implements Filter {    private String allowOrigin;    private String allowMethods;    private String allowCredentials;    private String allowHeaders;    private String exposeHeaders;    @Override    public void init(FilterConfig filterConfig) throws ServletException {        allowOrigin = filterConfig.getInitParameter("allowOrigin");        allowMethods = filterConfig.getInitParameter("allowMethods");        allowCredentials = filterConfig.getInitParameter("allowCredentials");        allowHeaders = filterConfig.getInitParameter("allowHeaders");        exposeHeaders = filterConfig.getInitParameter("exposeHeaders");    }    @Override    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest) req;        HttpServletResponse response = (HttpServletResponse) res;        if (StringUtil.isNotEmpty(allowOrigin)) {            List<String> allowOriginList = Arrays.asList(allowOrigin.split(","));            if (CollectionUtil.isNotEmpty(allowOriginList)) {                String currentOrigin = request.getHeader("Origin");                if (allowOriginList.contains(currentOrigin)) {                    response.setHeader("Access-Control-Allow-Origin", currentOrigin);                }            }        }        if (StringUtil.isNotEmpty(allowMethods)) {            response.setHeader("Access-Control-Allow-Methods", allowMethods);        }        if (StringUtil.isNotEmpty(allowCredentials)) {            response.setHeader("Access-Control-Allow-Credentials", allowCredentials);        }        if (StringUtil.isNotEmpty(allowHeaders)) {            response.setHeader("Access-Control-Allow-Headers", allowHeaders);        }        if (StringUtil.isNotEmpty(exposeHeaders)) {            response.setHeader("Access-Control-Expose-Headers", exposeHeaders);        }        chain.doFilter(req, res);    }    @Override    public void destroy() {    }}

以上 CorsFilter 将从 web.xml 中读取相关 Filter 初始化参数,并将在处理 HTTP 请求时将这些参数写入对应的 CORS 响应头中,下面大致描述一下这些 CORS 响应头的意义:

  • Access-Control-Allow-Origin :允许访问的客户端域名,例如: http://web.xxx.com,若为  *,则表示从任意域都能访问,即不做任何限制。
  • Access-Control-Allow-Methods :允许访问的方法名,多个方法名用逗号分割,例如:GET,POST,PUT,DELETE,OPTIONS。
  • Access-Control-Allow-Credentials :是否允许请求带有验证信息,若要获取客户端域下的 cookie 时,需要将其设置为 true。
  • Access-Control-Allow-Headers :允许服务端访问的客户端请求头,多个请求头用逗号分割,例如:Content-Type。
  • Access-Control-Expose-Headers :允许客户端访问的服务端响应头,多个响应头用逗号分割。

需要注意的是,CORS 规范中定义 Access-Control-Allow-Origin 只允许两种取值,要么为 *,要么为具体的域名,也就是说,不支持同时配置多个域名。为了解决跨多个域的问题,需要在代码中做一些处理,这里将 Filter 初始化参数作为一个域名的集合(用逗号分隔),只需从当前请求中获取 Origin 请求头,就知道是从哪个域中发出的请求,若该请求在以上允许的域名集合中,则将其放入 Access-Control-Allow-Origin 响应头,这样跨多个域的问题就轻松解决了。

以下是 web.xml 中配置 CorsFilter 的方法:

<filter>    <filter-name>corsFilter</filter-name>    <filter-class>com.xxx.api.cors.CorsFilter</filter-class>    <init-param>        <param-name>allowOrigin</param-name>        <param-value>http://web.xxx.com</param-value>    </init-param>    <init-param>        <param-name>allowMethods</param-name>        <param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>    </init-param>    <init-param>        <param-name>allowCredentials</param-name>        <param-value>true</param-value>    </init-param>    <init-param>        <param-name>allowHeaders</param-name>        <param-value>Content-Type</param-value>    </init-param></filter><filter-mapping>    <filter-name>corsFilter</filter-name>    <url-pattern>/*</url-pattern></filter-mapping>

完成以上过程即可实现 AJAX 跨域功能了,但似乎还存在另外一个问题,由于 REST 是无状态的,后端应用发布的 REST API 可在用户未登录的情况下被任意调用,这显然是不安全的,如何解决这个问题呢?我们需要为 REST 请求提供安全机制。

4.6 提供安全机制

解决 REST 安全调用问题,可以做得很复杂,也可以做得特简单,可按照以下过程提供 REST 安全机制:

  1. 当用户登录成功后,在服务端生成一个 token,并将其放入内存中(可放入 JVM 或 Redis 中),同时将该 token 返回到客户端。
  2. 在客户端中将返回的 token 写入 cookie 中,并且每次请求时都将 token 随请求头一起发送到服务端。
  3. 提供一个 AOP 切面,用于拦截所有的 Controller 方法,在切面中判断 token 的有效性。
  4. 当登出时,只需清理掉 cookie 中的 token 即可,服务端 token 可设置过期时间,使其自行移除。

首先,我们需要定义一个用于管理 token 的接口,包括创建 token 与检查 token 有效性的功能。代码如下:

public interface TokenManager {    String createToken(String username);    boolean checkToken(String token);}

然后,我们可提供一个简单的 TokenManager 实现类,将 token 存储到 JVM 内存中。代码如下:

public class DefaultTokenManager implements TokenManager {    private static Map<String, String> tokenMap = new ConcurrentHashMap<>();    @Override    public String createToken(String username) {        String token = CodecUtil.createUUID();        tokenMap.put(token, username);        return token;    }    @Override    public boolean checkToken(String token) {        return !StringUtil.isEmpty(token) && tokenMap.containsKey(token);    }}

需要注意的是,如果需要做到分布式集群,建议基于 Redis 提供一个实现类,将 token 存储到 Redis 中,并利用 Redis 与生俱来的特性,做到 token 的分布式一致性。

然后,我们可以基于 Spring AOP 写一个切面类,用于拦截 Controller 类的方法,并从请求头中获取 token,最后对 token 有效性进行判断。代码如下:

public class SecurityAspect {    private static final String DEFAULT_TOKEN_NAME = "X-Token";    private TokenManager tokenManager;    private String tokenName;    public void setTokenManager(TokenManager tokenManager) {        this.tokenManager = tokenManager;    }    public void setTokenName(String tokenName) {        if (StringUtil.isEmpty(tokenName)) {            tokenName = DEFAULT_TOKEN_NAME;        }        this.tokenName = tokenName;    }    public Object execute(ProceedingJoinPoint pjp) throws Throwable {        // 从切点上获取目标方法        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();        Method method = methodSignature.getMethod();        // 若目标方法忽略了安全性检查,则直接调用目标方法        if (method.isAnnotationPresent(IgnoreSecurity.class)) {            return pjp.proceed();        }        // 从 request header 中获取当前 token        String token = WebContext.getRequest().getHeader(tokenName);        // 检查 token 有效性        if (!tokenManager.checkToken(token)) {            String message = String.format("token [%s] is invalid", token);            throw new TokenException(message);        }        // 调用目标方法        return pjp.proceed();    }}

若要使 SecurityAspect 生效,则需要添加如下 Spring 配置:

<bean id="securityAspect" class="com.xxx.api.security.SecurityAspect">    <property name="tokenManager" ref="tokenManager"/>    <property name="tokenName" value="X-Token"/></bean><aop:config>    <aop:aspect ref="securityAspect">        <aop:around method="execute" pointcut="@annotation(org.springframework.web.bind.annotation.RequestMapping)"/>    </aop:aspect></aop:config>

最后,别忘了在 web.xml 中添加允许的 X-Token 响应头,配置如下:

<init-param>    <param-name>allowHeaders</param-name>    <param-value>Content-Type,X-Token</param-value></init-param>
0 0
原创粉丝点击