zoukankan      html  css  js  c++  java
  • 前后端分离之接口登陆权限token

      随着业务的需求普通的springmvc+jsp已经不能满足我们的系统了,会逐渐把后台和前端展示分离开来,下面我们就来把普通的springmvc+jsp分为 springmvc只提供rest接口,前端用ajax请求接口渲染到html中。

      后台提供接口是一个tomcat服务器

      前台访问数据是nginx访问rest接口

      但是有一个问题 ,发现没有。就是两个是不同的域名,所以存在跨域,下面我会把一些关键的代码贴出来。

      首先解决接口访问跨域的问题。

      自定义一个拦截请求的Filter

      

    /**
     * post 跨域拦截
    * @Project: children-watch-web-api 
    * @Class JsonpPostFilter 
    * @Description: TODO
    * @author cd 14163548@qq.com
    * @date 2018年1月10日 下午4:12:11 
    * @version V1.0
     */
    @Component
    public class JsonpPostFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            
        }
        @Override
        public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {
            
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            //String origin = (String) servletRequest.getRemoteHost() + ":"+ servletRequest.getRemotePort();
            //构造头部信息
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods","POST, GET, OPTIONS, DELETE");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers","x-requested-with,Authorization,X-Token");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            filterChain.doFilter(servletRequest, servletResponse);
        }
    
        @Override
        public void destroy() {
            
            
        }
        
    }

      然后再配置web.xml

      

    <!-- 跨域配置-->
        <filter>
            <filter-name>cors</filter-name>
            <filter-class>com.axq.watch.web.api.config.JsonpPostFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>cors</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>

      这样就可以实现跨域访问了。

      接下来就是登陆的问题,

      思路:

      1.用户输入账号密码,到后台查询,正确返回服务器生成的token,错误返回相应的错误信息。

      2.用户拿到token保存到本地cookie.

      3.用户要调用相应的接口需要把token传入头部。

      4.后台获取访问的接口,看头部是否有token,在比对是否过期。

      实现代码

      token接口

      

    /**
     * REST 鉴权   
    * @Project: children-watch-api 
    * @Class TokenService 
    * @Description: 登录用户的身份鉴权
    * @author cd 14163548@qq.com
    * @date 2018年1月24日 上午11:43:28 
    * @version V1.0
     */
    public interface TokenLoginService {
    
        String createToken(String openid);  
    
        boolean checkToken(String token); 
        
        String getOpenId(String token);
        
        void deleteToken(String token);
    }

    token接口登陆实现

    /**
     * 
    * @Project: children-watch-service 
    * @Class TokenServiceImpl 
    * @Description: 登录用户的身份鉴权 的实现  这里存入redis
    * @author cd 14163548@qq.com
    * @date 2018年1月24日 上午11:47:23 
    * @version V1.0
     */
    @Service("tokenLoginService")
    public class TokenLoginServiceImpl implements TokenLoginService {
    
        @Autowired
        private RedisCache redisCache;
        
        /**
         * 利用UUID创建Token(用户登录时,创建Token)
         */
        @Override
        public String createToken(String openid) {
            String token = RandomString.createUUID().toUpperCase();
            redisCache.set(token, openid);
            redisCache.expire(token, TokenConstant.TOKEN_EXPIRES_HOUR);
            return token;
        }
    
        @Override
        public boolean checkToken(String token) {
            return StringUtils.isNotBlank(token) && redisCache.hasKey(token);
        }
    
        @Override
        public void deleteToken(String token) {
            redisCache.del(token);
        }
    
        @Override
        public String getOpenId(String token) {
            if(checkToken(token)){
                return (String) redisCache.get(token);
            }
            return "";
        }

      这里我是存入redis中的,方便集群

      自定义一个注解,标识是否忽略REST安全性检查

      

    /**
    * @Project: children-watch-web-api 
    * @Class IgnoreSecurity  自定义注解
    * @Description: 标识是否忽略REST安全性检查
    * @author cd 14163548@qq.com
    * @date 2018年1月24日 下午12:13:21 
    * @version V1.0
     */
    @Target(ElementType.METHOD) //指明该类型的注解可以注解的程序元素的范围 
    @Retention(RetentionPolicy.RUNTIME) //指明了该Annotation被保留的时间长短 
    @Documented  //指明拥有这个注解的元素可以被javadoc此类的工具文档化
    public @interface IgnoreSecurity {
    
    }

    自定义异常

    /**
     * 
    * @Project: children-watch-web-api 
    * @Class TokenLoginException  自定义的RuntimeException
    * @Description: tokenlogin过期时抛出
    * @author cd 14163548@qq.com
    * @date 2018年1月24日 下午2:28:41 
    * @version V1.0
     */
    public class TokenLoginException extends RuntimeException {
    
        private static final long serialVersionUID = 1L;
        
        private String msg;
    
        public TokenLoginException(String msg) {
            super();
            this.msg = msg;
        }
    
        public String getMsg() {
            return msg;
        }
    
        public void setMsg(String msg) {
            this.msg = msg;
        }
    
    }

    异常统一处理

    /**
     * 
    * @Project: children-watch-web-api 
    * @Class ExceptionHandler 统一异常返回处理
    * @Description: 统一异常返回处理
    * @author cd 14163548@qq.com
    * @date 2018年1月24日 下午2:37:07 
    * @version V1.0
     */
    @ControllerAdvice
    @ResponseBody
    public class ExceptionHandle {
    
        private final static Logger logger = LoggerFactory.getLogger(ExceptionHandle.class);
        
        /**
         * 500 - Token is invaild
         */
        @ExceptionHandler(TokenLoginException.class)
        public R handleTokenException(Exception e) {
            logger.error("Token is invaild...", e);
            return R.error("Token is invaild");
        }
        
        /**
         * 500 - Internal Server Error
         */
        @ExceptionHandler(Exception.class)
        public R handleException(Exception e) {
            logger.error("Internal Server Error...", e);
            return R.error("Internal Server Error");
        }
        
        /**
         * 404 - Internal Server Error
         */
        @ExceptionHandler(NotFoundException.class)
        public R notHandleException(Exception e) {
            logger.error("Not Found Error...", e);
            return R.error("Not Found Error");
        }
    }

    aop拦截访问是否忽略登陆检查

    /**
     * 
    * @Project: children-watch-web-api 
    * @Class SecurityAspect 安全检查切面(是否登录检查) 
    * @Description: 通过验证Token维持登录状态
    * @author cd 14163548@qq.com
    * @date 2018年1月24日 下午12:23:19 
    * @version V1.0
     */
    @Component
    @Aspect
    public class SecurityAspect {
    
        /** Log4j日志处理 */
        private static final Logger log = Logger.getLogger(SecurityAspect.class);
        
        @Autowired
        private TokenLoginService tokenLoginService;
        
        @Autowired
        private RedisCache redisCache;
        
        
        /**
         * 環繞通知 前後都通知 
         * aop检测注解为 RequestMapping 就调用此方法
         * @param pjp
         * @return
         * @throws Throwable
         */    
        @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
        public Object execute(ProceedingJoinPoint pjp) throws Throwable {
            // 从切点上获取目标方法
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            log.info("methodSignature : " + methodSignature);
            Method method = methodSignature.getMethod();
            log.info("Method : " + method.getName() + " : "
                    + method.isAnnotationPresent(IgnoreSecurity.class));
            // 若目标方法忽略了安全性检查,则直接调用目标方法
            if(method.isAnnotationPresent(IgnoreSecurity.class)){
                // 调用目标方法
                return pjp.proceed();
            }
            //忽略 api接口测试安全性检查
            if("getDocumentation".equalsIgnoreCase(method.getName())){
                // 调用目标方法
                return pjp.proceed();
            }
            // 从 request header 中获取当前 token
            String token = HttpContextUtils.getHttpServletRequest().getHeader(TokenConstant.LONGIN_TOKEN_NAME);
            // 检查 token 有效性
            if(!tokenLoginService.checkToken(token)){
                String message = String.format("token [%s] is invalid", token);
                log.info("message : " + message);
                throw new TokenLoginException(message);
            }
            //每次调用接口就刷新过期时间
            redisCache.expire(token, TokenConstant.TOKEN_EXPIRES_HOUR);
            // 调用目标方法
            return pjp.proceed();
        }
    }
    
    

    一些工具类

    public class HttpContextUtils {
        public static HttpServletRequest getHttpServletRequest() {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        }
    }
    
    
    /**
     * rest
    * @Project: children-watch-commons 
    * @Class TokenConstant 
    * @Description: 接口登陆 token有效期
    * @author cd 14163548@qq.com
    * @date 2018年1月24日 上午11:55:41 
    * @version V1.0
     */
    public class TokenConstant {
    
        /**
         * token有效期(秒)
         * 1天
         */
        public static final long TOKEN_EXPIRES_HOUR = 86400;
    
        /**  存放Token的header字段 */      
        public static final String LONGIN_TOKEN_NAME = "X-Token";
    }

    接口调用

    @Controller
    public class WeChatLoginController extends BaseController{
    /**
         * 本地测试
         * @param openid
         * @return
         */
        @RequestMapping("/login")
        @ResponseBody
        @IgnoreSecurity //忽略安全性检查
        public R login(String openid){
            logger.info("**** openid **** : " + openid);
            if(StringUtils.isNotBlank(openid)){
                 //创建token
                String createToken = tokenLoginService.createToken(openid);
                logger.info("**** Generate Token **** : " + createToken);
                return R.ok(createToken);
            }
            return R.Empty();
        }
    
        /**
         * 获取openID
         * @return
         */
        @RequestMapping("/openid")
        @ResponseBody
        public R getValue(HttpServletRequest request) {
            String openid = super.getOpenId(request);
            return R.ok(openid);
        }
    }

    spring-context.xml中应配置扫描全部。

    spring-mvc.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans" 
        xmlns:mvc="http://www.springframework.org/schema/mvc" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:p="http://www.springframework.org/schema/p" 
        xmlns:context="http://www.springframework.org/schema/context" 
        xmlns:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context-3.0.xsd 
            http://www.springframework.org/schema/mvc 
            http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
            http://www.springframework.org/schema/aop 
            http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
       
        <!-- spring扫描com.axq.watch.web.*.controller下面所有带注解的类 -->  
        <context:component-scan base-package="com.axq.watch.web" />
    
        <!-- 默认servlet -->  
          <mvc:default-servlet-handler />  
         <!-- 这个标签表示使用注解来驱动 -->  
        <mvc:annotation-driven/>
        
        <!-- 支持Controller的AOP代理 -->
        <aop:aspectj-autoproxy />
    <bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="supportedMediaTypes">
                <list>
                    <value>text/html;charset=UTF-8</value>
                </list>
            </property>
        </bean>
    
    
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" 
              p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
    
        <!-- 上传 -->
        <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">  
            <property name="defaultEncoding" value="utf-8"></property>   
            <property name="maxUploadSize" value="10485760000"></property>  
            <property name="maxInMemorySize" value="40960"></property>  
       </bean>  
    </beans>

    前端代码

    <html>
    <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
    <script type="text/javascript" src="http://www.w3school.com.cn/jquery/jquery.js"></script>
    <script type="text/javascript">
    $(document).ready(function(){
    //登陆获取到的token
     var token = "";
      $("#b01").click(function(){
       $.ajax({  
    //          url:"http://localhost:8081/rest/itemcat/list?callback=getMessage",  
                url:"http://localhost:8081/children-watch-web-api/login",      
                type:"post",  
                cache:false,  
                data:{openid:"ocHCAwMdYLevTBbcYrKh07FJJ56E"},
                dataType:"json",  
                /**beforeSend: function(request) {
                    request.setRequestHeader("X-Token", token);
                },*/
                success:function(data){  
                    
                    var html = data.msg;
                    token = html ;
                    $("#myDiv").html("token:"+html);
                },  
                error:function(){  
                    alert("发生异常");  
                }  
         });  
        
      });
      
       $("#b02").click(function(){
       $.ajax({  
                url:"http://localhost:8081/children-watch-web-api/openid",    
                //url:"http://localhost:8081/children-watch-web-api/config/list",     
                //url:"http://localhost:8081/children-watch-web-api/student/list",                             
                type:"get",  
                cache:false,  
                dataType:"json",  
                beforeSend: function(request) {
                    request.setRequestHeader("X-Token", token);
                },
                success:function(data){  
                    var html = data.msg;
                    $("#myDiv").html("openId:"+html);
                },  
                error:function(e){  
                    alert("发生异常"+e);  
                },
                complete: function(XMLHttpRequest, status) { //请求完成后最终执行参数     
                    alert(status);
                }
         });  
        
      });
    });
    </script>
    </head>
    <body>
    
    <div id="myDiv"><h2>通过 AJAX 改变文本</h2></div>
    <button id="b01" type="button">登陆</button>
    
    <button id="b02" type="button">查询</button>
    </body>
    </html>

    效果演示。

    直接点查询没有token

    点登陆 获取了token

    在点查询 就可以获取到值了。

    收工。

  • 相关阅读:
    数据结构学习
    古兰查询 之查询页面隐藏
    Qt只QSetting
    学习下知然网友写的taskqueue
    producter-consumer 他山之石
    unix缓冲
    Buffering of C streams
    POCO Log库
    linux下open和fopen的区别
    dup2替换
  • 原文地址:https://www.cnblogs.com/iathanasy/p/8350156.html
Copyright © 2011-2022 走看看