最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot實現(xiàn)接口校驗簽名調(diào)用的項目實踐

 更新時間:2023年09月16日 10:02:43   作者:passerbyYSQ  
在以SpringBoot開發(fā)后臺API接口時,會存在哪些接口不安全的因素呢?通常如何去解決的呢?本文主要介紹了SpringBoot實現(xiàn)接口校驗簽名調(diào)用的項目實踐,感興趣的可以了解一下

概念

開放接口

開放接口是指不需要登錄憑證就允許被第三方系統(tǒng)調(diào)用的接口。為了防止開放接口被惡意調(diào)用,開放接口一般都需要驗簽才能被調(diào)用。提供開放接口的系統(tǒng)下面統(tǒng)一簡稱為"原系統(tǒng)"。

驗簽

驗簽是指第三方系統(tǒng)在調(diào)用接口之前,需要按照原系統(tǒng)的規(guī)則根據(jù)所有請求參數(shù)生成一個簽名(字符串),在調(diào)用接口時攜帶該簽名。原系統(tǒng)會驗證簽名的有效性,只有簽名驗證有效才能正常調(diào)用接口,否則請求會被駁回。

接口驗簽調(diào)用流程

1. 約定簽名算法

第三方系統(tǒng)作為調(diào)用方,需要與原系統(tǒng)協(xié)商約定簽名算法(下面以SHA256withRSA簽名算法為例)。同時約定一個名稱(callerID),以便在原系統(tǒng)中來唯一標識調(diào)用方系統(tǒng)。

2. 頒發(fā)非對稱密鑰對

簽名算法約定后之后,原系統(tǒng)會為每一個調(diào)用方系統(tǒng)專門生成一個專屬的非對稱密鑰對(RSA密鑰對)。私鑰頒發(fā)給調(diào)用方系統(tǒng),公鑰由原系統(tǒng)持有。注意,調(diào)用方系統(tǒng)需要保管好私鑰(存到調(diào)用方系統(tǒng)的后端)。因為對于原系統(tǒng)而言,調(diào)用方系統(tǒng)是消息的發(fā)送方,其持有的私鑰唯一標識了它的身份是原系統(tǒng)受信任的調(diào)用方。調(diào)用方系統(tǒng)的私鑰一旦泄露,調(diào)用方對原系統(tǒng)毫無信任可言。

3. 生成請求參數(shù)簽名

簽名算法約定后之后,生成簽名的原理如下(活動圖)。為了確保生成簽名的處理細節(jié)與原系統(tǒng)的驗簽邏輯是匹配的,原系統(tǒng)一般都提供jar包或者代碼片段給調(diào)用方來生成簽名,否則可能會因為一些處理細節(jié)不一致導致生成的簽名是無效的。

4. 請求攜帶簽名調(diào)用

路徑參數(shù)中放入約定好的callerID,請求頭中放入調(diào)用方自己生成的簽名

代碼設(shè)計

1. 簽名配置類

相關(guān)的自定義yml配置如下。RSA的公鑰和私鑰可以使用hutool的SecureUtil工具類來生成,注意公鑰和私鑰是base64編碼后的字符串

定義一個配置類來存儲上述相關(guān)的自定義yml配置

import cn.hutool.crypto.asymmetric.SignAlgorithm;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
 * 簽名的相關(guān)配置
 */
@Data
@ConditionalOnProperty(value = "secure.signature.enable", havingValue = "true")  // 根據(jù)條件注入bean
@Component
@ConfigurationProperties("secure.signature")
public class SignatureProps {
    private Boolean enable;
    private Map<String, KeyPairProps> keyPair;
    @Data
    public static class KeyPairProps {
        private SignAlgorithm algorithm;
        private String publicKeyPath;
        private String publicKey;
        private String privateKeyPath;
        private String privateKey;
    }
}

2. 簽名管理類

定義一個管理類,持有上述配置,并暴露生成簽名和校驗簽名的方法。

注意,生成的簽名是將字節(jié)數(shù)組進行十六進制編碼后的字符串,驗簽時需要將簽名字符串進行十六進制解碼成字節(jié)數(shù)組

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import top.ysqorz.signature.model.SignatureProps;
import java.nio.charset.StandardCharsets;
@ConditionalOnBean(SignatureProps.class)
@Component
public class SignatureManager {
    private final SignatureProps signatureProps;
    public SignatureManager(SignatureProps signatureProps) {
        this.signatureProps = signatureProps;
        loadKeyPairByPath();
    }
    /**
     * 驗簽。驗證不通過可能拋出運行時異常CryptoException
     *
     * @param callerID  調(diào)用方的唯一標識
     * @param rawData   原數(shù)據(jù)
     * @param signature 待驗證的簽名(十六進制字符串)
     * @return 驗證是否通過
     */
    public boolean verifySignature(String callerID, String rawData, String signature) {
        Sign sign = getSignByCallerID(callerID);
        if (ObjectUtils.isEmpty(sign)) {
            return false;
        }
        // 使用公鑰驗簽
        return sign.verify(rawData.getBytes(StandardCharsets.UTF_8), HexUtil.decodeHex(signature));
    }
    /**
     * 生成簽名
     *
     * @param callerID 調(diào)用方的唯一標識
     * @param rawData  原數(shù)據(jù)
     * @return 簽名(十六進制字符串)
     */
    public String sign(String callerID, String rawData) {
        Sign sign = getSignByCallerID(callerID);
        if (ObjectUtils.isEmpty(sign)) {
            return null;
        }
        return sign.signHex(rawData);
    }
    public SignatureProps getSignatureProps() {
        return signatureProps;
    }
    public SignatureProps.KeyPairProps getKeyPairPropsByCallerID(String callerID) {
        return signatureProps.getKeyPair().get(callerID);
    }
    private Sign getSignByCallerID(String callerID) {
        SignatureProps.KeyPairProps keyPairProps = signatureProps.getKeyPair().get(callerID);
        if (ObjectUtils.isEmpty(keyPairProps)) {
            return null; // 無效的、不受信任的調(diào)用方
        }
        return SecureUtil.sign(keyPairProps.getAlgorithm(), keyPairProps.getPrivateKey(), keyPairProps.getPublicKey());
    }
    /**
     * 加載非對稱密鑰對
     */
    private void loadKeyPairByPath() {
        // 支持類路徑配置,形如:classpath:secure/public.txt
        // 公鑰和私鑰都是base64編碼后的字符串
        signatureProps.getKeyPair()
                .forEach((key, keyPairProps) -> {
                    // 如果配置了XxxKeyPath,則優(yōu)先XxxKeyPath
                    keyPairProps.setPublicKey(loadKeyByPath(keyPairProps.getPublicKeyPath()));
                    keyPairProps.setPrivateKey(loadKeyByPath(keyPairProps.getPrivateKeyPath()));
                    if (ObjectUtils.isEmpty(keyPairProps.getPublicKey()) ||
                            ObjectUtils.isEmpty(keyPairProps.getPrivateKey())) {
                        throw new RuntimeException("No public and private key files configured");
                    }
                });
    }
    private String loadKeyByPath(String path) {
        if (ObjectUtils.isEmpty(path)) {
            return null;
        }
        return IoUtil.readUtf8(ResourceUtil.getStream(path));
    }
}

3. 自定義驗簽注解

有些接口需要驗簽,但有些接口并不需要,為了靈活控制哪些接口需要驗簽,自定義一個驗簽注解

import java.lang.annotation.*;

/**
 * 該注解標注于Controller類的方法上,表明該請求的參數(shù)需要校驗簽名
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface VerifySignature {
}

4. AOP實現(xiàn)驗簽邏輯

驗簽邏輯不能放在攔截器中,因為攔截器中不能直接讀取body的輸入流,否則會造成后續(xù)@RequestBody的參數(shù)解析器讀取不到body。

由于body輸入流只能讀取一次,因此需要使用ContentCachingRequestWrapper包裝請求,緩存body內(nèi)容(見第5點),但是該類的緩存時機是在@RequestBody的參數(shù)解析器中。

因此,滿足2個條件才能獲取到ContentCachingRequestWrapper中的body緩存:

  • 接口的入?yún)⒈仨毚嬖贎RequestBody
  • 讀取body緩存的時機必須在@RequestBody的參數(shù)解析之后,比如說:AOP、Controller層的邏輯內(nèi)。注意攔截器的時機是在參數(shù)解析之前的

綜上,注意,標注了@VerifySignature注解的controlle層方法的入?yún)⒈仨毚嬖贎RequestBody,AOP中驗簽時才能獲取到body的緩存!

import cn.hutool.crypto.CryptoException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.ContentCachingRequestWrapper;
import top.ysqorz.common.constant.BaseConstant;
import top.ysqorz.config.SpringContextHolder;
import top.ysqorz.config.aspect.PointCutDef;
import top.ysqorz.exception.auth.AuthorizationException;
import top.ysqorz.exception.param.ParamInvalidException;
import top.ysqorz.signature.model.SignStatusCode;
import top.ysqorz.signature.model.SignatureProps;
import top.ysqorz.signature.util.CommonUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@ConditionalOnBean(SignatureProps.class)
@Component
@Slf4j
@Aspect
public class RequestSignatureAspect implements PointCutDef {
    @Resource
    private SignatureManager signatureManager;
    @Pointcut("@annotation(top.ysqorz.signature.enumeration.VerifySignature)")
    public void annotatedMethod() {
    }
    @Pointcut("@within(top.ysqorz.signature.enumeration.VerifySignature)")
    public void annotatedClass() {
    }
    @Before("apiMethod() && (annotatedMethod() || annotatedClass())")
    public void verifySignature() {
        HttpServletRequest request = SpringContextHolder.getRequest();
        String callerID = request.getParameter(BaseConstant.PARAM_CALLER_ID);
        if (ObjectUtils.isEmpty(callerID)) {
            throw new AuthorizationException(SignStatusCode.UNTRUSTED_CALLER); // 不受信任的調(diào)用方
        }
        // 從請求頭中提取簽名,不存在直接駁回
        String signature = request.getHeader(BaseConstant.X_REQUEST_SIGNATURE);
        if (ObjectUtils.isEmpty(signature)) {
            throw new ParamInvalidException(SignStatusCode.REQUEST_SIGNATURE_INVALID); // 無效簽名
        }
        // 提取請求參數(shù)
        String requestParamsStr = extractRequestParams(request);
        // 驗簽。驗簽不通過拋出業(yè)務異常
        verifySignature(callerID, requestParamsStr, signature);
    }
    @SuppressWarnings("unchecked")
    public String extractRequestParams(HttpServletRequest request) {
        // @RequestBody
        String body = null;
        // 驗簽邏輯不能放在攔截器中,因為攔截器中不能直接讀取body的輸入流,否則會造成后續(xù)@RequestBody的參數(shù)解析器讀取不到body
        // 由于body輸入流只能讀取一次,因此需要使用ContentCachingRequestWrapper包裝請求,緩存body內(nèi)容,但是該類的緩存時機是在@RequestBody的參數(shù)解析器中
        // 因此滿足2個條件才能使用ContentCachingRequestWrapper中的body緩存
        // 1. 接口的入?yún)⒈仨毚嬖贎RequestBody
        // 2. 讀取body緩存的時機必須在@RequestBody的參數(shù)解析之后,比如說:AOP、Controller層的邏輯內(nèi)。注意攔截器的時機是在參數(shù)解析之前的
        if (request instanceof ContentCachingRequestWrapper) {
            ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
            body = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
        }
        // @RequestParam
        Map<String, String[]> paramMap = request.getParameterMap();
        // @PathVariable
        ServletWebRequest webRequest = new ServletWebRequest(request, null);
        Map<String, String> uriTemplateVarNap = (Map<String, String>) webRequest.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
        return CommonUtils.extractRequestParams(body, paramMap, uriTemplateVarNap);
    }
    /**
     * 驗證請求參數(shù)的簽名
     */
    public void verifySignature(String callerID, String requestParamsStr, String signature) {
        try {
            boolean verified = signatureManager.verifySignature(callerID, requestParamsStr, signature);
            if (!verified) {
                throw new CryptoException("The signature verification result is false.");
            }
        } catch (Exception ex) {
            log.error("Failed to verify signature", ex);
            throw new AuthorizationException(SignStatusCode.REQUEST_SIGNATURE_INVALID); // 轉(zhuǎn)換為業(yè)務異常拋出
        }
    }
}
import org.aspectj.lang.annotation.Pointcut;
public interface PointCutDef {
    @Pointcut("execution(public * top.ysqorz..controller.*.*(..))")
    default void controllerMethod() {
    }
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    default void postMapping() {
    }
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    default void getMapping() {
    }
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
    default void putMapping() {
    }
    @Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
    default void deleteMapping() {
    }
    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    default void requestMapping() {
    }
    @Pointcut("controllerMethod() && (requestMapping() || postMapping() || getMapping() || putMapping() || deleteMapping())")
    default void apiMethod() {
    }
}

5. 解決請求體只能讀取一次

解決方案就是包裝請求,緩存請求體。SpringBoot也提供了ContentCachingRequestWrapper來解決這個問題。但是第4點中也詳細描述了,由于它的緩存時機,所以它的使用有限制條件。也可以參考網(wǎng)上的方案,自己實現(xiàn)一個請求的包裝類來緩存請求體

import lombok.NonNull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import top.ysqorz.signature.model.SignatureProps;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@ConditionalOnBean(SignatureProps.class)
@Component
public class RequestCachingFilter extends OncePerRequestFilter {
    /**
     * This {@code doFilter} implementation stores a request attribute for
     * "already filtered", proceeding without filtering again if the
     * attribute is already there.
     *
     * @param request     request
     * @param response    response
     * @param filterChain filterChain
     * @see #getAlreadyFilteredAttributeName
     * @see #shouldNotFilter
     * @see #doFilterInternal
     */
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
            throws ServletException, IOException {
        boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestWrapper = request;
        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestWrapper = new ContentCachingRequestWrapper(request);
        }
        filterChain.doFilter(requestWrapper, response);
    }
}

注冊過濾器

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.ysqorz.signature.model.SignatureProps;
@Configuration
public class FilterConfig {
    @ConditionalOnBean(SignatureProps.class)
    @Bean
    public FilterRegistrationBean<RequestCachingFilter> requestCachingFilterRegistration(
            RequestCachingFilter requestCachingFilter) {
        FilterRegistrationBean<RequestCachingFilter> bean = new FilterRegistrationBean<>(requestCachingFilter);
        bean.setOrder(1);
        return bean;
    }
}

6. 自定義工具類

import cn.hutool.core.util.StrUtil;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public class CommonUtils {
    /**
     * 提取所有的請求參數(shù),按照固定規(guī)則拼接成一個字符串
     *
     * @param body              post請求的請求體
     * @param paramMap          路徑參數(shù)(QueryString)。形如:name=zhangsan&age=18&label=A&label=B
     * @param uriTemplateVarNap 路徑變量(PathVariable)。形如:/{name}/{age}
     * @return 所有的請求參數(shù)按照固定規(guī)則拼接成的一個字符串
     */
    public static String extractRequestParams(@Nullable String body, @Nullable Map<String, String[]> paramMap,
                                              @Nullable Map<String, String> uriTemplateVarNap) {
        // body: { userID: "xxx" }
        // 路徑參數(shù)
        // name=zhangsan&age=18&label=A&label=B
        // => ["name=zhangsan", "age=18", "label=A,B"]
        // => name=zhangsan&age=18&label=A,B
        String paramStr = null;
        if (!ObjectUtils.isEmpty(paramMap)) {
            paramStr = paramMap.entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .map(entry -> {
                        // 拷貝一份按字典序升序排序
                        String[] sortedValue = Arrays.stream(entry.getValue()).sorted().toArray(String[]::new);
                        return entry.getKey() + "=" + joinStr(",", sortedValue);
                    })
                    .collect(Collectors.joining("&"));
        }
        // 路徑變量
        // /{name}/{age} => /zhangsan/18 => zhangsan,18
        String uriVarStr = null;
        if (!ObjectUtils.isEmpty(uriTemplateVarNap)) {
            uriVarStr = joinStr(",", uriTemplateVarNap.values().stream().sorted().toArray(String[]::new));
        }
        // { userID: "xxx" }#name=zhangsan&age=18&label=A,B#zhangsan,18
        return joinStr("#", body, paramStr, uriVarStr);
    }
    /**
     * 使用指定分隔符,拼接字符串
     *
     * @param delimiter 分隔符
     * @param strs      需要拼接的多個字符串,可以為null
     * @return 拼接后的新字符串
     */
    public static String joinStr(String delimiter, @Nullable String... strs) {
        if (ObjectUtils.isEmpty(strs)) {
            return StrUtil.EMPTY;
        }
        StringBuilder sbd = new StringBuilder();
        for (int i = 0; i < strs.length; i++) {
            if (ObjectUtils.isEmpty(strs[i])) {
                continue;
            }
            sbd.append(strs[i].trim());
            if (!ObjectUtils.isEmpty(sbd) && i < strs.length - 1 && !ObjectUtils.isEmpty(strs[i + 1])) {
                sbd.append(delimiter);
            }
        }
        return sbd.toString();
    }
}

代碼地址:GitHub - passerbyYSQ/DemoRepository: 各種開發(fā)小demo

到此這篇關(guān)于SpringBoot實現(xiàn)接口校驗簽名調(diào)用的項目實踐的文章就介紹到這了,更多相關(guān)SpringBoot 接口校驗簽名調(diào)用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 6種方法初始化JAVA中的list集合

    6種方法初始化JAVA中的list集合

    這篇文章主要介紹了6種方法初始化JAVA中的list集合,文中講解非常詳細,代碼幫助大家更好的理解和學習,感興趣的朋友可以了解下
    2020-07-07
  • Feign調(diào)用服務時丟失Cookie和Header信息的解決方案

    Feign調(diào)用服務時丟失Cookie和Header信息的解決方案

    這篇文章主要介紹了Feign調(diào)用服務時丟失Cookie和Header信息的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • 淺談Spring Cloud Ribbon的原理

    淺談Spring Cloud Ribbon的原理

    這篇文章主要介紹了淺談Spring Cloud Ribbon的原理,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-02-02
  • 將本地的jar包打到Maven的倉庫中實例

    將本地的jar包打到Maven的倉庫中實例

    下面小編就為大家分享一篇將本地的jar包打到Maven的倉庫中實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-02-02
  • Java集合框架中迭代器Iterator解析

    Java集合框架中迭代器Iterator解析

    這篇文章主要為大家簡單介紹了Java集合框架中迭代器Iterator的相關(guān)資料,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-03-03
  • 一篇文章帶你了解spring事務失效的多種場景

    一篇文章帶你了解spring事務失效的多種場景

    在日常編碼過程中常常涉及到事務,在前兩天看到一篇文章提到了Spring事務,那么在此總結(jié)下在Spring環(huán)境下事務失效的幾種原因.
    2021-09-09
  • Java垃圾回收機制算法詳解

    Java垃圾回收機制算法詳解

    這篇文章主要介紹了Java垃圾回收機制算法詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-08-08
  • IDEA的maven設(shè)置代理方式

    IDEA的maven設(shè)置代理方式

    文章介紹了如何設(shè)置IntelliJ?IDEA系統(tǒng)代理,并在Maven的Importing和Runner中添加代理配置,包括設(shè)置代理主機和端口
    2025-01-01
  • 從入門到超神進階的Netty群聊系統(tǒng)

    從入門到超神進階的Netty群聊系統(tǒng)

    本篇文章基于Netty做一個聊天室案例加強Netty的熟練度,案例的效果是服務端可以廣播某客戶端的消息給所有客戶端。每個客戶端監(jiān)聽鍵盤輸入來獲取消息,然后發(fā)送給服務端
    2021-08-08
  • Java利用Spire.XLS for Java輕松獲取Excel工作表的名稱

    Java利用Spire.XLS for Java輕松獲取Excel工作表的名稱

    在日常的數(shù)據(jù)處理和自動化報告場景中,我們經(jīng)常需要與 Excel 文件打交道,本文將深入探討如何利用功能強大的 Spire.XLS for Java 庫獲取普通可見工作表的名稱,有需要的小伙伴可以了解下
    2025-10-10

最新評論

黎川县| 河东区| 湘阴县| 哈巴河县| 西乌珠穆沁旗| 惠州市| 林周县| 桃江县| 永清县| 读书| 张家港市| 伊通| 甘德县| 泊头市| 兴城市| 勃利县| 湟源县| 凤城市| 盱眙县| 浠水县| 湘潭县| 织金县| 隆回县| 满洲里市| 正镶白旗| 平顺县| 光山县| 班玛县| 定州市| 英吉沙县| 茶陵县| 平南县| 永靖县| 利辛县| 天等县| 台南县| 桃园县| 淮北市| 岳阳市| 思南县| 呼玛县|