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

    场景:

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

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

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

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

    简图说明:

      

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

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

    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

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

      

  • 相关阅读:
    SSM 框架-05-详细整合教程(Eclipse版)(Spring+SpringMVC+MyBatis)
    SSM 框架-04-使用maven创建web项目
    SSM 框架-03-MyEclipse+Tomcat+MAVEN+SVN项目完整环境搭建
    SSM 框架-02-MyEclipse 2017 安装与破解
    什么是J2EE
    Web前端和后端开发的区别和要求
    SSM 框架集-01-详细介绍-入门问题篇
    MUI框架-11-MUI前端 +php后台接入百度文字识别API
    MUI框架-10-MUI 数据交互-跳转详情页面
    MUI框架-09-MUI 与后台数据交互
  • 原文地址:https://www.cnblogs.com/sxdcgaq8080/p/9013955.html
Copyright © 2011-2022 走看看