193、商城业务-异步-异步复习
线程基础百度吧
异步编排参考网上链接即可:https://blog.csdn.net/weixin_45762031/article/details/103519459
线程回顾
初始化线程的4种方式:
1、继承Thread
2、实现Runnable接口
3、实现Callable接口 + FutureTask (可以拿到返回结果,可以处理异常)
4、线程池
方式1和方式2:主进程无法获取线程的运算结果。不适合当前场景
方式3:主进程可以获取线程的运算结果,并设置给itemVO,但是不利于控制服务器中的线程资源。可以导致服务器资源耗尽。
方式4:通过如下两种方式初始化线程池:
Executors.newFiexedThreadPool(3);
//或者
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, handler);
通过线程池性能稳定,也可以获取执行结果,并捕获异常。但是,在业务复杂情况下,一个异步调用可能会依赖于另一个异步调用的执行结果。
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main....start....");
// CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// System.out.println("当前线程:" + Thread.currentThread().getId());
// int i = 10 / 2;
// System.out.println("运行结果:" + i);
// }, executor);
/**
* 方法完成后的感知
*/
// CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// System.out.println("当前线程:" + Thread.currentThread().getId());
// int i = 10 / 0;
// System.out.println("运行结果:" + i);
// return i;
// }, executor).whenComplete((res,excption)->{
// //虽然能得到异常信息,但是没法修改返回数据。
// System.out.println("异步任务成功完成了...结果是:"+res+";异常是:"+excption);
// }).exceptionally(throwable -> {
// //可以感知异常,同时返回默认值
// return 10;
// });
/**
* 方法执行完成后的处理
*/
// CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
// System.out.println("当前线程:" + Thread.currentThread().getId());
// int i = 10 / 4;
// System.out.println("运行结果:" + i);
// return i;
// }, executor).handle((res, thr) -> {
// if (res != null) {
// return res * 2;
// }
// if (thr != null) {
// return 0;
// }
// return 0;
// });
//R apply(T t, U u);
/**
* 线程串行化
* 1)、thenRun:不能获取到上一步的执行结果,无返回值
* .thenRunAsync(() -> {
* System.out.println("任务2启动了...");
* }, executor);
* 2)、thenAcceptAsync;能接受上一步结果,但是无返回值
* 3)、thenApplyAsync:;能接受上一步结果,有返回值
*/
// CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// System.out.println("当前线程:" + Thread.currentThread().getId());
// int i = 10 / 4;
// System.out.println("运行结果:" + i);
// return i;
// }, executor).thenApplyAsync(res -> {
// System.out.println("任务2启动了..." + res);
//
// return "Hello " + res;
// }, executor);
//void accept(T t);
//R apply(T t);
//future.get()
/**
* 两个都完成
*/
// CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
// System.out.println("任务1线程:" + Thread.currentThread().getId());
// int i = 10 / 4;
// System.out.println("任务1结束:" );
// return i;
// }, executor);
//
// CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
// System.out.println("任务2线程:" + Thread.currentThread().getId());
//
// try {
// Thread.sleep(3000);
// System.out.println("任务2结束:" );
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// return "Hello";
// }, executor);
// future01.runAfterBothAsync(future02,()->{
// System.out.println("任务3开始...");
// },executor);
// void accept(T t, U u);
// future01.thenAcceptBothAsync(future02,(f1,f2)->{
// System.out.println("任务3开始...之前的结果:"+f1+"--》"+f2);
// },executor);
//R apply(T t, U u);
// CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
// return f1 + ":" + f2 + " -> Haha";
// }, executor);
/**
* 两个任务,只要有一个完成,我们就执行任务3
* runAfterEitherAsync:不感知结果,自己没有返回值
* acceptEitherAsync:感知结果,自己没有返回值
* applyToEitherAsync:感知结果,自己有返回值
*/
// future01.runAfterEitherAsync(future02,()->{
// System.out.println("任务3开始...之前的结果:");
// },executor);
//void accept(T t);
// future01.acceptEitherAsync(future02,(res)->{
// System.out.println("任务3开始...之前的结果:"+res);
// },executor);
// CompletableFuture<String> future = future01.applyToEitherAsync(future02, res -> {
// System.out.println("任务3开始...之前的结果:" + res);
// return res.toString() + "->哈哈";
// }, executor);
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的图片信息");
return "hello.jpg";
},executor);
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的属性");
return "黑色+256G";
},executor);
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
System.out.println("查询商品介绍");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "华为";
},executor);
// CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get();//等待所有结果完成
// System.out.println("main....end...."+futureImg.get()+"=>"+futureAttr.get()+"=>"+futureDesc.get());
System.out.println("main....end...."+anyOf.get());
}
public void thread(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main....start....");
/**
* 1)、继承Thread
* Thread01 thread = new Thread01();
* thread.start();//启动线程
*
* 2)、实现Runnable接口
* Runable01 runable01 = new Runable01();
* new Thread(runable01).start();
* 3)、实现Callable接口 + FutureTask (可以拿到返回结果,可以处理异常)
* FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
* new Thread(futureTask).start();
* //阻塞等待整个线程执行完成,获取返回结果
* Integer integer = futureTask.get();
* 4)、线程池[ExecutorService]
* 给线程池直接提交任务。
* service.execute(new Runable01());
* 1、创建:
* 1)、Executors
* 2)、new ThreadPoolExecutor
*
* Future:可以获取到异步结果
*
* 区别;
* 1、2不能得到返回值。3可以获取返回值
* 1、2、3都不能控制资源
* 4可以控制资源,性能稳定。
*/
//我们以后再业务代码里面,以上三种启动线程的方式都不用。【将所有的多线程异步任务都交给线程池执行】
// new Thread(()-> System.out.println("hello")).start();
//当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行
/**
* 七大参数
* corePoolSize:[5] 核心线程数[一直存在除非(allowCoreThreadTimeOut)]; 线程池,创建好以后就准备就绪的线程数量,就等待来接受异步任务去执行。
* 5个 Thread thread = new Thread(); thread.start();
* maximumPoolSize:[200] 最大线程数量; 控制资源
* keepAliveTime:存活时间。如果当前的线程数量大于core数量。
* 释放空闲的线程(maximumPoolSize-corePoolSize)。只要线程空闲大于指定的keepAliveTime;
* unit:时间单位
* BlockingQueue<Runnable> workQueue:阻塞队列。如果任务有很多,就会将目前多的任务放在队列里面。
* 只要有线程空闲,就会去队列里面取出新的任务继续执行。
* threadFactory:线程的创建工厂。
* RejectedExecutionHandler handler:如果队列满了,按照我们指定的拒绝策略拒绝执行任务
*
*
*
* 工作顺序:
* 1)、线程池创建,准备好core数量的核心线程,准备接受任务
* 1.1、core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行
* 1.2、阻塞队列满了,就直接开新线程执行,最大只能开到max指定的数量
* 1.3、max满了就用RejectedExecutionHandler拒绝任务
* 1.4、max都执行完成,有很多空闲.在指定的时间keepAliveTime以后,释放max-core这些线程
*
* new LinkedBlockingDeque<>():默认是Integer的最大值。内存不够
*
* 一个线程池 core 7; max 20 ,queue:50,100并发进来怎么分配的;
* 7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。
* 如果不想抛弃还要执行。CallerRunsPolicy;
*
*/
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// Executors.newCachedThreadPool() core是0,所有都可回收
// Executors.newFixedThreadPool() 固定大小,core=max;都不可回收
// Executors.newScheduledThreadPool() 定时任务的线程池
// Executors.newSingleThreadExecutor() 单线程的线程池,后台从队列里面获取任务,挨个执行
//
System.out.println("main....end....");
}
public static class Thread01 extends Thread{
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:"+i);
}
}
public static class Runable01 implements Runnable{
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:"+i);
}
}
public static class Callable01 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:"+i);
return i;
}
}
}
194、商城业务-异步-线程池详解
### 线程池的七大参数
corePoolSize 池中一直保持的线程的数量,即使线程空闲。除非设置了allowCoreThreadTimeOut
maximumPoolSize 池中允许的最大的线程数
keepAliveTime 当线程数大于核心线程数的时候,线程在最大多长时间没有接到新任务就会终止释放,
最终线程池维持在corePoolSize大小
unit 时间单位
workQueue 阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了corePoolSize大小,就会放在这里等待空闲线程执行。
threadFactory 创建线程的工厂,比如指定线程名等
handler 拒绝策略,如果线程满了,线程池就会使用拒绝策略。
# 运行流程:
1、线程池创建,准备好 core 数量的核心线程,准备接受任务
2、新的任务进来,用 core 准备好的空闲线程执行。
(1) 、core 满了,就将再进来的任务放入阻塞队列中。空闲的 core 就会自己去阻塞队列获取任务执行
(2) 、阻塞队列满了,就直接开新线程执行,最大只能开到 max 指定的数量
(3) 、max 都执行好了。Max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁。最终保持到 core 大小
(4) 、如果线程数开到了 max 的数量,还有新任务进来,就会使用 reject 指定的拒绝策略进行处理
3、所有的线程创建都是由指定的 factory 创建的。
面试:
一个线程池 core 7; max 20 ,queue:50,100 并发进来怎么分配的;先有 7 个能直接得到执行,接下来 50 个进入队列排队,在多开 13 个继续执行。现在 70 个被安排上了。剩下 30 个默认拒绝策略。
### 3、常见的 4 种线程池
1、 newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2、newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
4、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
### 4、开发中为什么使用线程池
l 、降低资源的消耗
通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗
2、提高响应速度
因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
3、 提高线程的可管理性
线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配
线程池处理jdk提供的以外,spring也提供了线程池的封装 具体查看下面的连接
spring 线程池和java线程池 - 简书 (jianshu.com) https://www.jianshu.com/p/77720cb0fdfb
195、商城业务-异步-CompletableFuture
业务场景:
查询商品详情页的逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间。
假如商品详情页的每个查询,需要如下标注的时间才能完成那么,用户需要 5.5s 后才能看到商品详情页的内容。很显然是不能接受的。 如果有多个线程同时完成这 6 步操作,也许只需要 1.5s 即可完成响应。
Future 是 Java 5 添加的类,用来描述一个异步计算的结果。你可以使用isDone
方法检查计算是否完成,或者使用get
阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel
方法停止任务的执行。 虽然Future
以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不 方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的 CPU 资源,而且也不能及时地得到计算结果,为什么不能用观察者设计模式当计算结果完成及时通知监听者呢? 很多语言,比如 Node.js,采用回调的方式实现异步编程。Java 的一些框架,比如 Netty,自己扩展了 Java 的 Future
接口,提供了addListener
等多个扩展方法;Google guava 也提供了通用的扩展 Future;Scala 也提供了简单易用且功能强大的 Future/Promise 异步编程模式。 作为正统的 Java 类库,是不是应该做点什么,加强一下自身库的功能呢?在 Java 8 中, 新增加了一个包含 50 个方法左右的类: CompletableFuture,提供了非常强大的Future 的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以 通过回调的方式处理计算结果,并且提供了转换和组合 CompletableFuture 的方法。 CompletableFuture 类实现了 Future 接口,所以你还是可以像以前一样通过get
方法阻塞或 者轮询的方式获得结果,但是这种方式不推荐使用。CompletableFuture 和 FutureTask 同属于 Future 接口的实现类,都可以获取线程的执行结果。
196、商城业务-异步-CompletableFuture-启动异步任务
1 、创建异步对象
CompletableFuture 提供了四个静态方法来创建一个异步操作。
static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
1、runXxxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的
2、可以传入自定义的线程池,否则就用默认的线程池;
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。runAsync方法不支持返回值。runXxxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的
supplyAsync可以支持返回值。
2、编码实现
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main....start....");
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor);
System.out.println(future1.get());
}
}
197、商城业务-异步-CompletableFuture-完成回调与异常感知
1、计算完成时回调方法
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor);
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn);
whenComplete 可以处理正常和异常的计算结果,exceptionally 处理异常情况。
whenComplete 和 whenCompleteAsync 的区别:
whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程 执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)
/**
* 方法完成后的感知
*/
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 0;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res,excption)->{
//虽然能得到异常信息,但是没法修改返回数据。
System.out.println("异步任务成功完成了...结果是:"+res+";异常是:"+excption);
}).exceptionally(throwable -> {
//可以感知异常,同时返回默认值
return 10;
});
System.out.println(future.get());
案列二
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture future = CompletableFuture.supplyAsync(new Supplier<Object>() {
@Override
public Object get() {
System.out.println(Thread.currentThread().getName() + "\t completableFuture");
int i = 10 / 0;
return 1024;
}
}).whenComplete(new BiConsumer<Object, Throwable>() {
@Override
public void accept(Object o, Throwable throwable) {
System.out.println("-------o=" + o.toString());
System.out.println("-------throwable=" + throwable);
}
}).exceptionally(new Function<Throwable, Object>() {
@Override
public Object apply(Throwable throwable) {
System.out.println("throwable=" + throwable);
return 6666;
}
});
System.out.println(future.get());
}
}
198、商城业务-异步-CompletableFuture-handle最终处理
handle 是执行任务完成时对结果的处理。
handle 是在任务完成后再执行,还可以处理异常的任务。
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);
/**
* 方法执行完成后的处理
*/
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("运行结果:" + i);
return i;
}, executor).handle((res, thr) -> {
if (res != null) {
return res * 2;
}
if (thr != null) {
return 0;
}
return 0;
});
System.out.println(future.get());
199、商城业务-异步-CompletableFuture-线程串行化
1、线程串行化方法
thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作
带有Async默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);
public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);
Function<? super T,? extends U>
T:上一个任务返回结果的类型
U:当前任务的返回值类型
代码演示 1:
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(new Supplier<Integer>() {
@Override
public Integer get() {
System.out.println(Thread.currentThread().getName() + "\t completableFuture");
//int i = 10 / 0;
return 1024;
}
}).thenApply(new Function<Integer, Integer>() {
@Override
public Integer apply(Integer o) {
System.out.println("thenApply方法,上次返回结果:" + o);
return o * 2;
}
}).whenComplete(new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer o, Throwable throwable) {
System.out.println("-------o=" + o);
System.out.println("-------throwable=" + throwable);
}
}).exceptionally(new Function<Throwable, Integer>() {
@Override
public Integer apply(Throwable throwable) {
System.out.println("throwable=" + throwable);
return 6666;
}
}).handle(new BiFunction<Integer, Throwable, Integer>() {
@Override
public Integer apply(Integer integer, Throwable throwable) {
System.out.println("handle o=" + integer);
System.out.println("handle throwable=" + throwable);
return 8888;
}
});
System.out.println(future.get());
}
演示 二
/**
* 线程串行化
* 1)、thenRun:不能获取到上一步的执行结果,无返回值
* .thenRunAsync(() -> {
* System.out.println("任务2启动了...");
* }, executor);
* 2)、thenAcceptAsync;能接受上一步结果,但是无返回值
* 3)、thenApplyAsync:;能接受上一步结果,有返回值
*/
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("运行结果:" + i);
return i;
}, executor).thenApplyAsync(res -> {
System.out.println("任务2启动了..." + res);
return "Hello " + res;
}, executor);
//void accept(T t);
//R apply(T t);
future.get()
200、商城业务-异步-CompletableFuture-两任务组合都要完成
1、两任务组合 - 都要完成
两个任务必须都完成,触发该任务。
thenCombine:组合两个future,获取两个future的返回结果,并返回当前任务的返回值
thenAcceptBoth:组合两个future,获取两个future任务的返回结果,然后处理任务,没有返回值。
runAfterBoth:组合两个future,不需要获取future的结果,只需两个future处理完任务后,处理该任务。
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletableFuture<V> thenCombineAsync(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletableFuture<V> thenCombineAsync(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn, Executor executor);
public <U> CompletableFuture<Void> thenAcceptBoth(
CompletionStage<? extends U> other,
BiConsumer<? super T, ? super U> action);
public <U> CompletableFuture<Void> thenAcceptBothAsync(
CompletionStage<? extends U> other,
BiConsumer<? super T, ? super U> action);
public <U> CompletableFuture<Void> thenAcceptBothAsync(
CompletionStage<? extends U> other,
BiConsumer<? super T, ? super U> action, Executor executor);
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,
Runnable action);
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,
Runnable action);
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,
Runnable action,
Executor executor);
测试案例:
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
return "hello";
}).thenApplyAsync(t -> {
return t + " world!";
}).thenCombineAsync(CompletableFuture.completedFuture(" CompletableFuture"), (t, u) -> {
return t + u;
}).whenComplete((t, u) -> {
System.out.println(t);
});
}
输出:hello world! CompletableFuture
案列二:
/**
* 两个都完成
*/
CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("任务1结束:" );
return i;
}, executor);
CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程:" + Thread.currentThread().getId());
try {
Thread.sleep(3000);
System.out.println("任务2结束:" );
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
}, executor);
//但是都不接受上面两个的返回值
future01.runAfterBothAsync(future02,()->{
System.out.println("任务3开始...");
},executor);
// void accept(T t, U u); 接收上面的但是自己不返回
future01.thenAcceptBothAsync(future02,(f1,f2)->{
System.out.println("任务3开始...之前的结果:"+f1+"--》"+f2);
},executor);
// R apply(T t, U u);接收上面的同时自己也有返回值
CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
return f1 + ":" + f2 + " -> Haha";
}, executor);
201、商城业务-异步-CompletableFuture-两任务组合完成一个
1、两任务组合 - 一个完成
当两个任务中,任意一个future任务完成的时候,执行任务。
applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。
acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。
runAfterEither:两个任务有一个执行完成,不需要获取future的结果,处理任务,也没有返回值。
public <U> CompletableFuture<U> applyToEither(
CompletionStage<? extends T> other, Function<? super T, U> fn);
public <U> CompletableFuture<U> applyToEitherAsync(
CompletionStage<? extends T> other, Function<? super T, U> fn);
public <U> CompletableFuture<U> applyToEitherAsync(
CompletionStage<? extends T> other, Function<? super T, U> fn,
Executor executor);
public CompletableFuture<Void> acceptEither(
CompletionStage<? extends T> other, Consumer<? super T> action);
public CompletableFuture<Void> acceptEitherAsync(
CompletionStage<? extends T> other, Consumer<? super T> action);
public CompletableFuture<Void> acceptEitherAsync(
CompletionStage<? extends T> other, Consumer<? super T> action,
Executor executor);
public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,
Runnable action);
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,
Runnable action);
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,
Runnable action,
Executor executor);
CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("任务1结束:" );
return i;
}, executor);
CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程:" + Thread.currentThread().getId());
try {
Thread.sleep(3000);
System.out.println("任务2结束:" );
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello";
}, executor);
/**
* 两个任务,只要有一个完成,我们就执行任务3
* runAfterEitherAsync:不感知结果,自己没有返回值
* acceptEitherAsync:感知结果,自己没有返回值
* applyToEitherAsync:感知结果,自己有返回值
*/
future01.runAfterEitherAsync(future02,()->{
System.out.println("任务3开始...之前的结果:");
},executor);
//void accept(T t);
future01.acceptEitherAsync(future02,(res)->{
System.out.println("任务3开始...之前的结果:"+res);
},executor);
CompletableFuture<String> future = future01.applyToEitherAsync(future02, res -> {
System.out.println("任务3开始...之前的结果:" + res);
return res.toString() + "->哈哈";
}, executor);
202、商城业务-异步-CompletableFuture-多任务组合
1、多任务组合
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs);
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs);
allOf:等待所有任务完成
anyOf:只要有一个任务完成
public static void main(String[] args) {
List<CompletableFuture> futures = Arrays.asList(CompletableFuture.completedFuture("hello"),
CompletableFuture.completedFuture(" world!"),
CompletableFuture.completedFuture(" hello"),
CompletableFuture.completedFuture("java!"));
final CompletableFuture<Void> allCompleted = CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{}));
allCompleted.thenRun(() -> {
futures.stream().forEach(future -> {
try {
System.out.println("get future at:"+System.currentTimeMillis()+", result:"+future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
});
}
测试结果:
get future at:1568892339473, result:hello
get future at:1568892339473, result: world!
get future at:1568892339473, result: hello
get future at:1568892339473, result:java!
几乎同时完成任务!
案列二:
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的图片信息");
return "hello.jpg";
},executor);
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品的属性");
return "黑色+256G";
},executor);
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
System.out.println("查询商品介绍");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "华为";
},executor);
// 所有的都执行完才算是成功
// CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get();//等待所有结果完成
//只要有一个执行成功就算成功
// System.out.println("main....end...."+futureImg.get()+"=>"+futureAttr.get()+"=>"+futureDesc.get());
System.out.println("main....end...."+anyOf.get());
203、商城业务-商品详情-环境搭建
1、添加上域名映射
通过域名可以直接跳转到我们的详情页
2、修改nginx配置
server {
listen 80;
server_name *.gulimall.com gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
3、配置gateway跳转
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=gulimall.com,item.gulimall.com
4、创建页面
gulimall> 代码> html> 详情页 中的item.html复制到templates下面,并且修改其中的静态路径,然后讲gulimall> 代码> html> 详情页 中的静态资源上传到nginx的 static 的 item文件夹中 静态分离
5、页面跳转
修改search服务下的item.html中的a标签的跳转地址
<p class="da">
<a th:href="|http://item.gulimall.com/${product.skuId}.html|">
<img th:src="${product.skuImg}" class="dim">
</a>
</p>
在product中编写对应的controlle的跳转
package com.atguigu.gulimall.product.web;
/**
* @创建人: 放生
* @创建时间: 2022/5/1
* @描述:
*/
@Controller
public class ItemController {
/**
* 展示当前sku的详情
* @param skuId
* @return
*/
@GetMapping("/{skuId}.html")
public String skuItem(@PathVariable("skuId") Long skuId, Model model) throws ExecutionException, InterruptedException {
return "item";
}
}
204、商城业务-商品详情-模型抽取
在product模块下的vo包中新建以下的vo bean
1、SkuItemVo
package com.atguigu.gulimall.product.vo;
import com.atguigu.gulimall.product.entity.SkuImagesEntity;
import com.atguigu.gulimall.product.entity.SkuInfoEntity;
import com.atguigu.gulimall.product.entity.SpuInfoDescEntity;
import lombok.Data;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/5/1
* @描述:
*/
@Data
public class SkuItemVo {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info;
boolean hasStock = true;
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images;
//3、获取spu的销售属性组合。
List<SkuItemSaleAttrVo> saleAttr;
//4、获取spu的介绍
SpuInfoDescEntity desp;
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> groupAttrs;
SeckillInfoVo seckillInfo;//当前商品的秒杀优惠信息
}
2、SkuItemSaleAttrVo
package com.atguigu.gulimall.product.vo;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/5/1
* @描述:
*/
@ToString
@Data
public class SkuItemSaleAttrVo {
private Long attrId;
private String attrName;
private List<AttrValueWithSkuIdVo> attrValues;
}
3、AttrValueWithSkuIdVo
package com.atguigu.gulimall.product.vo;
import lombok.Data;
/**
* @创建人: 放生
* @创建时间: 2022/5/1
* @描述:
*/
@Data
public class AttrValueWithSkuIdVo {
private String attrValue;
private String skuIds;
}
4、SpuItemAttrGroupVo
package com.atguigu.gulimall.product.vo;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/5/1
* @描述:
*/
@ToString
@Data
public class SpuItemAttrGroupVo {
private String groupName;
private List<Attr> attrs;
5、Attr
package com.atguigu.gulimall.product.vo;
import lombok.Data;
@Data
public class Attr {
private Long attrId;
private String attrName;
private String attrValue;
}
205、商城业务-商品详情-规格参数
@Override //SkuInfoServiceImpl @TableName("pms_sku_info")
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
//1、sku基本信息的获取 pms_sku_info
SkuInfoEntity skuInfoEntity = this.getById(skuId);
skuItemVo.setInfo(skuInfoEntity);
Long spuId = skuInfoEntity.getSpuId();
Long catalogId = skuInfoEntity.getCatalogId();
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> skuImagesEntities = skuimagesService.list(new QueryWrapper<SkuimagesEntity>().eq("sku_id", skuId));
skuItemVo.setimages(skuimagesEntities);
//3、获取spu的销售属性组合-> 依赖1 获取spuId
List<SkuItemSaleAttrVo> saleAttrVos=skuSaleAttrValueService.listSaleAttrs(spuId);
skuItemVo.setSaleAttr(saleAttrVos);
//4、获取spu的介绍-> 依赖1 获取spuId
SpuInfoDescEntity byId = spuInfoDescService.getById(spuId);
skuItemVo.setDesc(byId);
//5、获取spu的规格参数信息-> 依赖1 获取spuId catalogId
List<SpuItemAttrGroupVo> spuItemAttrGroupVos=productAttrValueService.getProductGroupAttrsBySpuId(spuId, catalogId);
skuItemVo.setGroupAttrs(spuItemAttrGroupVos);
//TODO 6、秒杀商品的优惠信息
return skuItemVo;
}
206、商城业务-商品详情-销售属性组合
1、sql构建
我们观察商品页面与VO,可以大致分为5个部分需要封装。1 2 4比较简单,单表就查出来了。我们分析3、5
我们在url中首先有sku_id,在从sku_info表查标题的时候,顺便查到了spu_id、catelog_id,这样我们就可以操作剩下表了。
分组规格参数
在5查询规格参数中
pms_product_attr_value 根据spu_id获得spu相关属性
pms_attr_attrgroup_relation根据catelog_id获得属性的分组
@Override
public List<SpuItemAttrGroup> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {
// 1.出当前Spu对应的所有属性的分组信息 以及当前分组下所有属性对应的值
// 1.1 查询所有分组
AttrGroupDao baseMapper = this.getBaseMapper();
return baseMapper.getAttrGroupWithAttrsBySpuId(spuId, catalogId);
}
<!-- 封装自定义结果集 -->
<resultMap id="SpuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroup">
<result column="attr_group_name" property="groupName" javaType="string"></result>
<collection property="attrs" ofType="com.atguigu.gulimall.product.vo.SpuBaseAttrVo">
<result column="attr_name" property="attrName" javaType="string"></result>
<result column="attr_value" property="attrValue" javaType="string"></result>
</collection>
</resultMap>
<select id="getAttrGroupWithAttrsBySpuId" resultMap="SpuItemAttrGroupVo">
SELECT pav.`spu_id`, ag.`attr_group_name`, ag.`attr_group_id`, aar.`attr_id`, attr.`attr_name`,pav.`attr_value`
FROM `pms_attr_group` ag
LEFT JOIN `pms_attr_attrgroup_relation` aar ON aar.`attr_group_id` = ag.`attr_group_id`
LEFT JOIN `pms_attr` attr ON attr.`attr_id` = aar.`attr_id`
LEFT JOIN `pms_product_attr_value` pav ON pav.`attr_id` = attr.`attr_id`
WHERE ag.catelog_id = #{catalogId} AND pav.`spu_id` = #{spuId}
</select>
sku售卖属性
在3查询售卖参数中,
为什么是spu的销售属性,而不是sku的销售属性:url是skuID,但是销售属性要显示所有spu的sku[],为了提前看有无货、快速获得其他的sku_id。
从pms_sku_info查出该spuId对应的skuId
根据spu获取销售属性对应的所有值。首先知道spu是没有销售属性的,而是spu对应sku[]的销售属性
根据各种选项决定一个sku是如何做到的?我们可以利用一下ES的倒排索引。比较难想到,先正序看一下吧
pms_sku_info根据spu得到所有sku_id[]
pms_sku_sale_attr_value根据sku得到销售属性
查询出来之后需要根据属性attr_id分组,分组要查询的列得在group by之后出现过,或者查询的列是用分组函数聚合出的。
而GROUP_CONCAT就把没分组的列都聚合到一起。比如分组后name为zs的对应id有1、2、3,那么GROUP_CONCAT(id)该列就是123
而聚合后如果有重复值,比如id有1,2,2,那么就可以用DISTINCT聚合成1,2
最后GROUP_CONCAT(DISTINCT info.sku_id) sku_ids
查询得到的结果特别像ES中的倒排索引
//3、获取spu的销售属性组合-> 依赖1 获取spuId
List<SkuItemSaleAttrVo> saleAttrVos=skuSaleAttrValueService.listSaleAttrs(spuId);
skuItemVo.setSaleAttr(saleAttrVos);
<resultMap id="SkuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.ItemSaleAttrVo">
<result column="attr_id" property="attrId"></result>
<result column="attr_name" property="attrName"></result>
<collection property="attrValues" ofType="com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo">
<result column="attr_value" property="attrValue"></result>
<result column="sku_ids" property="skuIds"></result>
</collection>
</resultMap>
<select id="getSaleAttrsBySpuId" resultMap="SkuItemSaleAttrVo">
SELECT ssav.`attr_id`,ssav.`attr_name`,ssav.`attr_value`,
GROUP_CONCAT(DISTINCT info.`sku_id`) sku_ids
FROM `pms_sku_info` info LEFT JOIN `pms_sku_sale_attr_value` ssav
ON ssav.`sku_id` = info.`sku_id`
WHERE info.`spu_id` = #{spuId}
GROUP BY ssav.`attr_id`,ssav.`attr_name`,ssav.`attr_value`
</select>
207、商城业务-商品详情-详情页渲染
本章节主要是渲染后端返回的数据,查看thymeleaf的各种语法 渲染数据 参考item.html的代码
208、商城业务-商品详情-销售属性渲染
本章节主要是渲染后端返回的数据,查看thymeleaf的各种语法 渲染数据 参考item.html的代码
209、商城业务-商品详情-sku组和切换
$(".sku_attr_value").click(function(){
//1、点击的元素先添加上自定义的属性。为了识别我们是刚才被点击的
var skus = new Array();
$(this).addClass("clicked");
var curr = $(this).attr("skus").split(",");
//当前被点击的所有sku组合数组放进去
skus.push(curr);
//去掉同一行的所有的checked
$(this).parent().parent().find(".sku_attr_value").removeClass("checked");
$("a[class='sku_attr_value checked']").each(function(){
skus.push($(this).attr("skus").split(","));
});
console.log(skus);
//2、取出他们的交集,得到skuId
var filterEle = skus[0];
for(var i = 1;i<skus.length;i++){
filterEle = $(filterEle).filter(skus[i]);
}
console.log(filterEle[0]);
location.href = "http://item.gulimall.com/"+filterEle[0]+".html";
//4、跳转
});
$(function(){
$(".sku_attr_value").parent().css({"border":"solid 1px #CCC"});
$("a[class='sku_attr_value checked']").parent().css({"border":"solid 1px red"});
})
210、商城业务-商品详情-异步编排优化
1、编写线程池配置类
package com.atguigu.gulimall.product.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(pool.getCoreSize(),
pool.getMaxSize(),pool.getKeepAliveTime(),
TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
2、properties类
package com.atguigu.gulimall.product.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
3、application.properties
# 配置缓存的 类型为 redis
spring.cache.type=redis
#缓存的时间 单位是以毫秒为单位
spring.cache.redis.time-to-live=3600000
# spring.cache.cache-names=cache1,cache2 .... 配置缓存的名字,如果这里配置了那么所有的缓存的名字
# 都要在这里事先配置,会给你禁用掉动态生产缓存名字
# 缓存的前缀,这里指定了就回你用我们指定的前缀,如果没有指定默认就使用缓存的名字(分区)作为前缀,建议使用默认的分区
# spring.cache.redis.key-prefix=CACHE_
# 是否开启缓存的前缀
spring.cache.redis.use-key-prefix=false
# 是否缓存null值 防止缓存穿透
spring.cache.redis.cache-null-values=true
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
4、优化之前的代码
@Autowired
SpuInfoDescService spuInfoDescService;
@Autowired
AttrGroupService attrGroupService;
@Autowired
SeckillFeignService seckillFeignService;
@Autowired
SkuSaleAttrValueService skuSaleAttrValueService;
@Autowired
ThreadPoolExecutor executor;
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info = getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3、获取spu的销售属性组合。
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(res -> {
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
//2、sku的图片信息 pms_sku_images
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> images = imagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
//等到所有任务都完成
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture/*,secKillFuture*/).get();
return skuItemVo;
}
211、商城业务-认证服务-环境搭建
1、新建子模块
2、引入common模块
引入common模块后排除数据库相关的,并将配置注册中心和配置中心中去,主方法添加相关注解开启注册中心服务等
<dependencies>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.1 配置中心
spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=119.3.105.108:8848
server.port=20000
spring.thymeleaf.cache=false
2.2 注册中心
spring.cloud.nacos.config.server-addr=119.3.105.108:8848
spring.application.name=gulimall-auth-server
3.3 主启动
@EnableDiscoveryClient @EnableFeignClients
3、配置域名映射
将老师的资料中的静态文件\gulimall\代码\html\登录页面 复制到 mydata>nginx>html>static>login
和 \gulimall\代码\html\注册页 面复制到mydata>nginx>html>static>reg
4、配置gateway
- id: gulimall_auth_route
uri: lb://gulimall-auth-server
predicates:
- Host=auth.gulimall.com
5、复制index.html
将老师的资料中的静态文件\gulimall\代码\html\登录页面 和 注册页中的index复制到auth 服务的temples的目录下,并把注册页面的index.html改成reg.html
然后将其中的src 和 href 所有引用静态资源的地方引用到nginx中的静态资源中去
6、启动服务测试
212、商城业务-认证服务-好玩的验证码倒计时
1、页面跳转
登录:http://auth.gulimall.com/
注册:http://auth.gulimall.com/reg.html
主页:http://gulimall.com/
将登入页面跳转到注册页,主页的跳转调通,修改页面的对应的按钮的方法即可
2、倒计时
$(function () {
$("#sendCode").click(function () {
if ($(this).hasClass("disabled")) {
// 1.进入倒计时效果
} else {
$.get("/sms/sendcode?phone=" + $("#phoneNum").val(), function (data) {
if (data.code != 0) {
layer.msg(data.msg)
}
});
// 2.给指定手机号发送验证码
timeoutChangeStyle()
}
})
})
// 外部变量计时
let num = 60;
function timeoutChangeStyle() {
$("#sendCode").attr("class", "disabled")
if (num == 0) {//可以再次发送
num = 60;
$("#sendCode").attr("class", "");//取消disabled
$("#sendCode").text("发送验证码");
} else {
var str = num + "s 后再次发送";
$("#sendCode").text(str);
// 1s后回调
setTimeout("timeoutChangeStyle()", 1000);
}
num--
}
3、自定义试图控制器
package com.atguigu.gulimall.auth.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
/**
* 视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
/**
* @GetMapping("/login.html")
* public String loginPage(){
*
* return "login";
* }
*/
registry.addViewController("/login.html").setViewName("login");
//只是get请求能映射
registry.addViewController("/reg.html").setViewName("reg");
}
}
213、商城业务-认证服务-整合短信验证码
1、阿里云-短信服务
短信服务也是第三方服务,我们会写在gulimall-third-party中
https://market.aliyun.com/products/?keywords=短信
购买页面下有请求的url,点击去调试测试
请求参数:
名称 | 类型 | 是否必须 | 描述 |
---|---|---|---|
content | STRING | 必选 | 模板中变量名与参数值,多项值以","分隔 |
phone_number | STRING | 必选 | 手机号码 |
template_id | STRING | 必选 | 模板ID |
一般来说,就是html发送给java,java再发送给短信服务商。用户接收到验证码后,发送过来填写的验证码,进行验证。
2、测试代码
package com.atguigu.gulimall.thirdparty;
import com.atguigu.gulimall.thirdparty.util.HttpUtils;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest
class GulimallThirdPartyApplicationTests {
@Test
public void sendSms(){
String host = "https://smsmsgs.market.alicloudapi.com"; //阿里认证地址
String path = "/sms/";//短信的固定地址
String method = "GET";
String appcode = "93b7e19861a24c519a7548b17dc16d75";//appcode为阿里云的appcode购买后会有
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("code", "6789");
querys.put("phone", "17512080612");
querys.put("skin", "1");// 模板类型
querys.put("sign", "175622"); //公司的签名名称
//JDK 1.8示例代码请在这里下载: http://code.fegine.com/Tools.zip
try {
HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
//System.out.println(response.toString());如不输出json, 请打开这行代码,打印调试头部状态码。
//状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
//获取response的body
System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、整合成配置类注入短信功能
整个短息功能的代码都是封装在了gulimall-third-party 服务,在controller层 提供了对外的接口
3.1 utils
package com.atguigu.gulimall.thirdparty.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
Map<String, String> bodys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
@Override
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
3.2 SmsComponent
package com.atguigu.gulimall.thirdparty.component;
import com.atguigu.gulimall.thirdparty.util.HttpUtils;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2021/6/6
* @描述:
*/
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data
@Component
public class SmsComponent {
private String host;
private String path;
private String skin;
private String sign;
private String appcode;
public void sendSmsCode(String phone,String code){
String method = "GET";
String appcode = "93b7e19861a24c519a7548b17dc16d75";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("code", code);
querys.put("phone", phone);
querys.put("skin", skin);
querys.put("sign", sign);
//JDK 1.8示例代码请在这里下载: http://code.fegine.com/Tools.zip
try {
HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
//System.out.println(response.toString());如不输出json, 请打开这行代码,打印调试头部状态码。
//状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
//获取response的body
System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.3、自定义配置提示
需要引入 在重启编译下项目才能生效
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
3.4 、yaml
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.10.11:8848
alicloud:
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
bucket: gulimall-fangsheng
access-key: LTAI4GFT2csXkBc4jo8Cmphn
secret-key: b8XkWVAT8L8UCoY4HrGzyqsw4LZjXb
sms:
host: https://smsmsgs.market.alicloudapi.com
path: /sms/
skin: 1
sign: 1
appcode: 93b7e19861a24c519a7548b17dc16d75
application:
name: gulimall-third-party
server:
port: 30000
logging:
level:
com.atguigu.gulimall.thirdparty: debug
3.5 、逻辑
认证服务中短信controller接收到电话号请求后,认证服务生成一个验证码
认证服务发送电话+验证码,调用第三方服务
第三方服务调用短信服务商提供的接口,让短信服务商给手机发送生成好的验证码信息
手机接收到验证码后,封装到账号信息中,发送给注册controller
在gulimall-third-party中编写发送短信组件,其中host、path、appcode可以在配置文件中使用前缀spring.cloud.alicloud.sms进行配置
@Data
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Component
public class SmsComponent {
private String host;
private String path;
private String skin;
private String sign;
private String appCode;
public String sendSmsCode(String phone, String code){
String method = "GET";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + this.appCode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("code", code);
querys.put("phone", phone);
querys.put("skin", this.skin);
querys.put("sign", this.sign);
HttpResponse response = null;
try {
response = HttpUtils.doGet(this.host, this.path, method, headers, querys);
//获取response的body
if(response.getStatusLine().getStatusCode() == 200){
return EntityUtils.toString(response.getEntity());
}
} catch (Exception e) {
e.printStackTrace();
}
return "fail_" + response.getStatusLine().getStatusCode();
}
}
编写controller,给别的服务提供远程调用发送验证码的接口
@Controller
@RequestMapping("/sms")
public class SmsSendController {
@Autowired
private SmsComponent smsComponent;
/*** 提供给别的服务进行调用的
该controller是发给短信服务的,不是验证的
*/
@GetMapping("/sendcode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code){
if(!"fail".equals(smsComponent.sendSmsCode(phone, code).split("_")[0])){
return R.ok();
}
return R.error(BizCodeEnum.SMS_SEND_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_SEND_CODE_EXCEPTION.getMsg());
}
}
短信服务编写好后,我们在认证微服务中远程调用。
214、商城业务-认证服务-验证码方刷校验
在gulimall-auth-server服务编写controller去使用openFeign调用我们的之前在gulimall-third-party的controller中的短信接口实现认证
并且要实现防刷问题,比如我点击发送了验证码60s之内点击都不会再次发送,我们会结合redis解决
为了防止恶意攻击短信接口,用redis缓存电话号
在redis中以phone-code为前缀将电话号码和验证码进行存储并将当前时间与code一起存储
如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息
60s以后再次调用,需要删除之前存储的phone-code
code存在一个过期时间,我们设置为10min,10min内验证该验证码有效
1、引入redis和编写配置
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
spring.redis.host=119.3.105.108
spring.redis.port=6379
2、编写feign接口
package com.atguigu.gulimall.auth.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
3、controller
package com.atguigu.gulimall.auth.controller;
import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.auth.feign.ThirdPartFeignService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@Controller
public class LoginController {
@Autowired
ThirdPartFeignService thirdPartFeignService;
@Autowired
StringRedisTemplate redisTemplate;
/**
* 获取短信验证码
* @param phone
* @return
*/
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone){
//TODO 1、接口防刷。
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if(!StringUtils.isEmpty(redisCode)){
long l = Long.parseLong(redisCode.split("_")[1]);
if(System.currentTimeMillis() - l < 60000){
//60秒内不能再发
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
//2、验证码的再次校验。redis。存key-phone,value-code sms:code:17512080612 -> 45678
String code = UUID.randomUUID().toString().substring(0, 5);
String substring = code+"_"+System.currentTimeMillis();
//redis缓存验证码,防止同一个phone在60秒内再次发送验证码
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,substring,10, TimeUnit.MINUTES);
thirdPartFeignService.sendCode(phone,code);
return R.ok();
}
}
4、AuthServerConstant
在common编写常量类
package com.atguigu.common.constant;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
public class AuthServerConstant {
public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
public static final String LOGIN_USER = "loginUser";
}
215、商城业务-认证服务-一步一坑的注册页环境
编写注册的接口 ,前端的代码参照老师的源码即可
1、编写注册vo
package com.atguigu.gulimall.auth.vo;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@Data
public class UserRegistVo {
@NotEmpty(message = "用户名必须提交")
@Length(min = 6,max = 18,message = "用户名必须是6-18位字符")
private String userName;
@NotEmpty(message = "密码必须填写")
@Length(min = 6,max = 18,message = "密码必须是6-18位字符")
private String password;
@NotEmpty(message = "手机号必须填写")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
private String phone;
@NotEmpty(message = "验证码必须填写")
private String code;
}
前面的JSR303校验怎么用:
JSR303校验的结果,被封装到BindingResult
,再结合BindingResult.getFieldErrors()
方法获取错误信息,有错误就重定向至注册页面
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result,
RedirectAttributes redirectAttributes,
HttpSession session) {
if (result.hasErrors()) {
/**
* .map(fieldError -> {
* String field = fieldError.getField();
* String defaultMessage = fieldError.getDefaultMessage();
* errors.put(field,defaultMessage);
* return
* })
*/
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
// model.addAttribute("errors",errors);
redirectAttributes.addFlashAttribute("errors", errors);
//Request method 'POST' not supported
//用户注册->/regist[post]----》转发/reg.html(路径映射默认都是get方式访问的。)
// session.set
//校验出错,转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
@ExceptionHandler
@ControllerAdvice
2、注册用户保存
在gulimall-auth-server服务中编写注册的主体逻辑
从redis中确认手机验证码是否正确,一致则删除验证码,(令牌机制)
会员服务调用成功后,重定向至登录页(防止表单重复提交),否则封装远程服务返回的错误信息返回至注册页面
重定向的请求数据,可以利用RedirectAttributes参数转发
但是他是利用的session原理,所以后期我们需要解决分布式的session问题
重定向取一次后,session数据就消失了,因为使用的是.addFlashAttribute(
重定向时,如果不指定host,就直接显示了注册服务的ip,所以我们重定义写http://…
注: RedirectAttributes可以通过session保存信息并在重定向的时候携带过去
216、商城业务-认证服务-异常机制
在通过验证码,属性校验后,接下来就是调用 member 服务保存我们的注册用户的数据,保存用户前我们需要对用户的账户和手机号做唯一的校验,如果存在,我们通过异常机制,抛出异常,然后被controller感知
member远程服务
通过gulimall-member会员服务注册逻辑
通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间
1、auth 服务的注册接口
/**
*
* //TODO 重定向携带数据,利用session原理。将数据放在session中。
* 只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
*
* //TODO 1、分布式下的session问题。
* RedirectAttributes redirectAttributes:模拟重定向携带数据
* @param vo
* @param result
* @param redirectAttributes
* @return
*/
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result,
RedirectAttributes redirectAttributes,
HttpSession session) {
if (result.hasErrors()) {
/**
* .map(fieldError -> {
* String field = fieldError.getField();
* String defaultMessage = fieldError.getDefaultMessage();
* errors.put(field,defaultMessage);
* return
* })
*/
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
// model.addAttribute("errors",errors);
redirectAttributes.addFlashAttribute("errors", errors);
//Request method 'POST' not supported
//用户注册->/regist[post]----》转发/reg.html(路径映射默认都是get方式访问的。)
// session.set
//校验出错,转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
//1、校验验证码
String code = vo.getCode();
String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)) {
if (code.equals(s.split("_")[0])) {
//删除验证码;令牌机制
redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//验证码通过。 //真正注册。调用远程服务进行注册
R r = memberFeignService.regist(vo);
if (r.getCode() == 0) {
//成功
return "redirect:http://auth.gulimall.com/login.html";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData("msg", new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
//校验出错,转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
}
package com.atguigu.gulimall.auth.feign;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.auth.vo.SocialUser;
import com.atguigu.gulimall.auth.vo.UserLoginVo;
import com.atguigu.gulimall.auth.vo.UserRegistVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
R regist(@RequestBody UserRegistVo vo);
}
2、编写接收远程服务的vo
在member 服务编写接受数据的vo
package com.atguigu.gulimall.member.vo;
import lombok.Data;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@Data
public class MemberRegistVo {
private String userName;
private String password;
private String phone;
}
3、member编写controller
member编写controller接收auth服务的feign调用
package com.atguigu.gulimall.member.controller;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.member.entity.MemberEntity;
import com.atguigu.gulimall.member.exception.PhoneExsitException;
import com.atguigu.gulimall.member.exception.UsernameExistException;
import com.atguigu.gulimall.member.service.MemberService;
import com.atguigu.gulimall.member.vo.MemberRegistVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Map;
/**
* 会员
*
* @author fangsheng
* @email fangsheng@gmail.com
* @date 2022-04-11 11:46:37
*/
@RestController
@RequestMapping("member/member")
public class MemberController {
@Autowired
private MemberService memberService;
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
try{
memberService.regist(vo);
}catch (PhoneExsitException e){
return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
}catch (UsernameExistException e){
return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
.......
3.1 编写PhoneExsitException和UsernameExistException
在member服务编写两个异常类
package com.atguigu.gulimall.member.exception;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
public class PhoneExsitException extends RuntimeException {
public PhoneExsitException() {
super("手机号存在");
}
}
package com.atguigu.gulimall.member.exception;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
public class UsernameExistException extends RuntimeException {
public UsernameExistException() {
super("用户名存在");
}
}
3.2 MemberService
package com.atguigu.gulimall.member.service;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.gulimall.member.entity.MemberEntity;
import com.atguigu.gulimall.member.vo.MemberRegistVo;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.Map;
/**
* 会员
*
* @author fangsheng
* @email fangsheng@gmail.com
* @date 2022-04-11 11:46:37
*/
public interface MemberService extends IService<MemberEntity> {
PageUtils queryPage(Map<String, Object> params);
+ void regist(MemberRegistVo vo);
}
3.3 、MemberServiceImpl
@Autowired
MemberLevelDao memberLevelDao;
@Override
public void regist(MemberRegistVo vo) {
MemberDao memberDao = this.baseMapper;
MemberEntity entity = new MemberEntity();
//设置默认等级
MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
entity.setLevelId(levelEntity.getId());
//检查用户名和手机号是否唯一。为了让controller能感知异常,异常机制
checkPhoneUnique(vo.getPhone());
checkUsernameUnique(vo.getUserName());
entity.setMobile(vo.getPhone());
entity.setUsername(vo.getUserName());
entity.setNickname(vo.getUserName());
//密码要进行加密存储。
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());
entity.setPassword(encode);
//其他的默认信息
//保存
memberDao.insert(entity);
}
@Override
public void checkPhoneUnique(String phone) {
MemberDao memberDao = this.baseMapper;
Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (mobile > 0) {
throw new PhoneExsitException();
}
}
@Override
public void checkUsernameUnique(String username) {
MemberDao memberDao = this.baseMapper;
Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
if (count > 0) {
throw new UsernameExistException();
}
}
217、商城业务-认证服务-MD5&盐值&BCrypt
MD5&MD5盐值加密
• MD5
• Message Digest algorithm 5,信息摘要算法
• 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
• 容易计算:从原数据计算出MD5值很容易。
• 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
• 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
• 不可逆
• 加盐:
• 通过生成随机数与MD5生成字符串进行组合
• 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可
//@RunWith(SpringRunner.class)
//@SpringBootTest
public class GulimallMemberApplicationTests {
@Test
public void contextLoads() {
//e10adc3949ba59abbe56e057f20f883e
//抗修改性: 彩虹表。123456->xxxx 234567->dddd
// String s = DigestUtils.md5Hex("123456");
//MD5不能直接进行密码的加密存储;
//"123456"+System.currentTimeMillis();
//盐值加密;随机值 加盐:$1$+8位字符
//$1$Q4jfb2Xz$jbIxdBvSdYXUQRNjyITL11
//$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1
//$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1 123456
//验证: 123456进行盐值(去数据库查)加密
// String s1 = Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq");
// System.out.println(s1);
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//$2a$10$4IP4F/2iFO2gbSvQKyJzGuI3RhU5Qdtr519KsyoXGAy.b7WT4P1RW
//$2a$10$iv6H6nqQ/NWOMkzgZSJdPeMOBGbn0ayhZ9WAewOk0ssWScSHOgsAW
String encode = passwordEncoder.encode("123456");
boolean matches = passwordEncoder.matches("123456", "$2a$10$4IP4F/2iFO2gbSvQKyJzGuI3RhU5Qdtr519KsyoXGAy.b7WT4P1RW");
System.out.println(encode+"=>"+matches);
}
}
最后我们采用的是spring提供的BCryptPasswordEncoder 加密器
java密码安全可以参考:https://blog.csdn.net/hancoder/article/details/111464250
本文采样md5信息加密算法,但其实他不安全,可以加盐提高安全性Md5Crypt.md5Crypt(bytes,salt)
spring有个加密的BCryptPasswordEncoder.match()
218、商城业务-认证服务-注册完成
debug 测试即可
219、商城业务-认证服务-密码账号登入完成
在gulimall-auth-server模块中的主体逻辑
通过会员服务远程调用登录接口
如果调用成功,重定向至首页
如果调用失败,则封装错误信息并携带错误信息重定向至登录页
1、前端登入编码
参照老师的编码
2、登入的vo
package com.atguigu.gulimall.auth.vo;
import lombok.Data;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@Data
public class UserLoginVo {
private String loginacct;
private String password;
}
响应的vo MemberRespVo
package com.atguigu.common.vo;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@ToString
@Data
public class MemberRespVo implements Serializable {
private Long id;
/**
* 会员等级id
*/
private Long levelId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 手机号码
*/
private String mobile;
/**
* 邮箱
*/
private String email;
/**
* 头像
*/
private String header;
/**
* 性别
*/
private Integer gender;
/**
* 生日
*/
private Date birth;
/**
* 所在城市
*/
private String city;
/**
* 职业
*/
private String job;
/**
* 个性签名
*/
private String sign;
/**
* 用户来源
*/
private Integer sourceType;
/**
* 积分
*/
private Integer integration;
/**
* 成长值
*/
private Integer growth;
/**
* 启用状态
*/
private Integer status;
/**
* 注册时间
*/
private Date createTime;
private String socialUid;
private String accessToken;
private Long expiresIn;
}
3、auth 服务 controller
package com.atguigu.gulimall.auth.controller;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberRespVo;
import com.atguigu.gulimall.auth.feign.MemberFeignService;
import com.atguigu.gulimall.auth.feign.ThirdPartFeignService;
import com.atguigu.gulimall.auth.vo.UserLoginVo;
import com.atguigu.gulimall.auth.vo.UserRegistVo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@Controller
public class LoginController {
@Autowired
ThirdPartFeignService thirdPartFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
MemberFeignService memberFeignService;
.........
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes,
HttpSession session){
//远程登录
R login = memberFeignService.login(vo);
if(login.getCode()==0){
MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
});
成功放到session中
session.setAttribute(AuthServerConstant.LOGIN_USER,data);
return "redirect:http://gulimall.com";
}else {
Map<String,String > errors = new HashMap<>();
errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
}
feign
package com.atguigu.gulimall.auth.feign;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.auth.vo.SocialUser;
import com.atguigu.gulimall.auth.vo.UserLoginVo;
import com.atguigu.gulimall.auth.vo.UserRegistVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
R regist(@RequestBody UserRegistVo vo);
@PostMapping("/member/member/login")
+ R login(@RequestBody UserLoginVo vo);
}
4、远程调用member 登入
package com.atguigu.gulimall.member.controller;
import com.atguigu.common.exception.BizCodeEnume;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.member.entity.MemberEntity;
import com.atguigu.gulimall.member.exception.PhoneExsitException;
import com.atguigu.gulimall.member.exception.UsernameExistException;
import com.atguigu.gulimall.member.service.MemberService;
import com.atguigu.gulimall.member.vo.MemberLoginVo;
import com.atguigu.gulimall.member.vo.MemberRegistVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Map;
/**
* 会员
*
* @author fangsheng
* @email fangsheng@gmail.com
* @date 2022-04-11 11:46:37
*/
@RestController
@RequestMapping("member/member")
public class MemberController {
@Autowired
private MemberService memberService;
......
@PostMapping("/login")
public R login(@RequestBody MemberLoginVo vo){
MemberEntity entity = memberService.login(vo);
if(entity!=null){
return R.ok().setData(entity);
}else{
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
}
}
}
5、member服务 MemberServiceImpl
@Override
public MemberEntity login(MemberLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword(); //123456
//1、去数据库查询 SELECT * FROM `ums_member` WHERE username=? OR mobile=?
MemberDao memberDao = this.baseMapper;
MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct)
.or().eq("mobile", loginacct));
if (entity == null) {
//登录失败
return null;
} else {
//1、获取到数据库的password $2a$10$2xOI1.2DTQxpWeWd3Rk0qOVPTpauodlYkafTjNb4LOMuS1zBEZc5K
String passwordDb = entity.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//2、密码匹配
boolean matches = passwordEncoder.matches(password, passwordDb);
if (matches) {
return entity;
} else {
return null;
}
}
}
6、测试即可
220、商城业务-认证服务-Oauth2.0简介
1、社交登陆
QQ、微博、github 等网站的用户量非常大,别的网站为了
简化自我网站的登陆与注册逻辑,引入社交登陆功能;
步骤:
1)、用户点击 QQ 按钮
2)、引导跳转到 QQ 授权页
3)、用户主动点击授权,跳回之前网页
1 、 OAuth2.0
- OAuth : OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储
在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们
数据的所有内容。
- OAuth2.0 **:**对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分
享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向
用户征求授权。
- 官方版流程:
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
221、商城业务-认证服务-微博登入测试
所有的第三方登入QQ,微信 ,微博都是大同小异,我们以微博为列子
1、第三方登入流程如下
2、我们自己的登入流程
上图说明:第一,二,三,四,五 都是我们的浏览器和微博的服务端在交互,第六步才由微博的服务端回调我们的自己的后端服务,回调的地址是提前要在微博的平台提前配置好的。
微信:https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
客户端是
资源拥有者:用户本人
授权服务器:QQ服务器,微信服务器等。返回访问令牌
资源服务器:拿着令牌访问资源服务器看令牌合法性
1、使用Code换取AccessToken,Code只能用一次
2、同一个用户的accessToken一段时间是不会变化的,即使多次获取
(2) 微博开放平台使用
https://open.weibo.com/authentication
https://open.weibo.com/connect 点击网站接入
3、httpUtils复制到common中
因为我们会调用微博第三方服务获取对应的token,将我们之前在gulimall-third-party的httpUtils复制到common中,并且把pom的引入
4、集成步骤
上图解析
# 1. 引导需要授权的用户到如下地址:
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
该地址是写在前端的跳转到微博的按钮,也就是<a>微博登入</a>的href,其中YOUR_CLIENT_ID 是微博平台申请后给的,YOUR_REGISTERED_REDIRECT_URI写你自己项目需要跳转回的地址,该地址是返回到我们自己应用的controller的处理地址不是页面。
# 2. 如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
# 3. 换取Access Token
https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE
其中client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET可以使用basic方式加入header中,返回值
根据返回的code,发送3步的请求换取Access Token,注意code只能换取一次但是Access Token是一段时间内都有效的。
# 4、又Access Token就能提取微博公开的用户的资源,可以获取的资源在微博的开发文档中有写明的哪些对外接口。
填写一些个人信息后,https://open.weibo.com/apps/new?sort=web 创建新应用gulimallxxx,会得到APP KEY和APP Secret
在高级信息里填写
授权回调页:gulimall.com/success
取消授权回调页:gulimall.com/fail
https://open.weibo.com/wiki/授权机制说明 查看OAuth2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E1tFy3Hp-1652453751102)(C:/Users/LENOVO/AppData/Roaming/Typora/typora-user-images/image-20210606122819490.png)]
-
引导需要授权的用户到如下地址
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=授权后跳转的uri 示例: https://api.weibo.com/oauth2/authorize? client_id=刚才申请的APP-KEY & response_type=code& redirect_uri=http://gulimall.com/success
- 如果用户同意授权(输入账号密码),带着code,页面跳转至 gulimall.com/success/?code=CODE
跳回我们网站的时候,带了一个code码,这个code码可以理解为用户登录的sessionID
- POST拿着code码换取Access Token
https://api.weibo.com/oauth2/access_token?
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
grant_type=authorization_code&
redirect_uri=YOUR_REGISTERED_REDIRECT_URI&
code=CODE
其中client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET可以使用basic方式加入header中,返回值
{
"access_token": "SlAV32hkKG",
"remind_in": 3600, # 也是声明周期,但将废弃
"expires_in": 3600 # access_token的生命周期;
}
- 使用获得的Access Token调用API,可以获取头像等信息 https://open.weibo.com/wiki/2/users/show
结果返回json
(3) 代码编写
注意点:
登录成功得到了code,这不应该提供给用户
拿着code还有其他信息APP-KEY去获取token,更不应该给用户看到
应该回调的是后台的controller,在后台处理完token逻辑后返回
把成功后回调改为:gulimall.com/oauthn2.0/weibo/success
/weibo/success
通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录
进行账号保存,主要有uid、token、expires_in
若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页
登录成功跳转到首页,但是怎么保证没有验证情况下访问不了首页:用shiro等拦截器功能
222、商城业务-认证服务-社交登入回调
在auth 服务编写回调的接口
@GetMapping("/weibo/success") // Oath2Controller
public String weiBo(@RequestParam("code") String code, HttpSession session) throws Exception {
// 根据code换取 Access Token
Map<String,String> map = new HashMap<>();
map.put("client_id", "1294828100");
map.put("client_secret", "a8e8900e15fba6077591cdfa3105af44");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
map.put("code", code);
Map<String, String> headers = new HashMap<>();
// 去获取token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);
if(response.getStatusLine().getStatusCode() == 200){
// 获取响应体: Access Token
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
// 相当于我们知道了当前是那个用户
// 1.如果用户是第一次进来 自动注册进来(为当前社交用户生成一个会员信息 以后这个账户就会关联这个账号)
R login = memberFeignService.login(socialUser);
if(login.getCode() == 0){
MemberRsepVo rsepVo = login.getData("data" ,new TypeReference<MemberRsepVo>() {});
log.info("\n欢迎 [" + rsepVo.getUsername() + "] 使用社交账号登录");
// 第一次使用session 命令浏览器保存这个用户信息 JESSIONSEID 每次只要访问这个网站就会带上这个cookie
// 在发卡的时候扩大session作用域 (指定域名为父域名)
// TODO 1.默认发的当前域的session (需要解决子域session共享问题)
// TODO 2.使用JSON的方式序列化到redis
// new Cookie("JSESSIONID","").setDomain("gulimall.com");
session.setAttribute(AuthServerConstant.LOGIN_USER, rsepVo);
// 登录成功 跳回首页
return "redirect:http://gulimall.com";
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}else{
return "redirect:http://auth.gulimall.com/login.html";
}
}
223、商城业务-认证服务-社交登入完成
member 服务保存从第三方取回的信息,如果第一次登入,就在我们的服务自动注册一个账号,如果已经存在就更新相关信息即可
token保存
- 登录包含两种流程,实际上包括了注册和登录
- 如果之前未使用该社交账号登录,则使用
token
调用开放api获取社交账号相关信息(头像等),然后将获取来的用户信息(uid 等作为信息自动注册一个用户信息)注册并将结果返回 - 如果之前已经使用该社交账号登录,则更新
token
并将结果返回
1、auth服务 feign
package com.atguigu.gulimall.auth.feign;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.auth.vo.SocialUser;
import com.atguigu.gulimall.auth.vo.UserLoginVo;
import com.atguigu.gulimall.auth.vo.UserRegistVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @创建人: 放生
* @创建时间: 2022/5/2
* @描述:
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
R regist(@RequestBody UserRegistVo vo);
@PostMapping("/member/member/login")
R login(@RequestBody UserLoginVo vo);
@PostMapping("/member/member/oauth2/login")
+ R oauthlogin(@RequestBody SocialUser socialUser) throws Exception;
}
2、member 服务 MemberController
@PostMapping("/oauth2/login")
public R oauthlogin(@RequestBody SocialUser socialUser) throws Exception {
MemberEntity entity = memberService.login(socialUser);
if(entity!=null){
//TODO 1、登录成功处理
return R.ok().setData(entity);
}else{
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
}
}
3、member服务的 MemberServiceImpl
同时在实体类和数据添加上对应的字段:socialUid , accessToken ,expiresIn
/**
* 社交登录
* @param socialUser
* @return
* @throws Exception
*/
@Override
public MemberEntity login(SocialUser socialUser) {
//登录和注册合并逻辑
String uid = socialUser.getUid();
//1、判断当前社交用户是否已经登录过系统;
MemberDao memberDao = this.baseMapper;
MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity != null) {
//这个用户已经注册
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
memberDao.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
}else{
//2、没有查到当前社交用户对应的记录我们就需要注册一个
MemberEntity regist = new MemberEntity();
try{
//3、查询当前社交用户的社交账号信息(昵称,性别等)
Map<String,String> query = new HashMap<>();
query.put("access_token",socialUser.getAccess_token());
query.put("uid",socialUser.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<String, String>(), query);
if(response.getStatusLine().getStatusCode() == 200){
//查询成功
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
//昵称
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
//........
regist.setNickname(name);
regist.setGender("m".equals(gender)?1:0);
//........
}
}catch (Exception e){}
regist.setSocialUid(socialUser.getUid());
regist.setAccessToken(socialUser.getAccess_token());
regist.setExpiresIn(socialUser.getExpires_in());
memberDao.insert(regist);
return regist;
}
}
224、商城业务-认证服务-社交登入测试完成
debug 调试即可
225、商城业务-认证服务-分布式session不共享不同步问题
以上步骤我们实现了注册登入,以及第三方的登入等功能,现在我们需要把我们登入成功后的用户昵称或者名字显示在我们的首页,单体应用的话我们是把用户数据存储在session中的,但是我们现在登入是在auth服务的auth.gulimall.com,在登入成功后就跳转到了首页gulimall.com 所以无法在首页取出保存在auth服务端的session,所以我们需要解决分布式session不共享的问题。
1、了解session原理
当我们在后端登入成功后,我们把数据存入session中(实际上就是存入了服务器的内存中,可以理解为就是一个map中),然后命令浏览器自动保存jsessionid,然后在同一个会话内,下次访问会带上这个jsessionid,就能取出内存中的session的数据,而每个jsessionid是有其作用域的(Domain), 我们登入的是在auth.gulimall.com,登入到首页后在gulimall.com域中无法取到,所以session的共享问题就产生了。
session存储在服务端,jsessionId存在客户端,每次通过jsessionid
取出保存的数据
问题:但是正常情况下session
不可跨域,它有自己的作用范围
这个session被sessionManager管理着
JsessionId列 | 说明 |
---|---|
Value | XXXXXX… |
Domain | gulimall.com要放大域名作用域 |
Path | / |
/Expires/Max-Age | 40 |
226、商城业务-认证服务-分布式session解决方案原理
分布式session解决方案
session要能在不同服务和同服务的集群的共享
1、 session复制
用户登录后得到session后,服务把session也复制到别的机器上,显然这种处理很不好
2、hash一致性
根据用户,到指定的机器上登录。但是远程调用还是不好解决
3、客户端存储
4、 redis统一存储
最终的选择方案,把session放到redis中
....
R oauthlogin = memberFeignService.oauthlogin(socialUser);
if(oauthlogin.getCode() == 0){
MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
});
log.info("登录成功:用户:{}",data.toString());
//1、第一次使用session;命令浏览器保存卡号。JSESSIONID这个cookie;
//以后浏览器访问哪个网站就会带上这个网站的cookie;
//子域之间; gulimall.com auth.gulimall.com order.gulimall.com
//发卡的时候(指定域名为父域名),即使是子域系统发的卡,也能让父域直接使用。
//TODO 1、默认发的令牌。session=dsajkdjl。作用域:当前域;(解决子域session共享问题)
//TODO 2、使用JSON的序列化方式来序列化对象数据到redis中
session.setAttribute("loginUser",data);
// new Cookie("JSESSIONID","dadaa").setDomain("");
// servletResponse.addCookie();
//2、登录成功就跳回首页
return "redirect:http://gulimall.com";
....
227、商城业务-认证服务-springSession整合
1、SpringSession整合redis
然后在结合了父子域的作用范围,把sessionid颁给父域名,从而达到域名共享。
https://spring.io/projects/spring-session-data-redis
https://docs.spring.io/spring-session/docs/2.4.2/reference/html5/#modules
通过SpringSession修改session的作用域
会员服务、订单服务、商品服务,都是去redis里存储session
1) 环境搭建
Oauth服务导入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2) 添加配置
spring.session.store-type=redis
server.servlet.session.timeout=30m
spring.redis.host=119.3.105.108
3) 在启动类上添加注解
@EnableRedisHttpSession //创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
public class GulimallAuthServerApplication {
4) 扩大session作用域
序列化的问题
cookie的domain的问题
扩大session作用域
由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化
并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com
@Configuration
public class GulimallSessionConfig {
@Bean // redis的json序列化
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean // cookie
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("GULISESSIONID"); // cookie的键
serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
return serializer;
}
}
注意 把这个配置放到每个微服务下,也可以放在common模块下(以上每一步骤都要配置 放一遍)
228、商城业务-认证服务-自定义springSession完成子域session共享
将我们的session域保存设置为父域中,如上一章节的GulimallSessionConfig的配置文件所示
serializer.setDomainName(“gulimall.com”); // 扩大session作用域,也就是cookie的有效域
229、商城业务-认证服务-springSession原理
SpringSession核心原理 - 装饰者模式
网上百度一下:https://blog.csdn.net/m0_46539364/article/details/110533408
# SpringSession 核心原理
1)、@EnableRedisHttpSession导入RedisHttpSessionConfiguration配置
1、给容器中添加了一个组件
SessionRepository = 》》》【RedisOperationsSessionRepository】==》redis操作session。session的增删改查封装类
2、SessionRepositoryFilter == 》Filter: session'存储过滤器;每个请求过来都必须经过filter
1、创建的时候,就自动从容器中获取到了SessionRepository;
2、原始的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
3、以后获取session。request.getSession();
//SessionRepositoryRequestWrapper
4、wrappedRequest.getSession();===> SessionRepository 中获取到的。
装饰者模式;
自动延期;redis中的数据也是有过期时间。
就是分析@EnableRedisHttpSession,
@Import({RedisHttpSessionConfiguration.class})
@Configuration( proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
public class RedisHttpSessionConfiguration
extends SpringHttpSessionConfiguration // 继承
implements 。。。{
// 后面SessionRepositoryFilter会构造时候自动注入他
@Bean // 操作session的方法,如getSession() deleteById()
public RedisIndexedSessionRepository sessionRepository() {
SessionRepositoryFilter,每个请求都要经过该filter
public class SpringHttpSessionConfiguration
implements ApplicationContextAware {
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) { // 注入前面的bean
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
前面我们@Bean注入了sessionRepositoryFilter,他是一个过滤器,那我们需要知道他过滤做了什么事情:
原生的获取session时是通过HttpServletRequest获取的
这里对request进行包装,并且重写了包装request的getSession()方法
@Override // SessionRepositoryFilter.java
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//对原生的request、response进行包装
// SessionRepositoryRequestWrapper.getSession()
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
绣花前面的代码,controller层加参数HttpSession,直接session.setAttribute(“user”,user)即可
前端页面的显示可以用<li th:if="${session.loginUser} ==null">
230、商城业务-认证服务-页面效果完成
之前只是第三方存入了session,需要把登入接口也存入session
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes,
HttpSession session){
//远程登录
R login = memberFeignService.login(vo);
if(login.getCode()==0){
MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
});
成功放到session中
session.setAttribute(AuthServerConstant.LOGIN_USER,data);
return "redirect:http://gulimall.com";
}else {
Map<String,String > errors = new HashMap<>();
errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
分布式登录总结
登录url:http://auth.gulimall.com/login.html
(注意是url,不是页面。)
判断session中是否有user对象
没有user对象,渲染login.html页面
用户输入账号密码后发送给 url:auth.gulimall.com/login
根据表单传过来的VO对象,远程调用memberFeignService验证密码
如果验证失败,取出远程调用返回的错误信息,放到新的请求域,重定向到登录url
如果验证成功,远程服务就返回了对应的MemberRespVo对象,
然后放到分布式redis-session中,key为"loginUser",重定向到首页gulimall.com,
同时也会带着的GULISESSIONID
重定向到非auth项目后,先经过拦截器看session里有没有loginUser对象
有,放到静态threadLocal中,这样就可以操作本地内存,无需远程调用session
没有,重定向到登录页
有user对象,代表登录过了,重定向到首页,session数据还依靠sessionID持有着
额外说明:
问题1:我们有sessionId不就可以了吗?为什么还要在session中放到User对象?
为了其他服务可以根据这个user查数据库,只有session的话不能再次找到登录session的用户
问题2:threadlocal的作用?
他是为了放到当前session的线程里,threadlocal就是这个作用,随着线程创建和消亡。把threadlocal定义为static的,这样当前会话的线程中任何代码地方都可以获取到。如果只是在session中的话,一是每次还得去redis查询,二是去调用service还得传入session参数,多麻烦啊
问题3:cookie怎么回事?不是在config中定义了cookie的key和序列化器?
序列化器没什么好讲的,就是为了易读和来回转换。而cookie的key其实是无所谓的,只要两个项目里的key相同,然后访问同一个域名都带着该cookie即可。
231、商城业务-认证服务-单点登入简介
上面解决了同域名的session问题,但如果taobao.com和tianmao.com这种不同的域名也想共享session呢?
去百度了解下:https://www.jianshu.com/p/75edcc05acfd
最终解决方案:都去中央认证器
spring session已经解决不了不同域名的问题了。无法扩大域名
sso思路
记住一个核心思想:建议一个公共的登陆点server,他登录了代表这个集团的产品就登录过了
上图是CAS官网上的标准流程,具体流程如下:有两个子系统app1、app2
用户访问app1系统,app1系统是需要登录的,但用户现在没有登录。
跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app1系统,同时将ST作为参数传递给app1系统。
app1系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。用户访问app2系统,app2系统没有登录,跳转到SSO。
由于SSO已经登录了,不需要重新登录认证。
SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
app2拿到ST,后台访问SSO,验证ST是否有效。
验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的。SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。如果想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?
其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。
1、前置概念
1.1 单点登入业务介绍
早期单一服务器,用户认证。
缺点:单点性能压力,无法扩展
分布式,SSO(single sign on)模式
解决 :
用户身份信息独立管理,更好的分布式管理。
可以自己扩展安全策略
跨域不是问题
缺点:
认证服务器访问压力较大。
1.2 基本概念
什么是跨域 Web SSO 。
域名通过“.”号切分后,从右往左看,不包含“.”的是顶级域名,包含一个“.”的是一级域名,包含两个“.”的是二级域名,以此类推。
例如对网址 http://www.cnblogs.com/baibaomen,域名部分是 www.cnblogs.com。
用“.”拆分后从右往左看:
cookie.setDomain(“.cnblogs.com”);//最多设置到本域的一级域名这里 cookie.setDomain(“.baidu.com”);//最多设置到本域的一级域名这里 ”com”不包含“.”,是顶级域名; “cnblogs.com”包含一个“.”,是一级域名; www.cnblogs.com 包含两个“.”,是二级域名。 blog.cnblogs.com news.cnblogs.com
跨域 Web SSO 指的是针对 Web 站点,各级域名不同都能处理的单点登录方案。
浏览器读写 cookie 的安全性限制:一级或顶级域名不同的网站, 无法读到彼此写的 cookie
所以 baidu.com 无法读到 cnblogs.com 写的 cookie。 一级域名相同,只是二级或更高级域名不同的站点,可以通过设置 domain 参数共享 cookie 读写。这种场景可以选择不跨域的 SSO 方案。 域名相同,只是 https 和 http 协议不同的 URL,默认 cookie 可以共享。知道这一点对处理 SSO 服务中心要登出
http 协议是无状态协议。浏览器访问服务器时,要让服务器知道 你是谁,只有两种方式:
方式一:把“你是谁”写入 cookie。它会随每次 HTTP 请求带到服务端;
方式二:在 URL、表单数据中带上你的用户信息(也可能在 HTTP 头部)。这种方式依赖
于从特定的网页入口进入,因为只有走特定的入口,才有机会拼装出相应的信息,提交到服 务端。 大部分 SSO 需求都希望不依赖特定的网页入口(集成门户除外),所以后一种方式有局限 性。适应性强的方式是第一种,即在浏览器通过 cookie 保存用户信息相关凭据,随每次请 求传递到服务端。我们采用的方案是第一种。
2、Cookies接入方式
3、Token 接入方式
类似社交登陆
4、有状态登入
为了保证客户端 cookie 的安全性,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 tomcat 中的 session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的 session。然后下次请求,用户携带 cookie 值来,我们就能识别到对应 session,从而找到用户的信息。
缺点是什么?
-
服务端保存大量数据,增加服务端压力
-
服务端保存用户状态,无法进行水平扩展
-
客户端请求依赖服务端,多次请求必须访问同一台服务器
即使使用 redis 保存用户的信息,也会损耗服务器资源。
5、无状态登入
微服务集群中的每个服务,对外提供的都是 Rest 风格的接口。而 Rest 风格的一个最重要的
规范就是:服务的无状态性,即:
-
服务端不保存任何客户端请求者信息
-
客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份带来的好处是什么呢?
-
客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
-
服务端的集群和状态对客户端透明
-
服务端可以任意的迁移和伸缩
-
减小服务端存储压力
6、集成社交登入
前面已经讲过
7、JWT
7.1 简介
JWT,全称是 Json Web Token, 是 JSON 风格轻量级的授权和身份认证规范,可实现无状态、
分布式的 Web 应用授权;官网:https://jwt.io
GitHub 上 jwt 的 java 客户端:https://github.com/jwtk/jjwt
我们最终可以利用 jwt 实现无状态登录
7.2 数据格式
JWT 包含三部分数据:
- Header :头部,通常头部有两部分信息:
- token 类型:JWT
- 加密方式:base64(HS256)
- Payload :载荷,就是有效数据,一般包含下面信息:
- 用户身份信息(注意,这里因为采用 base64 编码,可解码,因此不要存放敏感信息)
- 注册声明:如 token 的签发时间,过期时间,签发人等
这部分也会采用 base64 编码,得到第二部分数据
- Signature :签名,是整个数据的认证信息。根据前两步的数据,再加上指定的密钥(secret)
(不要泄漏,最好周期性更换),通过 base64 编码生成。用于验证整个数据完整和可靠性
7.3 交互流程
步骤:
- 1、用户登录
- 2、服务的认证,通过后根据 secret 生成 token
- 3、将生成的 token 返回给浏览器
- 4、用户每次请求携带 token
- 5、服务端利用秘钥解读 jwt 签名,判断签名有效后,从 Payload 中获取用户信息
- 6、处理请求,返回响应结果
因为 JWT 签发的 token 中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的
就无需保存用户信息,甚至无需去数据库查询,完全符合了 Rest 的无状态规范。
7.4 授权中心流程
7.5 JWT优势
-
易于水平扩展
-
在 cookie-session 方案中,cookie 内仅包含一个 session 标识符,而诸如用户信息、授权列表等都保存在服务端的 session 中。如果把 session 中的认证信息都保存在 JWT 中,在服务端就没有 session 存在的必要了。当服务端水平扩展的时候,就不 用处理 session 复制(session replication)/ session 黏连(sticky session)或是引入外部 session 存储了[实际上 spring-session 和 hazelcast 能完美解决这个问题]。
-
防护 CSRF(跨站请求伪造)攻击
-
访问某个网站会携带这个域名下的 cookie。所以可能导致攻击。但是我们可以把 jwt放在请求头中发送。
-
Jwt 放在请求头中,就必须把 jwt 保存在 cookie 或者 localStorage 中。保存这里 js 就会读写,又会导致 xss 攻击。可以设置 cookie,httponly=true 来防止 xss
-
安全
-
只是 base64 编码了,cookie+session 直接将数据保存在服务端,看都看不见,请问哪个更安全?
7.6 使用JWT带来的问题
我们不建议使用 jwt+cookie 代替 session+cookie 机制,jwt 更适合 restful api
-
jwt token 泄露了怎么办?
-
这个问题可以不考虑,因为 session+cookie 同样泄露了 cookie 的 jsessionid 也会有这个问题
-
我们可以遵循以下规范减少风险
-
使用 https 加密应用
-
返 回 jwt 给 客 户 端 时 设 置 httpOnly=true 并 且 使 用 cookie 而 不 是LocalStorage 存储 jwt,防止 XSS 攻击和 CSRF 攻击
-
secret 如果泄露会导致大面积风险
-
定期更新
-
Secret 设计可以和用户关联起来,每个用户不一样。防止全用一个 secret
-
注销和修改密码
-
传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态 保存在服务端。我们不害怕注销后的假登录
-
Jwt 会有问题。用户如果注销了或者修改密码了。恶意用户还使用之前非法盗取来 的 token,可以在不重新登录的情况下继续使用
-
可以按程度使用如下设计,减少一定的风险
-
清空客户端的 cookie,这样用户访问时就不会携带 jwt,服务端就认为用 户需要重新登录。这是一个典型的假注销,对于用户表现出退出的行为,实际上这个时候携带对应的 jwt 依旧可以访问系统。
-
清空或修改服务端的用户对应的 secret,这样在用户注销后,jwt 本身不 变,但是由于 secret 不存在或改变,则无法完成校验。这也是为什么将secret 设计成和用户相关的原因
-
借助第三方存储,管理 jwt 的状态,可以以 jwt 为 key,去 redis 校验存在性。但这样,就把无状态的 jwt 硬生生变成了有状态了,违背了 jwt的初衷。实际上这个方案和 session 都差不多了。
-
修改密码则略微有些不同,假设号被到了,修改密码(是用户密码,不是 jwt 的 secret)之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,所以仅仅清空 cookie 自然是不够的,这时,需要强制性的修改 secret
-
续签问题
-
传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。而 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,而 jwt 想延期 这个 exp 就有点身不由己了,因为 payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签!
-
可如下解决,但都不是完美方案
-
每次请求刷新 jwt:简单暴力,性能低下,浪费资源。
-
只要快要过期的时候刷新 jwt:jwt 最后的几分钟,换新一下。但是如果用户连续操作了 27 分钟,只有最后的 3 分钟没有操作,导致未刷新jwt,就很难受。
-
完 善 refreshToken : 借 鉴 oauth2 的 设 计 , 返 回 给 客 户 端 一 个refreshToken,允许客户端主动刷新 jwt。这样做,还不如用 oauth2
-
使用 redis 记录独立的过期时间:jwt 作为 key,在 redis 中保存过期时间,每次使用在 redis 中续期,如果 redis 没有就认为过期。但是这样做,还不如用 session+cookie
-
总结
-
在 Web 应用中,别再把 JWT 当做 session 使用,绝大多数情况下,传统的 cookie-session 机制工作得更好
-
JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也 很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态
232、商城业务-认证服务-补-框架效果演示
可以直接跳过本章节看233节的案列演示
1、开源项目
先看一下开源sso的项目:https://gitee.com/xuxueli0323/xxl-sso
ssoserver.com 登录认证服务
client1.com
cleitn2.com
修改HOSTS:127.0.0.1 ssoserver.com+client1.com+client2.com
server:登录服务器、8080 、ssoserver.com
web-sample1:项目1 、8081 、client1.com
web-sample2:项目1 、8082 、client2.com
# 根项目下
mvn clean package -Dmaven.skip.test=true
# 打包生成了server和client包
# 启动server和client
#server8080 cient1:web-sample8081 cient2:web-sample8082
# 让client12登录一次即可
java -jar server.jar # 8080
java -jar client.jar
# 启动多个web-sample模拟多个微服务
流程
发送8081/employees请求,判断没登录就跳转到server.com:8080/login.html登录页,并带上现url
server登录页的时候,有之前带过来的url信息,发送登录请求的时候也把url继续带着
doLogin登录成功后返回一个token(保存到server域名下)然后重定向
登录完后重定向到带的url参数的地址。
跳转回业务层的时候,业务层要能感知是登录过的,调回去的时候带个uuid,用uuid去redis里(课上说的是去server里再访问一遍,为了安全性?)看user信息,保存到它系统里自己的session
以后无论哪个系统访问,如果session里没有指定的内容的话,就去server登录,登录过的话已经有了server的cookie,所以不用再登录了。回来的时候就告诉了子系统应该去redis里怎么查你的用户内容
还得得补充一句,老师课上讲得把票据放到controller里太不合适了,你最起码得放到filter或拦截器里
sso解决
client1.com 8081 和 client2.com 8082 都跳转到ssoserver 8080
给登录服务器留下痕迹
登录服务器要将token信息重定向的时候,带到url地址上
其他系统要处理url地址上的token,只要有,将token对应的用户保存到自己的session
自己系统将用户保存在自己的session中
<body>
<form action="/employee" method="get">
<input type="text" name="username" value="test">
<button type="submit">查询</button>
</form>
</body>
@GetMapping(value = "/employees") // a系统
public String employees(Model model,
HttpSession session,
@RequestParam(value = "redisKey", required = false) String redisKey) {
// 有loginToken这个参数,代表去过server端登录过了,server端里在redis里保存了个对象,而key:uuid给你发过来了
// 有loginToken这个参数的话代表是从登录页跳回来的,而不是系统a直接传过来的
// 你再拿着uuid再去查一遍user object,返回后设置到当前的系统session里
// 提个问题:为什么当时不直接返回user对象,而是只返回个uuid?其实也可以,但是参数的地方也得是required = false。可能也有一些安全问题
if (!StringUtils.isEmpty(redisKey)) { // 这个逻辑应该写到过滤器或拦截器里
RestTemplate restTemplate=new RestTemplate();
// 拿着token去服务器,在服务端从redis中查出来他的username
ResponseEntity<Object> forEntity =
restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?redisKey="+ redisKey, Object.class);
Object loginUser = forEntity.getBody();
// 设置到自己的session中
session.setAttribute("loginUser", loginUser);
}
// session里有就代表登录过 // 获得user
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null) { // 又没有loginToken,session里又没有object,去登录页登录
return "redirect:" + "http://ssoserver.com:8080/login.html"
+ "?url=http://clientA.com/employees";
} else {// 登录过,执行正常的业务
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
server端
-
子系统都先去
login.html
这个请求,
- 这个请求会告诉登录过的系统的令牌,
- 如果没登录过就带着url重新去server端,server给一个登录页,如下
<body>
<form action="/doLogin" method="post">
<!--刚才要请求数据的url,没有也没关系,就不跳转了呗-->
<input type="hidden" name="url" th:value="${url}">
<!--带上当前登录的username-->
<!-- <input type="hidden" name="user" th:value="${username}">-->
用户名:<input name="username" value="test"><br/>
密码:<input name="password" type="password" value="test">
<input type="submit" value="登录">
</form>
</body>
当点击登录之后,server端返回一个cookie,子系统重新返回去重新请去业务。于是又来server端验证,这回server端有cookie了,该cookie里有用户在redis中的key,重定向时把key带到url后面,子系统就知道怎么找用户信息了
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ResponseBody
@GetMapping("/userInfo") // 得到redis中的存储过的user信息,返回给子系统的session中
public Object userInfo(@RequestParam("redisKey") String redisKey){
// 拿着其他域名转发过来的token去redis里查
Object loginUser = stringRedisTemplate.opsForValue().get(redisKey);
return loginUser;
}
@GetMapping("/login.html") // 子系统都来这
public String loginPage(@RequestParam("url") String url,
Model model,
@CookieValue(value = "redisKey", required = false) String redisKey) {
// 非空代表就登录过了
if (!StringUtils.isEmpty(redisKey)) {
// 告诉子系统他的redisKey,拿着该token就可以查redis了
return "redirect:" + url + "?redisKey=" + redisKey;
}
model.addAttribute("url", url);
// 子系统都没登录过才去登录页
return "login";
}
@PostMapping("/doLogin") // server端统一认证
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletResponse response,
@RequestParam(value="url",required = false) String url){
// 确认用户后,生成cookie、redis中存储 // if内代表取查完数据库了
if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){//简单认为登录正确
// 登录成功跳转 跳回之前的页面
String redisKey = UUID.randomUUID().toString().replace("-", "");
// 存储cookie, 是在server.com域名下存
Cookie cookie = new Cookie("redisKey", redisKey);
response.addCookie(cookie);
// redis中存储
stringRedisTemplate.opsForValue().set(redisKey, username+password+"...", 30, TimeUnit.MINUTES);
// user中存储的url 重定向时候带着token
return "redirect:" + url + "?redisKey=" + redisKey;
}
// 登录失败
return "login";
}
}
233-235、商城业务-认证服务-单点登入流程
1、搭建服务
分别搭建单点登入的服务端和客户端
sso-server
sso-client
加入父工程
<module>gulimall-test-sso-server</module>
<module>gulimall-test-sso-client</module>
<module>gulimall-test-sso-client2</module>
2、域名映射
ssoserver.com 登录认证服务
client1.com
cleitn2.com
修改HOSTS:127.0.0.1 ssoserver.com+client1.com+client2.com
C:\Windows\System32\drivers\etc hosts
119.3.105.108 gulimall.com
119.3.105.108 auth.gulimall.com
119.3.105.108 search.gulimall.com
119.3.105.108 item.gulimall.com127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
3、流程图
过程描述:
1、浏览器访问客户端中的controller中的受保护资源 http://client1.com/employees, 会先判断当前的用户是否登入,(判断条件1:请求参数中是否有token,因为有可能也是从ssoserver中刚验证回来,刚验证回来就会带token的,条件2:client1的session中存在登入过的信息)
2、如果两个判断条件都不满足,则说明当前用户确实没有登入,则会重定向ssoserver认证服务器,进行登入,在重定向的时候一定要带上当前重定向页面的url(redirect_url),因为认证服务器需要记录下这个url方便认证成功后认证服务器重定向返回客户端(否则不知道您当初是在客户端的哪一个页面操作的认证)
3、认证服务器接受到认证请求后,先判断cookies中有没有sso_token的值(这个值是在认证服务器认证成功后,让浏览器保存的痕迹),如果有说明用户认证过(当浏览器访问client1的时候,认证后,认证服务器ssoserver就会在浏览器的cookies中打上痕迹set cookies sso_tiken = 某个值,这样当同个用户操作浏览器向clien2.com 发送访问受限资源而到认证服务器的时候就能取到这个sso_tiken = 某个值 )就直接返回,否则进入登入页面。
4、登入,收入密码账号,验证成功后,就将用户信息保存到redis中,同时认证服务器ssoserver就会在浏览器的cookies中打上痕迹set cookies sso_tiken = 某个值,命令浏览器保存它,最后才根据之前带来的redirect_url 返回回客户端,并将token(这个token就是存在redis用户的数据key)
5、返回回客户端后,根据返回的key取出redis的用户信息,并存入当前client1的session中去,从而认证完成
6、如果此时在发起 http://client2.com/boss 的后保护的资源,又进入了步骤一,显然这个时候浏览器中会有token,这个时候就不用在验证了,直接返回,并根据token取出redis中的用户信息保存在client2 session即可。
4、编码实现
4.1 ssoserver
修改pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
修改properties
server.port=8080
spring.redis.host=119.3.105.108
添加controller
package com.atguigu.gulimall.ssoserver.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token){
String s = redisTemplate.opsForValue().get(token);
return s;
}
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url, Model model,
@CookieValue(value = "sso_token",required = false) String sso_token){
if(!StringUtils.isEmpty(sso_token)){
//说明之前有人登录过,浏览器留下了痕迹
return "redirect:"+url+"?token="+sso_token;
}
model.addAttribute("url",url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password")String password,
@RequestParam("url")String url,
HttpServletResponse response){
if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
//登录成功,跳回之前页面
//把登录成功的用户存起来。
String uuid = UUID.randomUUID().toString().replace("-","");
redisTemplate.opsForValue().set(uuid,username);
Cookie sso_token = new Cookie("sso_token",uuid);
response.addCookie(sso_token);
return "redirect:"+url+"?token="+uuid;
}
//登录失败,展示登录页
return "login";
}
}
添加html
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input name="username" /><br/>
密码:<input name="password" type="password"/><br/>
<input type="hidden" name="url" th:value="${url}"/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
4.2 sso-client1
修改pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
修改properties
server.port=8081
sso.server.url=http://ssoserver.com:8080/login.html
添加controller
package com.atguigu.gulimall.ssoclient.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
/**
* 无需登录就可访问
* @return
*/
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
/**
* 感知这次是在 ssoserver 登录成功跳回来的。
* @param model
* @param session
* @param token 只要去ssoserver登录成功跳回来就会带上
* @return
*/
@GetMapping("/employees")
public String employees(Model model, HttpSession session,
@RequestParam(value = "token",required = false) String token){
//
if(!StringUtils.isEmpty(token)){
//去ssoserver登录成功跳回来就会带上
//TODO 1、去ssoserver获取当前token真正对应的用户信息
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
String body = forEntity.getBody();
session.setAttribute("loginUser",body);
}
Object loginUser = session.getAttribute("loginUser");
if(loginUser==null){
//没登录,跳转到登录服务器进行登录
//跳转过去以后,使用url上的查询参数标识我们自己是哪个页面
//redirect_url=http://client1.com:8080/employees
return "redirect:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
}else{
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps",emps);
return "list";
}
}
}
添加html
list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工列表</title>
</head>
<body>
<h1>欢迎:[[${session.loginUser}]]</h1>
<ul>
<li th:each="emp:${emps}"> 姓名:[[${emp}]] </li>
</ul>
</body>
</html>
4.3 sso-client2
修改pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
修改properties
server.port=8082
sso.server.url=http://ssoserver.com:8080/login.html
添加controller
package com.atguigu.gulimall.ssoclient2.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
/**
* 无需登录就可访问
* @return
*/
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
/**
* 感知这次是在 ssoserver 登录成功跳回来的。
* @param model
* @param session
* @param token 只要去ssoserver登录成功跳回来就会带上
* @return
*/
@GetMapping("/boss")
public String employees(Model model, HttpSession session,
@RequestParam(value = "token",required = false) String token){
//
if(!StringUtils.isEmpty(token)){
//去ssoserver登录成功跳回来就会带上
//TODO 1、去ssoserver获取当前token真正对应的用户信息
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
String body = forEntity.getBody();
session.setAttribute("loginUser",body);
}
Object loginUser = session.getAttribute("loginUser");
if(loginUser==null){
//没登录,跳转到登录服务器进行登录
//跳转过去以后,使用url上的查询参数标识我们自己是哪个页面
//redirect_url=http://client1.com:8080/employees
return "redirect:"+ssoServerUrl+"?redirect_url=http://client2.com:8082/boss";
}else{
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps",emps);
return "list";
}
}
}
添加html
list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工列表</title>
</head>
<body>
<h1>欢迎:[[${session.loginUser}]]</h1>
<ul>
<li th:each="emp:${emps}"> 姓名:[[${emp}]] </li>
</ul>
</body>
</html>
5、测试
http://client1.com:8081/employees
http://client2.com:8082/boss
236、商城业务-购物车-环境搭建
1、新建模块
2、映射域名,动静分离
添加本地hosts映射
119.3.105.108 cat.gulimall.com
将\gulimall\代码\html\购物车 下的静态资源上传到nginx中的 /mydata> nginx> html>statci>cart 目录下
3、cart服务端
3.1 将\gulimall\代码\html\购物车 下的 cartList.html 和 success.html 复制到cart模块的templates下面,并修改其中的静态资源的地址路径,改为我们nginx中的
3.2 引入commom模块
编写配置文件,端口,服务名,配置中心,注册中心,主启动排除数据源的依赖
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
application.properties
spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=119.3.105.108:8848
server.port=30000
spring.thymeleaf.cache=false
bootstrap.properties
spring.cloud.nacos.config.server-addr=119.3.105.108:8848
spring.application.name=gulimall-cart
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCatApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallCatApplication.class, args);
}
}
4、配置网关
修改gateway的yml配置文件
- id: gulimall_cart_route
uri: lb://gulimall-cart
predicates:
- Host=cart.gulimall.com
5、启动测
能注册到注册中心
237、商城业务-购物车-数据模型分析
1、购物车需求
1)、需求描述:
-
用户可以在登录状态下将商品添加到购物车**【用户购物车/在线购物车】**
-
放入数据库
-
mongodb
-
放入 redis (采用)
登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车;
-
用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
-
放入 localstorage(客户端存储,后台不存)
-
cookie
-
WebSQL
- 放入 redis (采用)
浏览器即使关闭,下次进入,临时购物车数据都在
-
用户可以使用购物车一起结算下单
-
给购物车添加商品
-
用户可以查询自己的购物车
-
用户可以在购物车中修改购买商品的数量。
-
用户可以在购物车中删除商品。
-
选中不选中商品
-
在购物车中展示商品优惠信息
-
提示购物车商品价格变化
2、数据结构分析
因此每一个购物项信息,都是一个对象,基本字段包括:
{
skuId: 2131241,
check: true,
title: "Apple iphone.....",
defaultImage: "...",
price: 4999,
count: 1,
totalPrice: 4999,
skuSaleVO: {...}
}
另外,购物车中不止一条数据,因此最终会是对象的数组。即:
[
{...},{...},{...}
]
Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map<String, List>
-
首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,Value 是用户的所有购物车信息。这样看来基本的
k-v
结构就可以了。 -
但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是
k-v
结构,key 是商品 id,value 才是这个商品的购物车信息。
综上所述,我们的购物车结构是一个双层 Map:Map<String,Map<String,String>>
-
第一层 Map,Key 是用户 id
-
第二层 Map,Key 是购物车中商品 id,值是购物项数据
3、流程分析
参照京东
user-key 是随机生成的 id,不管有没有登录都会有这个 cookie 信息。
两个功能:新增商品到购物车、查询购物车。
新增商品:判断是否登录
-
是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key。
-
否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key。 查询购物车列表:判断是否登录
-
否:直接根据 user-key 查询 redis 中数据并展示
-
是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
-
有:需要提交到后台添加到 redis,合并数据,而后查询。
-
否:直接去后台查询 redis,而后返回。
238、商城业务-购物车-vo编写
package com.atguigu.gulimall.cart.vo;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
import java.math.BigDecimal;
import java.util.List;
/**
* 购物项内容
*/
public class CartItem {
private Long skuId;
private Boolean check = true;
private String title;
private String image;
private List<String> skuAttr;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
public Boolean getCheck() {
return check;
}
public void setCheck(Boolean check) {
this.check = check;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public List<String> getSkuAttr() {
return skuAttr;
}
public void setSkuAttr(List<String> skuAttr) {
this.skuAttr = skuAttr;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
/**
* 计算当前项的总价
* @return
*/
public BigDecimal getTotalPrice() {
return this.price.multiply(new BigDecimal("" + this.count));
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
}
package com.atguigu.gulimall.cart.vo;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
import java.math.BigDecimal;
import java.util.List;
/**
* 整个购物车
* 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
*/
public class Cart {
List<CartItem> items;
private Integer countNum;//商品数量
private Integer countType;//商品类型数量
private BigDecimal totalAmount;//商品总价
private BigDecimal reduce = new BigDecimal("0.00");//减免价格
public List<CartItem> getItems() {
return items;
}
public void setItems(List<CartItem> items) {
this.items = items;
}
public Integer getCountNum() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count += 1;
}
}
return count;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
//1、计算购物项总价
if (items != null && items.size() > 0) {
for (CartItem item : items) {
if(item.getCheck()){
BigDecimal totalPrice = item.getTotalPrice();
amount = amount.add(totalPrice);
}
}
}
//2、减去优惠总价
BigDecimal subtract = amount.subtract(getReduce());
return subtract;
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
239、商城业务-购物车-ThreadLocal用户身份鉴别
1、购物车编码
1.1 配置类准备
添加redis依赖要和spring session (要取出用户信息从而判定是临时用户还是登入用户)整合
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置类
package com.atguigu.gulimall.cart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
@Configuration
public class GulimallSessionConfig {
@Bean // redis的json序列化
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean // cookie
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("GULISESSIONID"); // cookie的键
serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
return serializer;
}
}
spring.redis.host=119.3.105.108
spring.redis.port=6379
1.2 在common定义常量CartConstant
package com.atguigu.common.constant;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
public class CartConstant {
public static final String TEMP_USER_COOKIE_NAME = "user-key";
public static final int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;
}
1.3 编写拦截器
我们需要编写一个拦截器,来拦截任何的请求,来判断当前的请求用户又没有登入,如果没有登入我们在拦截器的前置处理中生成一个临时的购物车,如果登入了就用登入的,在后置的处理器中命令cookies中保存userKey(时用户)
拦截器
package com.atguigu.gulimall.cart.interceptor;
import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.constant.CartConstant;
import com.atguigu.common.vo.MemberRespVo;
import com.atguigu.gulimall.cart.vo.UserInfoTo;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述: 在执行目标方法之前,判断用户的登录状态。并封装传递(用户信息)给controller
*/
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfoTo userInfoTo = new UserInfoTo();
HttpSession session = request.getSession();
MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if(member != null){
//用户登录
userInfoTo.setUserId(member.getId());
}
Cookie[] cookies = request.getCookies();
if(cookies!=null && cookies.length>0){
for (Cookie cookie : cookies) {
//user-key
String name = cookie.getName();
if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
userInfoTo.setTempUser(true);
}
}
}
//如果没有临时用户一定分配一个临时用户
if(StringUtils.isEmpty(userInfoTo.getUserKey())){
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
//目标方法执行之前
threadLocal.set(userInfoTo);
return true;
}
/**
* 业务执行之后;分配临时用户,让浏览器保存
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
//如果没有临时用户一定保存一个临时用户
if(!userInfoTo.isTempUser()){
//持续的延长临时用户的过期时间
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
}
拦截器加入配置中
package com.atguigu.gulimall.cart.config;
import com.atguigu.gulimall.cart.interceptor.CartInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述: 加入定义的拦截器,并且拦截所有的请求
*/
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}
1.4 controller
package com.atguigu.gulimall.cart.controller;
import com.atguigu.gulimall.cart.service.CartService;
import com.atguigu.gulimall.cart.vo.Cart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.concurrent.ExecutionException;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
@Controller
public class CartController {
@Autowired
CartService cartService;
/**
*
* 浏览器有一个cookie;user-key;标识用户身份,一个月后过期;
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
* 浏览器以后保存,每次访问都会带上这个cookie;
*
* 登录:session有
* 没登录:按照cookie里面带来user-key来做。
* 第一次:如果没有临时用户,帮忙创建一个临时用户。
* @return
*/
@GetMapping("/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
//1、快速得到用户信息,id,user-key
// System.out.println(userInfoTo);
Cart cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
}
service
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
public interface CartService {
List<CartItem> getUserCartItems();
Cart getCart();
}
serviceImpl
目前没有任何的处理 后面会做业务处理
package com.atguigu.gulimall.cart.service.impl;
import com.atguigu.gulimall.cart.interceptor.CartInterceptor;
import com.atguigu.gulimall.cart.service.CartService;
import com.atguigu.gulimall.cart.vo.Cart;
import com.atguigu.gulimall.cart.vo.CartItem;
import com.atguigu.gulimall.cart.vo.UserInfoTo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
@Service
public class CartServiceImpl implements CartService {
@Autowired
StringRedisTemplate redisTemplate;
@Override
public List<CartItem> getUserCartItems() {
return null;
}
@Override
public Cart getCart() {
Cart cart = new Cart();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
return cart;
}
}
拦截器中创建的临时用户的信息是通过LocalThread 传递数据的
线程共享的对象,底层是一个map,比如:拦截器intercepted—>controller—>impl这样一个过程就可以在interconnected中放入一个数据,然后在下impl中要使用然后取出来。
在intercepted中存入UserInfoTo对象。
240、商城业务-购物车-页面环境搭建
本章节主要是购物车的页面跳转功能,html的代码参照老师的代码
241-243、商城业务-购物车-添加购物车
242、商城业务-购物车-添加购物车细节
243、商城业务-购物车-RedirectAttribute
1、配置线程池
将product中的ThreadPoolConfigProperties 和 MyThreadConfig复制到cart 模块,并在properties中配置好对应的核心线程数 等参数
2、CartController
第一个方法添加后 调用第二个方法查询除购物车的数据,通过重定向RedirectAttribute 解决单个方法,如果页面重复刷新导致重复添加的问题
/**
* 添加商品到购物车
*
* RedirectAttributes ra
* ra.addFlashAttribute();将数据放在session里面可以在页面取出,但是只能取一次
* ra.addAttribute("skuId",skuId);将数据放在url后面
* @Param skuId 哪一个商品
* @Param num 要添加的数量
* @return
*/
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes ra) throws ExecutionException, InterruptedException {
cartService.addToCart(skuId,num);
// model.addAttribute("skuId",skuId);
ra.addAttribute("skuId",skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
/**
* 跳转到成功页
* @param skuId
* @param model
* @return
*/
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model){
//重定向到成功页面。再次查询购物车数据即可
CartItem item = cartService.getCartItem(skuId);
model.addAttribute("item",item);
return "success";
}
3、CartServiceImpl
package com.atguigu.gulimall.cart.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.cart.feign.ProductFeignService;
import com.atguigu.gulimall.cart.interceptor.CartInterceptor;
import com.atguigu.gulimall.cart.service.CartService;
import com.atguigu.gulimall.cart.vo.Cart;
import com.atguigu.gulimall.cart.vo.CartItem;
import com.atguigu.gulimall.cart.vo.SkuInfoVo;
import com.atguigu.gulimall.cart.vo.UserInfoTo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @创建人: 放生
* @创建时间: 2022/5/3
* @描述:
*/
@Service
public class CartServiceImpl implements CartService {
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ProductFeignService productFeignService;
@Autowired
ThreadPoolExecutor executor;
private final String CART_PREFIX = "gulimall:cart:";
@Override
public List<CartItem> getUserCartItems() {
return null;
}
@Override
public Cart getCart() {
Cart cart = new Cart();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
return cart;
}
@Override
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String res = (String) cartOps.get(skuId.toString());
if(StringUtils.isEmpty(res)){
//购物车无此商品
//2、添加新商品到购物车
//1、远程查询当前要添加的商品的信息
CartItem cartItem = new CartItem();
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
R skuInfo = productFeignService.getSkuInfo(skuId);
SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItem.setCheck(true);
cartItem.setCount(num);
cartItem.setImage(data.getSkuDefaultImg());
cartItem.setTitle(data.getSkuTitle());
cartItem.setSkuId(skuId);
cartItem.setPrice(data.getPrice());
},executor);
//2、远程查询sku的组合信息
CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
cartItem.setSkuAttr(values);
}, executor);
CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
return cartItem;
}
else{
//购物车有此商品,修改数量
CartItem cartItem = JSON.parseObject(res, CartItem.class);
cartItem.setCount(cartItem.getCount()+num);
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
return cartItem;
}
}
@Override
public CartItem getCartItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String str = (String) cartOps.get(skuId.toString());
CartItem cartItem = JSON.parseObject(str, CartItem.class);
return cartItem;
}
/**
* 获取到我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey = "";
if (userInfoTo.getUserId() != null) {
//gulimall:cart:1
cartKey = CART_PREFIX + userInfoTo.getUserId();
} else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
}
244、商城业务-购物车-获取&合并购物车
1、CartController
/**
*
* 浏览器有一个cookie;user-key;标识用户身份,一个月后过期;
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
* 浏览器以后保存,每次访问都会带上这个cookie;
*
* 登录:session有
* 没登录:按照cookie里面带来user-key来做。
* 第一次:如果没有临时用户,帮忙创建一个临时用户。
* @return
*/
@GetMapping("/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
//1、快速得到用户信息,id,user-key
// System.out.println(userInfoTo);
Cart cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
2、CartServiceImpl
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if(userInfoTo.getUserId()!=null){
//1、登录
String cartKey =CART_PREFIX+ userInfoTo.getUserId();
//2、如果临时购物车的数据还没有进行合并【合并购物车】
String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
List<CartItem> tempCartItems = getCartItems(tempCartKey);
if(tempCartItems!=null){
//临时购物车有数据,需要合并
for (CartItem item : tempCartItems) {
addToCart(item.getSkuId(),item.getCount());
}
//清除临时购物车的数据
clearCart(tempCartKey);
}
//3、获取登录后的购物车的数据【包含合并过来的临时购物车的数据,和登录后的购物车的数据】
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}else{
//2、没登录
String cartKey =CART_PREFIX+ userInfoTo.getUserKey();
//获取临时购物车的所有购物项
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}
return cart;
}
private List<CartItem> getCartItems(String cartKey) {
BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(cartKey);
List<Object> values = hashOps.values();
if(values!=null && values.size()>0){
List<CartItem> collect = values.stream().map((obj) -> {
String str = (String) obj;
CartItem cartItem = JSON.parseObject(str, CartItem.class);
return cartItem;
}).collect(Collectors.toList());
return collect;
}
return null;
}
@Override
public void clearCart(String cartkey) {
redisTemplate.delete(cartkey);
}
245、商城业务-购物车-选中购物项
1、CartController
@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId,
@RequestParam("check") Integer check){
cartService.checkItem(skuId,check);
return "redirect:http://cart.gulimall.com/cart.html";
}
2、CartServiceImpl
@Override
public void checkItem(Long skuId, Integer check) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
CartItem cartItem = getCartItem(skuId);
cartItem.setCheck(check==1?true:false);
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
}
246、商城业务-购物车-改变购物项
改变购物项的数量
1、CartController
@GetMapping("/countItem")
public String countItem(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num){
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.gulimall.com/cart.html";
}
2、CartServiceImpl
@Override
public void changeItemCount(Long skuId, Integer num) {
CartItem cartItem = getCartItem(skuId);
cartItem.setCount(num);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}
247、商城业务-购物车-删除购物项
1、CartController
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId){
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";
}
2、CartServiceImpl
@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}
ductFeignService.getSkuSaleAttrValues(skuId);
cartItem.setSkuAttr(values);
}, executor);
CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
return cartItem;
}
else{
//购物车有此商品,修改数量
CartItem cartItem = JSON.parseObject(res, CartItem.class);
cartItem.setCount(cartItem.getCount()+num);
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
return cartItem;
}
}
@Override
public CartItem getCartItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String str = (String) cartOps.get(skuId.toString());
CartItem cartItem = JSON.parseObject(str, CartItem.class);
return cartItem;
}
/**
* 获取到我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey = "";
if (userInfoTo.getUserId() != null) {
//gulimall:cart:1
cartKey = CART_PREFIX + userInfoTo.getUserId();
} else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
}
# 244、商城业务-购物车-获取&合并购物车
## 1、CartController
```java
/**
*
* 浏览器有一个cookie;user-key;标识用户身份,一个月后过期;
* 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
* 浏览器以后保存,每次访问都会带上这个cookie;
*
* 登录:session有
* 没登录:按照cookie里面带来user-key来做。
* 第一次:如果没有临时用户,帮忙创建一个临时用户。
* @return
*/
@GetMapping("/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
//1、快速得到用户信息,id,user-key
// System.out.println(userInfoTo);
Cart cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
2、CartServiceImpl
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if(userInfoTo.getUserId()!=null){
//1、登录
String cartKey =CART_PREFIX+ userInfoTo.getUserId();
//2、如果临时购物车的数据还没有进行合并【合并购物车】
String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
List<CartItem> tempCartItems = getCartItems(tempCartKey);
if(tempCartItems!=null){
//临时购物车有数据,需要合并
for (CartItem item : tempCartItems) {
addToCart(item.getSkuId(),item.getCount());
}
//清除临时购物车的数据
clearCart(tempCartKey);
}
//3、获取登录后的购物车的数据【包含合并过来的临时购物车的数据,和登录后的购物车的数据】
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}else{
//2、没登录
String cartKey =CART_PREFIX+ userInfoTo.getUserKey();
//获取临时购物车的所有购物项
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}
return cart;
}
private List<CartItem> getCartItems(String cartKey) {
BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(cartKey);
List<Object> values = hashOps.values();
if(values!=null && values.size()>0){
List<CartItem> collect = values.stream().map((obj) -> {
String str = (String) obj;
CartItem cartItem = JSON.parseObject(str, CartItem.class);
return cartItem;
}).collect(Collectors.toList());
return collect;
}
return null;
}
@Override
public void clearCart(String cartkey) {
redisTemplate.delete(cartkey);
}
245、商城业务-购物车-选中购物项
1、CartController
@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId,
@RequestParam("check") Integer check){
cartService.checkItem(skuId,check);
return "redirect:http://cart.gulimall.com/cart.html";
}
2、CartServiceImpl
@Override
public void checkItem(Long skuId, Integer check) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
CartItem cartItem = getCartItem(skuId);
cartItem.setCheck(check==1?true:false);
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
}
246、商城业务-购物车-改变购物项
改变购物项的数量
1、CartController
@GetMapping("/countItem")
public String countItem(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num){
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.gulimall.com/cart.html";
}
2、CartServiceImpl
@Override
public void changeItemCount(Long skuId, Integer num) {
CartItem cartItem = getCartItem(skuId);
cartItem.setCount(num);
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}
247、商城业务-购物车-删除购物项
1、CartController
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId){
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";
}
2、CartServiceImpl
@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}