精华内容
下载资源
问答
  • java 创建线程的三种方式、创建线程池的四种方式

    万次阅读 多人点赞 2019-02-23 21:01:44
    java创建线程的三种方式: 继承Thread类创建线程类 实现Runnable接口 通过Callable和Future创建线程 java创建线程池的四种方式: newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理...

    概要:

    java创建线程的三种方式:

    1.     继承Thread类创建线程类
    2.     实现Runnable接口
    3.     通过Callable和Future创建线程

       

    java创建线程池的四种方式:

         newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程

        newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

        newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行

        newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定

    顺序(FIFO,LIFO,优先级)执行

     

    线程池的优点:

         a. 重用存在的线程,减少对象创建、消亡的开销,性能佳
         b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
         c. 提供定时执行、定期执行、单线程、并发数控制等功能。

     

    第一 Java中创建线程主要有三种方式:

    1、继承Thread类创建线程类 (extends)

    (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体(线程体)

    (2)创建Thread子类的实例,即创建了线程对象。

    (3)调用线程对象的start()方法来启动该线程。  

      
    public class FirstThreadTest extends Thread{  
        int i = 0;  
        //重写run方法,run方法的方法体就是现场执行体  
        public void run()  
        {  
            for(;i<100;i++){  
            log.info(getName()+"  "+i);  
            }  
        }  
    
        public static void main(String[] args)  
        {  
            for(int i = 0;i< 100;i++)  
            {  
                log.info(Thread.currentThread().getName()+"  : "+i);  
                if(i==20)  
                {  
                    new FirstThreadTest().start();  
                    new FirstThreadTest().start();  
                }  
            }  
        }  
    } 

     

    上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。

     

    2、通过Runnable接口创建线程类

    (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

    (2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

    (3)调用线程对象的start()方法来启动该线程。

    public class RunnableThreadTest implements Runnable  
    {  
      
        private int i;  
        public void run()  
        {  
            for(i = 0;i <100;i++)  
            {  
               log.info(Thread.currentThread().getName()+" "+i);  
            }  
        }  
    
        public static void main(String[] args)  
        {  
            for(int i = 0;i < 100;i++)  
            {  
                log.info(Thread.currentThread().getName()+" "+i);  
                if(i==20)  
                {  
                    RunnableThreadTest runner= new RunnableThreadTest();  
                    new Thread(runner,"新线程1").start();  
                    new Thread(runner,"新线程2").start();  
                }  
            }  
        }   
    }  

     

    线程的执行流程很简单,当执行代码start()时,就会执行对象中重写的void run()方法,该方法执行完成后,线程就消亡了。

     

    3、通过Callable和Future创建线程

    (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值

    public interface Callable
    {
      V call() throws Exception;
    }

    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方

    法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)

    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    public class CallableThreadTest implements Callable<Integer>  
    {  
      
        public static void main(String[] args)  
        {  
            CallableThreadTest ctt = new CallableThreadTest();  
            FutureTask<Integer> ft = new FutureTask<>(ctt);  
            for(int i = 0;i < 100;i++)  
            {  
                log.info(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
                if(i==20)  
                {  
                    new Thread(ft,"有返回值的线程").start();  
                }  
            }  
            try  
            {  
                log.info("子线程的返回值:"+ft.get());  
            } catch (InterruptedException e)  
            {  
                e.printStackTrace();  
            } catch (ExecutionException e)  
            {  
                e.printStackTrace();  
            }  
      
        }  
      
        @Override  
        public Integer call() throws Exception  
        {  
            int i = 0;  
            for(;i<100;i++)  
            {  
                log.info(Thread.currentThread().getName()+" "+i);  
            }  
            return i;  
        }  
      
    }  

     

    二、创建线程的三种方式的对比

    1、采用实现Runnable、Callable接口的方式创建多线程

          优势:

           线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

           在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

           劣势

         编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法

    2、使用继承Thread类的方式创建多线程

          优势:

          编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程

          劣势:

          线程类已经继承了Thread类,所以不能再继承其他父类。

    3、Runnable和Callable的区别

         (1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。

         (2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

         (3) call方法可以抛出异常,run方法不可以。

         (4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的

    完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。

     

    第二 创建四种线程池的方式

    1、new Thread的弊端

    执行一个异步任务你还只是如下new Thread吗?

    new Thread(new Runnable() {
    
    
      @Override
    
      public void run() {
    
        // TODO Auto-generated method stub
    
        }
    
      }
    
    ).start();

     

    那你就out太多了,new Thread的弊端如下:

         a. 每次new Thread新建对象性能差
         b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom(out of memory)。
         c. 缺乏更多功能,如定时执行、定期执行、线程中断。

    相比new Thread,Java提供的四种线程池的好处在于:

              a. 重用存在的线程,减少对象创建、消亡的开销,性能佳
              b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
              c. 提供定时执行、定期执行、单线程、并发数控制等功能。

    2、Java 线程池

    Java通过Executors提供四种线程池,分别为:

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。


    (1) newCachedThreadPool:

    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例代码如下:

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    
      for (int i = 0; i < 10; i++) {
    
        final int index = i;
    
      try {
    
        Thread.sleep(index * 1000);
    
       } catch (InterruptedException e) {
    
          e.printStackTrace();
    
      }
    
    
    
       cachedThreadPool.execute(new Runnable() {
    
         @Override
    
         public void run() {
    
            log.info(index);
    
          }
    
       });
    
    }

    线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

     

    (2) newFixedThreadPool:---  需要指定线程池的大小

    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
    
      for (int i = 0; i < 10; i++) {
    
      final int index = i;
    
    
      fixedThreadPool.execute(new Runnable() {
    
    
      @Override
    
      public void run() {
     
          try {
             log.info(index);
             Thread.sleep(2000);
          } catch (InterruptedException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
           }
        }
      });
    }

    因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。

    定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。

     

    (3)newScheduledThreadPool:

    创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
    
     scheduledThreadPool.schedule(new Runnable() {
    
    
          @Override
    
          public void run() {
    
             log.info("delay 3 seconds");
           }
    
     }, 3, TimeUnit.SECONDS);

    表示延迟3秒执行。

    定期执行示例代码如下:

    scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
    
          @Override
    
          public void run() {
    
              log.info("delay 1 seconds, and excute every 3 seconds");
    
          }
    
    }, 1, 3, TimeUnit.SECONDS);

    表示延迟1秒后每3秒执行一次。

    ScheduledExecutorService比Timer更安全,功能更强大

     

    (4)newSingleThreadExecutor:

    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:

    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    
    for (int i = 0; i < 10; i++) {
      final int index = i;
      singleThreadExecutor.execute(new Runnable() {
    
        @Override
        public void run() {
        try {
            log.info(index);
            Thread.sleep(2000);
         } catch (InterruptedException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
        }
      });
    }

    结果依次输出,相当于顺序执行各个任务。

    线程池的作用:

    线程池作用就是限制系统中执行线程的数量。

    根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率

    不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没

    有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;

    否则进入等待队列。

    为什么要用线程池:

    1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

    2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大

    约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

    Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的

    线程池接口是ExecutorService。

    比较重要的几个类:

    ExecutorService: 真正的线程池接口。

    ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

    ThreadPoolExecutor: ExecutorService的默认实现。

    ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此

    在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

    展开全文
  • 互联网从工作方式上可以划分为两大块: (1)边缘部分:由连接在互联网上的大量主机组成,由用户直接使用的部分。 这些主机包括了一切可以联网的设备,包括电脑,手机,联网的摄像头,更大的有互联网公司中的...

    互联网的组成

    互联网从工作方式上可以划分为两大块

    (1)边缘部分:由连接在互联网上的大量主机组成,由用户直接使用的部分。

    这些主机包括了一切可以联网的设备,包括电脑,手机,联网的摄像头,更大的有互联网公司中的服务主机等。

    (2)核心部分:由大量网络和连接这些网络的路由器组成。主要为边缘部分提供服务。

    互联网的边缘部分

    两台主机之间的相互通信,称为计算机之间的通信,在网络的边缘部分,主机之间的通信方式分为两种:

    • 客户 - 服务器方式

    两台主机间的通信,其实就是分别运行在两台主机上的进程之间的相互通信。客户 - 服务器方式通信,从名字上看,就是一个是客户,一个是服务器。客户就是要被服务的,而服务器就是提供服务的。客户向服务器发送请求,服务器响应提供服务。

    场景:我们使用搜索引擎搜索东西,这就是典型的客户服务器的方式。

    【1】客户与服务器都要使用网络核心部分提供的服务
    【2】客户发送请求时需要知道服务器的地址
    【3】服务器可以同时接受处理多台服务器提供的服务请求,且不需要知道客户程序的地址
    【4】在客户与服务器之间建立连接后,可以进行双向通信
    【5】服务器启动后会一直运行,服务器需要较为复杂的硬件和系统的支持

    • 对等方式

    对等的连接方式就是指通信双方的地位是平等的,并不区分客户和服务器,两者相互平等的进行访问

    互联网的核心部分

    互联网的核心部分是由网络和连接网络的路由器组成。

    核心部分主要为边缘部分提供联通服务。就是将两个通信的主机联通,使主机之间可以相互的通信。

    路由器:(一种特殊计算机,并不是主机)核心部分中的主要组成。路由器将一个个的网络进行连接,使网络与网络之间相互连通。而实现联通的功能就是实现分组交换,就是路由器将收到的分组再进行转发,就相当于一个转发消息的中继站。

    三种交换方式

    首先说在信发展过程中的三种交换方式:

    • 电路交换

    固定电话之间的通信方式就是电路交换,两台电话机之间通过一根电话线进行通话。 电话之间是同过交换机将许多的电话连接起来,从而可以使其两两之间可以相互的通话。而交换机就是起着交换的作用。当许多交换机连接起来就构成了一个巨大的电话网络。

    交换:(将物理链路虚拟化)按照某种方式动态的分配传输线路的资源。

    当电话铃声响起,接通后通信双方之间就建立起了一条用于通信的物理通路。这条通路就是通信两端所占用的通信资源,而这份通信资源在双方结束通话前,是不能被其他的用户所占用的,具有独占性,只有在通信完毕后交换机才会释放这份通信资源。

    【1】电路交换是面向连接的,安全性好,可靠性高

    【2】通话的时间内,通话的两个用户始终占用着端对端的资源。

    【3】电路交换的个步骤:建立连接(占用通信资源)— 通话(始终占用通信资源) —释放连接(释放通信资源)

    缺点:线路的传输效率低,对于计算机的是数据交换来说,计算机机数据的传输是突发性的,在被用户占用的通信资源在大部分的时间里都是空闲着的,因此浪费了大量的通信资源。

    • 分组交换
    分组交换采用存储转发的技术,路由器使用分组交换的技术

    报文:我们要发送的整块数据称为一个报文
    分组:在发送前将较长的报文划分为较小的数据段,每个大小为的1024 bit,每个数据段前加一个保存控制信息的首部就构成了一个分组。每个分组又称为,而首部又称为包头,是互联网中传输的是数据单元。

    首部:其中包含了目的地址源地址等重要信息,因此,分组才能在网络中选择正确的路径。

    路由器每收到一个分组,先将其存储下来,再根据分组的首部,检查转发表,将其转发到另一个路由器。经过不断的转发最终将分组转发到目的地址。路由器之间必须不交换彼此掌握的路由信息,来创建和动态维护路由器中的转发表,使得转发表的信息能够不断更新。

    短分组存储在路由器的内存中,提高了转发的速率。

    分组转发的过程中,转发时之占用到下一个路由器之间的来链路,动态的占用一段一段的链路,并不会占用整个端对端的通信线路,对于突发性的计算机数据传输来说,提高通信资源的利用率。

    优点

    • 高效:在分组的传输过程中是动态的分配传输带宽的,对通信链路是逐段占用的
    • 灵活:为每一个分组独立选则最合适的转发路由
    • 迅速:以分组作为传输单位可以先不建立连接就能向其他主机转发分组
    • 可靠:保证可靠性的网络协议,分布式的多路由交换的分组交换网,使网络有很好的生存性

    缺点

    • 时延:分组转发时需要排队会造成一定的时延。

    • 分组携带的(首部)控制信息也带来了一定的额外开销

    • 整个分组交换网需要专门的管理和控制机制

    • 报文交换

    在古代的邮政通信,也属于存储转发的技术,在近代的电报通信采用了基于存储转发的报文交换。

    报文交换的本质与分组交换类似,都是进行断续的转发,先将数据转发到某一节点,再将数据转发到下一节点,直至到达目的地址。

    与分组交换的最大区别就是,在传输时并不将报文分成小的分组,而是对于整个报文进行转发。

    三种交换的主要特点:

    • 电路交换:整个报文从源点到达终点,不中断,像在一条管道中传输
    • 分组交换:将报文分成多个分组,再将单个分组传送到相邻的节点,存储下来,再根据转发表进行转发到笑一个节点
    • 报文交换:整个报文传输到相邻节点,存储下来,再根据转发表,转发到下一个节点
    展开全文
  • 函数参数传递的三种方式

    千次阅读 2019-05-15 20:28:58
    C语言中函数参数传递的三种方式 (1)传值,就是把你的变量的值传递给函数的形式参数,实际就是用变量的值来新生成一个形式参数,因而在函数里对形参的改变不会影响到函数外的变量的值。 (2)传址,就是传变量的...

    C语言中函数参数传递的三种方式

    (1)传值,就是把你的变量的值传递给函数的形式参数,实际就是用变量的值来新生成一个形式参数,因而在函数里对形参的改变不会影响到函数外的变量的值。
    (2)传址,就是传变量的地址赋给函数里形式参数的指针,使指针指向真实的变量的地址,因为对指针所指地址的内容的改变能反映到函数外,也就是能改变函数外的变量的值。
    (3)传引用,实际是通过指针来实现的,能达到使用的效果如传址,可是使用方式如传值。
    说几点建议:如果传值的话,会生成新的对象,花费时间和空间,而在退出函数的时候,又会销毁该对象,花费时间和空间。
    因而如果int,char等固有类型,而是你自己定义的类或结构等,都建议传指针或引用,因为他们不会创建新的对象。

    例1:下面这段代码的输出结果为:
    #include<stdio.h>
    void change(int*a, int&b, int c)
    {
    c=*a;
    b=30;
    *a=20;
    }
    int main ( )
    {
    int a=10, b=20, c=30;
    change(&a,b,c);
    printf(“%d,%d,%d,”,a,b,c);
    return 0;
    }
    结果:20 30 30

    解析:
    该题考察函数传参问题。
    1,指针传参 -> 将变量的地址直接传入函数,函数中可以对其值进行修改。
    2,引用传参 -> 将变量的引用传入函数,效果和指针相同,同样函数中可以对其值进行修改。
    3,值传参 -> 在传参过程中,首先将c的值复制给函数c变量,然后在函数中修改的即是函数的c变量,然后函数返回时,系统自动释放变量c。而对main函数的c没有影响。

    例2:
    #include<stdio.h>
    void myswap(int x, int y)
    {
    int t;
    t=x;
    x=y;
    y=t;
    }
    int main()
    {
    int a, b;
    printf(“请输入待交换的两个整数:”);
    scanf("%d %d", &a, &b);
    myswap(a,b); //作为对比,直接交换两个整数,显然不行
    printf(“调用交换函数后的结果是:%d 和 %d\n”, a, b);
    return 0;
    }

    #include<stdio.h>
    void myswap(int *p1, int *p2)
    {
    int t;
    t=*p1;
    *p1=*p2;
    *p2=t;
    }
    int main()
    {
    int a, b;
    printf(“请输入待交换的两个整数:”);
    scanf("%d %d", &a, &b);
    myswap(&a,&b); //交换两个整数的地址
    printf(“调用交换函数后的结果是:%d 和 %d\n”, a, b);
    return 0;
    }

    #include<stdio.h>
    void myswap(int &x, int &y)
    {
    int t;
    t=x;
    x=y;
    y=t;
    }

    int main()
    {
    int a, b;
    printf(“请输入待交换的两个整数:”);
    scanf("%d %d", &a, &b);
    myswap(a,b); //直接以变量a和b作为实参交换
    printf(“调用交换函数后的结果是:%d 和 %d\n”, a, b);
    return 0;
    }
    第一个的运行结果:输入2 3,输出2 3
    第二个的运行结果:输入2 3,输出3 2
    第三个的运行结果:输入2 3,输出3 2

    解析:
    在第一个程序中,传值不成功的原因是指在形参上改变了数值,没有在实参上改变数值。
    在第二个程序中,传地址成功的原因利用指针改变了原来的地址,所以实参就交换了。
    在第三个程序中,引用是直接改变两个实参变量a,b的值,所以就交换了。

    下文会通过例子详细说明关于值传递,指针传递,引用传递

    1)值传递:

    形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,
    

    不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。

    2)指针传递:

    形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作
    

    3)引用传递:

    形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
    
    下面的代码对此作出了细致解释(从实参,形参在内存中存放地址的角度 说明了问题的本质,容易理解  )
    

    1 #include
    2 using namespace std;
    3 //值传递
    4 void change1(int n){
    5 cout<<“值传递–函数操作地址”<<&n<<endl; //显示的是拷贝的地址而不是源地址
    6 n++;
    7 }
    8
    9 //引用传递
    10 void change2(int & n){
    11 cout<<“引用传递–函数操作地址”<<&n<<endl;
    12 n++;
    13 }
    14 //指针传递
    15 void change3(int *n){
    16 cout<<"指针传递–函数操作地址 "<<n<<endl;
    17 *n=*n+1;
    18 }
    19 int main(){
    20 int n=10;
    21 cout<<“实参的地址”<<&n<<endl;
    22 change1(n);
    23 cout<<“after change1() n=”<<n<<endl;
    24 change2(n);
    25 cout<<“after change2() n=”<<n<<endl;
    26 change3(&n);
    27 cout<<“after change3() n=”<<n<<endl;
    28 return true;
    29 }
    运行结果如下,(不同的机器可能会有所差别)

    可以看出,实参的地址为0x22ff44

    采用值传递的时候,函数操作的地址是0x22ff20并不是实参本身,所以对它进行操作并不能改变实参的值
    

    再看引用传递,操作地址就是实参地址 ,只是相当于实参的一个别名,对它的操作就是对实参的操作
    接下来是指针传递,也可发现操作地址是实参地址
    那么,引用传递和指针传递有什么区别吗?

    引用的规则:
    引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

    不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
    一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
    指针传递的实质:

    指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的
    
    任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里是在说实参指针本身的地址值不会变)如果理解不了大可跳过这段
    

    指针传递和引用传递一般适用于:

    函数内部修改参数并且希望改动影响调用者。对比指针/引用传递可以将改变由形参“传给”实参(实际上就是直接在实参的内存上修改,不像值传递将实参的值拷贝到另外的内存地址中才修改)。
    
    另外一种用法是:当一个函数实际需要返回多个值,而只能显式返回一个值时,可以将另外需要返回的变量以指针/引用传递给函数,这样在函数内部修改并且返回后,调用者可以拿到被修改过后的变量,也相当于一个隐式的返回值传递吧。
    
    以下是我觉得关于指针和引用写得很不错的文章,大家可参照看一下,原文出处地址:http://xinklabi.iteye.com/blog/653643 
    
    从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
    
    而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
    
    在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:
    
    指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里是在说实参指针本身的地址值不会变)
    

    而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

    引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。
    

    为了进一步加深大家对指针和引用的区别,下面我从编译的角度来阐述它们之间的区别:

    程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

    最后,总结一下指针和引用的相同点和不同点:

    1)相同点:

    都是地址的概念;
    指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。

    2)不同点:

    指针是一个实体,而引用仅是个别名;
    引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
    引用没有const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a是有 的, 前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
    引用不能为空,指针可以为空;
    “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
    指针和引用的自增(++)运算意义不一样;
    引用是类型安全的,而指针不是 (引用比指针多了类型检查)

    一、引用的概念

    引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。
    例如: Point pt1(10,10);
    Point &pt2=pt1; 定义了pt2为pt1的引用。通过这样的定义,pt1和pt2表示同一对象。
    需要特别强调的是引用并不产生对象的副本,仅仅是对象的同义词。因此,当下面的语句执行后:
    pt1.offset(2,2);
    pt1和pt2都具有(12,12)的值。
    引用必须在定义时马上被初始化,因为它必须是某个东西的同义词。你不能先定义一个引用后才
    初始化它。例如下面语句是非法的:
    Point &pt3;
    pt3=pt1;
    那么既然引用只是某个东西的同义词,它有什么用途呢?
    下面讨论引用的两个主要用途:作为函数参数以及从函数中返回左值。

    二、引用参数

    1、传递可变参数
    传统的c中,函数在调用时参数是通过值来传递的,这就是说函数的参数不具备返回值的能力。
    所以在传统的c中,如果需要函数的参数具有返回值的能力,往往是通过指针来实现的。比如,实现
    两整数变量值交换的c程序如下:
    一、引用的概念

    引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。
    例如: Point pt1(10,10);
    Point &pt2=pt1; 定义了pt2为pt1的引用。通过这样的定义,pt1和pt2表示同一对象。
    需要特别强调的是引用并不产生对象的副本,仅仅是对象的同义词。因此,当下面的语句执行后:
    pt1.offset(2,2);
    pt1和pt2都具有(12,12)的值。
    引用必须在定义时马上被初始化,因为它必须是某个东西的同义词。你不能先定义一个引用后才
    初始化它。例如下面语句是非法的:
    Point &pt3;
    pt3=pt1;
    那么既然引用只是某个东西的同义词,它有什么用途呢?
    下面讨论引用的两个主要用途:作为函数参数以及从函数中返回左值。

    二、引用参数

    1、传递可变参数
    传统的c中,函数在调用时参数是通过值来传递的,这就是说函数的参数不具备返回值的能力。
    所以在传统的c中,如果需要函数的参数具有返回值的能力,往往是通过指针来实现的。比如,实现
    两整数变量值交换的c程序如下:
    void swapint(int *a,int *b)
    {
    int temp;
    temp=*a;
    a=*b;
    *b=temp;
    }

    使用引用机制后,以上程序的c++版本为:
    void swapint(int &a,int &b)
    {
    int temp;
    temp=a;
    a=b;
    b=temp;
    }
    调用该函数的c++方法为:swapint(x,y); c++自动把x,y的地址作为参数传递给swapint函数。

    2、给函数传递大型对象
    当大型对象被传递给函数时,使用引用参数可使参数传递效率得到提高,因为引用并不产生对象的
    副本,也就是参数传递时,对象无须复制。下面的例子定义了一个有限整数集合的类:
    const maxCard=100;
    Class Set
    {
    int elems[maxCard]; // 集和中的元素,maxCard 表示集合中元素个数的最大值。
    int card; // 集合中元素的个数。
    public:
    Set () {card=0;} //构造函数
    friend Set operator * (Set ,Set ) ; //重载运算符号*,用于计算集合的交集 用对象作为传值参数
    // friend Set operator * (Set & ,Set & ) 重载运算符号*,用于计算集合的交集 用对象的引用作为传值参数

    }
    先考虑集合交集的实现
    Set operator ( Set Set1,Set Set2)
    {
    Set res;
    for(int i=0;i<Set1.card;++i)
    for(int j=0;j>Set2.card;++j)
    if(Set1.elems[i]==Set2.elems[j])
    {
    res.elems[res.card++]=Set1.elems[i];
    break;
    }
    return res;
    }
    由于重载运算符不能对指针单独操作,我们必须把运算数声明为 Set 类型而不是 Set * 。
    每次使用
    做交集运算时,整个集合都被复制,这样效率很低。我们可以用引用来避免这种情况。
    Set operator *( Set &Set1,Set &Set2)
    { Set res;
    for(int i=0;i<Set1.card;++i)
    for(int j=0;j>Set2.card;++j)
    if(Set1.elems[i]==Set2.elems[j])
    {
    res.elems[res.card++]=Set1.elems[i];
    break;
    }
    return res;
    }

    三、引用返回值

    如果一个函数返回了引用,那么该函数的调用也可以被赋值。这里有一函数,它拥有两个引用参数并返回一个双精度数的引用:
    double &max(double &d1,double &d2)
    {
    return d1>d2?d1:d2;
    }
    由于max()函数返回一个对双精度数的引用,那么我们就可以用max() 来对其中较大的双精度数加1:
    max(x,y)+=1.0;


    作者:魏波-
    来源:CSDN
    原文:https://blog.csdn.net/weibo1230123/article/details/75541862
    版权声明:本文为博主原创文章,转载请附上博文链接!

    展开全文
  • http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,次请求/响应对之间没有任何联系  但这也同时意味着,任何用户都能通过浏览器访问服务器资源,...

    单点登录原理与简单实现

    一、单系统登录机制

    1、http无状态协议

      web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次请求/响应对之间没有任何联系

    3c91a3bf-25d8-4b1f-8e4a-68535c51aaa8

      但这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然http协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制

    2、会话机制

      浏览器第一次请求服务器,服务器创建一个会话,并将会话的id作为响应的一部分发送给浏览器,浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,服务器取得请求中的会话id就知道是不是同一个用户了,这个过程用下图说明,后续请求与第一次请求产生了关联

    8a9fb230-d506-4b19-b821-4001c68c4588

      服务器在内存中保存会话对象,浏览器怎么保存会话id呢?你可能会想到两种方式

    1. 请求参数
    2. cookie

      将会话id作为每一个请求的参数,服务器接收请求自然能解析参数获得会话id,并借此判断是否来自同一会话,很明显,这种方式不靠谱。那就浏览器自己来维护这个会话id吧,每次发送http请求时浏览器自动发送会话id,cookie机制正好用来做这件事。cookie是浏览器用来存储少量数据的一种机制,数据以”key/value“形式存储,浏览器发送http请求时自动附带cookie信息

      tomcat会话机制当然也实现了cookie,访问tomcat服务器时,浏览器中可以看到一个名为“JSESSIONID”的cookie,这就是tomcat会话机制维护的会话id,使用了cookie的请求响应过程如下图

    518293d9-64b2-459c-9d45-9f353c757d1f

    3、登录状态

      有了会话机制,登录状态就好明白了,我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话标记为“已授权”或者“已登录”等等之类的状态,既然是会话的状态,自然要保存在会话对象中,tomcat在会话对象中设置登录状态如下

    1

    2

    HttpSession session = request.getSession();

    session.setAttribute("isLogin"true);

      用户再次访问时,tomcat在会话对象中查看登录状态

    1

    2

    HttpSession session = request.getSession();

    session.getAttribute("isLogin");

      实现了登录状态的浏览器请求服务器模型如下图描述

    70e396fa-1bf2-42f8-a504-ce20306e31fa

      每次请求受保护资源时都会检查会话对象中的登录状态,只有 isLogin=true 的会话才能访问,登录机制因此而实现。

    二、多系统的复杂性

      web系统早已从久远的单系统发展成为如今由多系统组成的应用群,面对如此众多的系统,用户难道要一个一个登录、然后一个一个注销吗?就像下图描述的这样

    6dfbb0b1-46c0-4945-a3bf-5f060fa80710

      web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了

    9fe14ab3-4254-447b-b850-0436e628c254

      虽然单系统的登录解决方案很完美,但对于多系统应用群已经不再适用了,为什么呢?

      单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(通常对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的cookie,而不是所有cookie

    4d58ccfa-0114-486d-bec2-c28f2f9eb513

      既然这样,为什么不将web应用群中所有子系统的域名统一在一个顶级域名下,例如“*.baidu.com”,然后将它们的cookie域设置为“baidu.com”,这种做法理论上是可以的,甚至早期很多多系统登录就采用这种同域名共享cookie的方式。

      然而,可行并不代表好,共享cookie的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是web服务器)要相同,不然cookie的key值(tomcat为JSESSIONID)不同,无法维持会话,共享cookie的方式是无法实现跨语言技术平台登录的,比如java、php、.net系统之间;第三,cookie本身不安全。

      因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录

    三、单点登录

      什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分

    1、登录

      相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明

      下面对上图简要描述

    1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
    2. sso认证中心发现用户未登录,将用户引导至登录页面
    3. 用户输入用户名密码提交登录申请
    4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
    5. sso认证中心带着令牌跳转会最初的请求地址(系统1)
    6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
    7. sso认证中心校验令牌,返回有效,注册系统1
    8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
    9. 用户访问系统2的受保护资源
    10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
    11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
    12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
    13. sso认证中心校验令牌,返回有效,注册系统2
    14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

      用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系

    1. 局部会话存在,全局会话一定存在
    2. 全局会话存在,局部会话不一定存在
    3. 全局会话销毁,局部会话必须销毁

      你可以通过博客园、百度、csdn、淘宝等网站的登录过程加深对单点登录的理解,注意观察登录过程中的跳转url与参数

    2、注销

      单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明

    3b139d2e-0b83-4a69-b4f2-316adb8997ce

      sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

      下面对上图简要说明

    1. 用户向系统1发起注销请求
    2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
    3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
    4. sso认证中心向所有注册系统发起注销请求
    5. 各注册系统接收sso认证中心的注销请求,销毁局部会话
    6. sso认证中心引导用户至登录页面

    四、部署图

      单点登录涉及sso认证中心与众子系统,子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成sso的客户端,sso认证中心则是sso服务端,整个单点登录过程实质是sso客户端与服务端通信的过程,用下图描述

    fb29685c-487c-42b9-9ceb-6c7ee29e98c9

      sso认证中心与sso客户端通信方式有多种,这里以简单好用的httpClient为例,web service、rpc、restful api都可以

    五、实现

      只是简要介绍下基于java的实现过程,不提供完整源码,明白了原理,我相信你们可以自己实现。sso采用客户端/服务端架构,我们先看sso-client与sso-server要实现的功能(下面:sso认证中心=sso-server)

      sso-client

    1. 拦截子系统未登录用户请求,跳转至sso认证中心
    2. 接收并存储sso认证中心发送的令牌
    3. 与sso-server通信,校验令牌的有效性
    4. 建立局部会话
    5. 拦截用户注销请求,向sso认证中心发送注销请求
    6. 接收sso认证中心发出的注销请求,销毁局部会话

      sso-server

    1. 验证用户的登录信息
    2. 创建全局会话
    3. 创建授权令牌
    4. 与sso-client通信发送令牌
    5. 校验sso-client令牌有效性
    6. 系统注册
    7. 接收sso-client注销请求,注销所有会话

      接下来,我们按照原理来一步步实现sso吧!

    1、sso-client拦截未登录请求

      java拦截请求的方式有servlet、filter、listener三种方式,我们采用filter。在sso-client中新建LoginFilter.java类并实现Filter接口,在doFilter()方法中加入对未登录用户的拦截

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;

        HttpServletResponse res = (HttpServletResponse) response;

        HttpSession session = req.getSession();

         

        if (session.getAttribute("isLogin")) {

            chain.doFilter(request, response);

            return;

        }

        //跳转至sso认证中心

        res.sendRedirect("sso-server-url-with-system-url");

    }

    2、sso-server拦截未登录请求

      拦截从sso-client跳转至sso认证中心的未登录请求,跳转至登录页面,这个过程与sso-client完全一样

    3、sso-server验证用户登录信息

      用户在登录页面输入用户名密码,请求登录,sso认证中心校验用户信息,校验成功,将会话状态标记为“已登录”

    1

    2

    3

    4

    5

    6

    @RequestMapping("/login")

    public String login(String username, String password, HttpServletRequest req) {

        this.checkLoginInfo(username, password);

        req.getSession().setAttribute("isLogin"true);

        return "success";

    }

    4、sso-server创建授权令牌

      授权令牌是一串随机字符,以什么样的方式生成都没有关系,只要不重复、不易伪造即可,下面是一个例子

    1

    String token = UUID.randomUUID().toString();

    5、sso-client取得令牌并校验

      sso认证中心登录后,跳转回子系统并附上令牌,子系统(sso-client)取得令牌,然后去sso认证中心校验,在LoginFilter.java的doFilter()中添加几行

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    // 请求附带token参数

    String token = req.getParameter("token");

    if (token != null) {

        // 去sso认证中心校验token

        boolean verifyResult = this.verify("sso-server-verify-url", token);

        if (!verifyResult) {

            res.sendRedirect("sso-server-url");

            return;

        }

        chain.doFilter(request, response);

    }

      verify()方法使用httpClient实现,这里仅简略介绍,httpClient详细使用方法请参考官方文档

    1

    2

    HttpPost httpPost = new HttpPost("sso-server-verify-url-with-token");

    HttpResponse httpResponse = httpClient.execute(httpPost);

    6、sso-server接收并处理校验令牌请求

      用户在sso认证中心登录成功后,sso-server创建授权令牌并存储该令牌,所以,sso-server对令牌的校验就是去查找这个令牌是否存在以及是否过期,令牌校验成功后sso-server将发送校验请求的系统注册到sso认证中心(就是存储起来的意思)

      令牌与注册系统地址通常存储在key-value数据库(如redis)中,redis可以为key设置有效时间也就是令牌的有效期。redis运行在内存中,速度非常快,正好sso-server不需要持久化任何数据。

      令牌与注册系统地址可以用下图描述的结构存储在redis中,可能你会问,为什么要存储这些系统的地址?如果不存储,注销的时候就麻烦了,用户向sso认证中心提交注销请求,sso认证中心注销全局会话,但不知道哪些系统用此全局会话建立了自己的局部会话,也不知道要向哪些子系统发送注销请求注销局部会话

    3b221593-f9c4-45af-a567-4937786993e8

    7、sso-client校验令牌成功创建局部会话

      令牌校验成功后,sso-client将当前局部会话标记为“已登录”,修改LoginFilter.java,添加几行

    1

    2

    3

    if (verifyResult) {

        session.setAttribute("isLogin"true);

    }

      sso-client还需将当前会话id与令牌绑定,表示这个会话的登录状态与令牌相关,此关系可以用java的hashmap保存,保存的数据用来处理sso认证中心发来的注销请求

    8、注销过程

      用户向子系统发送带有“logout”参数的请求(注销请求),sso-client拦截器拦截该请求,向sso认证中心发起注销请求

    1

    2

    3

    4

    String logout = req.getParameter("logout");

    if (logout != null) {

        this.ssoServer.logout(token);

    }

      sso认证中心也用同样的方式识别出sso-client的请求是注销请求(带有“logout”参数),sso认证中心注销全局会话

    1

    2

    3

    4

    5

    6

    7

    8

    @RequestMapping("/logout")

    public String logout(HttpServletRequest req) {

        HttpSession session = req.getSession();

        if (session != null) {

            session.invalidate();//触发LogoutListener

        }

        return "redirect:/";

    }

      sso认证中心有一个全局会话的监听器,一旦全局会话注销,将通知所有注册系统注销

    1

    2

    3

    4

    5

    6

    7

    8

    public class LogoutListener implements HttpSessionListener {

        @Override

        public void sessionCreated(HttpSessionEvent event) {}

        @Override

        public void sessionDestroyed(HttpSessionEvent event) {

            //通过httpClient向所有注册系统发送注销请求

        }

    }

    单点登录的三种实现方式

    单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得非常频繁,例如像阿里巴巴这样的网站,在网站的背后是成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。实现单点登录说到底就是要解决如何产生和存储那个信任,再就是其他系统如何验证这个信任的有效性,因此要点也就以下两个:

    • 存储信任
    • 验证信任

    如果一个系统做到了开头所讲的效果,也就算单点登录,单点登录有不同的实现方式,本文就罗列我开发中所遇见过的实现方式。

    以Cookie作为凭证媒介

    最简单的单点登录实现方式,是使用cookie作为媒介,存放用户凭证。 用户登录父应用之后,应用返回一个加密的cookie,当用户访问子应用的时候,携带上这个cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户。

    Auth via cookie

    不难发现以上方式把信任存储在客户端的Cookie中,这种方式很容易令人质疑:

    • Cookie不安全
    • 不能跨域实现免登

    对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的。 对于第二个问题,更是硬伤。

    通过JSONP实现

    对于跨域问题,可以使用JSONP实现。 用户在父应用中登录后,跟Session匹配的Cookie会存到客户端中,当用户需要登录子应用的时候,授权应用访问父应用提供的JSONP接口,并在请求中带上父应用域名下的Cookie,父应用接收到请求,验证用户的登录状态,返回加密的信息,子应用通过解析返回来的加密信息来验证用户,如果通过验证则登录用户。

    Auth via jsonp

    这种方式虽然能解决跨域问题,但是安全性其实跟把信任存储到Cookie是差不多的。如果一旦加密算法泄露了,攻击者可以在本地建立一个实现了登录接口的假冒父应用,通过绑定Host来把子应用发起的请求指向本地的假冒父应用,并作出回应。 因为攻击者完全可以按照加密算法来伪造响应请求,子应用接收到这个响应之后一样可以通过验证,并且登录特定用户。

    通过页面重定向的方式

    最后一种介绍的方式,是通过父应用和子应用来回重定向中进行通信,实现信息的安全传递。 父应用提供一个GET方式的登录接口,用户通过子应用重定向连接的方式访问这个接口,如果用户还没有登录,则返回一个的登录页面,用户输入账号密码进行登录。如果用户已经登录了,则生成加密的Token,并且重定向到子应用提供的验证Token的接口,通过解密和校验之后,子应用登录当前用户。

    Auth via redirect

    这种方式较前面两种方式,接解决了上面两种方法暴露出来的安全性问题和跨域的问题,但是并没有前面两种方式方便。 安全与方便,本来就是一对矛盾。

    使用独立登录系统

    一般说来,大型应用会把授权的逻辑与用户信息的相关逻辑独立成一个应用,称为用户中心。 用户中心不处理业务逻辑,只是处理用户信息的管理以及授权给第三方应用。第三方应用需要登录的时候,则把用户的登录请求转发给用户中心进行处理,用户处理完毕返回凭证,第三方应用验证凭证,通过后就登录用户。

    展开全文
  • 因此,每当用户上传任何文件进行第一重复数据删除检查时,随后的三种错误检测方式将有效地验证文件阴影区域单元被黑或被黑; 最终,快速恢复技术可以比现有方法更快地恢复文件。 当我执行上述现有方案时,我的小说...
  • 单点登录的三种实现方式

    千次阅读 2020-10-18 11:01:09
    当用户登录成功后,一般会将登录状态记录到 Session 中,或者是给用户签发一个 Token,无论哪一种方式,都需要在客户端保存一些信息(Session ID 或 Token ),并要求客户端在之后的每次请求中携带它们。在这样的...
  • C++中函数调用时的三种参数传递方式详解

    万次阅读 多人点赞 2017-08-31 20:44:51
    原文地址:http://blog.csdn.net/cocohufei/article/details/6143476;  ...   在C++中,参数传递的方式是“实虚结合”。 按值传递(pass by value) 地址传递(pass by pointer) 引用传递(pass b...
  • C语言中函数参数传递的三种方式

    万次阅读 多人点赞 2017-07-22 21:09:40
    C语言中函数参数传递的三种方式(1)传值,就是把你的变量的值传递给函数的形式参数,实际就是用变量的值来新生成一个形式参数,因而在函数里对形参的改变不会影响到函数外的变量的值。(2)传址,就是传变量的地址...
  • 有了容器数据卷技术,即使docker中的容器被删除,数据依旧会保存在我们的硬盘中使得我们的数据更加安全 类型 指定目录挂载 指定宿主机路径映射到容器 命令: docker run -v /宿主机中的路径:容器内的路径 -其它...
  • 企业安全检查指南.pdf

    2021-09-10 16:16:45
    为企业压实了网络安全主体责任,均要求企业应当至少...并且,常态化的事情 总是外包成本也比较高,因此,建议有能力的企业可考虑建立常态化的自我安全检查机制,在 内部组建安全检查小组,负责本单位的安全检查工作。
  • 前端三种本地存储方式

    万次阅读 2018-01-29 17:30:22
    关于第方cookie和cookie的安全问题可以查看 点击打开链接 c localStorage(本地存储) HTML5新方法,不过 IE8及以上 浏览器都兼容。 特点: 生命周期:持久化的本地...
  • Java单例模式的三种实现方式

    千次阅读 2016-09-06 19:37:28
    只有在访问到单例对象的时候才去检查和实例化单例对象,满足延迟加载,但多线程访问时需要加线程同步,影响访问效率 public class LazySingleton { private LazySingleton () {} private ...
  • 传统的芯片检查方法难以检测运行时的攻击及故障,文中提出了一芯片运行时安全检查架构,由策略引擎和IP包装器组成。通过配置策略引擎中的策略检查电路,在系统运行时可对芯片执行相应的安全检查并实时报告检查结果...
  • final 意思是,这个对象的值(基本...这首先是一特殊的需求,就是某个类的实例在JVM中只能存在一个,跟前面的static,线程安全都不一样 与线程安全的关系:实现单例需要考虑复杂的多线程的情况,这个东西需要线程安全
  • 三种线程安全的List

    千次阅读 2021-05-30 12:10:45
    在单线程开发环境中,我们经常使用ArrayList作容器来存储我们的数据,但它不是线程安全的,在多线程环境中使用它可能会出现意想不到的结果。 多线程中的ArrayList: 我们可以从一段代码了解并发环境下使用ArrayList...
  • android与js交互的方式(包括三种

    千次阅读 2016-12-12 17:11:37
    关于android月js或者说html交互的方式,在很早的版本中是通过android端添加js支持,然后传递一个js操作本地方法...但是这样的调用方式还是存在安全的漏掉,所有后边就有了另外的俩调用方式方式 一: WebSetting
  • 三种创建线程的方法分别在上述文章中已介绍,分别为:CreateThread,AfxBeginThread,_beginthread/beginthreadex 区别: CreateThread是Windows API函数,提供操作系统级别操作,不用于MFC及RTL函数中。
  • 安全策略(缺省情况下,域内...  域间或域内安全策略是包过滤、UTM应用层检测等多种安全检查同时实施的一体化策略。2.应用在接口上的包过滤规则:用于控制接口的流量,就是传统的包过滤功能,基于IP、MAC地址等...
  • 一般情况下,有下面的几方法实现这种通信任务:使用全局变量(上一节的例子其实使用的就是这种方法)、使用事件对象、使用消息。这里我们主要介绍后两方法。    (一) 利用用户定义的消息通信    在...
  • response设置编码的三种方式以及比较

    千次阅读 2019-05-24 14:50:12
    response设置编码的三种方式以及比较 2017年01月03日 14:12:46Listen_Silently阅读数:18021 在很多场合会发现我们输出的中文是乱码,这主要可能有两方面的原因: 一个是浏览器的解析方式,另一个是我们服务器端...
  • 获取Class字节码对象的三种方式

    万次阅读 2016-09-17 13:07:17
    在Java中获得Class对象通常有三种方式方式一,使用类的class属性: Class clz1 = java.util.Date.class; 方式二,通过Class类中的静态方法forName(String className),传入类的全限定名(必须添加完整包名...
  • Set、Map、List三种集合的差别

    万次阅读 多人点赞 2019-05-30 16:15:27
    1.集合类型主要有3:set(集)、list(列表)和map(映射)。 2.者关系 3.Set set接口时Collection接口的一个子接口,是无序的,set中不包含重复的元素,也就是说set中不存在两个这样的元素a1.equals(a2)结果为...
  • SVN备份的三种方式

    万次阅读 2019-07-05 10:06:59
    备份策略 ============== ...svn备份一般采用三种方式: 1)svnadmin dump  2)svnadmin hotcopy  3)svnsync.  注意,svn备份不宜采用普通的文件拷贝方式(除非你备份的时候将库暂停),...
  • 三种移动APP(应用程序)开发方式比较

    万次阅读 多人点赞 2016-05-16 11:13:28
    页面存放于本地和服务器两种方式,部署应用程序(受限于UIwebview) 缺点: 不确定上线时间 虽然说你可以专注在界面以及交互开发上了,但是这页会成为一个缺点,比如说要仿造一个iOS的默认设置界面,就需要...
  • ORACLE ASM提供的三种冗余方式

    千次阅读 2018-09-21 07:03:27
    ORACLE ASM提供的三种冗余方式 redundancy---冗余 三种模式:external、normal、high  外部、正常、高 一般情况下三种模式需要的磁盘组: external 1块 normal 3块 high 5块 1、 外部冗余(external ...
  • 在任何一本讲vlan的网络书上都会提到vlan的三种划分方式: 1.按物理地址划分 2.按交换机端口划分 3.按ip地址划分 本文的结论为:pvlan就是 按交换机端口划分 的一实现 子网就是 按ip地址划分 的一实现  按物理...
  • 防火墙的三种配置方案(转)

    千次阅读 2019-04-22 12:51:06
    21世纪是网络经济时代,Internet已经走进千家万户,当我们尽情地在Internet上畅游时,往往把网络的安全问题抛在脑后。其实危险无处不在,防火墙是网络安全的一个重要防护措施,用于对网络和系统的保护。监控通过...
  • php 三种文件下载的实现

    万次阅读 2018-03-27 09:26:56
    感谢: ...的支持 1、直接添加文件链接 ...//检查文件是否存在 ...总结:第一个和第二个操作比较简单,但是容易暴露文件的真实地址,安全性不高,第三种能够较好的把文件的真实地址隐藏起来
  • 2020年科大讯飞X光安检图像识别前名队伍分享

    万次阅读 多人点赞 2020-12-04 17:52:01
    名:胖星 3.1 团队介绍 3.2 算法方案 3.2.1 数据分析 3.2.2 模型结构 3.2.3 形变卷积 3.2.4 数据增强 3.2.5 GridMask 3.2.6 Mixup 3.2.7 同比例缩放 3.2.8 SWA 3.2.9 WBF 3.3 实验结果 3.4 方案总结 3.5 后序...
  • opencv学习(四)之像素遍历三种方式

    万次阅读 多人点赞 2016-11-07 20:52:15
    三种像素遍历方式在速度上有所不同,上一篇文章介绍过用C操作符[]是最快的访问方式。下面会通过对同一幅图像进行处理来直观的比较三种访问方式的速度差异。首先介绍一下opencv中提供的计时函数 1.计时

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 547,345
精华内容 218,938
关键字:

安全检查三种形式