解決ajax跨域訪問 爲了方便以後查看

來源: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>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章