【spring cloud】对接口调用者提供API使用的安全验证微服务【这里仅通过代码展示一种设计思想】【后续可以加入redis限流的功能,某段时间某个IP可以访问API几次】

场景:

  公司的微服务集群,有些API 会对外提供接口,供其他厂商进行调用。这些公开的API接口,由一个OpenAPI微服务统一提供给大家。

  那么所有的调用者在调用公开API接口的时候,需要验证是否有权限调用API 接口。

  这套验证的工作,同样也在OpenAPI中为调用者提供验证。

==============================================================================================

简图说明:

  【spring cloud】对接口调用者提供API使用的安全验证微服务【这里仅通过代码展示一种设计思想】【后续可以加入redis限流的功能,某段时间某个IP可以访问API几次】

===============================================================================================================

 正文仅通过贴出来的代码展示

OpenAPI这个微服务在整个服务体系中做了什么事情

1.用户通过提供loginName和loginPwd来获取sessionKey

2.在获取sessionKey过程中,将

  [loginName:sessionKey]

  [sessionKey:JSON.toJSONString(userInfo)]

存入redis,并设置了有效期

3.用户每次访问接口,都要提供loginName+sign+调用API所需的参数列表

4.服务器端自定义拦截器,拦截到用户request,根据loginName取出sessionKey,按照规则生成sign

5.对比用户传入的sign和服务器端生成的sign,如果一致,则允许调用API

===============================================================================================================

代码说明:

1.pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.pisen</groupId>
        <artifactId>pisen-cloud-luna</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>pisen-cloud-luna-ms-openapi</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <!-- Spring boot 1.5.x 的 data-jpa 依赖暂时还没有 所有采用 1.4.x的 data-jpa -->
            <version>1.4.7.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.googlecode.log4jdbc</groupId>
            <artifactId>log4jdbc</artifactId>
            <version>1.2</version>
        </dependency>

                 <!-- ======================== 工具 ======================== END -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.6</version>
        </dependency>


        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.0</version>
        </dependency>

        <dependency>
            <groupId>com.xiaoleilu</groupId>
            <artifactId>hutool-all</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>com.belerweb</groupId>
            <artifactId>pinyin4j</artifactId>
            <version>2.5.0</version>
        </dependency>

        <!-- ======================== 工具 ======================== END -->

        <!-- 连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.11</version>
        </dependency>


        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
            <!-- Spring boot 1.5.x 的Redis依赖暂时还没有 所有采用 1.4.x的Redis -->
            <version>1.4.7.RELEASE</version>
        </dependency>


        <dependency>
            <groupId>com.pisen</groupId>
            <artifactId>pisen-cloud-luna-core</artifactId>
            <version>${parent.version}</version>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>com.pisen</groupId>
            <artifactId>pisen-cloud-luna-feign-ten</artifactId>
            <version>${parent.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.4</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
    
View Code

2.提供获取sessionKey的API 接口,地址是/free/sessionKey

FreeAPi 

package com.pisen.cloud.luna.ms.openapi.api;

import com.pisen.cloud.luna.core.result.AjaxResult;
import com.pisen.cloud.luna.ms.openapi.base.domain.SysUser;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * 开发者说明文档 以静态资源文件提供
 */
@RequestMapping("/free")
public interface IFreeApi {

    /**
     * 获取sessionKey
     *
     *  帐号/密码 非空
     * @param sysUser
     * @return  返回sessionkey
     * @throws Exception
     */
    @RequestMapping(value = "/sessionKey",method = RequestMethod.POST)
    public AjaxResult<String> getSessionKey(SysUser sysUser) throws Exception;

}
View Code
package com.pisen.cloud.luna.ms.openapi.api.impl;

import com.pisen.cloud.luna.core.result.AjaxResult;
import com.pisen.cloud.luna.core.result.LunaResultBean;

import com.pisen.cloud.luna.ms.openapi.base.domain.SysUser;
import com.pisen.cloud.luna.ms.openapi.api.IFreeApi;
import com.pisen.cloud.luna.ms.openapi.base.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FreeApi implements IFreeApi {

    @Autowired
    SysUserService sysUserService;

    @Override
    public AjaxResult<String> getSessionKey(@RequestBody SysUser sysUser) throws Exception {

        AjaxResult<String> res = new AjaxResult<String>();

        LunaResultBean.checkField(sysUser, "loginName","loginPwd");

        String sessionKey = sysUserService.login(sysUser);

        if(StringUtils.isNotBlank(sessionKey)){

            res.initTrue(sessionKey);
        }else{
            res.initFalse("获取sessionKey失败:帐号密码错误", AjaxResult.ERROR_BUSINESS);
        }

        return res;
    }
}
View Code

统一响应体

package com.pisen.cloud.luna.core.result;

public class AjaxResult<T> extends LunaResultBean{
    
    public AjaxResult(){}

    public AjaxResult(boolean success, String msg, int code, T obj) {
        super(success,msg,code);
        this.obj = obj;
    }
    
    private T obj;

    
    public boolean isSuccess() {
        return success;
    }

    
    public void setSuccess(boolean success) {
        this.success = success;
    }

    
    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    
    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
    
    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
    
    public void initTrue(T obj){
        this.success = true;
        this.msg = "successful";
        this.code = SUCCESS_REQUEST;
        this.obj = obj;
    }
    
    public void initFalse(String msg, int code,T obj){
        initFalse(msg, code);
        this.obj = obj;
    }
    
    
    
}
View Code
package com.pisen.cloud.luna.core.result;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;

import com.pisen.cloud.luna.core.enums.LunaZullErrorMSG;
import com.pisen.cloud.luna.core.exceptions.LunaException;

public class LunaResultBean {
    
    /**
     * 参数错误返回码
     */
    public static final int SUCCESS_REQUEST = 200;
    /**
     * 参数错误返回码
     */
    public static final int ERROR_PARAMS = 100001;
    
    /**
     * 业务错误返回码
     */
    public static final int ERROR_BUSINESS = 200001;
    /**
     * 系统异常返回码
     */
    public static final int ERROR_SYS_EXCPTION = 500001;
    
    
    protected boolean success;
    
    protected String msg;
    
    protected int code;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
    
    public LunaResultBean() {
        super();
    }

    public LunaResultBean(boolean success, String msg, int code) {
        super();
        this.success = success;
        this.msg = msg;
        this.code = code;
    }

    public void initFalse(String msg, int code){
        throw new LunaException(msg, code);
    }
    public void initFalse2(String msg, int code){
        this.success = false;
        this.msg = msg;
        this.code = code;
    }
    
    
    public void initFalse(LunaZullErrorMSG lunaZullErrorMSG){
        this.success = false;
        this.msg = lunaZullErrorMSG.getMsg();
        this.code = lunaZullErrorMSG.getCode();
    }
    
    
    
    /**
     * 字段检验 方法 
     * @param clazz 需要检验的对象 
     * @param propertys
     * @return
     */
    public static void checkField(Object obj,String...propertys) throws LunaException{
        
        if(obj != null && propertys != null && propertys.length > 0){
            //字节码
            Class<? extends Object> clazz = obj.getClass();
            
            //遍历所有属性
            for (int i = 0; i < propertys.length; i++) {
                String property = propertys[i];
                //内省机制获取属性信息
                PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(clazz,property );
                if(pd != null){
                    //获取当前字段的javabean读方法
                    Method readMethod = pd.getReadMethod();
                    if(readMethod != null){
                        
                        Object invoke = null;
                        
                        try {
                            invoke = readMethod.invoke(obj);
                        } catch (Exception e) {
                            throw new LunaException("方法 "+ readMethod.getName() +"无法执行",AjaxResult.ERROR_SYS_EXCPTION);
                        }
                        
                        if(invoke != null){
                            //String类型单独处理
                            Class<?> propertyType = pd.getPropertyType();
                            if("java.lang.String".equals(propertyType.getName())){
                                
                                if(StringUtils.isBlank((String)invoke)){
                                    throw new LunaException("错误 : [ " + property + " ] 不能为空!",AjaxResult.ERROR_PARAMS);
                                }
                                
                            }else if("java.util.List".equals(propertyType.getName())){
                                List list = (List)invoke;
                                if(list.size() == 0){
                                    throw new LunaException("错误 : [ " + property + " ] 不能为空!",AjaxResult.ERROR_PARAMS);
                                }
                            }
                        }else{
                            throw new LunaException("错误 : [ " + property + " ] 不能为空!",AjaxResult.ERROR_PARAMS);
                        }
                        
                    }else{
                        //抛出异常
                        throw new LunaException("在 " + clazz +"中 找不到"+"[ " + property + " ] 的 读方法",AjaxResult.ERROR_SYS_EXCPTION);
                    }
                    
                }else{
                    //抛出异常
                    throw new LunaException("在 " + clazz +"中 找不到"+"[ " + property + " ] 属性",AjaxResult.ERROR_SYS_EXCPTION);
                }
            }
        }
    }
    
    /**
     * 单一字段验证
     * @param obj 需要验证的对象
     * @param property 对象字段名
     * @throws LunaException 
     */
    public static void simplCheckField(Object obj,String property) throws LunaException{
        
        if(obj instanceof String){
            if(StringUtils.isBlank((String)obj)){
                throw new LunaException("错误 : [ " + property + " ] 不能为空!",AjaxResult.ERROR_PARAMS);
            }
        }else if(obj instanceof List){
            List list = (List)obj;
            if(list.size() == 0){
                throw new LunaException("错误 : [ " + property + " ] 不能为空!",AjaxResult.ERROR_PARAMS);
            }
        }else{
            if(obj == null){
                throw new LunaException("错误 : [ " + property + " ] 不能为空!",AjaxResult.ERROR_PARAMS);
            }
        }
    }
    
    
}
View Code

service层

package com.pisen.cloud.luna.ms.openapi.base.service.impl;


import com.pisen.cloud.luna.ms.openapi.base.dao.SysUserDao;
import com.pisen.cloud.luna.ms.openapi.base.domain.SysUser;
import com.pisen.cloud.luna.ms.openapi.base.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    SysUserDao sysUserDao;

    @Autowired
    RedisService redisService;

    @Override
    public String login(SysUser sysUser) {
        SysUser user = sysUserDao.findSysUserByLoginNameAndLoginPwdAndEnabled(sysUser.getLoginName(),sysUser.getLoginPwd(),1);

        String sessionKey = null;
        if (user != null){
            sessionKey = redisService.getSessionKey(user);
        }
        return sessionKey;
    }
}
View Code

需要引入的dao层和RedisService

package com.pisen.cloud.luna.ms.openapi.base.dao;
;
import com.pisen.cloud.luna.ms.openapi.base.domain.SysUser;
import feign.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;

public interface SysUserDao extends JpaRepository<SysUser, Long>,JpaSpecificationExecutor<SysUser> {

    //通过 登录名 + 登录密码 + 是否启用 查找user
    SysUser findSysUserByLoginNameAndLoginPwdAndEnabled(String loginName,String loginPwd,int enable);
    
}
View Code
package com.pisen.cloud.luna.ms.openapi.base.service.impl;

import com.alibaba.fastjson.JSON;
import com.pisen.cloud.luna.core.utils.MD5Util;
import com.pisen.cloud.luna.ms.openapi.base.domain.SysUser;
import com.pisen.cloud.luna.ms.openapi.base.utils.MsOpenApiRedisUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Redis 操作接口调用者 的 信息
 *
 */
@Component
public class RedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;


    /**
     * 调用者认证信息存入在redis为[K:V],分别存入两层
     *  1.[loginName:sessionKey]
     *  2.[sessionKey:userInfo]
     *  层次存储,所以具体的存入规则为
     *  1.[MS-OPENAPI:SESSION_KEY_IN_LOGIN_NAME:loginName,sessionKey]
     *  2.[MS-OPENAPI:USER_INFO_IN_SESSION_KEY:sessionKey,JSON.toJSONString(user对象)]
     *
     * sessionKey是随机生成的,这里使用UUID生成,生成规则为
     * String key1 = 登录名+"_"+ UUID.randomUUID().toString();
     * String key2 = MD5Util.GetMD5Code(key1);
     * String key3 = MD5Util.GetMD5Code(key2+"openApi");
     * 最后的key2就是返回给调用者的sessionKey
     * 最后的key3就是redis中存储使用的sessionKey
     * @param sysUser
     * @return
     */
    public String getSessionKey(SysUser sysUser){
        String loginName = sysUser.getLoginName();
        String sessionkeyInLoginName = MsOpenApiRedisUtil.SESSION_KEY_IN_LOGIN_NAME+loginName;

        String oldSessionKey = redisTemplate.opsForValue().get(sessionkeyInLoginName);

        //如果 oldSessionKey存在
        if (StringUtils.isNotBlank(oldSessionKey)){
            //本次属于重复登录,则需要删除原本的登录信息
            redisTemplate.delete(MsOpenApiRedisUtil.USER_INFO_IN_SESSION_KEY+oldSessionKey);
        }

        //重新生成sessionKey
        String key1 = loginName+"_"+ UUID.randomUUID().toString();
        //返回给用户的sessionKey
        String key2 = MD5Util.GetMD5Code(key1);
        //redis中存储使用的sessionKey
        String key3 = MD5Util.GetMD5Code(key2+"openApi");

        //存储  用户名:sessionKey 30分钟过期
        redisTemplate.opsForValue().set(MsOpenApiRedisUtil.SESSION_KEY_IN_LOGIN_NAME+loginName,key3,30, TimeUnit.MINUTES);
        //存储  sessionKey:userInfo 30分钟过期
        redisTemplate.opsForValue().set(MsOpenApiRedisUtil.USER_INFO_IN_SESSION_KEY+key3, JSON.toJSONString(sysUser),30,TimeUnit.MINUTES);

        //返回给调用者的是 初次MD5的sessionKey,防止调用者可以直接用sessionKey获取用户信息
        return key2;
    }


    /**
     * 根据sessionKey获取用户信息
     * @param sessionKey
     * @return
     */
    public SysUser getUserInfo(String sessionKey){
        String userInfoInSessionKey = MsOpenApiRedisUtil.USER_INFO_IN_SESSION_KEY+MD5Util.GetMD5Code(sessionKey+"openApi");

        String jsonStr = redisTemplate.opsForValue().get(userInfoInSessionKey);
        if (StringUtils.isNotBlank(jsonStr)){

            try {
                SysUser sysUser = JSON.parseObject(jsonStr,SysUser.class);
                String loginName = sysUser.getLoginName();

                //重新设置过期时间为30分钟,刷新时间
                redisTemplate.expire(MsOpenApiRedisUtil.SESSION_KEY_IN_LOGIN_NAME+loginName,30,TimeUnit.MINUTES);
                redisTemplate.expire(userInfoInSessionKey,30,TimeUnit.MINUTES);

                return sysUser;
            }catch (Exception e){
                e.printStackTrace();
            }

        }
        return null;
    }

    /**
     * 取消用户登录状态
     * @param loginName
     */
    public boolean deleteSessionKey(String loginName){
        String sessionInLoginName = MsOpenApiRedisUtil.SESSION_KEY_IN_LOGIN_NAME+loginName;
        String oldSessionKey =  redisTemplate.opsForValue().get(sessionInLoginName);

        //如果用户在登录状态,则清除用户相关信息
        if (StringUtils.isNotBlank(oldSessionKey)) {
            redisTemplate.delete(MsOpenApiRedisUtil.USER_INFO_IN_SESSION_KEY+oldSessionKey);
            redisTemplate.delete(sessionInLoginName);

            return true;
        }

        return false;
    }

}
View Code

redis用到的key规则

package com.pisen.cloud.luna.ms.openapi.base.utils;

public class MsOpenApiRedisUtil {

    //redis空间名称
    private static final String NAME_SPACE = "MS-OPENAPI:";

    //通过 loginName 获取对应的 SESSION KEY
    public static final String SESSION_KEY_IN_LOGIN_NAME = NAME_SPACE + "SESSION_KEY_IN_LOGIN_NAME:";

    //通过 session key 获取对应的 用户信息
    public static final String USER_INFO_IN_SESSION_KEY = NAME_SPACE + "USER_INFO_IN_SESSION_KEY:";
}
View Code

3.spring boot项目中自定义拦截器

package com.pisen.cloud.luna.ms.openapi.api.interceptors;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.pisen.cloud.luna.core.result.AjaxResult;
import com.pisen.cloud.luna.core.result.LunaResultBean;
import com.pisen.cloud.luna.ms.openapi.api.beans.OpenApiResult;
import com.pisen.cloud.luna.ms.openapi.base.feign.tenement.client.FeignMsTenClient;
import com.pisen.cloud.luna.ms.openapi.base.utils.MsOpenApiRedisUtil;
import com.pisen.cloud.luna.ms.openapi.base.utils.OpenApiSecureUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Set;
import java.util.TreeMap;

/**
 * OpenApi拦截器
 *
 */
public class OpenApiInterceptor implements HandlerInterceptor {

    public static final String[] FREE_URIS = new String[] { "/sessionKey" };

    public static final ThreadLocal<String> OPENAPI_REQUEST_DATA = new ThreadLocal<>();

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    FeignMsTenClient feignMsTenClient;

    /**
     * 该方法将在请求处理之前进行调用,只有该方法返回true,才会继续执行后续的Interceptor和Controller,
     * 当返回值为true 时就会继续调用下一个Interceptor的preHandle 方法,
     * 如果已经是最后一个Interceptor的时候就会是调用当前请求的Controller方法
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o
     * @return
     * @throws Exception
     *
     * httpServletRequest 中提供 account  sign  API所需参数列表
     * redis中存储的[K,V]是 [account,sessionKey][sessionKey,JSON(用户信息)]
     * 【注意】用户提供的sign是使用 调用了API提供的getSessionKey()方法之后得到的sessionKey 再进行 MD5Util.GetMD5Code(sessionKey+"openApi")处理,再按照下面的规则生成的
     *
     * 本拦截器的作用:拦截指定路径的API调用,在调用API,进入API之前【即sign生成规则】
     * 1.从request中获取到account,根据这个用户提供的account 从redis中获取到sessionKey
     * 2.将request中除了sign之外的所有请求参数,按照字段名ASCII码字典序,从小到大排序
     * 3.将排序好的按照URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1
     * 4.string1+sessionKey 得到string2
     * 5.对string2作md5签名成32位小写字符串,也就是进行hash一致性算法,得到服务器端[也就是本拦截器]生成的sign值
     * 6.对比服务器端sign和API调用者传进来的sign,如果签名一致,return true;允许调用API接口
     */
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {

        //标识  是否通过拦截器往下一层拦截器走或往controller走
        boolean toNext = false;

        //URL 过滤
        String requestURI = getCleanUri(httpServletRequest.getRequestURI());

        if(StringUtils.isNotBlank(requestURI)){
            for (String freeUri : FREE_URIS) {
                // 如果当前路径中包含不需要拦截的路径的话 则放行
                if (requestURI.contains(freeUri)) {
                    toNext = true;
                }
            }
            OPENAPI_REQUEST_DATA.set(OpenApiInterceptor.getOpenApiRequestData(httpServletRequest));
        }

        OpenApiResult result = new OpenApiResult();

        if (!toNext){
            //拿到request中的数据
            String requestData = OPENAPI_REQUEST_DATA.get();
            //json序列化 request中的数据
            JSONObject jsonObject = JSON.parseObject(requestData);

            //获取 接口调用者提供的 账户名和签名
            String account = jsonObject.getString("account");
            String sign = jsonObject.getString("sign");


            if (StringUtils.isNotBlank(account) && StringUtils.isNotBlank(sign)){
                ValueOperations<String,String> operations = redisTemplate.opsForValue();

                //获取Redis中存储的本账户的sessionKey[30分钟有效期]
                String oldSessionKey = operations.get(MsOpenApiRedisUtil.SESSION_KEY_IN_LOGIN_NAME+account);
                //如果SessionKey有效 则验证 签名 sign
                if (StringUtils.isNotBlank(oldSessionKey)){
                    TreeMap<String,String> treeMap = new TreeMap<>();
                    Set<String> objSet = jsonObject.keySet();

                    for (String key: objSet){

                        if (!"sign".equals(key)){
                            //放进treeMap就是字典序排序,除了sign之外其余都要参与排序
                            treeMap.put(key,jsonObject.getString(key));
                        }
                    }

                    String createSign = OpenApiSecureUtil.getSign(treeMap,oldSessionKey);

                    if (sign.equals(createSign)){
                        toNext = true;
                    }
                }else{
                    toNext = false;
                    result.setMessage("sessionKey过期,调用失败");
                }
            }else{
                toNext = false;
                result.setMessage("account/sign无效,调用失败");
            }
        }

        if (!toNext){
            httpServletResponse.setCharacterEncoding("utf-8");
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.write(JSON.toJSONString(result));
        }

        httpServletResponse.setContentType("text/plain;charset=UTF-8");
        return toNext;
    }

    /**
     * 该方法将在请求处理之后,DispatcherServlet进行视图返回渲染之前进行调用,
     * 可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 该方法也是需要当前对应的Interceptor的preHandle方法的返回值为true时才会执行,该方法将在整个请求结束之后,
     * 也就是在DispatcherServlet 渲染了对应的视图之后执行。用于进行资源清理。
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o
     * @param e
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }

    // 获取干净的API 去除多余的/
    public static String getCleanUri(String uri) {

        String[] split = uri.split("\/");

        StringBuffer sb = new StringBuffer();
        for (String string : split) {
            if (StringUtils.isNotBlank(string)) {
                sb.append("/" + string);
            }
        }
        return sb.toString();
    }

    /**
     * 获取request中的数据
     * @param request
     * @return
     */
    public static String getOpenApiRequestData(HttpServletRequest request){
        try {

            int contentLength = request.getContentLength();
            if (contentLength < 0) {
                return null;
            }
            byte buffer[] = new byte[contentLength];
            for (int i = 0; i < contentLength;) {

                int readlen = request.getInputStream().read(buffer, i, contentLength - i);
                if (readlen == -1) {
                    break;
                }
                i += readlen;
            }

            String charEncoding = request.getCharacterEncoding();
            if (charEncoding == null) {
                charEncoding = "UTF-8";
            }
            return new String(buffer, charEncoding);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}
View Code

需要返回的结构

package com.pisen.cloud.luna.ms.openapi.api.beans;

public class OpenApiResult<T> {

    private String message;

    private int result;

    private String sessionKey;

    private T object;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public int getResult() {
        return result;
    }

    public void setResult(int result) {
        this.result = result;
    }

    public String getSessionKey() {
        return sessionKey;
    }

    public void setSessionKey(String sessionKey) {
        this.sessionKey = sessionKey;
    }

    public T getObject() {
        return object;
    }

    public void setObject(T object) {
        this.object = object;
    }

    public void initTrue(String token) {
        message = "调用成功";
        result = 1;
        sessionKey = token;
        object = null;
    }

    public void initTrue(String token,T object){
        message = "调用成功";
        result = 1;
        sessionKey = token;
        this.object = object;
    }

    public void initFalse(String message) {
        this.message = message;
        result = 2;
        sessionKey = "";
        object = null;
    }
}
View Code

生成sign的工具类

package com.pisen.cloud.luna.ms.openapi.base.utils;

import org.apache.commons.codec.digest.DigestUtils;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class OpenApiSecureUtil {


    /**
     * 完成算法的3.4.5步 获取到服务器端自己生成的sign
     * 3.将排序好的按照URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1
     * 4.string1+sessionKey 得到string2
     * 5.对string2作md5签名成32位小写字符串,也就是进行hash一致性算法,得到服务器端[也就是本拦截器]生成的sign值
     * @param treeMap
     * @param sessionKey
     * @return
     */
    public static String getSign(TreeMap<String,String> treeMap,String sessionKey){
        StringBuilder stringBuilder = new StringBuilder(200);
        Set<Map.Entry<String, String>> entrySet = treeMap.entrySet();
        for (Map.Entry<String, String> entry : entrySet) {
            stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        String string = stringBuilder.substring(0, stringBuilder.length() - 1) + sessionKey;

        try {

            byte[] array = computeHash(string);

            stringBuilder.delete(0,stringBuilder.length());

            for (int i = 0; i < array.length; i++) {

                byte b = array[i];

                String text = Integer.toHexString(b & 0xFF);

                if (text.length() == 1) {
                    stringBuilder.append("0");
                }
                stringBuilder.append(text);
            }

            return stringBuilder.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     *  hash一致性算法
     *  对比DigestUtils.md5Hex(string);到底有什么区别
     * @param string
     * @return
     * @throws NoSuchAlgorithmException
     */
    public static byte[] computeHash(String string) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance("MD5");
        digest.reset();
        byte[] utf8bytes = null;
        try {
            utf8bytes = string.getBytes("UTF-8");
            // digest.update(utf8bytes);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return digest.digest(utf8bytes);
    }

}
View Code

4.把自定义的拦截器 添加到配置中可以自动被加载

package com.pisen.cloud.luna.ms.openapi.init.config;

import com.pisen.cloud.luna.ms.openapi.api.interceptors.OpenApiInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class OpenApiConfiguration  extends WebMvcConfigurerAdapter{

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new OpenApiInterceptor()).addPathPatterns("/openApi/**").excludePathPatterns("/free/**");
        super.addInterceptors(registry);
    }

}
View Code

5.当然 最后可以写一个 示例,提供给调用者调用的API接口

@RestController
@RequestMapping("/openApi")
public class OpenApi {

    @RequestMapping("/test")
    public AjaxResult<String> test(){
        System.out.println(123);
        AjaxResult<String> result = new AjaxResult<>();
        result.initTrue("123");
        return result;
    }
}
View Code

整个的思想 就是上面的这样。

  

相关推荐