跳至主要內容

apzs...大约 162 分钟

5.7、认证中心

认证中心对用户进行统一的登录认证

5.7.1、添加认证中心

1、认证中心初始化

1、新建认证中心模块

选中IDEAProjectgulimall,右键依次点击New->Module->Spring Initializr->Next

New Module对话框里Group里输入com.atguigu.gulimallArtifact里输入gulimall-auth-serverJava Version选择8Description里输入认证中心(社交登录、OAuth2.0、单点登录)Package里输入com.atguigu.gulimall.auth,然后点击Next

com.atguigu.gulimall
gulimall-auth-server
0.0.1-SNAPSHOT
gulimall-auth-server
认证中心(社交登录、OAuth2.0、单点登录) 
com.atguigu.gulimall.auth
image-20220803110137987
image-20220803110137987

选择Devloper Tools里的Spring Boot DevToolsLombox

image-20220803110344173
image-20220803110344173

选择Web里的Spring Web

image-20220803110414437
image-20220803110414437

选择Template Engines里的Thymeleaf

image-20220803110442958
image-20220803110442958

选择Spring Cloud Routing里的OpenFeign,然后点击Next

image-20220803110528317
image-20220803110528317

最后点击Finish

image-20220803110555561
image-20220803110555561

复制gulimall-auth-server模块的pom.xml文件的dependencies项目信息的部分,(properties里的不要)

<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-auth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-auth-server</name>
<description>认证中心(社交登录、OAuth2.0、单点登录) </description>
<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
   </dependency>
   ......
</dependencies>

然后复制gulimall-product模块的pom.xml文件,粘贴到gulimall-auth-server模块的pom.xml文件里,删除dependencies项目信息的部分,替换为gulimall-auth-server模块的pom.xml文件的dependencies项目信息

如果pom.xml文件颜色为赤橙色,可以选中pom.xml文件,右键选择Add as Maven Project就好了(最好先替换文件,再加入到项目)

点击查看完整pom.xml文件

image-20220803111313234
image-20220803111313234

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.GulimallAuthServerApplicationTests测试类为junit4

package com.atguigu.gulimall.auth;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallAuthServerApplicationTests {

   @Test
   public void contextLoads() {
      System.out.println("hello...");
   }

}
image-20220803111831437
image-20220803111831437
2、添加依赖

gulimall-auth-server模块的pom.xml<dependencies>标签里引入gulimall-common依赖,由于gulimall-auth-server模块不操作数据库可以移除gulimall-common模块的mybatis-plus依赖

<dependency>
   <groupId>com.atguigu.gulimall</groupId>
   <artifactId>gulimall-common</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <exclusions>
      <exclusion>
         <groupId>com.baomidou</groupId>
         <artifactId>mybatis-plus-boot-starter</artifactId>
      </exclusion>
   </exclusions>
</dependency>
image-20220803112729182
image-20220803112729182

gulimall-auth-server模块的src/main/resources/application.properties配置文件里配置应用名和注册中心的地址

spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=20000
image-20220803113146718
image-20220803113146718

gulimall-auth-server模块的com.atguigu.gulimall.auth.GulimallAuthServerApplication启动类上添加@EnableDiscoveryClient注解,开启服务发现

@EnableDiscoveryClient
image-20220803113235752
image-20220803113235752

由于gulimall-auth-server模块的pom.xml文件已经引入了远程调用的openfeign,因此就不用引了

image-20220803113318794
image-20220803113318794

gulimall-auth-server模块的com.atguigu.gulimall.auth.GulimallAuthServerApplication启动类上添加@EnableFeignClients注解,开启远程调用

@EnableFeignClients
image-20220803113414085
image-20220803113414085

使用浏览器访问http://localhost:8848/nacos页面,用户名和密码都为nacos

点击服务管理/服务列表可以看到gulimall-auth-server已经加入进来了

image-20220803113608915
image-20220803113608915
3、导入静态资源

2.分布式高级篇(微服务架构篇)\资料源码\代码\html\登录页面里把index.html复制到gulimall-auth-server模块的src/main/resources/templates文件里面,并将index.html改名为login.html

GIF 2022-8-3 11-38-10
GIF 2022-8-3 11-38-10

2.分布式高级篇(微服务架构篇)\资料源码\代码\html\注册页面里,把index.html复制到gulimall-auth-server模块的src/main/resources/templates文件里面,并将index.html改名为reg.html

GIF 2022-8-3 11-44-00
GIF 2022-8-3 11-44-00

打开SwitchHosts软件,依次点击hosts->本地方案->gulimall,在后面添加192.168.56.10 auth.gulimall.com,然后点击对勾图标

# gulimall
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
192.168.56.10 auth.gulimall.com
image-20220803114604485
image-20220803114604485

linux虚拟机/mydata/nginx/html/static目录下新建login目录,把2.分布式高级篇(微服务架构篇)\资料源码\代码\html\登录页面里的所有文件夹都复制到linux虚拟机/mydata/nginx/html/static/login里面

GIF 2022-8-3 11-54-19
GIF 2022-8-3 11-54-19

linux虚拟机/mydata/nginx/html/static目录下新建reg目录,把2.分布式高级篇(微服务架构篇)\资料源码\代码\html\注册页面里的所有文件夹都复制到linux虚拟机/mydata/nginx/html/static/reg里面

GIF 2022-8-3 11-56-22
GIF 2022-8-3 11-56-22

gulimall-auth-server模块的src/main/resources/templates/login.html文件里,将src="(除src='某url')替换为 src="/static/login/,将href="(除href='某url'href='#')替换为href="/static/login/

完整代码:gulimall-auth-server模块的src/main/resources/templates/login.html文件

image-20220803115948128
image-20220803115948128

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里,将src="(除src='某url')替换为 src="/static/reg/,将href="(除href='某url'href='#')替换为href="/static/reg/

完整代码:gulimall-auth-server模块的src/main/resources/templates/reg.html文件

image-20220803162601349
image-20220803162601349
4、添加到gateway

gulimall-gateway模块的src/main/resources/application.yml文件里添加如下配置

spring:
  cloud:
    gateway:
      routes:
        - id: gulimall_auth_route
          uri: lb://gulimall-auth-server
          predicates:
            - Host=auth.gulimall.com
image-20220803163319730
image-20220803163319730
5、访问页面

gulimall-auth-server模块的src/main/resources/templates/login.html文件改名为index.html

image-20220803163331963
image-20220803163331963

启动GulimallGatewayApplication服务和GulimallAuthServerApplication服务

在浏览器里打开http://auth.gulimall.com/页面,可以看到已经访问成功了

image-20220803163348978
image-20220803163348978
6、修改页面

http://auth.gulimall.com页面里,打开控制台,定位到谷粒商城的图标,复制/static/login/JD_img/logo.jpg

image-20220803163520948
image-20220803163520948

gulimall-auth-server模块的src/main/resources/templates/index.html文件里,搜索/static/login/JD_img/logo.jpg,将该行修改为如下代码,点击谷粒商城图标可以跳转到主页

<a href="http://gulimall.com"><img src="/static/login/JD_img/logo.jpg" /></a>
image-20220803163704631
image-20220803163704631

gulimall-auth-server模块的src/main/resources/application.properties配置文件里关闭thymeleaf缓存

spring.thymeleaf.cache=false
image-20220803163911690
image-20220803163911690

http://auth.gulimall.com页面里点击谷粒商城图标,可以正常跳转到http://gulimall.com页面

GIF 2022-8-3 16-40-23
GIF 2022-8-3 16-40-23

2、完善页面跳转

1、主页跳转到登录页和注册页

gulimall.com页面里,打开控制台,定位到你好,请登录,复制你好,请登录

image-20220803164134463
image-20220803164134463

gulimall-product模块的src/main/resources/templates/index.html文件里搜索你好,请登录,修改登录注册 href属性

<ul>
  <li>
    <a href="http://auth.gulimall.com/login.html">你好,请登录</a>
  </li>
  <li>
    <a href="http://auth.gulimall.com/reg.html" class="li_2">免费注册</a>
  </li>
  <span>|</span>
  <li>
    <a href="#">我的订单</a>
  </li>
</ul>
image-20220803164336789
image-20220803164336789

重新启动GulimallProductApplication服务和GulimallAuthServerApplication服务,在http://gulimall.com页面里点击你好,请登录来到了http://auth.gulimall.com/login.html页面,但是网页没有正常显示。返回到http://gulimall.com页面,点击免费注册来到了http://auth.gulimall.com/reg.html页面,但是网页也没有正常显示。

GIF 2022-8-3 16-44-56
GIF 2022-8-3 16-44-56

gulimall-auth-server模块里,重新将src/main/resources/templates/index.html文件修改为login.html

gulimall-auth-server模块的com.atguigu.gulimall.auth包下新建controller文件夹,在controller文件夹里新建LoginController类,用来映射login.htmlreg.html文件

package com.atguigu.gulimall.auth.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description:
 */
@Controller
public class LoginController {

    @GetMapping("/login.html")
    public String loginPage(){
        return "login";
    }

    @GetMapping("/reg.html")
    public String regPage(){
        return "reg";
    }

}
image-20220803164844472
image-20220803164844472

重新启动GulimallProductApplication服务和GulimallAuthServerApplication服务,在http://gulimall.com页面里点击你好,请登录来到了http://auth.gulimall.com/login.html页面,页面也正确显示。返回到http://gulimall.com页面,点击免费注册来到了http://auth.gulimall.com/reg.html页面,页面也正确显示。

GIF 2022-8-3 16-50-21
GIF 2022-8-3 16-50-21
2、登录页跳转到注册页

http://auth.gulimall.com/login.html页面里,打开控制台,定位到立即注册,复制立即注册

image-20220803165137388
image-20220803165137388

gulimall-auth-server模块的src/main/resources/templates/login.html文件里搜索立即注册,修改其href属性

<h5 class="rig">
   <img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_25.png" />
   <span><a href="http://auth.gulimall.com/reg.html">立即注册</a></span>
</h5>
image-20220803165341685
image-20220803165341685

点击Build -> Recompile 'login.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

http://auth.gulimall.com/login.html页面里,点击立即注册,正确来到了http://auth.gulimall.com/reg.html页面

GIF 2022-8-3 16-53-59
GIF 2022-8-3 16-53-59
3、注册页跳转到登录页

打开http://auth.gulimall.com/reg.html页面,点击同意并继续,然后打开控制台,定位到请登录,复制请登录

image-20220803165522990
image-20220803165522990

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里搜索请登录,修改其href属性

<div class="dfg">
   <span>已有账号?</span>
   <a href="http://auth.gulimall.com/login.html">请登录</a>
</div>
image-20220803165726261
image-20220803165726261

点击Build -> Recompile 'reg.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

打开http://auth.gulimall.com/reg.html页面,点击同意并继续,然后点击请登录,成功跳转到http://auth.gulimall.com/login.html页面

GIF 2022-8-3 16-58-06
GIF 2022-8-3 16-58-06

3、修改验证码图片

1、修改前端页面

打开http://auth.gulimall.com/reg.html页面,点击同意并继续,然后打开控制台,定位到验证码的图片,复制id="code"

image-20220803165946124
image-20220803165946124

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里搜索id="code",将<span id="code"></span>修改为<span>发送验证码</span>

<span>发送验证码</span>
image-20220803170107555
image-20220803170107555

点击Build -> Recompile 'reg.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

打开http://auth.gulimall.com/reg.html页面,点击同意并继续,图片验证码的位置已经替换为发送验证码

image-20220803170149877
image-20220803170149877
2、添加倒计时

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里,给发送验证码<a>标签添加id="sendCode"属性

<a id="sendCode">发送验证码</a>
image-20220803170307131
image-20220803170307131

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里,在<script>标签里,添加如下方法:

// 发送验证码
$(function(){
   $("#sendCode").click(function(){
      //1、给指定手机号发送验证码
      //2、倒计时
      if($(this).hasClass("disabled")){
         //正在倒计时。
      }else{
         timeoutChangeStyle();
      }
   });
})
var num = 10;
function timeoutChangeStyle() {
   $("#sendCode").attr("class", "disabled");
   if (num == 0) {
      $("#sendCode").text("发送验证码");
      num = 10;
      $("#sendCode").attr("class", "");
   } else {
      var str = num + "s 后再次发送";
      $("#sendCode").text(str);
      setTimeout("timeoutChangeStyle()", 1000);
      num--;
   }
}
image-20220803171507886
image-20220803171507886

点击Build -> Recompile 'reg.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

打开http://auth.gulimall.com/reg.html页面,点击同意并继续,点击发送验证码,已经出现了10s的倒计时了

GIF 2022-8-3 17-14-08
GIF 2022-8-3 17-14-08
3、批量配置简单视图映射

org.springframework.web.servlet.config.annotation.WebMvcConfigurer接口里有一个addViewControllers方法可以批量配置视图的映射

/**
 * Configure simple automated controllers pre-configured with the response
 * status code and/or a view to render the response body. This is useful in
 * cases where there is no need for custom controller logic -- e.g. render a
 * home page, perform simple site URL redirects, return a 404 status with
 * HTML content, a 204 with no content, and more.
 */
default void addViewControllers(ViewControllerRegistry registry) {
}
image-20220803172040830
image-20220803172040830

gulimall-auth-server模块的com.atguigu.gulimall.auth包下新建config文件夹,在config文件夹里新建GulimallWebConfig类,实现WebMvcConfigurer接口,重写addViewControllers方法,配置对login.html文件和reg.html文件的路径映射

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;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description:
 */
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

    /**
     * @GetMapping("/login.html")
     * public String loginPage(){
     *     return "login";
     * }
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}
image-20220803172800659
image-20220803172800659

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类里删掉对login.html文件和reg.html文件的路径映射

package com.atguigu.gulimall.auth.controller;

import org.springframework.stereotype.Controller;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description:
 */
@Controller
public class LoginController {

    //@GetMapping("/login.html")
    //public String loginPage(){
    //    return "login";
    //}
    //
    //@GetMapping("/reg.html")
    //public String regPage(){
    //    return "reg";
    //}

}
image-20220803172434752
image-20220803172434752

重启GulimallAuthServerApplication服务,可以看到http://auth.gulimall.com/login.htmlhttp://auth.gulimall.com/reg.html也能正确访问

GIF 2022-8-3 17-28-32
GIF 2022-8-3 17-28-32

5.7.2、短信服务&邮件服务

1、短信服务简单测试

1、购买短信服务

在阿里云网站里点击云市场,在云市场里搜索短信,随便点击一个商家,购买免费的几次短信服务

GIF 2022-8-3 18-43-05
GIF 2022-8-3 18-43-05
2、调试工具测试

短信单条发送

*调用地址:*http(s)😕/gyytz.market.alicloudapi.com/sms/smsSend

*请求方式:*POST

*返回类型:*JSON

请求参数(Query)

名称类型是否必须描述
mobileSTRING必选需要发送的手机号。(同一手机号码,同一签名验证码,一分钟一次,频率过快可能会导致运营商系统屏蔽,用户无法正常接收。)
paramSTRING可选短信模板变量替换。(字符串格式:key:value,key:value。例如:code:12345,minute:5。如模板中有多个变量请使用英文逗号隔开。建议对参数进行URLEncode编码,以免出现乱码等异常情况)
smsSignIdSTRING必选短信前缀ID(签名ID),联系客服申请。(测试ID请用:2e65b1bb3d054466b82f0c9d125465e2,对应短信前缀为【国阳云】。测试签名短信限流规则,同一个号码,1分钟1次,1小时5次,24小时10次,且仅支持接口调试少量测试,不支持大量商用)
templateIdSTRING必选短信正文ID(模板ID),联系客服申请。(测试ID请用:908e94ccf08b4476ba6c876d13f084ad,对应短信正文为 { 验证码:codeminute分钟内有效,请勿泄漏于他人!})

点击API接口->短信单条发送->调试工具里的去调试,可以进入到短信调试界面

image-20220803191838428
image-20220803191838428

短信服务提供商已自动把需要的Query参数和AppCode填写好了,直接输入自己的电话号就行了

点击查看调试工具的控制台完整信息

image-20220803190740766
image-20220803190740766

AppCode可以在云市场里的已购买的服务中查看

image-20220803190422539
image-20220803190422539

收到的短信:

无标题

本服务商的可选模板有以下几种,可以修改templateId参数选择想要的模板

模版类型模版内容templateId
通用验证码验证码:codeminute分钟内有效,请勿泄漏于他人!908e94ccf08b4476ba6c876d13f084ad
通用验证码验证码:code,如非本人操作,请忽略本短信!63698e3463bd490dbc3edc46a20c55f5
注册验证码验证码:codeminute分钟内有效,您正在进行注册,若非本人操作,请勿泄露。a09602b817fd47e59e7c6e603d3f088d
注册验证码尊敬的用户,您的注册验证码为:code,请勿泄漏于他人!305b8db49cdb4b18964ffe255188cb20
注册验证码验证码:code,您正在进行注册操作,感谢您的支持!47990cc6d3ca42e2bbaad2dd06371238
修改密码验证码验证码:code,您正在进行密码重置操作,如非本人操作,请忽略本短信!96d32c69f15a4fbf89410bdba185cbdc
修改密码验证码验证码:codeminute分钟内有效,您正在进行密码重置操作,请妥善保管账户信息。29833afb9ae94f21a3f66af908d54627
修改密码验证码验证码:code,您正在尝试修改登录密码,请妥善保管账户信息。8166a0ae27b7499fa8bdda1ed12a07bd
身份验证验证码验证码:code,您正在进行身份验证,打死不要告诉别人哦!d6d95d8fb03c4246b944abcc1ea7efd8
登录确认验证码验证码:code,您正在登录,若非本人操作,请勿泄露。f7e31e0d8c264a9c8d6e9756de806767
登录确认验证码验证码:codeminute分钟内容有效,您正在登录,若非本人操作,请勿泄露。02551a4313154fe4805794ca069d70bf
登录异常验证码验证码:code,您正尝试异地登录,若非本人操作,请勿泄露。dd7423a5749840f4ae6836ab31b7839e
登录异常验证码验证码:codeminute分钟内容有效,您正尝试异地登录,若非本人操作,请勿泄露。81e8a442ea904694a37d2cec6ea6f2bc
信息变更验证码验证码:code,您正在尝试变更重要信息,请妥善保管账户信息。9c16efaf248d41c59334e926634b4dc0
信息变更验证码验证码:codeminute分钟内容有效,您正在尝试变更重要信息,请妥善保管账户信息。ea66d14c664649a69a19a6b47ba028db
image-20220803191201175
image-20220803191201175
3、使用Postman测试

复制给的调用地址http(s)://gyytz.market.alicloudapi.com/sms/smsSend,然后去掉(s),再添加服务商指定的参数

image-20220803193515856
image-20220803193515856

在请求Header中添加的Authorization字段;配置Authorization字段的值为APPCODE + 半角空格 +APPCODE值

格式如下:

Authorization:APPCODE AppCode值

示例如下:

Authorization:APPCODE 3F2504E04F8911D39A0C0305E82C3301
image-20220803193305242
image-20220803193305242

点击Postman里的HeadersKEY输入AuthorizationVALUE输入自己购买的商品的AppCode值

image-20220803193638377
image-20220803193638377

我的服务商的请求方式要求是Post,因此修改为Post方式,然后点击Send,可以看到响应里面已经显示成功

http://gyytz.market.alicloudapi.com/sms/smsSend?mobile=13xxxxxxx86&param=**code**:54321,**minute**:3&smsSignId=2e65b1bb3d054466b82f0c9d125465e2&templateId=908e94ccf08b4476ba6c876d13f084ad
image-20220803193745906
image-20220803193745906

收到的短信:

无标2题

修改以下验证码和模板,然后重新点击Send,再次点击发送

http://gyytz.market.alicloudapi.com/sms/smsSend?mobile=13xxxxxxx86&param=**code**:565331,**minute**:10&smsSignId=2e65b1bb3d054466b82f0c9d125465e2&templateId=305b8db49cdb4b18964ffe255188cb20

响应的内容:

{"msg":"成功","smsid":"165952695412518315203379241","code":"0","balance":"18"}
image-20220803194314348
image-20220803194314348

收到的短信:

无标3题

2、使用java测试短信接口

1、查看文档

查看服务商提供的java代码发送短信的示例代码

image-20220803195021539

其给的代码如下所示:

public static void main(String[] args) {
	    String host = "https://gyytz.market.alicloudapi.com";
	    String path = "/sms/smsSend";
	    String method = "POST";
	    String 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("mobile", "mobile");
	    querys.put("param", "**code**:12345,**minute**:5");
	    querys.put("smsSignId", "2e65b1bb3d054466b82f0c9d125465e2");
	    querys.put("templateId", "908e94ccf08b4476ba6c876d13f084ad");
	    Map<String, String> bodys = new HashMap<String, String>();


	    try {
	    	/**
	    	* 重要提示如下:
	    	* HttpUtils请从
	    	* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
	    	* 下载
	    	*
	    	* 相应的依赖请参照
	    	* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
	    	*/
	    	HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
	    	System.out.println(response.toString());
	    	//获取response的body
	    	//System.out.println(EntityUtils.toString(response.getEntity()));
	    } catch (Exception e) {
	    	e.printStackTrace();
	    }
	}

正常返回示例

{
    "msg": "成功", 
    "smsid": "16565614329364584123421", //批次号,该值做为应答及状态报告中的消息ID一一对应。
    "code": "0"
}

失败返回示例

{
    "code":"XXXX",
    "msg":"错误提示内容",
    "ILLEGAL_WORDS":["XX","XX"]    // 如有则显示
     // 1、http响应状态码对照表请参考:https://help.aliyun.com/document_detail/43906.html;
     // 2、如果次数用完会返回 403,Quota Exhausted,此时继续购买就可以;
     // 3、如果appCode输入不正确会返回 403,Unauthorized;
}

错误码定义

错误码错误信息描述
1204签名未报备请联系客服申请。
1205签名不可用签名一般为:公司名简称、产品名、商城名称、网站名称、APP名称、系统名称、公众号、小程序名称等等。不可以是纯数字、电话号码或者无意义的签名,如:【温馨提示】【测试】【你好】等;
1302短信内容包含敏感词短信内容包含敏感词
1304短信内容过长短信内容实际长度=短信签名+短信内容。(短信计费方式:70字内按1条计费,超出按67字每条计费;一个汉字、数字、字母、符号都算一个字;带变量短信按实际替换后的长度计费)
1320模板ID不存在请联系客服申请。
1403手机号码不正确手机号码不正确
1905验证未通过验证未通过
2、测试

根据提示下载HttpUtils工具类,在gulimall-third-party模块的com.atguigu.gulimall.thirdparty包里新建util文件夹,在util文件夹里新建HttpUtils类,将提供的HttpUtils工具类粘贴到这里面

完整代码: gulimall-third-party模块的com.atguigu.gulimall.thirdparty.util.HttpUtils类

image-20220803195801902
image-20220803195801902

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests测试类里添加sendSms方法,对发送短信的接口进行测试

@Test
public void sendSms() {
    String host = "https://gyytz.market.alicloudapi.com";
    String path = "/sms/smsSend";
    String method = "POST";
    String 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("mobile", "要发送的手机号");
    querys.put("param", "**code**:12345,**minute**:5");
    querys.put("smsSignId", "2e65b1bb3d054466b82f0c9d125465e2");
    querys.put("templateId", "908e94ccf08b4476ba6c876d13f084ad");
    Map<String, String> bodys = new HashMap<String, String>();


    try {
        /**
         * 重要提示如下:
         * HttpUtils请从
         * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
         * 下载
         *
         * 相应的依赖请参照
         * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
         */
        HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
        System.out.println(response.toString());
        //获取response的body
        System.out.println(EntityUtils.toString(response.getEntity()));
    } catch (Exception e) {
        e.printStackTrace();
    }

}
image-20220803200305027
image-20220803200305027

测试结果如下,可以看出来把所有的连接信息都显示出来了,而不是想要的json数据

HTTP/1.1 200 OK [Date: Wed, 03 Aug 2022 12:03:28 GMT, Content-Type: text/html;charset=utf-8, Content-Length: 80, Connection: keep-alive, Keep-Alive: timeout=25, Access-Control-Allow-Headers: Origin, Accept, x-auth-token,Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Server: Jetty(9.4.9.v20180320), X-Ca-Request-Id: E5D22290-F3DF-4827-9DEF-27BA89272C6F, Content-Disposition: inline;filename=f.txt, Access-Control-Allow-Origin: *, X-Ca-Market-Billing-Result: 1, Access-Control-Allow-Credentials: true, Accept-Charset: big5, big5-hkscs, cesu-8, euc-jp, euc-kr, gb18030, gb2312, gbk, ibm-thai, ibm00858, ibm01140, ibm01141, ibm01142, ibm01143, ibm01144, ibm01145, ibm01146, ibm01147, ibm01148, ibm01149, ibm037, ibm1026, ibm1047, ibm273, ibm277, ibm278, ibm280, ibm284, ibm285, ibm290, ibm297, ibm420, ibm424, ibm437, ibm500, ibm775, ibm850, ibm852, ibm855, ibm857, ibm860, ibm861, ibm862, ibm863, ibm864, ibm865, ibm866, ibm868, ibm869, ibm870, ibm871, ibm918, iso-2022-cn, iso-2022-jp, iso-2022-jp-2, iso-2022-kr, iso-8859-1, iso-8859-13, iso-8859-15, iso-8859-2, iso-8859-3, iso-8859-4, iso-8859-5, iso-8859-6, iso-8859-7, iso-8859-8, iso-8859-9, jis_x0201, jis_x0212-1990, koi8-r, koi8-u, shift_jis, tis-620, us-ascii, utf-16, utf-16be, utf-16le, utf-32, utf-32be, utf-32le, utf-8, windows-1250, windows-1251, windows-1252, windows-1253, windows-1254, windows-1255, windows-1256, windows-1257, windows-1258, windows-31j, x-big5-hkscs-2001, x-big5-solaris, x-compound_text, x-euc-jp-linux, x-euc-tw, x-eucjp-open, x-ibm1006, x-ibm1025, x-ibm1046, x-ibm1097, x-ibm1098, x-ibm1112, x-ibm1122, x-ibm1123, x-ibm1124, x-ibm1166, x-ibm1364, x-ibm1381, x-ibm1383, x-ibm300, x-ibm33722, x-ibm737, x-ibm833, x-ibm834, x-ibm856, x-ibm874, x-ibm875, x-ibm921, x-ibm922, x-ibm930, x-ibm933, x-ibm935, x-ibm937, x-ibm939, x-ibm942, x-ibm942c, x-ibm943, x-ibm943c, x-ibm948, x-ibm949, x-ibm949c, x-ibm950, x-ibm964, x-ibm970, x-iscii91, x-iso-2022-cn-cns, x-iso-2022-cn-gb, x-iso-8859-11, x-jis0208, x-jisautodetect, x-johab, x-macarabic, x-maccentraleurope, x-maccroatian, x-maccyrillic, x-macdingbat, x-macgreek, x-machebrew, x-maciceland, x-macroman, x-macromania, x-macsymbol, x-macthai, x-macturkish, x-macukraine, x-ms932_0213, x-ms950-hkscs, x-ms950-hkscs-xp, x-mswin-936, x-pck, x-sjis_0213, x-utf-16le-bom, x-utf-32be-bom, x-utf-32le-bom, x-windows-50220, x-windows-50221, x-windows-874, x-windows-949, x-windows-950, x-windows-iso2022jp, Access-Control-Allow-Methods: POST,GET,OPTIONS, Access-Control-Max-Age: 3600] org.apache.http.conn.BasicManagedEntity@335f5c69
image-20220803200411711
image-20220803200411711

System.out.println(response.toString());修改为System.out.println(response.getEntity());,并在其上打断点

image-20220803200925114
image-20220803200925114

debug方式执行sendSms方法方法们可以看到请求参数都正常封装了

image-20220803201512696
image-20220803201512696

获取到的response也都正常封装了,但是还是没有获取到json数据

image-20220803201238641
image-20220803201238641

后来发现取消System.out.println(EntityUtils.toString(response.getEntity()));的注释就行了,提供的代码已经写好获取json数据的方法了😥(只不过此手机号单日使用上限已经到了)

image-20220803201712329
image-20220803201712329

第二天再次测试,可以看到已经正确封装响应的json数据了

{"msg":"成功","smsid":"165957496829318315203372267","code":"0","balance":"13"}
image-20220804090324284
image-20220804090324284

3、添加短信注册业务

1、编写短信业务代码

gulimall-third-party模块的com.atguigu.gulimall.thirdparty包里新建component文件夹,在component文件夹里添加SmsComponent类,在该类里添加发送短信短信的方法

package com.atguigu.gulimall.thirdparty.component;

import com.atguigu.gulimall.thirdparty.util.HttpUtils;
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;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description:
 */
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Component
public class SmsComponent {

    private String host;
    private String path;
    private String method;
    private String appcode;
    private String smsSignId;
    private String templateId;


    public void sendSms(String phone,String code) {
        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("mobile", phone);
        querys.put("param", "**code**:"+code+",**minute**:5");
        querys.put("smsSignId", smsSignId);
        querys.put("templateId", templateId);
        Map<String, String> bodys = new HashMap<String, String>();
        try {
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            //System.out.println(response.toString());
            //获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
image-20220803204051612
image-20220803204051612

gulimall-third-party模块的pom.xml文件里添加注释处理器,使用idea添加自定义配置会有提示

<!--添加注释处理器(使用idea添加自定义配置会有提示)-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
image-20220803203214363
image-20220803203214363

gulimall-third-party模块的src/main/resources/application.yml配置文件里添加如下配置,用于对短信服务的配置

spring:
  cloud:
    alicloud:
      sms:
        host: https://gyytz.market.alicloudapi.com
        path: /sms/smsSend
        method: POST
        appcode: 9448945d840d4a6493c905c145fb0a83
        sms-signId: 2e65b1bb3d054466b82f0c9d125465e2
        templateId: 908e94ccf08b4476ba6c876d13f084ad
image-20220803205817201
image-20220803205817201
2、测试

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests测试类里添加如下测试方法,用来对刚写的短信业务代码进行测试

@Autowired
SmsComponent smsComponent;
@Test
public void testSendCode(){
    smsComponent.sendSms("13235691886","432567");
}

执行该测试方法报了空指针异常

java.lang.NullPointerException
	at com.atguigu.gulimall.thirdparty.util.HttpUtils.wrapClient(HttpUtils.java:283)
	at com.atguigu.gulimall.thirdparty.util.HttpUtils.doPost(HttpUtils.java:82)
	at com.atguigu.gulimall.thirdparty.component.SmsComponent.sendSms(SmsComponent.java:40)
	at com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests.testSendCode(GulimallThirdPartyApplicationTests.java:83)
image-20220803205400993
image-20220803205400993

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component.SmsComponent类的sendSms方法的第一行上打断点,以debug方式启动gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests测试类的testSendCode方法

可以看到这些配置类里的配置都没有获取到

image-20220803205527093
image-20220803205527093

在配置文件里,写以spring.cloud.alicloud.sms为前缀的配置会有其他不是刚刚写的配置类里的字段的提示

image-20220803205847105
image-20220803205847105

随便选择一个配置属性,然后按住ctrl并点击鼠标左键,可以看到com.alibaba.alicloud.context.sms.SmsProperties类的前缀也为spring.cloud.alicloud.sms,和我们的前缀一样

image-20220803205711848
image-20220803205711848

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component.SmsComponent类里,把@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")修改为@ConfigurationProperties(prefix = "spring.cloud.alicloud.mysms"),使配置文件的前缀为spring.cloud.alicloud.mysms

image-20220803210502786
image-20220803210502786

修改gulimall-third-party模块的src/main/resources/application.yml文件,把sms修改为mysms,使其前缀为spring.cloud.alicloud.mysms

image-20220803210431739
image-20220803210431739

再次以debug方式启动gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests测试类的testSendCode方法,可以看到这些属性还是为null

image-20220803210355776
image-20220803210355776

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component.SmsComponent类里的private String host;字段上添加如下注解,指定注入的配置

@Value("spring.cloud.alicloud.mysms.host")

可以看到此时hsot注入成功了,因此配置文件里的配置没有问题,因此我就想到是没有set方法使得容器无法自动注入

image-20220803210756368
image-20220803210756368

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component.SmsComponent类上添加@Data注解,再次以debug方式启动gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests测试类的testSendCode方法,可以看到这些属性都已经注入成功了

image-20220803211319559
image-20220803211319559

然后点击Resume Program F9,执行完该方法,可以看到控制台已经显示成功的json数据了

{"msg":"成功","smsid":"165957510676018315203379080","code":"0","balance":"12"}
image-20220804090604010
image-20220804090604010

注意:直接使用@ConfigurationProperties注解而不指定具体的类,该注解并不会生效

image-20220803211138228
image-20220803211138228

@EnableConfigurationProperties的作用是把springboot配置文件中的值与我们的xxxProperties.java的属性进行绑定,需要配合@ConfigurationProperties使用。

首先我想说的是,不使用@EnableConfigurationProperties能否进行属性绑定呢?答案是肯定的!我们只需要给xxxProperties.java加上@Component注解,把它放到容器中,即可实现属性绑定。

4、邮件服务简单测试

参考:springboot实现邮箱验证_冰咖啡iii的博客-CSDN博客_springboot邮箱验证open in new window

若想使用短信服务,需要开启POP3/SMTP服务(邮件发送与接收协议,邮件发送方发送到发送方的邮件服务器发送方的邮件服务器接收方邮件服务器通讯使用POP3协议,接收方邮件服务器邮件接收方通讯使用SMTP协议)

1、开启邮件服务

打开QQ邮箱,点击设置里的账户

image-20220803212054406
image-20220803212054406

往下滑,找到开启服务里的POP3/SMTP服务,点击开启按钮,然后根据要求,给指定的电话号码发送短信

image-20220803212047859
image-20220803212047859

给指定的电话号码发送短信后,即可获得一个授权码,复制该授权码

image-20220803212101247
image-20220803212101247
2、添加依赖

gulimall-third-party模块的pom.xml文件里添加如下依赖,引入邮件服务

<!--邮件服务-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
image-20220803212334978
image-20220803212334978

gulimall-third-party模块的src/main/resources/application.properties文件里添加如下配置,输入自己的QQ邮箱授权码(注:不同邮箱的spring.mail.host不同)

spring.mail.username=你的QQ邮箱([email protected])
spring.mail.password=授权码
spring.mail.host=smtp.qq.com
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.default-encoding=UTF-8
server.port=8085
image-20220803212546251
image-20220803212546251
3、测试

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplicationTests测试类里进行邮箱服务的简单测试

@Test
public void mailTest() throws MessagingException {
    int count = 1;//默认发送一次
    MimeMessage mimeMessage = mailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    while (count-- != 0) {
        String codeNum = "";
        int[] code = new int[3];
        Random random = new Random();
        //自动生成验证码
        for (int i = 0; i < 6; i++) {
            int num = random.nextInt(10) + 48;
            int uppercase = random.nextInt(26) + 65;
            int lowercase = random.nextInt(26) + 97;
            code[0] = num;
            code[1] = uppercase;
            code[2] = lowercase;
            codeNum += (char) code[random.nextInt(3)];
        }
        System.out.println(codeNum);
        //标题
        helper.setSubject("您的验证码为:" + codeNum);
        //内容
        helper.setText("您好!,您的验证码为:" + "<h2>" + codeNum + "</h2>" + "千万不能告诉别人哦!", true);
        //邮件接收者
        helper.setTo("[email protected]");
        //邮件发送者,必须和配置文件里的一样,不然授权码匹配不上
        helper.setFrom("[email protected]");
        mailSender.send(mimeMessage);
    }
}
image-20220803214739127
image-20220803214739127

此时打开接收者的QQ邮箱,可以看到已经收到邮件了

image-20220803214723363
image-20220803214723363

5、添加邮件服务

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component包里新建MailComponent类,用于邮件发送

package com.atguigu.gulimall.thirdparty.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Random;

/**
 * @author 无名氏
 * @date 2022/8/3
 * @Description:
 */
@Component
public class MailComponent {

    @Autowired
    JavaMailSenderImpl mailSender;
    @Value("spring.mail.username")
    String username;


    public void sendMail(String fromMail,String targetMail,int length) throws MessagingException {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        String codeNum = generateVerificationCode(length);
        System.out.println(codeNum);
        //标题
        helper.setSubject("您的验证码为:" + codeNum);
        //内容
        helper.setText("您好!,您的验证码为:" + "<h2>" + codeNum + "</h2>" + "千万不能告诉别人哦!", true);
        //邮件发送者,必须和配置文件里的一样,不然授权码匹配不上
        helper.setFrom(fromMail);
        //邮件接收者
        helper.setTo(targetMail);
        mailSender.send(mimeMessage);
        System.out.println("邮件发送成功!");
    }

    public void sendMail(String targetMail,int length) throws MessagingException {
        this.sendMail(username,targetMail,length);
    }

    public void sendMail(String targetMail) throws MessagingException {
        this.sendMail(username,targetMail,6);
    }

    /**
     * 生成指定数目的验证码
     * @param length
     * @return
     */
    private String generateVerificationCode(int length){
        String codeNum = "";
        int[] code = new int[3];
        Random random = new Random();
        //自动生成验证码
        for (int i = 0; i < length; i++) {
            int num = random.nextInt(10) + 48;
            int uppercase = random.nextInt(26) + 65;
            int lowercase = random.nextInt(26) + 97;
            code[0] = num;
            code[1] = uppercase;
            code[2] = lowercase;
            codeNum += (char) code[random.nextInt(3)];
        }
        return codeNum;
    }
}
image-20220803215137800
image-20220803215137800

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplication测试类了添加如下代码,测试邮件发送服务

@Autowired
MailComponent mailComponent;
@Test
public void sendMailTest() throws MessagingException {
    mailComponent.sendMail("[email protected]",6);
}

可以看到,执行报错了

image-20220803215423530
image-20220803215423530

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component.MailComponent类的sendMail(java.lang.String, java.lang.String, int)方法的第一行打断点,然后以debug方式运行gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplication测试类的sendMailTest方法,可以看到MailComponent类的username字段直接注入了spring.mail.username,忘记加${}

image-20220803215347004
image-20220803215347004

修改gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component.MailComponent类的username方法上的@Value注解

@Value("${spring.mail.username}")
String username;
image-20220803215813519
image-20220803215813519

再次执行gulimall-third-party模块的com.atguigu.gulimall.thirdparty.GulimallThirdPartyApplication测试类的sendMailTest方法,这次显示邮件发送成功了

image-20220803215745860
image-20220803215745860

此时打开接收者的QQ邮箱,可以看到已经收到邮件了

image-20220803215711630
image-20220803215711630

6、远程调用短信服务

1、远程调用third-party模块

gulimall-third-party模块的com.atguigu.gulimall.thirdparty.controller包里新建SmsSendController类,用于短信发送

package com.atguigu.gulimall.thirdparty.controller;

import com.atguigu.common.utils.R;
import com.atguigu.gulimall.thirdparty.component.SmsComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 无名氏
 * @date 2022/8/4
 * @Description:
 */
@RestController
@RequestMapping("/sms")
public class SmsSendController {

    @Autowired
    SmsComponent smsComponent;

    @GetMapping("/sendCode")
    public R sendCode(@RequestParam("phone") String phone,@RequestParam("code") String code){
        smsComponent.sendSms(phone,code);
        return R.ok();
    }
}
image-20220804092409452
image-20220804092409452

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类里添加sendCode方法,用于处理页面的发送验证码请求

@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){
    return R.ok();
}
image-20220804092128863
image-20220804092128863

gulimall-auth-server模块的com.atguigu.gulimall.auth包下新建feign文件夹,在feign文件夹里新建ThirdPartyFeignService接口,在这个接口里调用gulimall-third-party模块的短信接口

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;

/**
 * @author 无名氏
 * @date 2022/8/4
 * @Description:
 */
@FeignClient("gulimall-third-party")
public interface ThirdPartyFeignService {

    @GetMapping("/sms/sendCode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);

}
image-20220804092504819
image-20220804092504819

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类里修改sendCode方法,调用远程gulimall-third-party服务的短信接口

@Autowired
ThirdPartyFeignService thirdPartyFeignService;

/**
 * 给指定手机号发送验证码
 * @param phone
 * @return
 */
@ResponseBody
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){
    String code = UUID.randomUUID().toString().substring(0, 5);
    System.out.println(code);
    thirdPartyFeignService.sendCode(phone,code);
    return R.ok();
}
image-20220804093226524
image-20220804093226524
2、前端编写发送短信请求

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里的建议使用常用手机<input>标签上添加一个id

<input class="phone" id="phoneNum" maxlength="20" type="text" placeholder="建议使用常用手机">
image-20220804093353260
image-20220804093353260

gulimall-auth-server模块的src/main/resources/templates/reg.html文件的<script>标签里添加如下方法,用于向后端请求短信验证码

// 发送验证码
$(function(){
   $("#sendCode").click(function(){
      if($(this).hasClass("disabled")){
         //正在倒计时。
      }else{
         // 给指定手机号发送验证码
         $.get("/sms/sendCode?phone="+$("#phoneNum").val())
         timeoutChangeStyle();
      }
   });
})
var num = 10;
function timeoutChangeStyle() {
   $("#sendCode").attr("class", "disabled");
   if (num == 0) {
      $("#sendCode").text("发送验证码");
      num = 10;
      $("#sendCode").attr("class", "");
   } else {
      var str = num + "s 后再次发送";
      $("#sendCode").text(str);
      setTimeout("timeoutChangeStyle()", 1000);
      num--;
   }
}
image-20220804102603740
image-20220804102603740
3、测试

重启GulimallThirdPartyApplication服务和GulimallAuthServerApplication服务,在http://auth.gulimall.com/reg.html页面里点击发送验证码,可以看到请求已经发送出去了

image-20220804094540385
image-20220804094540385

打开GulimallAuthServerApplication服务的控制台,可以看到随机验证码已经生成成功了

image-20220804094624295
image-20220804094624295

切换到GulimallThirdPartyApplication服务的控制台,已经显示短信发送成功的json数据了

{"msg":"成功","smsid":"165957748665518315203378720","code":"0","balance":"11"}
image-20220804094619237
image-20220804094619237
4、优化短信接口

由于后端没有时长限制,别人获取到该发送短信的接口后,可能会不断地向该接口发送请求,导致消耗资源(前端也暴露了短信接口,也容易获取到短信接口。其次如果发送验证码在倒计时,用户刷新页面又可以点击发送送验证码了)因此可以把用途+发送验证码的手机存入redis,并设置过期时间,如果reids又该信息就不让注册

gulimall-auth-server模块的pom.xml文件里引入redis

<!--引入redis-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
image-20220804102816791
image-20220804102816791

gulimall-auth-server模块的src/main/resources/application.properties配置文件里配置主机地址和端口

spring.redis.host=192.168.56.10
spring.redis.port=6379
image-20220804102952554
image-20220804102952554

gulimall-common模块的com.atguigu.common.constant包下新建auth文件夹,在auth文件夹里新建AuthServerConstant类,在里面存放注册账户的短信验证码前缀

package com.atguigu.common.constant.auth;

/**
 * @author 无名氏
 * @date 2022/8/4
 * @Description:
 */
public class AuthServerConstant {

    /**
     * 短信验证码前缀
     */
    public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
}
image-20220804103452075
image-20220804103452075

并在gulimall-common模块的com.atguigu.common.exception.BizCodeException枚举类里添加同一手机号获取验证码频率太高错误的类型枚举

/**
 * 同一手机号获取验证码频率太高
 */
SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),
image-20220804104928293
image-20220804104928293

修改gulimall-third-party模块的com.atguigu.gulimall.thirdparty.component.SmsComponent类的sendSms方法

/**
 * 给指定手机号发送验证码
 * @param phone
 * @return
 */
@ResponseBody
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){
    //TODO 接口防刷
    String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
    //如果redis有该手机号的验证码,如果有则判断是否过了60s。如果没有证明没有发送过验证码,直接发送验证码
    if (StringUtils.hasText(redisCode)){
        String[] s = redisCode.split("_");
        if (s.length==2 && StringUtils.hasText(s[0])){
            long startTime = Long.parseLong(s[1]);
            if (System.currentTimeMillis() - startTime < 60*1000){
                //同一手机号获取验证码频率太高
                return R.error(BizCodeException.SMS_CODE_EXCEPTION);
            }
        }else {
            return R.error();
        }
    }
    String code = UUID.randomUUID().toString().substring(0, 5);
    //在code后添加当前系统时间,判断是否过了一分钟,防止同一个phone在60秒内再次发送验证码(用户刷新页面、拿接口直接发)
    String redisValue = code +"_" +System.currentTimeMillis();
    System.out.println(code);
    //redis中缓存验证码再次校验  sms:code:17512080612 -> 45678_系统时间

    stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,redisValue,
            10, TimeUnit.MINUTES);
    thirdPartyFeignService.sendCode(phone,code);
    return R.ok();
}
image-20220804110941872
image-20220804110941872

修改gulimall-auth-server模块的src/main/resources/templates/reg.html文件的<script>标签里的idsendCode的点击事件,获取发送验证码的返回值,如果状态码不等于0就弹出对话框,告诉用户msg的信息

// 发送验证码
$(function(){
   $("#sendCode").click(function(){
      if($(this).hasClass("disabled")){
         //正在倒计时。
      }else{
         // 给指定手机号发送验证码
         $.get("/sms/sendCode?phone="+$("#phoneNum").val(),function (data) {
            if (data.code !=0){
               alert(data.msg)
            }
         })
         timeoutChangeStyle();
      }
   });
})
image-20220804111315736
image-20220804111315736

auth.gulimall.com/reg.html页面里点击发送验证码,然后刷新页面再次点击发送验证码

image-20220804110419877
image-20220804110419877

可以看到,此时弹出了验证码获取频率太高,请稍后再试的提示框

image-20220804110422242
image-20220804110422242

此时也可以看到sms:code:手机号key的数据

image-20220804111113269
image-20220804111113269

7、注册

1、编写注册代码

gulimall-auth-server模块的com.atguigu.gulimall.auth包里新建vo文件夹,在vo文件夹里新建UserRegisterVo类,用于封装注册所填写的信息

package com.atguigu.gulimall.auth.vo;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

/**
 * @author 无名氏
 * @date 2022/8/4
 * @Description: 注册表单
 */
@Data
public class UserRegisterVo {

    /**
     * 用户名
     */
    @NotNull(message = "用户名必须填写")
    @Length(min = 6,max = 18,message = "用户名必须是6-18位字符")
    private String username;
    /**
     * 密码
     */
    @NotNull(message = "密码必须填写")
    @Length(min = 6,max = 18,message = "密码必须是6- 18位字符")
    private String password;
    /**
     * 手机号
     */
    @NotNull(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
    private String phone;
    /**
     * 验证码
     */
    @NotNull(message = "验证码必须填写")
    private String code;
}
image-20220805145043209
image-20220805145043209

auth.gulimall.com/reg.html页面里,打开控制台,定位到立 即 注 册,复制立 即 注 册

image-20220805150325410
image-20220805150325410

gulimall-auth-server模块的src/main/resources/templates/reg.html类里搜索立 即 注 册

立 即 注 册所在的<section>标签里套上一个<form>标签

image-20220805150614747
image-20220805150614747

gulimall-auth-server模块的src/main/resources/templates/reg.html类里修改立 即 注 册所在的<section>标签里的部分代码

<form action="/regist" method="post" class="one">
   <div class="register-box">
      <label class="username_label">
         用 户 名
<input maxlength="20" name="username" type="text" placeholder="您的用户名和登录名" >
      </label>
      <div class="tips">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">
         设 置 密 码
<input maxlength="20" name="password" type="password" placeholder="建议至少使用两种字符组合">
</label>
      <div class="tips">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">确 认 密 码
<input maxlength="20" type="password" placeholder="请再次输入密码">
</label>
      <div class="tips">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">
<span>中国 0086∨</span>
<input class="phone" name="phone" id="phoneNum" maxlength="20" type="text" placeholder="建议使用常用手机">
</label>
      <div class="tips">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">验 证 码
<input maxlength="20" name="code" type="text" placeholder="请输入验证码" class="caa">
</label>
      <!--<span id="code"></span>-->
      <a id="sendCode">发送验证码</a>
      <!--<div class="tips">-->

      <!--</div>-->
   </div>
   <div class="arguement">
      <input type="checkbox" id="xieyi"> 阅读并同意
      <a href="#">《谷粒商城用户注册协议》</a>
      <a href="#">《隐私政策》</a>
      <div class="tips">

      </div>
      <br />
      <div class="submit_btn">
         <button type="submit" id="submit_btn">立 即 注 册</button>
      </div>
   </div>

</form>
image-20220805152346218
image-20220805152346218

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类的regist方法

@PostMapping("/regist")
public String regist(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult, Model model){
    if (bindingResult.hasErrors()){
        Map<String, String> errors = bindingResult.getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        model.addAttribute("errors",errors);
        return "forward:/reg.html";
    }
    //调用远程服务注册

    //注册成功,回到登录页
    // (需要经过GulimallWebConfig类 registry.addViewController("/login.html").setViewName("login");)
    //再跳转到login的视图
    return "redirect:/login.html";
}
image-20220805153236781
image-20220805153236781

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里,将提交按钮注释下面的方法注释起来

image-20220805152530066
image-20220805152530066
2、测试

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类的regist方法的第一行打上断点,以debug方式启动GulimallAuthServerApplication服务

image-20220805152735520
image-20220805152735520

http://auth.gulimall.com/reg.html页面里随便输入数据,然后点击立即注册

image-20220805152853881
image-20220805152853881

切换到IDEA,可以看到后端的这些校验都正常判断出来了

image-20220805153011626
image-20220805153011626

取消断点,刷新http://auth.gulimall.com/reg.html页面,再次随便输入数据,然后点击立即注册,此时页面显示Request method 'POST' not supported

image-20220805154114694
image-20220805154114694

打开GulimallAuthServerApplication服务的控制台,显示如下警告

[org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]

/regist的请求方式为POST,转发将请求原封不动的发给了/reg.html,因此发送的使POST方式的请求,而/reg.html的路径映射的请求方式为GET(在gulimall-auth-server模块的com.atguigu.gulimall.auth.config.GulimallWebConfig类的addViewControllers方法里配置的)

image-20220805154123738
image-20220805154123738

return "forward:/reg.html";修改为return "reg";,直接返回reg.html,而不是转发给/reg.html的映射

image-20220805154609954
image-20220805154609954
3、修改页面

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里的<html>标签里添加xmlns:th="http://www.thymeleaf.org",引入thymeleaf

<html xmlns:th="http://www.thymeleaf.org">
image-20220805153328969
image-20220805153328969

用 户 名对应的class="tips"<div>标签上加上th:text="${errors!=null?errors.username:''}"属性

设 置 密 码对应的class="tips"<div>标签上加上th:text="${errors!=null?errors.password:''}"属性`

中国 0086∨对应的class="tips"<div>标签上加上th:text="${errors!=null?errors.phone:''}"属性`

发送验证码相应的位置加上<div class="tips" th:text="${errors!=null?errors.phone:''}"></div>

image-20220805155749434
image-20220805155749434

http://auth.gulimall.com/reg.html页面里随便输入数据,然后点击立即注册,来到了http://auth.gulimall.com/regist页面,点击同意并继续无反应

image-20220805155946328
image-20220805155946328

打开GulimallAuthServerApplication服务的控制台,报了如下错误:(code字段没有找到)

Caused by: org.attoparser.ParseException: Exception evaluating SpringEL expression: "errors!=null?errors.code:''" (template: "reg" - line 5054, col 24)

Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "errors!=null?errors.code:''" (template: "reg" - line 5054, col 24)

Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'code' cannot be found on object of type 'java.util.HashMap' - maybe not public or not valid?
image-20220805160353885
image-20220805160353885

修改gulimall-auth-server模块的src/main/resources/templates/reg.html文件里的<form>表单,对所有字段都判断是否为空

<form action="/regist" method="post" class="one">
   <div class="register-box">
      <label class="username_label">
         用 户 名
<input maxlength="20" name="username" type="text" placeholder="您的用户名和登录名" >
      </label>
      <div class="tips" style="color: red" th:text="${errors!=null?(#maps.containsKey(errors,'username')?errors.username:''):''}">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">
         设 置 密 码
<input maxlength="20" name="password" type="password" placeholder="建议至少使用两种字符组合">
</label>
      <div class="tips" style="color: red" th:text="${errors!=null?(#maps.containsKey(errors,'password')?errors.password:''):''}">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">确 认 密 码
<input maxlength="20" type="password" placeholder="请再次输入密码">
</label>
      <div class="tips">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">
<span>中国 0086∨</span>
<input class="phone" name="phone" id="phoneNum" maxlength="20" type="text" placeholder="建议使用常用手机">
</label>
      <div class="tips" style="color: red" th:text="${errors!=null?(#maps.containsKey(errors,'phone')?errors.phone:''):''}">

      </div>
   </div>
   <div class="register-box">
      <label class="other_label">验 证 码
<input maxlength="20" name="code" type="text" placeholder="请输入验证码" class="caa">
</label>
      <!--<span id="code"></span>-->
      <a id="sendCode">发送验证码</a>
      <div class="tips" style="color: red" th:text="${errors!=null?(#maps.containsKey(errors,'code')?errors.code:''):''}">

      </div>
   </div>
   <div class="arguement">
      <input type="checkbox" id="xieyi"> 阅读并同意
      <a href="#">《谷粒商城用户注册协议》</a>
      <a href="#">《隐私政策》</a>
      <div class="tips">

      </div>
      <br />
      <div class="submit_btn">
         <button type="submit" id="submit_btn">立 即 注 册</button>
      </div>
   </div>

</form>
image-20220805161711958
image-20220805161711958

直接返回页面导致url路径不变,所以再次刷新页面还是发送表单提交的那个请求,就会弹出如下提示框

确认重新提交表单
您所查找的网页要使用已输入的信息。返回此页可能需要重复已进行的所有操作。是否要继续操作?
image-20220805161117226
image-20220805161117226

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类的regist方法,使用重定向的方式返回页面。

可以调用RedirectAttributes类的redirectAttributes.addFlashAttribute("errors",errors);方法,来添加一个一闪而过的属性(只能取一次)。当然也可以使用常规的redirectAttributes.addAttribute("errors",errors);方法,不过最好只取一次,获取到数据后就删除提示信息

/**
 *
 * @param userRegisterVo
 * @param bindingResult 校验失败的错误信息
 * @param redirectAttributes 重定向携带数据
 * @return
 */
@PostMapping("/regist")
public String regist(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult, RedirectAttributes redirectAttributes){
    if (bindingResult.hasErrors()){
        Map<String, String> errors = bindingResult.getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        //添加一个一闪而过的属性(只需要取一次)
        redirectAttributes.addFlashAttribute("errors",errors);
        //  /regist为Post方式,转发将请求原封不动的发给了/reg.html,而/reg.html的路径映射的请求方式为GET
        return "redirect:/reg.html";
        //return "reg";
    }
    //调用远程服务注册

    //注册成功,回到登录页
    // (需要经过GulimallWebConfig类 registry.addViewController("/login.html").setViewName("login");)
    //再跳转到login的视图
    return "redirect:/login.html";
}
image-20220805162618835
image-20220805162618835

http://auth.gulimall.com/reg.html页面里,随便输入数据,然后点击立即注册,此时跳转到了http://10.66.114.92:20000/reg.html页面,使用了本服务器的以太网ip,而不是域名的方式

GIF 2022-8-5 16-26-51
GIF 2022-8-5 16-26-51

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类的regist方法,使用域名方式重定向页面

/**
 * TODO 重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
 * @param userRegisterVo
 * @param bindingResult 校验失败的错误信息
 * @param redirectAttributes 重定向携带数据
 * @return
 */
@PostMapping("/regist")
public String regist(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult, RedirectAttributes redirectAttributes){
    if (bindingResult.hasErrors()){
        Map<String, String> errors = bindingResult.getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        //添加一个一闪而过的属性(只需要取一次)
        redirectAttributes.addFlashAttribute("errors",errors);
        //  /regist为Post方式,转发将请求原封不动的发给了/reg.html,而/reg.html的路径映射的请求方式为GET
        return "redirect:http://auth.gulimall.com/reg.html";
        //return "reg";
    }
    //调用远程服务注册

    //注册成功,回到登录页
    // (需要经过GulimallWebConfig类 registry.addViewController("/login.html").setViewName("login");)
    //再跳转到login的视图
    return "redirect:/login.html";
}
image-20220805163358815
image-20220805163358815

重启GulimallAuthServerApplication服务,在http://auth.gulimall.com/reg.html页面里,随便输入数据,然后点击立即注册,此正确跳转到了http://auth.gulimall.com/reg.html页面

重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉。不过分布式项目使用session会有很多问题
GIF 2022-8-5 16-32-37
GIF 2022-8-5 16-32-37
4、调用会员服务

gulimall-member模块的com.atguigu.gulimall.member包下新建vo文件夹,在vo文件夹里新建MemberRegistVo类,用于注册会员

package com.atguigu.gulimall.member.vo;

import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/5
 * @Description:
 */
@Data
public class MemberRegistVo {

    private String username;

    private String password;

    private String phone;
}
image-20220805170602941
image-20220805170602941

gulimall-member模块的com.atguigu.gulimall.member.controller.MemberController类里新建regist方法

@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
    memberService.regist(vo);
    return R.ok();
}
image-20220805165310321
image-20220805165310321

gulimall-member模块的com.atguigu.gulimall.member.service.MemberService接口里添加regist抽象方法

void regist(MemberRegistVo vo);
image-20220805165338879
image-20220805165338879

gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类里实现regist抽象方法

@Autowired
MemberLevelDao memberLevelDao;

@Override
public void regist(MemberRegistVo vo) {
    MemberDao baseMapper = this.baseMapper;
    MemberEntity memberEntity = new MemberEntity();

    MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
    memberEntity.setLevelId(memberLevelEntity.getId());

    //检查手机号和用户名是否唯一,使用异常机制
    checkPhoneUnique(vo.getPhone());
    checkUsernameUnique(vo.getUsername());
    
    memberEntity.setMobile(vo.getPhone());
    memberEntity.setUsername(vo.getUsername());
    //TODO 密码加密存储
    memberEntity.setPassword(vo.getPassword());

    baseMapper.insert(memberEntity);
}
image-20220805171242685
image-20220805171242685

gulimall-member模块的com.atguigu.gulimall.member.dao.MemberLevelDao接口里添加getDefaultLevel抽象方法

MemberLevelEntity getDefaultLevel();
image-20220805171336691
image-20220805171336691

gulimall_ums数据库的ums_member_level表里的default_status字段,标识了当前用户的默认等级

image-20220805171519674
image-20220805171519674

gulimall-member模块的src/main/resources/mapper/member/MemberLevelDao.xml文件里添加id="getDefaultLevel"sql语句,用于查询用户默认等级

<select id="getDefaultLevel" resultType="com.atguigu.gulimall.member.entity.MemberLevelEntity">
    select * from gulimall_ums.ums_member_level where default_status=1
</select>
image-20220805171717875
image-20220805171717875

gulimall-member模块的com.atguigu.gulimall.member包里新建exception文件夹,在exception文件夹里新建UsernameExistException类,用于抛出用户名存在的异常

package com.atguigu.gulimall.member.exception;

/**
 * @author 无名氏
 * @date 2022/8/5
 * @Description:
 */
public class UsernameExistException extends RuntimeException{
    public UsernameExistException(){
        super("用户名存在");
    }
}
image-20220805171953569
image-20220805171953569

gulimall-member模块的com.atguigu.gulimall.member.exception包里新建PhoneExistException类,用于抛出手机号存在的异常

package com.atguigu.gulimall.member.exception;

/**
 * @author 无名氏
 * @date 2022/8/5
 * @Description:
 */
public class PhoneExistException extends RuntimeException{
    public PhoneExistException() {
        super("手机号存在");
    }
}
image-20220805172115826
image-20220805172115826

gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类里添加checkUsernameUnique方法和checkPhoneUnique方法

@Override
private void checkUsernameUnique(String username) throws UsernameExistException{
    LambdaQueryWrapper<MemberEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(MemberEntity::getUsername, username);
    Integer count = this.baseMapper.selectCount(lambdaQueryWrapper);
    if (count > 0) {
        throw new UsernameExistException();
    }
}

@Override
private void checkPhoneUnique(String phone) throws PhoneExistException{
    LambdaQueryWrapper<MemberEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(MemberEntity::getMobile, phone);
    Integer count = this.baseMapper.selectCount(lambdaQueryWrapper);
    if (count > 0) {
        throw new PhoneExistException();
    }
}
image-20220805172707071
image-20220805172707071

gulimall-member模块的com.atguigu.gulimall.member.service.MemberService接口里添加checkUsernameUnique抽象方法和checkPhoneUnique抽象方法

void checkUsernameUnique(String username) throws UsernameExistException;

void checkPhoneUnique(String phone) throws PhoneExistException;
image-20220805172743895
image-20220805172743895

5.7.3、MD5盐值加密

1、对用户密码进行加密

MD5:Message Digest algorithm 5,信息摘要算法

优点:

  • 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
  • 容易计算:从原数据计算出MD5值很容易。
  • 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
  • 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
  • 不可逆

但是一个明文(加密后)对应一个唯一的密文(加密前),只要维护一个彩虹表,记录明文和对应的密文,如果密文使用的是弱密码,可以直接根据明文查找密码表,就可以知道密文

加盐

  • 通过生成随机数与MD5生成字符串进行组合
  • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

gulimall-member模块的com.atguigu.gulimall.member.GulimallMemberApplicationTests测试类的contextLoads方法里测试常见的加密方式

@Test
public void contextLoads() {
   //e10adc3949ba59abbe56e057f20f883e
   //抗修改性:彩虹表。 123456->>xx
   //1234567- >dddd
   String s1 = DigestUtils.md5Hex("123456");
   System.out.println("s1=>" + s1);
    
   //MD5不能直接进行密码的加密存储;
   //"123456 "+System.currentTimeMillis();
   //盐值加密;随机值加盐: $1$+8位字符
   //$1$q4yw9ojS$YQk9WvivLoEWT04q/Fr2q1
   String s2 = Md5Crypt.md5Crypt("123456".getBytes());
   System.out.println("s2=>"+s2);
   //$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1
   //验证: 123456进行盐值(去数据库查)加密
   String s3 = Md5Crypt.md5Crypt ( "123456".getBytes(),"$1$qqqqqqqq");
   System. out.println("s3=>"+s3);
    
   BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
   //$2a$10$4li09amFs0Tfof8Y/0PjKe0ZWngU5tMHuAYNUkyGiM/2FuJ25oeBi
   String encode = passwordEncoder.encode("123456");
   System.out.println(encode);
   boolean matches = passwordEncoder.matches("123456",
         "$2a$10$4li09amFs0Tfof8Y/0PjKe0ZWngU5tMHuAYNUkyGiM/2FuJ25oeBi");
   System.out.println(matches);
}

测试结果:

s1=>e10adc3949ba59abbe56e057f20f883e
s2=>$1$oOb6v3rn$FsjQJmtHzO1Bm/CGUAhNH1
s3=>$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1
$2a$10$dR0/M9hUPbHxqgrjqjv5xeA2JxKNDu5CLfn/wTcSF/JUD8BVR0Uc.
true
image-20220805192733343
image-20220805192733343

修改gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的regist方法,使其使用加盐的md5来存储密文

@Override
public void regist(MemberRegistVo vo) {
    MemberDao baseMapper = this.baseMapper;
    MemberEntity memberEntity = new MemberEntity();

    MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
    memberEntity.setLevelId(memberLevelEntity.getId());

    //检查手机号和用户名是否唯一,使用异常机制
    checkPhoneUnique(vo.getPhone());
    checkUsernameUnique(vo.getUsername());

    memberEntity.setMobile(vo.getPhone());
    memberEntity.setUsername(vo.getUsername());
    //盐值加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode = bCryptPasswordEncoder.encode(vo.getPassword());
    memberEntity.setPassword(encode);

    baseMapper.insert(memberEntity);
}
image-20220805200911397
image-20220805200911397

2、远程调用member服务

1、添加错误信息

gulimall-common模块的com.atguigu.common.exception.BizCodeException枚举类里添加如下两个枚举

/**
 * 用户名重复
 */
USER_EXIST_EXCEPTION(15001,"用户存在"),
/**
 * 手机号重复
 */
PHONE_EXIST_EXCEPTION(15002,"手机号存在");
image-20220805193459836
image-20220805193459836

修改gulimall-member模块的com.atguigu.gulimall.member.controller.MemberController类的regist方法,捕获用户名重复手机号重复的异常

@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
    try {
        memberService.regist(vo);
    } catch (UsernameExistException e) {
        return R.error(BizCodeException.USER_EXIST_EXCEPTION);
    }catch (PhoneExistException e){
        return R.error(BizCodeException.PHONE_EXIST_EXCEPTION);
    }
    return R.ok();
}
image-20220805193835367
image-20220805193835367
2、远程调用会员服务

gulimall-auth-server模块的com.atguigu.gulimall.auth.feign包里新建MemberFeignService接口,用于调用会员服务

package com.atguigu.gulimall.auth.feign;

import com.atguigu.common.utils.R;
import com.atguigu.gulimall.auth.vo.UserRegisterVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * @author 无名氏
 * @date 2022/8/5
 * @Description:
 */
@FeignClient("gulimall-member")
public interface MemberFeignService {

    @PostMapping("/member/member/regist")
    public R regist(@RequestBody UserRegisterVo vo);
}
image-20220805194153146
image-20220805194153146

gulimall-common模块的com.atguigu.common.utils.R类里添加getMsg方法,用于获取消息

public String getMsg(){
   return (String) this.get("msg");
}
image-20220805194951039
image-20220805194951039

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类的regist方法,调用远程的会员服务,完成注册

/**
 * TODO 重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
 * @param userRegisterVo
 * @param bindingResult 校验失败的错误信息
 * @param redirectAttributes 重定向携带数据
 * @return
 */
@PostMapping("/regist")
public String regist(@Valid UserRegisterVo userRegisterVo, BindingResult bindingResult, RedirectAttributes redirectAttributes){
    if (bindingResult.hasErrors()){
        Map<String, String> errors = bindingResult.getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        //添加一个一闪而过的属性(只需要取一次)
        redirectAttributes.addFlashAttribute("errors",errors);
        //  /regist为Post方式,转发将请求原封不动的发给了/reg.html,而/reg.html的路径映射的请求方式为GET
        return "redirect:http://auth.gulimall.com/reg.html";
        //return "reg";
    }
    //调用远程服务注册
    String code = userRegisterVo.getCode();
    String key = AuthServerConstant.SMS_CODE_CACHE_PREFIX + userRegisterVo.getPhone();
    String redisCode = stringRedisTemplate.opsForValue().get(key);
    if (StringUtils.hasText(redisCode)){
        if (redisCode.contains("_") && code.equals(redisCode.split("_")[0])){
            stringRedisTemplate.delete(key);
            R r = memberFeignService.regist(userRegisterVo);
            if (r.getCode()==0){
                //注册成功,回到登录页
                // (需要经过GulimallWebConfig类 registry.addViewController("/login.html").setViewName("login");)
                //再跳转到login的视图
                return "redirect:http://auth.gulimall.com/login.html";
            }else {
                Map<String, String> errors = new HashMap<>();
                errors.put("msg",r.getMsg());
                redirectAttributes.addFlashAttribute("errors",errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }

        }
    }
    Map<String, String> errors = new HashMap<>();
    errors.put("code","验证码错误");
    redirectAttributes.addFlashAttribute("errors",errors);
    return "redirect:http://auth.gulimall.com/reg.html";
}
image-20220805200325158
image-20220805200325158

gulimall-auth-server模块的src/main/resources/templates/reg.html文件里的<form>标签的下面添加如下代码,用于获取注册失败的错误提示

<div class="tips" style="color: red"
    th:text="${errors!=null?(#maps.containsKey(errors,'msg')?errors.msg:''):''}">
</div>
image-20220805195512378
image-20220805195512378

重启GulimallAuthServerApplication服务和GulimallMemberApplication服务

http://auth.gulimall.com/reg.html页面填完信息提交后,成功来到了登录页http://auth.gulimall.com/login.html

image-20220805200605614
image-20220805200605614

打开Navicat,查看gulimall_ums数据库的ums_member表,可以看到刚刚注册的账户已经添加进来了,密码也是用的密文

image-20220805201315222
image-20220805201315222

3、用户登录

1、编写auth-server模块

gulimall-auth-server模块的com.atguigu.gulimall.auth.vo包里新建UserLoginVo类,用于封装用户登录所需要的数据

package com.atguigu.gulimall.auth.vo;

import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/5
 * @Description:
 */
@Data
public class UserLoginVo {

    /**
     * 登录的账号(邮箱/用户名/手机号)
     */
    private String loginAccount;
    /**
     * 密码
     */
    private String password;
}
image-20220805201802057
image-20220805201802057

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类里添加login方法,用于用户登录

页面传递的参数为url里的k,v,因此该方法上不加@RequestBody注解

@PostMapping("/login")
public String login(UserLoginVo vo){

    return "redirect:http://gulimall.com";
}
image-20220805210151060
image-20220805210151060

http://auth.gulimall.com/login.html页面里,打开控制台,定位到登录,复制登 &nbsp; &nbsp;录

image-20220805202828971
image-20220805202828971

gulimall-auth-server模块的src/main/resources/templates/login.html文件里搜索登 &nbsp; &nbsp;录,将登 &nbsp; &nbsp;录<button>标签类型设置为submit,在登 &nbsp; &nbsp;录所在的ul外围加一个<form>,删掉登录左边的<a class="a">和右边的</a>,给用户名和密码各一个name,方便取值

<form action="/login" method="post">
   <ul>
      <li class="top_1">
         <img src="/static/login/JD_img/user_03.png" class="err_img1" />
         <input type="text" name="loginAccount" placeholder=" 邮箱/用户名/已验证手机" class="user" />
      </li>
      <li>
         <img src="/static/login/JD_img/user_06.png" class="err_img2" />
         <input type="password" name="password" placeholder=" 密码" class="password" />
      </li>
      <li class="bri">
         <a href="">忘记密码</a>
      </li>
      <li class="ent"><button class="btn2" type="submit">&nbsp; &nbsp;</button></li>
   </ul>
</form>
image-20220805202926997
image-20220805202926997
2、编写member模块

gulimall-member模块的com.atguigu.gulimall.member.vo包里新建MemberLoginVo类,用于封装gulimall-auth-server模块调用本模块进行登录所需要的数据

package com.atguigu.gulimall.member.vo;

import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/5
 * @Description:
 */
@Data
public class MemberLoginVo {
    /**
     * 登录的账号(邮箱/用户名/手机号)
     */
    private String loginAccount;
    /**
     * 密码
     */
    private String password;
}
image-20220805203945860
image-20220805203945860

gulimall-common模块的com.atguigu.common.exception.BizCodeException枚举类里添加登录的账号或密码错误 或 该用户不存在的枚举

/**
 * 登录的账号或密码错误 或 该用户不存在
 */
ACCOUNT_PASSWORD_INVALID_EXCEPTION(15003,"账号或密码错误");
image-20220805205824129
image-20220805205824129

gulimall-member模块的com.atguigu.gulimall.member.controller.MemberController类里添加login方法用于查找数据库,处理gulimall-auth-server模块登录请求

@PostMapping("/login")
public R login(@RequestBody MemberLoginVo vo){
    MemberEntity entity = memberService.login(vo);
    if (entity!=null) {
        return R.ok().put("data", entity);
    }else {
        return R.error(BizCodeException.ACCOUNT_PASSWORD_INVALID_EXCEPTION);
    }
}
image-20220805210002751
image-20220805210002751

gulimall-member模块的com.atguigu.gulimall.member.service.MemberService接口里添加login抽象方法

MemberEntity login(MemberLoginVo vo);
image-20220805204448949
image-20220805204448949

gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类里实现login抽象方法

@Override
public MemberEntity login(MemberLoginVo vo) {
    LambdaQueryWrapper<MemberEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(MemberEntity::getUsername,vo.getLoginAccount())
            .or().eq(MemberEntity::getMobile,vo.getLoginAccount())
            .or().eq(MemberEntity::getEmail,vo.getLoginAccount());
    MemberEntity memberEntity = this.baseMapper.selectOne(lambdaQueryWrapper);
    if (memberEntity==null){
        return null;
    }
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    boolean matches = bCryptPasswordEncoder.matches(vo.getPassword(), memberEntity.getPassword());
    if (matches){
        return memberEntity;
    }else {
        return null;
    }
}
image-20220805205332991
image-20220805205332991
3、调用member服务

gulimall-auth-server模块的com.atguigu.gulimall.auth.feign.MemberFeignService类里添加login方法,用于调用member服务的登录接口

@PostMapping("/member/member/login")
public R login(@RequestBody UserLoginVo vo);
image-20220805210346363
image-20220805210346363

gulimall-auth-server模块的src/main/resources/templates/login.html类里的<html>标签里添加xmlns:th="http://www.thymeleaf.org"属性,引入thymeleaf

<html xmlns:th="http://www.thymeleaf.org">
image-20220805211142366
image-20220805211142366

<form>标签下,加入如下代码,用于提醒登录失败

<div  style="color: red"
     th:text="${errors!=null?(#maps.containsKey(errors,'msg')?errors.msg:''):''}">
</div>
image-20220805211230010
image-20220805211230010

重启GulimallAuthServerApplication服务和GulimallMemberApplication服务,在http://auth.gulimall.com/login.html页面里进行登录,可以看到当登录失败时,会有失败的提示(就是有点丑)

GIF 2022-8-5 21-17-02
GIF 2022-8-5 21-17-02

http://auth.gulimall.com/login.html页面里输入正确的账户和密码,点击登录,可以正确来到http://gulimall.com页面

GIF 2022-8-5 21-19-03
GIF 2022-8-5 21-19-03

5.7.4、社交登录

社交登录流程图

image-20220805213414019
image-20220805213414019

1、社交登录流程

1、OAuth2.0概述

OAuth2.0较1.0相比,整个授权验证流程更简单更安全,也是未来最主要的用户身份验证和授权方式。

关于OAuth2.0协议的授权流程可以参考下面的流程图,其中Client指第三方应用,Resource Owner指用户,Authorization Server是我们的授权服务器,Resource Server是API服务器。

1、使用Code换取AccessToken,Code只能用一次

2、同一个用户的accessToken一段时间是不会变化的,即使多次获取

image-20220805215519572
image-20220805215519572

Web网站的授权

image-20220805215615101

社交登录的时序图

社交登录流程
社交登录流程
2、gitee登录流程

参考文档:https://gitee.com/api/v5/swagger

1、获取code

点击第三方登录跳转到如下页面,{client_id}修改为自己申请的应用的client_id{redirect_uri}修改为授权成功返回到的接口地址。

https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code

eg:

https://gitee.com/oauth/authorize?client_id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d&redirect_uri=http://gulimall.com/oauth2.0/gitee/success&response_type=code

用户点击同意授权后,会跳转到如下页面:(此code码只能使用一次)

{redirect_uri}?code=abc&state=xyz

eg:

http://gulimall.com/oauth2.0/gitee/success?code=354dd0ceec0fe0457ae6ae03c93c5dace1ea28819aff74873ac4ac551e0907ab
2、获取token

对以下接口发送请求:

https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}

eg:

请求:

https://gitee.com/oauth/token?grant_type=authorization_code&code=354dd0ceec0fe0457ae6ae03c93c5dace1ea28819aff74873ac4ac551e0907ab&client_id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d&redirect_uri=http://gulimall.com/oauth2.0/gitee/success&client_secret=0c58d0cca9c3fe12bd6c6824f6dc04cdbce5b07cad784c9b8d5938342fc004f7

响应:

{
    "access_token": "93c7871550aab0ac3b99c5f2c1a017ca",
    "token_type": "bearer",
    "expires_in": 86400,
    "refresh_token": "d4da9bcc74d312d9f65239fd1b80f497f8d7959d668c382bf6bcab4e5c650312",
    "scope": "user_info",
    "created_at": 1659754964
}

响应参数说明:

access_token (必需) 授权服务器发出的访问令牌
token_type (必需)这是令牌的类型,通常只是字符串“bearer”。
expires_in (推荐)访问令牌的过期时间。
refresh_token(可选)刷新令牌,在访问令牌过期后,可使用此令牌刷新。
scope(可选)如果用户授予的范围与应用程序请求的范围相同,则此参数为可选。
image-20220806110618299
image-20220806110618299
3、获取授权用户的信息

请求:

https://gitee.com/api/v5/user?access_token={access_token}

响应:

{
    "id": 7559746,
    "login": "anonymouszs",
    "name": "无名氏",
    "avatar_url": "https://gitee.com/assets/no_portrait.png",
    "url": "https://gitee.com/api/v5/users/anonymouszs",
    "html_url": "https://gitee.com/anonymouszs",
    "remark": "",
    "followers_url": "https://gitee.com/api/v5/users/anonymouszs/followers",
    "following_url": "https://gitee.com/api/v5/users/anonymouszs/following_url{/other_user}",
    "gists_url": "https://gitee.com/api/v5/users/anonymouszs/gists{/gist_id}",
    "starred_url": "https://gitee.com/api/v5/users/anonymouszs/starred{/owner}{/repo}",
    "subscriptions_url": "https://gitee.com/api/v5/users/anonymouszs/subscriptions",
    "organizations_url": "https://gitee.com/api/v5/users/anonymouszs/orgs",
    "repos_url": "https://gitee.com/api/v5/users/anonymouszs/repos",
    "events_url": "https://gitee.com/api/v5/users/anonymouszs/events{/privacy}",
    "received_events_url": "https://gitee.com/api/v5/users/anonymouszs/received_events",
    "type": "User",
    "blog": null,
    "weibo": null,
    "bio": null,
    "public_repos": 8,
    "public_gists": 0,
    "followers": 0,
    "following": 1,
    "stared": 3,
    "watched": 14,
    "created_at": "2020-05-13T15:39:52+08:00",
    "updated_at": "2022-08-06T16:02:02+08:00",
    "email": null
}

响应各字段类型:

{
    "avatar_url": string
    "bio": string
    "blog": string
    "created_at": string
    "email": string
    "events_url": string
    "followers": string
    "followers_url": string
    "following": string
    "following_url": string
    "gists_url": string
    "html_url": string
    "id": integer
    "login": string
    "member_role": string
    "name": string
    "organizations_url": string
    "public_gists": string
    "public_repos": string
    "received_events_url": string
    "remark": string 企业备注名
    "repos_url": string
    "stared": string
    "starred_url": string
    "subscriptions_url": string
    "type": string
    "updated_at": string
    "url": string
    "watched": string
    "weibo": string
}
image-20220806161709372
image-20220806161709372

2、前端添加gitee登录

1、gitee创建第三方应用

登录gitee,点击头像右侧的下三角,点击设置,在左侧导航栏找到数据管理里的第三方应用

image-20220806091840266
image-20220806091840266

我的应用里,点击右侧的+创建应用,创建一个应用

image-20220806091843219
image-20220806091843219

应用名称输入谷粒商城,应用描述输入谷粒商城,应用主页输入http://gulimall.com/,应用回调地址输入http://gulimall.com/success,上传一个Logo,然后点击创建应用

image-20220806091952038
image-20220806091952038

此时就可以看到Client IDClient Secret

image-20220806092026961
image-20220806092026961
2、修改页面

修改gulimall-auth-server模块的src/main/resources/templates/login.html文件的class="si_out"div,将QQ微信图标修改为giteegithub,并修改gitee登录的登录请求地址和参数(参考5.7.4.2.gitee登录流程

<div class="si_out">
   <ul>
      <li>
         <a href="https://gitee.com/oauth/authorize?client_id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d&redirect_uri=http://gulimall.com/success&response_type=code">
            <img style="width: 55px;height: 45px" src="https://gitee.com/static/images/logo-black.svg?t=158106666" />
         </a>
      </li>
      <li class="f4"> | </li>
      <li>
         <a href="">
            <svg height="22" style="margin-top: 10px" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="22" data-view-component="true" >
               <path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
            </svg>
            <span style="vertical-align: top"><b>github</b></span>
         </a>
      </li>
   </ul>
   <h5 class="rig">
      <img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_25.png" />
      <span><a href="http://auth.gulimall.com/reg.html">立即注册</a></span>
   </h5>
</div>
image-20220806100848069
image-20220806100848069

点击Build -> Recompile 'login.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

3、测试

http://auth.gulimall.com/login.html页面里点击gitee图标,跳转到了http://gulimall.com/success?code=74d8f002dc70a88c28f22724ea7fec774ffbdcc983990ae65e518563940ce629页面,点击同意授权后,回调到了https://gitee.com/oauth/authorize?client_id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d&redirect_uri=http://gulimall.com/success&response_type=code,此接口还没写,所以没有访问到

GIF 2022-8-6 10-17-39
GIF 2022-8-6 10-17-39

3、换取token

1、修改回调地址

在刚刚创建的谷粒商城应用里,添加应用回调地址http://gulimall.com/oauth2.0/gitee/success,然后点击提交修改

image-20220806104444327
image-20220806104444327

gulimall-auth-server模块的src/main/resources/templates/login.html文件里,修改点击gitee图标跳转到的请求

<a href="https://gitee.com/oauth/authorize?client_id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d&redirect_uri=http://gulimall.com/oauth2.0/gitee/success&response_type=code">
image-20220806104507366
image-20220806104507366
2、配置gitee登录参数

gulimall-auth-server模块的com.atguigu.gulimall.auth.config包里新建Oauth2FormGitee类,用于配置使用gitee登录的参数

package com.atguigu.gulimall.auth.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@ConfigurationProperties(prefix = "oauth2.gitee")
@Component
@Data
public class Oauth2FormGitee {

    private String clientId;
    private String redirectUri;
    private String clientSecret;
}
image-20220806112915387
image-20220806112915387

gulimall-auth-server模块的src/main/resources/application.properties配置文件里,配置使用gitee登录的信息

oauth2.gitee.client-id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d
oauth2.gitee.redirect-uri=http://gulimall.com/oauth2.0/gitee/success
oauth2.gitee.client-secret=0c58d0cca9c3fe12bd6c6824f6dc04cdbce5b07cad784c9b8d5938342fc004f7
image-20220806112944780
image-20220806112944780

gulimall-auth-server模块的com.atguigu.gulimall.auth.config包里新建RestTemplateConfig类,用于配置RestTemplateConfig

package com.atguigu.gulimall.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@Configuration
public class RestTemplateConfig {

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        //连接超时时间/毫秒(连接上服务器(握手成功)的时间,超出抛出connect timeout)
        factory.setConnectTimeout(15000);
        //数据读取超时时间(socketTimeout)/毫秒(务器返回数据(response)的时间,超过抛出read timeout)
        factory.setReadTimeout(5000);
        return factory;
    }

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory){
         RestTemplate restTemplate = new RestTemplate(factory);
        // 设置UTF_8编码
        restTemplate.getMessageConverters().set(1,new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }
}
image-20220806114152845
image-20220806114152845

gulimall-auth-server模块的com.atguigu.gulimall.auth.vo包里新建GiteeCodeResponseVo类,用于封装跳转到gitee后,点击同意授权返回到回调地址,再从回调地址根据code换取token的响应数据

package com.atguigu.gulimall.auth.vo;

import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@Data
public class GiteeCodeResponseVo {
    private String accessToken;
    private String tokenType;
    private long expiresIn;
    private String refreshToken;
    private String scope;
    private long createdAt;
}
image-20220806114502358
image-20220806114502358
3、新建oauth_gitee

执行以下sql,新建oauth_gitee表,用于存储gitee登录的数据

CREATE TABLE `gulimall_ums`.`oauth_gitee`  (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `member_id` bigint(20) NULL COMMENT '会员id',
  `access_token` varchar(40) NULL COMMENT '授权服务器发出的访问令牌',
  `token_type` varchar(10) NULL COMMENT '这是令牌的类型,通常只是字符串“bearer”',
  `expires_in` bigint(20) NULL COMMENT '访问令牌的过期时间',
  `refresh_token` varchar(70) NULL COMMENT '刷新令牌,在访问令牌过期后,可使用此令牌刷新',
  `scope` varchar(255) NULL COMMENT '如果用户授予的范围与应用程序请求的范围相同,则此参数为可选',
  `created_at` bigint(255) NULL,
  `avatar_url` varchar(255) NULL COMMENT '用户的头像',
  `created_time` datetime NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
);
image-20220806161144329
image-20220806161144329

IDEA里依次点击Database -> 192.168.56.10 -> schemas -> gulimall_ums->oauth_gitee,然后右键鼠标悬浮到Scripted Extensions这里,然后点击Generate POJOs groovy,选择一个路径存放生成的实体类

gulimall-member模块的com.atguigu.gulimall.member.entity包里新建OauthGiteeEntity类,粘贴刚刚生成的实体类在该类里,然后稍作修改

package com.atguigu.gulimall.member.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

import java.time.LocalDate;


/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@Data
@TableName("oauth_gitee")
public class OauthGiteeEntity {
    @TableId
    private long id;
    private long memberId;
    private String accessToken;
    private String tokenType;
    private long expiresIn;
    private String refreshToken;
    private String scope;
    private long createdAt;
    private String avatarUrl;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT)
    private LocalDate createdTime;
}
image-20220806161307581
image-20220806161307581

使用Postman发送如下请求,用于换取获取用户信息(注意:要修改为自己的token

https://gitee.com/api/v5/user?access_token=4f1583c038a15f9e57344c868f281462
image-20220806161532759
image-20220806161532759
4、编写gitee登录逻辑

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller包里新建OAuth2Controller类,用于gitee登录注册

package com.atguigu.gulimall.auth.controller;

import com.atguigu.common.utils.R;
import com.atguigu.gulimall.auth.service.OAuth2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description: 社交登录
 */
@Controller
@Slf4j
@RequestMapping("/oauth2.0")
public class OAuth2Controller {


    @Autowired
    OAuth2Service oAuth2Service;

    @GetMapping("/gitee/success")
    public R giteeRegister(@RequestParam String code){
        try {
            oAuth2Service.giteeRegister(code);
        }catch (Exception e){
            log.error("第三方登录失败 :{}",e.getMessage());
            return R.error().put("msg","第三方登录失败");
        }
        return R.ok();
    }

}
image-20220806200442012
image-20220806200442012

gulimall-auth-server模块的com.atguigu.gulimall.auth包里新建service文件夹,在service文件夹里新建OAuth2Service接口,并添加giteeRegister注册抽象方法

package com.atguigu.gulimall.auth.service;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
public interface OAuth2Service {

    void giteeRegister(String code);
}
image-20220806200511415
image-20220806200511415

gulimall-gateway模块的src/main/resources/application.yml配置文件里添加如下参数,将路径为/oauth2.0/**的转给gulimall-auth-server模块(写在通过域名转载的前面)

- id: gulimall_oauth2_route
  uri: lb://gulimall-auth-server
  predicates:
    - Path=/oauth2.0/**
image-20220806203319034
image-20220806203319034

gulimall-auth-server模块的com.atguigu.gulimall.auth.vo包里新建GiteeTokenResponseVo类,用于封装根据tocken换取用户信息的响应数据,这里先不写字段,先完成获取token的测试后,再完善字段

package com.atguigu.gulimall.auth.vo;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
public class GiteeTokenResponseVo {
}
image-20220806211810567
image-20220806211810567

gulimall-auth-server模块的com.atguigu.gulimall.auth.service包里新建impl文件夹,在impl文件夹里新建OAuth2ServiceImpl类,用于处理通过gitee登陆后注册账户

package com.atguigu.gulimall.auth.service.impl;

import com.atguigu.gulimall.auth.config.Oauth2FormGitee;
import com.atguigu.gulimall.auth.service.OAuth2Service;
import com.atguigu.gulimall.auth.vo.GiteeCodeResponseVo;
import com.atguigu.gulimall.auth.vo.GiteeTokenResponseVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@Service
public class OAuth2ServiceImpl implements OAuth2Service {

    @Autowired
    Oauth2FormGitee oauth2FormGitee;
    @Autowired
    RestTemplate restTemplate;
    @Override
    public void giteeRegister(String code) {
        GiteeCodeResponseVo vo = getToken(code);
        if (vo==null || !StringUtils.hasText(vo.getAccessToken())){
            throw new RuntimeException("获取用户token失败");
        }
        getGiteeUserInfo(vo.getAccessToken());


    }

    private GiteeCodeResponseVo getToken(String code){
        GiteeCodeResponseVo vo = null;
        String url = "https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}" +
                "&redirect_uri={redirect_uri}&client_secret={client_secret}";
        Map<String,String> map = new HashMap<>();
        map.put("code",code);
        map.put("client_id",oauth2FormGitee.getClientId());
        map.put("redirect_uri",oauth2FormGitee.getRedirectUri());
        map.put("client_secret",oauth2FormGitee.getClientSecret());
        try {
            ResponseEntity<GiteeCodeResponseVo> response = restTemplate.postForEntity(url,null,GiteeCodeResponseVo.class,map);
            if (response.getStatusCodeValue()==200){
                vo = response.getBody();
            }else {
                throw new RuntimeException("连接gitee获取token状态异常");
            }
        }catch (IllegalArgumentException e){
            e.printStackTrace();
            throw new RuntimeException("连接gitee获取token响应参数异常");
        }
        catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("连接gitee获取token异常");
        }
        return vo;
    }

    private GiteeTokenResponseVo getGiteeUserInfo(String accessToken){
        GiteeTokenResponseVo vo = null;
        String url = "https://gitee.com/api/v5/user?access_token={access_token}";
        try {
            ResponseEntity<GiteeTokenResponseVo> response = restTemplate.getForEntity(url, GiteeTokenResponseVo.class, accessToken);
            if (response.getStatusCodeValue()==200){
                vo = response.getBody();
            }else {
                throw new RuntimeException("连接gitee获取token状态异常");
            }
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("连接gitee获取用户信息异常");
        }
        return vo;
    }
}
image-20220806205937535
image-20220806205937535

gulimall-auth-server模块的com.atguigu.gulimall.auth.service.impl.OAuth2ServiceImpl类里的getToken方法的ResponseEntity<GiteeCodeResponseVo> response = restTemplate.postForEntity(url,null,GiteeCodeResponseVo.class,map);这一行打上断点。以debug方式运行GulimallAuthServerApplication服务,在http://auth.gulimall.com/login.html页面里,点击gitee图标,然后点击同意授权,切换到IDEA,点击Step Over F8执行完根据code换取token的请求,可以看到返回的数据并没有封装到GiteeCodeResponseVo类里。这是因为返回的数据是蛇形命名法(以_区分各单词),而GiteeCodeResponseVo类的字段使用的是驼峰命名法

image-20220806205902981
image-20220806205902981
5、修改GiteeCodeResponseVo
方法一(不推荐)

可以将gulimall-auth-server模块的com.atguigu.gulimall.auth.vo.GiteeCodeResponseVo类的字段都修改为蛇形命名法,不过很明显不符合java代码规范

package com.atguigu.gulimall.auth.vo;

import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@Data
public class GiteeCodeResponseVo {
    private String access_token;
    private String token_type;
    private long expires_in;
    private String refresh_token;
    private String scope;
    private long created_at;
}
image-20220806210726716
image-20220806210726716
方法二(推荐)

gulimall-auth-server模块的com.atguigu.gulimall.auth.vo.GiteeCodeResponseVo类上添加@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)注解,指明json是采用蛇形命名法,java实体类使用驼峰命名法来进行转换

package com.atguigu.gulimall.auth.vo;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@Data
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GiteeCodeResponseVo {
    private String accessToken;
    private String tokenType;
    private long expiresIn;
    private String refreshToken;
    private String scope;
    private long createdAt;
}
image-20220807091632695
image-20220807091632695
6、测试

再次以debug方式运行GulimallAuthServerApplication服务,在http://auth.gulimall.com/login.html页面里,点击gitee图标,然后点击同意授权,切换到IDEA,点击Step Over F8执行完根据code换取token的请求,可以看到返回的数据已经成功封装到GiteeCodeResponseVo类里了。

image-20220806210625293
image-20220806210625293

4、完善gitee登录逻辑

1、封装giee用户信息

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.vo.GiteeTokenResponseVo类,用于封装用户信息(这里图省事就不写驼峰命名法了)

package com.atguigu.gulimall.auth.vo;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
import lombok.Data;

import java.util.Date;
@Data
public class GiteeTokenResponseVo {

    private long id;
    private String login;
    private String name;
    private String avatar_url;
    private String url;
    private String html_url;
    private String remark;
    private String followers_url;
    private String following_url;
    private String gists_url;
    private String starred_url;
    private String subscriptions_url;
    private String organizations_url;
    private String repos_url;
    private String events_url;
    private String received_events_url;
    private String type;
    private String blog;
    private String weibo;
    private String bio;
    private int public_repos;
    private int public_gists;
    private int followers;
    private int following;
    private int stared;
    private int watched;
    private Date created_at;
    private Date updated_at;
    private String email;

}
image-20220806211847697
image-20220806211847697

gulimall-auth-server模块的com.atguigu.gulimall.auth.service.impl.OAuth2ServiceImpl类里的getGiteeUserInfo方法的ResponseEntity<GiteeTokenResponseVo> response = restTemplate.getForEntity(url, GiteeTokenResponseVo.class, accessToken);这一行打上断点。以debug方式运行GulimallAuthServerApplication服务,在http://auth.gulimall.com/login.html页面里,点击gitee图标,然后点击同意授权,切换到IDEA,点击Step Over F8执行完根据token换取用户信息的请求,可以看到返回的数据已经封装到GiteeTokenResponseVo类里了。

image-20220806212041987
image-20220806212041987
2、编写giteeLogin接口

gulimall-common模块的com.atguigu.common.to包里新建Oauth2GiteeLoginTo类,用于传递获取到的token数据

package com.atguigu.common.to;

import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/6
 * @Description:
 */
@Data
public class Oauth2GiteeLoginTo {
    private Long id;
    private String accessToken;
    private String tokenType;
    private long expiresIn;
    private String refreshToken;
    private String scope;
    private String avatarUrl;
    private Long createdAt;
    private String name;
}
image-20220807105520842
image-20220807105520842

gulimall-common模块的com.atguigu.common.exception.BizCodeException枚举类里添加通过gitee登录失败的枚举

/**
 * 通过gitee登录失败
 */
GITEE_LOGIN_EXCEPTION(15004,"通过gitee登录失败");
image-20220807112541519
image-20220807112541519

gulimall-member模块的com.atguigu.gulimall.member.controller.MemberController类里添加giteeLogin方法

@PostMapping("/giteeLogin")
public R giteeLogin(@RequestBody Oauth2GiteeLoginTo to){
    MemberEntity entity = memberService.giteeLogin(to);
    if (entity!=null) {
        return R.ok().put("data", entity);
    }else {
        return R.error(BizCodeException.GITEE_LOGIN_EXCEPTION);
    }
}
image-20220807112628986
image-20220807112628986

gulimall-member模块的com.atguigu.gulimall.member.service.MemberService接口里添加giteeLogin抽象方法

MemberEntity giteeLogin(Oauth2GiteeLoginTo to);
image-20220807112704285
image-20220807112704285

gulimall-member模块的com.atguigu.gulimall.member.service包里新建OauthGiteeService接口,在OauthGiteeService接口里添加getMemberId方法

package com.atguigu.gulimall.member.service;

import com.atguigu.gulimall.member.entity.OauthGiteeEntity;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
public interface OauthGiteeService extends IService<OauthGiteeEntity> {
    /**
     * 根据gitee的Id查询MemberId
     * @param id
     * @return
     */
    public Long getMemberId(Long id);
}
image-20220807095734455
image-20220807095734455

gulimall-member模块的com.atguigu.gulimall.member.dao包里添加OauthGiteeDao接口

package com.atguigu.gulimall.member.dao;

import com.atguigu.gulimall.member.entity.OauthGiteeEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
@Mapper
public interface OauthGiteeDao extends BaseMapper<OauthGiteeEntity> {
}
image-20220807100029698
image-20220807100029698

gulimall-member模块的com.atguigu.gulimall.member.service.impl包里新建OauthGiteeServiceImpl类,实现抽象的getMemberId方法

package com.atguigu.gulimall.member.service.impl;

import com.atguigu.gulimall.member.dao.OauthGiteeDao;
import com.atguigu.gulimall.member.entity.OauthGiteeEntity;
import com.atguigu.gulimall.member.service.OauthGiteeService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
@Service
public class OauthGiteeServiceImpl extends ServiceImpl<OauthGiteeDao, OauthGiteeEntity> implements OauthGiteeService {
    @Override
    public Long getMemberId(Long id) {
        LambdaQueryWrapper<OauthGiteeEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(OauthGiteeEntity::getId, id).select(OauthGiteeEntity::getMemberId);
        OauthGiteeEntity oauthGiteeEntity = this.baseMapper.selectOne(lambdaQueryWrapper);
        if (oauthGiteeEntity!=null){
            return oauthGiteeEntity.getMemberId();
        }
        return null;
    }
}
image-20220807100732410
image-20220807100732410
3、添加SourceType枚举

可以使用枚举来判断SourceType(用户来源),在gulimall-member模块的com.atguigu.gulimall.member包里新建constant文件夹,在constant文件夹里新建SourceType枚举类

package com.atguigu.gulimall.member.constant;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
public enum SourceType {
    /**
     *
     */
    UN_KNOW(0,"未知方式"),
    /**
     * 用户通过本系统注册
     */
    REGISTER(1,"注册"),
    /**
     * 通过Gitte授权登录
     */
    GITEE_LOGIN(2,"gitee登录"),
    /**
     * 通过github授权登录
     */
    GITHUB_LOGIN(3,"github登录");

    /**
     * 根据sourceType的值向数据库中存储
     */
    @EnumValue
    private int sourceType;
    /**
     * JSON通过该值序列化(可用用在可以用在get方法或者属性字段上,一个类只能用一个,序列化只包含该值)
     */
    @JsonValue
    private String description;

    SourceType(int sourceType,String description) {
        this.sourceType = sourceType;
        this.description = description;
    }

    public int getSourceType() {
        return sourceType;
    }

    public void setSourceType(int sourceType) {
        this.sourceType = sourceType;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}
image-20220807105110164
image-20220807105110164

可以使用如下方式配置扫描枚举(只需配置一种即可),使用方式一可以在指定包下使用枚举、使用方式二可以全局使用枚举,但还需配置mybatisPlusPropertiesCustomizer

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
  type-aliases-package: com.atguigu.gulimall.member.entity
  #方式一:仅配置指定包内的枚举类使用 MybatisEnumTypeHandler
  #支持统配符 * 或者 ; 分割
  type-enums-package: com.atguigu.gulimall.member.constant
  #方式二:全局 修改 mybatis 使用的 EnumTypeHandler(还需配置mybatisPlusPropertiesCustomizer)
  configuration:
    default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler
image-20220807151436234
image-20220807151436234

gulimall-member模块的com.atguigu.gulimall.member.entity.MemberEntity实体类里,修改sourceType字段的类型,改为SourceType枚举类型

/**
 * 用户来源
 */
private SourceType sourceType;
image-20220807105315893
image-20220807105315893

gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类里添加giteeLogin方法

/**
 * gitee登录
 * @param to
 */
@Transactional(rollbackFor = Exception.class)
@Override
public MemberEntity giteeLogin(Oauth2GiteeLoginTo to) {
    MemberEntity memberEntity = null;
    Long memberId = oauthGiteeService.getMemberId(to.getId());
    //没有注册
    if (memberId==null){
        memberEntity = new MemberEntity();
        memberEntity.setSourceType(SourceType.GITEE_LOGIN);
        memberEntity.setNickname(to.getName());
        memberEntity.setCreateTime(new Date());
        MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
        memberEntity.setLevelId(memberLevelEntity.getId());
        this.save(memberEntity);

        OauthGiteeEntity oauthGiteeEntity = new OauthGiteeEntity();
        BeanUtils.copyProperties(to,oauthGiteeEntity);
        oauthGiteeEntity.setMemberId(memberEntity.getId());
        oauthGiteeService.save(oauthGiteeEntity);
    }else {
        memberEntity = this.getById(memberId);
    }
    return memberEntity;
}
image-20220807113100785
image-20220807113100785

5、调用member登录服务

1、修改auth-server模块的gitee登录

gulimall-auth-server模块的com.atguigu.gulimall.auth.feign.MemberFeignService接口里添加giteeLogin方法

@PostMapping("/member/member/giteeLogin")
public R giteeLogin(@RequestBody Oauth2GiteeLoginTo to);
image-20220807115217403
image-20220807115217403

gulimall-common模块的com.atguigu.common.to包里新建MemberEntityTo类,复制gulimall-member模块的com.atguigu.gulimall.member.entity.MemberEntity实体类的字段,把sourceType的类型由SourceType改为String(SourceType类的description字段添加了@JsonValue注解,只会显示该字段)

package com.atguigu.common.to;

import java.util.Date;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
public class MemberEntityTo {
    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 String sourceType;
    /**
     * 积分
     */
    private Integer integration;
    /**
     * 成长值
     */
    private Integer growth;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 注册时间
     */
    private Date createTime;
}
image-20220807113843737
image-20220807113843737

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.service.impl.OAuth2ServiceImpl类的giteeRegister方法

@Override
public MemberEntityTo giteeRegister(String code) {
    GiteeCodeResponseVo giteeCodeResponseVo = getToken(code);
    if (giteeCodeResponseVo==null || !StringUtils.hasText(giteeCodeResponseVo.getAccessToken())){
        throw new RuntimeException("通过gitee获取用户token失败");
    }
    GiteeTokenResponseVo giteeTokenResponseVo = getGiteeUserInfo(giteeCodeResponseVo.getAccessToken());
    if (giteeTokenResponseVo==null || giteeTokenResponseVo.getId()==0){
        throw new RuntimeException("通过gitee获取用户基本失败");
    }
    Oauth2GiteeLoginTo to = new Oauth2GiteeLoginTo();
    BeanUtils.copyProperties(giteeCodeResponseVo,to);
    to.setId(giteeTokenResponseVo.getId());
    to.setAvatarUrl(giteeTokenResponseVo.getAvatar_url());
    to.setName(giteeTokenResponseVo.getName());
    try {
        R r = memberFeignService.giteeLogin(to);
        if (r.getCode()==0){
            Object data = r.get("data");
            String jsonString = JSON.toJSONString(data);
            return JSON.parseObject(jsonString, MemberEntityTo.class);
        }else {
            throw new RuntimeException("社交登录失败");
        }
    }catch (Exception e){
        e.printStackTrace();
        throw new RuntimeException("社交登录失败");
    }
}
image-20220807114326306
image-20220807114326306

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.service.OAuth2Service接口的giteeRegister方法的返回类型为MemberEntityTo

MemberEntityTo giteeRegister(String code);
image-20220807114355606
image-20220807114355606

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.OAuth2Controller类的giteeRegister方法

@GetMapping("/gitee/success")
public String giteeRegister(@RequestParam String code, HttpSession session){

    try {
        MemberEntityTo memberEntityTo = oAuth2Service.giteeRegister(code);
        session.setAttribute("data",memberEntityTo);
        return "redirect:http://gulimall.com";
    }catch (Exception e){
        log.error("第三方登录失败 :{}",e.getMessage());
        return "redirect:http://auth.gulimall.com/login.html";
    }
}
image-20220807114528553
image-20220807114528553
2、测试

gulimall-auth-server模块的com.atguigu.gulimall.auth.service.impl.OAuth2ServiceImpl类的以下代码行打上断点

R r = memberFeignService.giteeLogin(to);

ResponseEntity<GiteeCodeResponseVo> response = restTemplate.postForEntity(url,null,GiteeCodeResponseVo.class,map);

ResponseEntity<GiteeTokenResponseVo> response = restTemplate.getForEntity(url, GiteeTokenResponseVo.class, accessToken);

gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的giteeLogin方法的this.save(memberEntity);这一行打上断点,以debug方式运行GulimallAuthServerApplication服务和GulimallMemberApplication服务,在http://auth.gulimall.com/login.html页面里,点击gitee图标,然后点击同意授权,切换到IDEA,此时就来到了gulimall-auth-server服务的com.atguigu.gulimall.auth.service.impl.OAuth2ServiceImpl

点击Step Over F8执完ResponseEntity<GiteeCodeResponseVo> response = restTemplate.postForEntity(url,null,GiteeCodeResponseVo.class,map);这一行,可以看到返回的数据已成功封装到GiteeCodeResponseVo实体类里了

image-20220807150729814
image-20220807150729814

点击Resume Program F9执行到下一处断点,点击Step Over F8执完ResponseEntity<GiteeTokenResponseVo> response = restTemplate.getForEntity(url, GiteeTokenResponseVo.class, accessToken);这一行,可以看到返回的数据已成功封装到GiteeTokenResponseVo实体类里了

image-20220807150824635
image-20220807150824635

点击Resume Program F9执行到下一处断点,点击Step Over F8执完R r = memberFeignService.giteeLogin(to);这一行,可以看到发送请求的数据已成功封装到Oauth2GiteeLoginTo实体类里了

image-20220807150853771
image-20220807150853771

点击Step Over F8就进入到了gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的giteeLogin方法,可以看到此时已正确封装了MemberEntity实体类数据

image-20220807151002732
image-20220807151002732

再点击Step Over F8就报错了,说的是source_type列的值不能为GITEE_LOGIN

### SQL: INSERT INTO ums_member  ( create_time, source_type, level_id,  nickname )  VALUES  ( ?, ?, ?,  ? )
### Cause: java.sql.SQLException: Incorrect integer value: 'GITEE_LOGIN' for column 'source_type' at row 1
; uncategorized SQLException; SQL state [HY000]; error code [1366]; Incorrect integer value: 'GITEE_LOGIN' for column 'source_type' at row 1; nested exception is java.sql.SQLException: Incorrect integer value: 'GITEE_LOGIN' for column 'source_type' at row 1] with root cause
image-20220807151224876
image-20220807151224876
3、修改代码再次测试
方法一

注释掉gulimall-member模块的src/main/resources/application.yml文件中为使用枚举而使用方式二的配置,只保留方式一的配置

image-20220807152047244
image-20220807152047244

重启GulimallMemberApplication服务,在http://auth.gulimall.com/login.html页面里,点击gitee图标,然后点击同意授权,切换到IDEA,执行完gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的giteeLogin方法的this.save(memberEntity);这一行,可以看到引进保存完数据了,此时返回的id1

image-20220807151923689
image-20220807151923689

点击Resume Program F9本应该执行完测试,但却停在了oauthGiteeService.save(oauthGiteeEntity);这一行,查看oauthGiteeEntity实体类的数据,可以看到也正常封装了

image-20220807153319902
image-20220807153319902

再点击Step Over F8就来到捕获异常的类了,证明保存oauthGiteeEntity实体类的数据出错了

image-20220807153348725
image-20220807153348725

点击Resume Program F9执行完异常处理类,可以看到控制台报了如下错误:id没有一个默认值

### SQL: INSERT INTO oauth_gitee  ( expires_in, created_at, avatar_url, scope, created_time, token_type, access_token, refresh_token, member_id )  VALUES  ( ?, ?, ?, ?, ?, ?, ?, ?, ? )
### Cause: java.sql.SQLException: Field 'id' doesn't have a default value
; Field 'id' doesn't have a default value; nested exception is java.sql.SQLException: Field 'id' doesn't have a default value] with root cause

java.sql.SQLException: Field 'id' doesn't have a default value
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.17.jar:8.0.17]
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.17.jar:8.0.17]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.17.jar:8.0.17]
image-20220807153427809
image-20220807153427809
方法二

注释掉gulimall-member模块的src/main/resources/application.yml文件中为使用枚举而使用方式一的配置,只保留方式二的配置

image-20220807152240568
image-20220807152240568

可以看到如果不配置MybatisPlusPropertiesCustomizer,执行完gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的giteeLogin方法的this.save(memberEntity);这一行还是会报错

image-20220807152244248
image-20220807152244248

点击Resume Program F9执行完异常处理类,可以看到控制台还是报了最开始的错误:source_type列的值不能为GITEE_LOGIN

image-20220807152316550
image-20220807152316550

gulimall-member模块的src/main/resources/application.yml配置文件的方式二添加注释

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
  type-aliases-package: com.atguigu.gulimall.member.entity
  #方式一:仅配置指定包内的枚举类使用 MybatisEnumTypeHandler
  # 支持统配符 * 或者 ; 分割
  #type-enums-package: com.atguigu.gulimall.member.constant
  #方式二:全局 修改 mybatis 使用的 EnumTypeHandler(还需配置mybatisPlusPropertiesCustomizer)
  configuration:
    default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler
image-20220807152820815
image-20220807152820815

gulimall-member模块的com.atguigu.gulimall.member.config包里新建MybatisPlusAutoConfiguration类,用于添加全局枚举配置

package com.atguigu.gulimall.member.config;

import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */

@Configuration
public class MybatisPlusAutoConfiguration {

    @Bean
    public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
        return properties -> {
            GlobalConfig globalConfig = properties.getGlobalConfig();
            globalConfig.setBanner(false);
            MybatisConfiguration configuration = new MybatisConfiguration();
            configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
            properties.setConfiguration(configuration);
        };
    }
}
image-20220807152813896
image-20220807152813896

再次执行完gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的giteeLogin方法的this.save(memberEntity);这一行,这次就可以看到成功保存数据了,此时也返回了刚刚保存返回的id

image-20220807152650962
image-20220807152650962
4、修改代码再次测试

gulimall-member模块里,修改com.atguigu.gulimall.member.entity.OauthGiteeEntity类的id字段的@TableId注解,添加type = IdType.INPUT指明这里的id是自己程序输入的,而非自动递增

@TableId(value = "id",type = IdType.INPUT)
private long id;
image-20220807154012178
image-20220807154012178

然后突然发现忘记设置头像了,可以看到gulimall_ums数据库的ums_member表里的header字段为用户的头像

image-20220807154101671
image-20220807154101671

gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的giteeLogin方法的this.save(memberEntity);这一行的上面添加如下代码,用于设置头像

memberEntity.setHeader(to.getAvatarUrl());
image-20220807154155577
image-20220807154155577

重启GulimallMemberApplication服务,在http://auth.gulimall.com/login.html页面里,点击gitee图标,然后点击同意授权,切换到IDEA,执行完gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的giteeLogin方法的oauthGiteeService.save(oauthGiteeEntity);这一行,可以看到此时memberEntity对象和oauthGiteeEntity对象的数据都封装正确

image-20220807154700666
image-20220807154700666

点击Step Over F8执行完oauthGiteeService.save(oauthGiteeEntity);这一行,此时就不报错了

image-20220807154723745
image-20220807154723745

此时切换到GulimallAuthServerApplication服务的控制台可以看到报了读取超时的异常,在调试的过程中很容易出现该异常,这是正常的

detailMessage = “Read timed out executing POST http://gulimall-member/member/member/giteeLogin”
image-20220807154832750
image-20220807154832750

打开Navicat软件,可以看到gulimall_ums数据库的ums_member表里已经有刚刚授权的gitee用户信息了

image-20220807154956105
image-20220807154956105

切换到gulimall_ums数据库的oauth_gitee表,表里面已经有刚刚授权的用户信息了

image-20220807154952161
image-20220807154952161
5、整体测试

取消gulimall-auth-server模块的com.atguigu.gulimall.auth.service.impl.OAuth2ServiceImpl类和gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类上的所有断点,重新运行GulimallAuthServerApplication服务和GulimallMemberApplication服务,可以看到使用gitee登录正常

http://auth.gulimall.com/login.html

https://gitee.com/login?redirect_to_url=https%3A%2F%2Fgitee.com%2Foauth%2Fauthorize%3Fclient_id%3D065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d%26redirect_uri%3Dhttp%3A%2F%2Fgulimall.com%2Foauth2.0%2Fgitee%2Fsuccess%26response_type%3Dcode

http://gulimall.com/
GIF 2022-8-7 15-56-48
GIF 2022-8-7 15-56-48
6、其他问题

由于回调为http://gulimall.com/oauth2.0/gitee/success,转给了gulimall-auth-server服务,所以回调的http://gulimall.com会有jsonId,如果回调为http://auth.gulimall.com/oauth2.0/gitee/success则只有http://auth.gulimall.com会有jsonId

image-20220807160743109
image-20220807160743109

回调为http://gulimall.com/oauth2.0/gitee/success时,http://auth.gulimall.com域名下没有以jsonIdkeycookie

image-20220807160024967
image-20220807160024967

回调为http://gulimall.com/oauth2.0/gitee/success时,http://gulimall.com域名下有以jsonIdkeycookie

image-20220807160028148
image-20220807160028148

虽然在gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.OAuth2Controller类的giteeRegister方法里可以设置cookie的作用范围,但默认只能在本域(不能为其父域名下)

@GetMapping("/gitee/success")
public String giteeRegister(@RequestParam String code, HttpServletResponse response){

    try {
        MemberEntityTo memberEntityTo = oAuth2Service.giteeRegister(code);
        //session.setAttribute("data",memberEntityTo);
        Cookie cookie = new Cookie("data",memberEntityTo.toString());
        cookie.setPath("/");
        response.addCookie(cookie);
        return "redirect:http://gulimall.com";
    }catch (Exception e){
        log.error("第三方登录失败 :{}",e.getMessage());
        return "redirect:http://auth.gulimall.com/login.html";
    }
}
image-20220807162107754
image-20220807162107754

如果回调为http://auth.gulimall.com/oauth2.0/gitee/success们可以修改jsonIdDoman作用域,将其修改为gulimall.com,但是这样http://auth.gulimall.com域名下又没有jsonId

image-20220807162042047
image-20220807162042047

不推荐这样做,所以还是改回来为妙

@GetMapping("/gitee/success")
public String giteeRegister(@RequestParam String code, HttpSession session){

    try {
        MemberEntityTo memberEntityTo = oAuth2Service.giteeRegister(code);
        session.setAttribute("data",memberEntityTo);
        //Cookie cookie = new Cookie("data",memberEntityTo.toString());
        //cookie.setPath("/");
        //response.addCookie(cookie);
        return "redirect:http://gulimall.com";
    }catch (Exception e){
        log.error("第三方登录失败 :{}",e.getMessage());
        return "redirect:http://auth.gulimall.com/login.html";
    }
}
image-20220807165342333
image-20220807165342333

6、gitee参考文档

参考文档: https://gitee.com/api/v5/oauth_doc#/

API 使用条款

  • OSCHINA 用户是资源的拥有者,需尊重和保护用户的权益。
  • 不能在应用中使用 OSCHINA 的名称。
  • 未经用户允许,不准爬取或存储用户的资源。
  • 禁止滥用 API,请求频率过快将导致请求终止。

OAuth2 认证基本流程

img
img

OAuth2 获取 AccessToken 认证步骤

  1. 授权码模式
  • 应用通过 浏览器 或 Webview 将用户引导到码云三方认证页面上( GET请求

    https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code
    
  • 用户对应用进行授权 注意: 如果之前已经授权过的需要跳过授权页面,需要在上面第一步的 URL 加上 scope 参数,且 scope 的值需要和用户上次授权的勾选的一致。如用户在上次授权了user_info、projects以及pull_requests。则步骤A 中 GET 请求应为:

    https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=user_info%20projects%20pull_requests
    
  • 码云认证服务器通过回调地址{redirect_uri}将 用户授权码 传递给 应用服务器 或者直接在 Webview 中跳转到携带 用户授权码的回调地址上,Webview 直接获取code即可({redirect_uri}?code=abc&state=xyz)

  • 应用服务器 或 Webview 使用 access_token API 向 码云认证服务器发送post请求传入 用户授权码 以及 回调地址( POST请求注:请求过程建议将 client_secret 放在 Body 中传值,以保证数据安全。``

    https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}
    
  • 码云认证服务器返回 access_token 应用通过 access_token 访问 Open API 使用用户数据。

  • 当 access_token 过期后(有效期为一天),你可以通过以下 refresh_token 方式重新获取 access_token( POST请求

    https://gitee.com/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}
    
  • 注意:如果获取 access_token 返回 403,可能是没有设置User-Agent的原因。 详见:获取Token时服务端响应状态403是什么情况open in new window

  1. 密码模式
  • 用户向客户端提供邮箱地址和密码。客户端将邮箱地址和密码发给码云认证服务器,并向码云认证服务器请求令牌。( POST请求。Content-Type: application/x-www-form-urlencoded

    curl -X POST --data-urlencode "grant_type=password" --data-urlencode "username={email}" --data-urlencode "password={password}" --data-urlencode "client_id={client_id}" --data-urlencode "client_secret={client_secret}" --data-urlencode "scope=projects user_info issues notes" https://gitee.com/oauth/token
    
  • scope表示权限范围,有以下选项,请求时使用空格隔开

    user_info projects pull_requests issues notes keys hook groups gists enterprises
    
  • 码云认证服务器返回 access_token 应用通过 access_token 访问 Open API 使用用户数据。

创建应用流程

  • 修改资料open in new window -> 第三方应用open in new window,创建要接入码云的应用。 img
  • 填写应用相关信息,勾选应用所需要的权限。其中: 回调地址是用户授权后,码云回调到应用,并且回传授权码的地址。 img
  • 创建成功后,会生成 Cliend IDClient Secret。他们将会在上述OAuth2 认证基本流程用到。 img

5.7.5、Json格式化问题

如何将使用蛇形命名法json数据与使用驼峰命名法java实体类进行互转?下面提供三种方式

参考地址: PropertyNamingStrategy_cn · alibaba/fastjson Wiki (github.com)open in new window

1、方式一(官方做法)

1. 简介

fastjson缺省使用CamelCase,在1.2.15版本之后,fastjson支持配置PropertyNamingStrategy,支持如下四种:

namedemo
CamelCasepersionId
PascalCasePersonId
SnakeCaseperson_id
KebabCaseperson-id
2. Serialization and Parser
SerializeConfig config = new SerializeConfig(); // 生产环境中,config要做singleton处理,要不然会存在性能问题
config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;

Model model = new Model();
model.personId = 1001;
String text = JSON.toJSONString(model, config);
Assert.assertEquals("{\"person_id\":1001}", text);

ParserConfig parserConfig = new ParserConfig(); // 生产环境中,parserConfig要做singleton处理,要不然会存在性能问题
parserConfig.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
Model model2 = JSON.parseObject(text, Model.class, parserConfig);
Assert.assertEquals(model.personId, model2.personId);
3. 修改全局缺省的命名策略
SerializeConfig.getGlobalInstance()
               .propertyNamingStrategy = PropertyNamingStrategy.PascalCase;
4. 基于JSONType配置PropertyNamingStrategy
   public void test_for_issue() throws Exception {
        Model model = new Model();
        model.userId = 1001;
        model.userName = "test";
        String text = JSON.toJSONString(model);
        assertEquals("{\"userName\":\"test\",\"user_id\":1001}", text);

        Model model2 = JSON.parseObject(text, Model.class);

        assertEquals(1001, model2.userId);
        assertEquals("test", model2.userName);
    }

    /**
     * 当某个字段有JSONField注解,JSONField中name属性不存在,并且类上有属性转换策略,
     * json属性名也要用类上的属性名转换策略为为准
     * @throws Exception
     */
    public void test_when_JSONField_have_not_name_attr() throws Exception {
        ModelTwo modelTwo = new ModelTwo();
        modelTwo.userId = 1001;
        modelTwo.userName = "test";
        String text = JSON.toJSONString(modelTwo);
        assertEquals("{\"userName\":\"test\",\"user_id\":\"1001\"}", text);

        Model model2 = JSON.parseObject(text, Model.class);

        assertEquals(1001, model2.userId);
        assertEquals("test", model2.userName);
    }

    @JSONType(naming = PropertyNamingStrategy.SnakeCase)
    public class Model {
        private int userId;
        @JSONField(name = "userName")
        private String userName;

        public int getUserId() {
            return userId;
        }

        public void setUserId(int userId) {
            this.userId = userId;
        }

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }
    }

    @JSONType(naming = PropertyNamingStrategy.SnakeCase)
    public class ModelTwo {
        /**
         * 此字段准备序列化为字符串类型
         */
        @JSONField(serializeUsing = StringSerializer.class)
        private int userId;
        @JSONField(name = "userName")
        private String userName;

        public int getUserId() {
            return userId;
        }

        public void setUserId(int userId) {
            this.userId = userId;
        }

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }
    }

   public class StringSerializer implements ObjectSerializer {

      public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
        serializer.write(String.valueOf(object));
    }

}

2、方式二(推荐)

可以使用如下方法直接在当前实体使用,不需要配置(亲测可用)

@Data
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class GiteeCodeResponseVo {
    private String accessToken;
    private String tokenType;
    private long expiresIn;
    private String refreshToken;
    private String scope;
    private long createdAt;
}

3、方式三(其他做法)

可以参考如下链接,亲测可用,就是有点麻烦

Java开发里遇到的奇奇怪怪的需求---JSON键值驼峰转下划线的实现 - 知乎 (zhihu.com)open in new window

1、导入依赖

首先导入Pom依赖,Jackson的三个Jar包和FastJson(可不要,就是习惯了用而已):

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.12.0-rc1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.12.0-rc1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.0-rc1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.74</version>
</dependency>
2、需求

我们模拟用户注册的场景,提交一系列基本信息,如:

{
  "user_name": "jjn",
  "org_id": "01",
  "org_name": "Class1",
  "age": 0,
  "email": "[email protected]",
  "create_time": "2020-11-01 09:11:03"
}

通过接口处理之后生成的数据如下:

{
    "code": 200,
    "message": "成功",
    "info": {
        "update_time": "2020-11-01 22:06:45",
        "create_time": "2020-11-01 09:11:03",
        "registerTime": "2020-11-01 22:06:45",
        "user_name": "jjn",
        "org_id": "01",
        "registered": true,
        "org_name": "Class1",
        "age": 0,
        "email": "[email protected]"
    }
}

那么是如何实现的呢?其实主要就是Jackson Json库的几个注解,来看看吧~

  • @JsonProperty
/**
 * Marker annotation that can be used to define a non-static
 * method as a "setter" or "getter" for a logical property
 * (depending on its signature),
 * or non-static object field to be used (serialized, deserialized) as
 * a logical property.
 *<p>
 * Default value ("") indicates that the field name is used
 * as the property name without any modifications, but it
 * can be specified to non-empty value to specify different
 * name. Property name refers to name used externally, as
 * the field name in JSON objects.
 *<p>
 * Starting with Jackson 2.6 this annotation may also be
 * used to change serialization of <code>Enum</code> like so:
 *<pre>
public enum MyEnum {
    {@literal @JsonProperty}("theFirstValue") THE_FIRST_VALUE,
    {@literal @JsonProperty}("another_value") ANOTHER_VALUE;
}
</pre>
 * as an alternative to using {@link JsonValue} annotation.
 */

注释里面写的很清楚了,指定了value的值之后,在生成JSON的时候,会按value的值来。

  • @JsonAlias
/**
 * Annotation that can be used to define one or more alternative names for
 * a property, accepted during deserialization as alternative to the official
 * name. Alias information is also exposed during POJO introspection, but has
 * no effect during serialization where primary name is always used.
 *<p>
 * Examples:
 *<pre>
 *public class Info {
 *  &#64;JsonAlias({ "n", "Name" })
 *  public String name;
 *}
 *</pre>
 *
 * @since 2.9
 */
3、实体类

使用@JsonProperty(value = "user_name")注解指定了value的值之后,在生成JSON的时候,会按value的值来

alias的意思是别名,value值指定了之后,可以接受多种可能的赋值。使用@JsonAlias(value = {"user_name", "userName"})注解后,jsonkeyuser_nameuserName时都能封装到javauserName字段

所以最后我们的用户实体类就会写成这样:

package com.zhihu.jjn.demoproject.entity;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author Jiang Jining
 * @date 2020/11/1 10:17
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
    @JsonProperty(value = "user_name")
    @JsonAlias(value = {"user_name", "userName"})
    private String userName;

    @JsonProperty(value = "org_id")
    @JsonAlias(value = {"org_id", "orgId"})
    private String orgId;

    @JsonProperty(value = "org_name")
    @JsonAlias(value = {"org_name", "orgName"})
    private String orgName;

    private Integer age;

    private String email;

    @JsonProperty(value = "create_time")
    @JsonAlias(value = {"create_time", "createTime"})
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    @JsonProperty(value = "update_time")
    @JsonAlias(value = {"update_time", "updateTime"})
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;

    private Boolean registered;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date registerTime;
}

返回实体类:

package com.zhihu.jjn.demoproject.entity;

import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Jiang Jining
 * @date 2020/11/1 11:08
 */
@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class Response {
    private Integer code;
    private String message;
    private JSONObject info;

    public static Response success(JSONObject data) {
        return Response.builder().code(200).message("成功").info(data).build();
    }

    public static Response error(JSONObject data) {
        return Response.builder().code(500).message("失败").info(data).build();
    }
}
4、service

service接口:

package com.zhihu.jjn.demoproject.service;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zhihu.jjn.demoproject.entity.User;

/**
 * @author Jiang Jining
 * @date 2020/11/1 11:12
 */
public interface UserService {
    /**
     * Register user demo.
     *
     * @param user user param from front end
     * @return json object
     */
    JSONObject registerUser(User user) throws JsonProcessingException;
}

service接口实现:

package com.zhihu.jjn.demoproject.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhihu.jjn.demoproject.entity.User;
import com.zhihu.jjn.demoproject.service.UserService;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * @author Jiang Jining
 * @date 2020/11/1 11:12
 */
@Service
public class UserServiceImpl implements UserService {
    @Override
    public JSONObject registerUser(User user) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        user.setRegistered(true);
        user.setRegisterTime(new Date());
        user.setUpdateTime(new Date());
        String string = objectMapper.writeValueAsString(user);
        return JSON.parseObject(string);
    }
}
5、Controller

最后的Controller实现:

package com.zhihu.jjn.demoproject.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.zhihu.jjn.demoproject.entity.Response;
import com.zhihu.jjn.demoproject.entity.User;
import com.zhihu.jjn.demoproject.service.UserService;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author Jiang Jining
 * @date 2020/11/1 11:06
 */
@RestController
@CrossOrigin
public class UserController {

    @Resource
    private UserService userService;

    @PostMapping(value = "/api/user/register")
    public Response userRegisterController(@RequestBody User user) throws JsonProcessingException {
        return Response.success(userService.registerUser(user));
    }
}

3、方式四(亲测不可用)

fastjson中的@JSONField注解(亲测不可用,也许是我技术不行吧)

链接: fastjson中的@JSONField注解_y_bccl27的博客-CSDN博客_fastjson jsonfield注解open in new window

使用fastjsonopen in new window之前需先引入下述依赖,当前版本为1.2.75

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.75</version>
</dependency>

JSONField中的name属性用来指定JSON串中key的名称

1.@JSONField作用在属性上
import com.alibaba.fastjson.annotation.JSONField;
 
public class Person {
 
    @JSONField(name = "userName")
    private String name;
 
    @JSONField(name = "AGE")
    private String age;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getAge() {
        return age;
    }
 
    public void setAge(String age) {
        this.age = age;
    }
}

序列化测试:

import com.alibaba.fastjson.JSONObject;
import com.bc.model.Person;
 
import java.util.Date;
 
public class Demo {
 
    public static void main(String[] args){
        Person person=new Person();
        person.setName("张三");
        person.setAge("20");
        person.setDate(new Date());
        String jsonStr = JSONObject.toJSONString(person);
        System.out.println(jsonStr);
    }
}

执行上述代码,其输出结果为:

{"AGE":"20","userName":"张三"}

反序列化测试:

import com.alibaba.fastjson.JSONObject;
import com.bc.model.Person;
 
public class Demo {
 
    public static void main(String[] args){
        String jsonStr="{\"AGE\":\"20\",\"userName\":\"张三\"}";
        Person person = JSONObject.toJavaObject(JSONObject.parseObject(jsonStr), Person.class);
        System.out.println("json to bean:" + person.getName());
    }
}

执行上述代码,其输出结果为:

json to bean:张三

@JSONField作用在Field时,其name不仅定义了输出的名称,同时也定义了输入key的名称

2.@JSONField作用在方法上
import com.alibaba.fastjson.annotation.JSONField;
 
public class Person {
 
    private String name;
 
    private String age;
 
    // 针对的是序列化操作
    @JSONField(name = "userName")
    public String getName() {
        return name;
    }
 
    // 针对的是反序列化操作
    @JSONField(name = "userName")
    public void setName(String name) {
        this.name = name;
    }
 
    @JSONField(name = "AGE")
    public String getAge() {
        return age;
    }
 
    @JSONField(name = "AGE")
    public void setAge(String age) {
        this.age = age;
    }
}
import com.alibaba.fastjson.JSONObject;
import com.bc.model.Person;
 
public class Demo {
 
    public static void main(String[] args){
        // 序列化
        Person person=new Person();
        person.setName("张三");
        person.setAge("20");
        String jsonStr = JSONObject.toJSONString(person);
        System.out.println(jsonStr);
 
        // 反序列化
        // String jsonStr="{\"AGE\":\"20\",\"userName\":\"张三\"}";
        // Person person = JSONObject.toJavaObject(JSONObject.parseObject(jsonStr), Person.class);
        // System.out.println("json to bean:" + person.getName());
    }
}

执行上述代码,其输出结果为:

{"AGE":"20","userName":"张三"}

fastjson在进行操作时,是根据getter和setter的方法进行的,并不是依据Field进行

3.@JSONField中的format属性

format属性用于规定序列化open in new window和反序列化时成员变量的日期格式

import com.alibaba.fastjson.annotation.JSONField;
import java.util.Date;
 
public class Person {
 
    private String name;
 
    private String age;
 
    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Date date;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getAge() {
        return age;
    }
 
    public void setAge(String age) {
        this.age = age;
    }
 
    public Date getDate() {
        return date;
    }
 
    public void setDate(Date date) {
        this.date = date;
    }
}
import com.alibaba.fastjson.JSONObject;
import com.bc.model.Person;
 
import java.util.Date;
 
public class Demo {
 
    public static void main(String[] args){
        // 序列化
        Person person=new Person();
        person.setName("张三");
        person.setAge("20");
        person.setDate(new Date());
        String jsonStr = JSONObject.toJSONString(person);
        System.out.println(jsonStr);
    }
}

执行上述代码,其输出结果为:

{"age":"20","date":"2022-06-21 09:52:37","name":"张三"}
4.@JSONField中的ordinal属性

ordinal属性用于规定序列化时字段的顺序

import com.alibaba.fastjson.annotation.JSONField;
 
public class Person {
 
    @JSONField(ordinal = 1)
    private String name;
 
    @JSONField(ordinal = 2)
    private String age;
 
    @JSONField(ordinal = 3)
    private String sex;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getAge() {
        return age;
    }
 
    public void setAge(String age) {
        this.age = age;
    }
 
    public String getSex() {
        return sex;
    }
 
    public void setSex(String sex) {
        this.sex = sex;
    }
}
import com.alibaba.fastjson.JSONObject;
import com.bc.model.Person;
 
public class Demo {
 
    public static void main(String[] args){
        // 序列化
        Person person=new Person();
        person.setName("张三");
        person.setAge("20");
        person.setSex("男");
        String jsonStr = JSONObject.toJSONString(person);
        System.out.println(jsonStr);
    }
}

执行上述代码,其输出结果为:

{"name":"张三","age":"20","sex":"男"}
5.@JSONField中的serialize属性

serialize属性其取值为false时表示该字段不进行序列化,就是转化为json字符串时不生成该字段

import com.alibaba.fastjson.annotation.JSONField;
import java.util.Date;
 
public class Person {
 
    private String name;
 
    private String age;
 
    // 指定字段不进行序列化,就是转化为json字符串时不生成该字段
    @JSONField(serialize=false)
    private Date date;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getAge() {
        return age;
    }
 
    public void setAge(String age) {
        this.age = age;
    }
 
    public Date getDate() {
        return date;
    }
 
    public void setDate(Date date) {
        this.date = date;
    }
}
import com.alibaba.fastjson.JSONObject;
import com.bc.model.Person;
 
import java.util.Date;
 
public class Demo {
 
    public static void main(String[] args){
        // 序列化
        Person person=new Person();
        person.setName("张三");
        person.setAge("20");
        person.setDate(new Date());
        String jsonStr = JSONObject.toJSONString(person);
        System.out.println(jsonStr);
    }
}

执行上述代码,其输出结果为:

{"age":"20","name":"张三"}

5.7.6、Spring Session

参考文档: https://docs.spring.io/spring-session/reference/guides/boot-redis.html

1、Session共享问题

原生的session方案不能解决父子域名共享session的问题

image-20220807162338249
image-20220807162338249

原生的session方案,同一个服务部署在不同的机器上,这些机器不能共享用户的session,用户在一个机器上登陆后,再次访问其他请求有可能分配到别的机器上,由于别的机器没有该用户的session,因此再次要求用户登录。不同服务之间session更无法共享。

image-20220807162401133
image-20220807162401133
1、session复制(不推荐)

可以让服务之间同步保存session,每台机器都保存一份相同的session,这样用户不管访问哪台机器都能得到用户session

image-20220807162511571
image-20220807162511571

优点:web-server(Tomcat)原生支持,只需要修改配置文件 缺点:session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力 任意一台web-server保存的数据都是所有web- server的session总和,受到内存限制无法水平扩展更多的web-server 大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。

2、客户端存储(不推荐)

让用户的浏览器保存session,用户每个请求都带上session,这样用户访问任何机器都能得到session,非常节约服务器资源,但保存在用户浏览器里非常的不安全,非常不推荐。

image-20220807162614238
image-20220807162614238

优点:服务器不需存储session,用户保存自己的session信息到cookie中。节省服务端资源

缺点:都是缺点,这只是一种思路。 具体如下: 每次http请求,携带用户在cookie中的完整信息, 浪费网络带宽 session数据放在cookie中,cookie有长度限制4K,不能保存大量信息 session数据放在cookie中,存在泄漏、篡改、窃取等安全隐患 这种方式不会使用。

3、hash一致性(推荐)

可以使用OSI 7层网络模型里的第4传输层(分析IP层(第3层)及TCP/UDP层(第四层),实现四层流量负载均衡)。根据用户的ip,然后做hash操作,使该用户每次请求都能负载均衡到相同的机器上

image-20220807162823059
image-20220807162823059

还可以使用OSI 7层网络模型里的第7应用层,通过http请求里存放的业务字段来做hash操作,使该用户每次请求都能负载均衡到相同的机器上(通过分析应用层的信息,如HTTP协议URI或Cookie信息)

image-20220807162831210
image-20220807162831210

优点:只需要改nginx配置,不需要修改应用代码 负载均衡,只要hash属性的值分布是均匀的,多台web-server的负载是均衡的 可以支持web-server水平扩展(session同步法是不行 的,受内存限制)

缺点:session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录 如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session 但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用

ip_hash

ip_hash是根据用户请求过来的ip,然后映射成hash值,然后分配到一个特定的服务器里面; 使用ip_hash这种负载均衡以后,可以保证用户的每一次会话都只会发送到同一台特定的Tomcat里面,它的session不会跨到其他的tomcat里面去的;

首先通过将ip地址映射成一个hash值,然后将hash值对Tomcat的数量3取模,得到Tomcat的索引0、1、2; 比如:5%3=2,则把这个请求发送到Tomcat3服务器,以此类推; 这样一来,只要用户的IP不发生改变,当前用户的会话就能够一直保持; nginx的ip_hash算法是取ip地址的前三段数字进行hash映射,如果只有最后一段不一样,也会发送到同一个Tomcat里面

在nginx里面使用ip_hash,直接添加ip_hash关键字即可,后续同一ip的访问将只会请求同一个服务器。

upstream tomcats {
    ip_hash;
    server 192.168.121.166:8080 weight=1 max_conns=2;
    server 192.168.121.167:8080 down;
    server 192.168.121.167:8088 weight=5 max_conns=2;
}

[root@Nginx ~]# cat <<END >> /usr/local/nginx/conf/nginx.conf
stream {
  upstream test_mysql {
    hash $remote_addr consistent;       # 通过配置一致性 hash 来防止调度异常
    server 192.168.1.1:3306 weight=5 max_fails=3 fail_timeout=30s;
  }
  server {
    listen 10086 so_keepalive=on;       # 开启 TCP 存活探测
    proxy_connect_timeout 10s;          # 连接超时时间
    proxy_timeout 300s;             # 端口保持时间
    proxy_pass test_mysql;
  }
}
END
[root@Nginx ~]# nginx -s reload

注意事项:

一旦使用了ip_hash,当我们需要移除一台服务器的时候,不能直接删除这个配置项,而是需要在这台服务器配置后面加上关键字down,表示不可用;因为如果直接移除配置项,会导致hash算法发生更改,后续所有的请求都会发生混乱;

  • 层是OSI 7层网络模型,OSI 模型是从上往下的,越底层越接近硬件,越往上越接近软件,这七层模型分别是物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。4层是指传输层的 tcp / udp、7层是指应用层,通常是http

  • 4层用的是NAT技术。NAT英文全称是“Network Address Translation”,中文意思是“网络地址转换”,请求进来的时候,nginx修改数据包里面的目标IP、源IP和源端口,然后把数据包发向目标服务器,服务器处理完成后,nginx再做一次修改,返回给请求的客户端。

  • 7层代理:需要读取并解析http请求内容,然后根据具体内容(url, 参数, cookie, 请求头)然后转发到相应的服务器。 转发的过程是:建立和目标机器的连接,然后转发请求,收到响应数据在转发给请求客户端。

  • 性能: 理论上4层要比7层快,因为7层代理需要解析数据包的具体内容,需要消耗额外的cpu。但nginx具体强大的网络并发处理能力, 对于一些慢连接,nginx可以先将网络请求数据缓冲完了一次性转发给上游server,这样对于上游网络并发处理能力弱的服务器(比如tomcat),这样对tomcat来说就是慢连接变成快连接(nginx到tomcat基本上都是可靠内网),从而节省网络数据缓冲时间,提供并发性能。

  • 灵活性: 由于4层代理用的是NAT,所以nginx不知道请求的具体内容,所以nginx啥也干不了。 用7层代理,可以根据请求内容(url,参数,cookie,请求头)做很多事情,比如: a.动态代理:不同的url转发到不同服务器。 b.风控:屏蔽外网IP请求某些敏感url;根据参数屏蔽某些刷单用户。 c.审计:在nginx层记录请求日志 …

4、统一存储(推荐)

可以将session统一存储在数据库中,每个服务都访问同样的数据库,本次采用的就是这种方式,采用的时非关系型数据库redis

image-20220807162955043
image-20220807162955043

优点:没有安全隐患

可以水平扩展,数据库/缓存水平切分即可

web-server重启或者扩容都不会有 session丢失

缺点:增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替 换为从Redis查数据的方式。redis获取数据比内存慢很多

上面缺点可以用SpringSession完美解决

不同服务,子域session共享问题

jsessionid这个cookie默认是当前系统域名的。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案;

修改cookiedomain.gulimall.com本域名及其子域名,然后所有的seesion都统一存储到redis

image-20220807163133974
image-20220807163133974

2、整合SpringSession

1、引入SpringSession

首先在gulimall-auth-server模块的pom.xml文件里引入spring-session-data-redisjar

<!--引入SpringSession-->
<dependency>
   <groupId>org.springframework.session</groupId>
   <artifactId>spring-session-data-redis</artifactId>
</dependency>
image-20220806190404319
image-20220806190404319

src/main/resources/application.properties配置文件中

2、添加配置
# Session store type.
spring.session.store-type=redis
# Session timeout. If a duration suffix is not specified, seconds is used.
server.servlet.session.timeout=30m
# Sessions flush mode.
spring.session.redis.flush-mode=on_save
# Namespace for keys used to store sessions.
spring.session.redis.namespace=spring:session

配置redis的连接信息

spring.redis.host=192.168.56.10
spring.redis.port=6379
#spring.redis.password=root
image-20221214090946947
image-20221214090946947

启动类上添加该注解

@EnableRedisHttpSession
image-20220806193029395
image-20220806193029395
3、测试

点击 http://auth.gulimall.com/login.html 页面的gitee,报了如下错误

GIF 2022-8-7 16-56-22
GIF 2022-8-7 16-56-22

由于要向reids里保存数据,而默认又使用jdk进行序列化,因此需要把MemberEntityTo继承Serializable接口,使其序列化为二进制串才可以

org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.atguigu.common.to.MemberEntityTo]
image-20220807165722818
image-20220807165722818

gulimall-common模块的com.atguigu.common.to.MemberEntityTo类里实现Serializable接口,即可实现序列化

public class MemberEntityTo implements Serializable {
	......
}
image-20220807165925806
image-20220807165925806

重启GulimallAuthServerApplication服务,退出gitee(或清空gitee网站的cookie)

image-20220807163633204
image-20220807163633204

重新测试,此时redis里就有数据了

GIF 2022-8-7 17-06-13
GIF 2022-8-7 17-06-13
4、其他模块引入SpringSession

gulimall-product模块的pom.xml文件中引入SpringSession

<!--引入SpringSession-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
image-20220807171900898
image-20220807171900898

gulimall-product模块的启动类com.atguigu.gulimall.product.GulimallProductApplication上添加开启使用redis存储session的注解

@EnableRedisHttpSession
image-20220807171943750
image-20220807171943750

gulimall-product模块的src/main/resources/application.properties配置文件里选择session存储在redis中

spring.session.store-type=redis
image-20220807172043746
image-20220807172043746

在redis里清空spring:session命令空间的数据

image-20220807190611065
image-20220807190611065
5、显示session中用户的昵称

gulimall-product模块的src/main/resources/templates/index.html文件里的你好,请登录后面添加如下代码,显示一下session里存储的昵称,看一下是否能正常工作

[[${session.data.nickname}]]
image-20220807190049190
image-20220807190049190

访问 http://gulimall.com/ 报了如下的错误,提示没有nickname这个字段

Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'nickname' cannot be found on null
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:213)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:104)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.access$000(PropertyOrFieldReference.java:51)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference$AccessorLValue.getValue(PropertyOrFieldReference.java:406)
	at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:90)
	at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:109)
	at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:328)
	at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:263)
	... 76 more
image-20220807190154121
image-20220807190154121

可以将刚刚添加的代码修改成如下方式(data后面加了个?,即session.data存在才获取nickname)

[[${session.data?.nickname}]]

不可以使用如下方式,使用如下方式会报错

[[*{session.data.nickname}]]

参考: https://github.com/thymeleaf/thymeleaf-spring/issues/186

image-20220807190247265
image-20220807190247265

再次刷新页面,这次没有报错

image-20220807190341361
image-20220807190341361

继续访问 http://auth.gulimall.com/login.html 页面使用gitee登录,回到 http://gulimall.com/ 页面继续报错

GIF 2022-8-7 19-11-31
GIF 2022-8-7 19-11-31

com.atguigu.common.to.MemberEntityTo类里没有nickname字段,这是没有getNickname()方法导致的

Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'nickname' cannot be found on object of type 'com.atguigu.common.to.MemberEntityTo' - maybe not public or not valid?
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:217)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:104)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference.access$000(PropertyOrFieldReference.java:51)
	at org.springframework.expression.spel.ast.PropertyOrFieldReference$AccessorLValue.getValue(PropertyOrFieldReference.java:406)
	at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:90)
	at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:109)
	at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:328)
	at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:263)
image-20220807190713579
image-20220807190713579

参考:https://stackoverflow.com/questions/57369016/el1008e-property-or-field-username-cannot-be-found-on-object-of-type-user

忘记使用@Data注解了,在gulimall-common模块的com.atguigu.common.to.MemberEntityTo类上添加@Data注解即可

image-20220807191103932
image-20220807191103932

重启GulimallAuthServerApplication服务,清空redis,再次使用gitee登录,这次就成功显示用户名了

GIF 2022-8-7 19-15-19
GIF 2022-8-7 19-15-19

但是这个使用cookie存储的session只有本域名有效,在 http://gulimall.com/ 页面下查看cookie,可以看到名为SESSIONcookie,它的Domaingulimall.com,即只在 http://gulimall.com/ 页面有效,在其子域名下无效

image-20220807192020569
image-20220807192020569

打开 http://auth.gulimall.com/login.html 页面,可以看到就没有这个名为SESSIONcookie

image-20220807192022495
image-20220807192022495
6、gitee添加成功回调

在gitee的设置 -> 第三方应用 -> 我的应用 -> 谷粒商城 里添加应用回调地址为如下所示

http://auth.gulimall.com/oauth2.0/gitee/success
image-20220807195355737
image-20220807195355737

修改gulimall-auth-server模块的的src/main/resources/templates/login.html模块的gitee登录的<a>标签的href,修改登陆成功的回调地址为http://auth.gulimall.com/oauth2.0/gitee/success

<li>
   <a href="https://gitee.com/oauth/authorize?client_id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d&redirect_uri=http://auth.gulimall.com/oauth2.0/gitee/success&response_type=code">
      <img style="width: 55px;height: 45px" src="https://gitee.com/static/images/logo-black.svg?t=158106666" />
   </a>
</li>
image-20220807195453473
image-20220807195453473

删除gulimall-gateway模块src/main/resources/application.yml文件的如下配置

- id: gulimall_oauth2_route
  uri: lb://gulimall-auth-server
  predicates:
    - Path=/oauth2.0/**
image-20220807195556960
image-20220807195556960

gulimall-auth-server模块的src/main/resources/application.properties配置文件里将相关配置修改为如下(即修改登录成功后,重定向的地址)

oauth2.gitee.client-id=065cf9a0adda5fdc2de82bb00bc97c447baf0ba6fc32aec45fe382008ccc9a6d
oauth2.gitee.redirect-uri=http://auth.gulimall.com/oauth2.0/gitee/success
oauth2.gitee.client-secret=0c58d0cca9c3fe12bd6c6824f6dc04cdbce5b07cad784c9b8d5938342fc004f7
image-20220807200648197
image-20220807200648197

修改成功并重启gulimall-gateway模块和gulimall-auth-server模块后,可以看到首页没有显示无名氏http://gulimall.com/域名没有名为SESSIONcookie,只有http://auth.gulimall.com域名有该cookie

GIF 2022-8-7 20-18-50
GIF 2022-8-7 20-18-50

3、修改cookie作用范围和JSON序列化器

1、cookie作用范围配置

查看官方文档,可以使用如下方式修改cookie作用范围为当前域名及其子域名,同时也可以设置cookie的名字等

1、默认发的令牌。session=dsajkdjl.作用域:当前域; (解决子域session共享问题)
2、使用JSON的序列化方式来序列化对象数据到redis中

https://docs.spring.io/spring-session/docs/2.5.0/reference/html5/guides/java-custom-cookie.html

@Bean
public CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    //We customize the name of the cookie to be JSESSIONID
    serializer.setCookieName("JSESSIONID");
    //We customize the path of the cookie to be / (rather than the default of the context root).
    serializer.setCookiePath("/");
    /**
     We customize the domain name pattern (a regular expression) to be ^.+?\\.(\\w+\\.[a-z]+)$.
     This allows sharing a session across domains and applications.
     If the regular expression does not match, no domain is set and the existing domain is used.
     If the regular expression matches, the first grouping is used as the domain.
     This means that a request to https://child.example.com sets the domain to example.com.
     However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset      and, thus, still works in development without any changes being necessary for production.
     **/
    serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
    return serializer;
}
image-20220807194309362
image-20220807194309362

默认使用jdk进行序列化,使用jdk序列化不能通用,因此可以修改为JSON格式

https://docs.spring.io/spring-session/reference/samples.html

image-20220807192255113
image-20220807192255113
2、Json序列化器配置

使用JSON进行序列化的相关代码如下所示:

https://github.com/spring-projects/spring-session/blob/2.7.0/spring-session-samples/spring-session-sample-boot-redis-json/src/main/java/sample/config/SessionConfig.java

@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
	return new GenericJackson2JsonRedisSerializer(objectMapper());
}
image-20220807194511140
image-20220807194511140

使用GenericJackson2JsonRedisSerializer会保存序列化对象的完全限定名

image-20220807194750745
image-20220807194750745
3、修改代码

gulimall-auth-server模块的com.atguigu.gulimall.auth.config包下添加GulimallSessionConfig配置类,用于修改cookie的作用范围为当前域名和子域名,修改redis的序列化器为json序列化器

使用该配置后,我们可以将Spring Session默认的Cookie Key从SESSION替换为GULIMALL_JSESSIONID。而CookiePath设置为根路径且DomainNamePattern配置了相关的正则表达式,可以达到同父域下的单点登录的效果,在未涉及跨域的单点登录系统中(跨域的单点登录系统指的是根域名不同的多个系统之间实现一个系统登录,全系统都登录。一个系统退出登录,全系统都退出登录),这是一个非常优雅的解决方案。如果我们的当前域名是 auth.gulimall.com,该正则会将Cookie设置在父域 gulimall.com中,如果有另一个相同父域的子域名search.gulimall.com也会识别这个Cookie,便可以很方便的实现同父域下的单点登录。

package com.atguigu.gulimall.auth.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;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        // We customize the name of the cookie to be JSESSIONID.
        serializer.setCookieName("GULIMALL_JSESSIONID");
        //We customize the path of the cookie to be / (rather than the default of the context root).
        serializer.setCookiePath("/");
        //If the regular expression matches, the first grouping is used as the domain.
        //This means that a request to https://child.example.com sets the domain to example.com.
        //However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and,
        // thus, still works in development without any changes being necessary for production.
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }


}
image-20220807202944114
image-20220807202944114

复制一份GulimallSessionConfig配置类,粘贴到gulimall-product模块的com.atguigu.gulimall.product.config包下

package com.atguigu.gulimall.product.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;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        //	We customize the name of the cookie to be JSESSIONID.
        serializer.setCookieName("GULIMALL_JSESSIONID");
        //We customize the path of the cookie to be / (rather than the default of the context root).
        serializer.setCookiePath("/");
        //If the regular expression matches, the first grouping is used as the domain.
        //This means that a request to https://child.example.com sets the domain to example.com.
        //However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and,
        // thus, still works in development without any changes being necessary for production.
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }


}
image-20220807204934777
image-20220807204934777

清空首页和登录页的cookie,在 http://auth.gulimall.com/login.html 页面刷新后查看cookie,名字改为了GULIMALL_JSESSIONID但是domain还是auth.gulimall.com,修改并未生效

image-20220807203331698
image-20220807203331698

打开 http://gulimall.com/ 页面查看cookie,这里面就没有名为GULIMALL_JSESSIONID的cookie了

image-20220807211710277
image-20220807211710277

查看redis里的数据,数据也变为json格式了,就只有cookie的作用范围有点问题

image-20220807203417640
image-20220807203417640
4、重新修改代码

只好将DomainName修改为gulimall.com

gulimall-auth-server模块的com.atguigu.gulimall.auth.config.GulimallSessionConfig类里,将cookieSerializer()方法的serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");修改为serializer.setDomainName("gulimall.com");

@Bean
public CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    // We customize the name of the cookie to be JSESSIONID.
    serializer.setCookieName("GULIMALL_JSESSIONID");
    serializer.setDomainName("gulimall.com");
    ////We customize the path of the cookie to be / (rather than the default of the context root).
    //serializer.setCookiePath("/");
    ////If the regular expression matches, the first grouping is used as the domain.
    ////This means that a request to https://child.example.com sets the domain to example.com.
    ////However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and,
    //// thus, still works in development without any changes being necessary for production.
    ////亲测不生效
    //serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
    return serializer;
}
image-20220807211555730
image-20220807211555730

gulimall-product模块的com.atguigu.gulimall.product.config.GulimallSessionConfig类里,将cookieSerializer()方法的serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");修改为serializer.setDomainName("gulimall.com");

image-20220807211600180
image-20220807211600180

清空首页和登录页的cookie, 打开 http://gulimall.com/ 页面并刷新,此时cookie 的作用范围才为当前域名和子域名(.gulimall.com前面有个.即表示当前域名和子域名)。

image-20220807211520607
image-20220807211520607

http://auth.gulimall.com/login.html 页面也有此 cookie 了

image-20220807211512492
image-20220807211512492

4、源码解读

我们在gulimall-auth-server模块的com.atguigu.gulimall.auth.GulimallAuthServerApplication启动类上使用了@EnableRedisHttpSession注解

org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession注解导入了RedisHttpSessionConfiguration

@Import({RedisHttpSessionConfiguration.class})
image-20220807211933163
image-20220807211933163

RedisHttpSessionConfiguration类里向容器中注入了RedisOperationsSessionRepository 使用redis操作Session的持久层仓库(session的增删改查封装类)

@Bean
public RedisOperationsSessionRepository sessionRepository() {
    RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate();
    RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
    sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
    if (this.defaultRedisSerializer != null) {
        sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
    }
image-20220807212155349
image-20220807212155349

RedisOperationsSessionRepository类里使用ctrl + F12快捷键,可以看到该类有增删查改redis的方法

image-20220807214019282
image-20220807214019282

返回到RedisHttpSessionConfiguration类,可以看到RedisHttpSessionConfiguration类继承于SpringHttpSessionConfiguration

image-20220807212706762
image-20220807212706762

SpringHttpSessionConfiguration构造器执行之后,初始化了一个CookieSerializer

@PostConstruct
public void init() {
    CookieSerializer cookieSerializer = this.cookieSerializer != null ? this.cookieSerializer : this.createDefaultCookieSerializer();
    this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}
image-20220807212802272
image-20220807212802272

SpringHttpSessionConfiguration向容器中放了一个SessionEventHttpSessionListenerAdapter监听器,监听服务器停机Session的序列化Session反序列化session的活化Session的钝化等各种过程

(一)钝化

当服务器正常关闭时,还存活着的session(在设置时间内没有销毁)会随着服务器的关闭被以文件(“SESSIONS.ser”)的形式存储在tomcat的work目录下,这个过程叫做Session的钝化。

(二)活化

当服务器再次正常开启时,服务器会找到之前的“SESSIONS.ser”文件,从中恢复之前保存起来的Session对象,这个过程叫做Session的活化。

(三)注意事项

1)想要随着Session被钝化、活化的对象它的类必须实现Serializable接口,还有要注意的是只有在服务器正常关闭的条件下,还未超时的Session才会被钝化成文件。当Session超时、调用invalidate方法或者服务器在非正常情况下关闭时,Session都不会被钝化,因此也就不存在活化。

2)在被钝化成“SESSIONS.ser”文件时,不会因为超过Session过期时间而消失,这个文件会一直存在,等到下一次服务器开启时消失。

3)当多个Session被钝化时,这些被钝化的Session都被保存在一个文件中,并不会为每个Session都建立一个文件。

4)session中的数据与服务器是否关闭无关,只跟浏览器是否关闭有关。

@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
    return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
image-20220807212910677
image-20220807212910677

SpringHttpSessionConfiguration类还向容器中存放了SessionRepositoryFilterSession存储过滤器)

@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
    sessionRepositoryFilter.setServletContext(this.servletContext);
    sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
    return sessionRepositoryFilter;
}
image-20220807213346332
image-20220807213346332

返回的org.springframework.session.web.http.SessionRepositoryFilter继承自org.springframework.session.web.http.OncePerRequestFilter

image-20220807213534778
image-20220807213534778

org.springframework.session.web.http.OncePerRequestFilter实现了一个Filter

image-20220807213552684
image-20220807213552684

而这个Filter就是javax.servlet.Filter,即SpringSession主要就是通过filter实现的

image-20220807213627268
image-20220807213627268

SessionRepositoryFilterSessionRepositoryFilter方法会到容器中找SessionRepository,而SessionRepository就是刚刚看到的RedisOperationsSessionRepository (使用redis操作Session的持久层仓库)

public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    if (sessionRepository == null) {
        throw new IllegalArgumentException("sessionRepository cannot be null");
    } else {
        this.sessionRepository = sessionRepository;
    }
}
image-20220807214300525
image-20220807214300525

org.springframework.session.web.http.SessionRepositoryFilter重写了父类org.springframework.session.web.http.OncePerRequestFilterdoFilterInternal方法

image-20220807214711076
image-20220807214711076

org.springframework.session.web.http.OncePerRequestFilter又重写了javax.servlet.FilterdoFilter,然后调用doFilterInternal(httpRequest, httpResponse, filterChain);方法执行相应功能(Spring和Tomcat都是这样,先实现jsr规范的接口做一些初始化或者异常捕获这些通用的事情,期间会调用xxxInternal方法执行真正的功能,xxxInternal方法为抽象方法,让其子类去实现该抽象方法以完成实际的功能)

/**
 * This {@code doFilter} implementation stores a request attribute for
 * "already filtered", proceeding without filtering again if the attribute is already
 * there.
 * @param request the request
 * @param response the response
 * @param filterChain the filter chain
 * @throws ServletException if request is not HTTP request
 * @throws IOException in case of I/O operation exception
 */
@Override
public final void doFilter(ServletRequest request, ServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {

   if (!(request instanceof HttpServletRequest)
         || !(response instanceof HttpServletResponse)) {
      throw new ServletException(
            "OncePerRequestFilter just supports HTTP requests");
   }
   HttpServletRequest httpRequest = (HttpServletRequest) request;
   HttpServletResponse httpResponse = (HttpServletResponse) response;
   String alreadyFilteredAttributeName = this.alreadyFilteredAttributeName;
   boolean hasAlreadyFilteredAttribute = request
         .getAttribute(alreadyFilteredAttributeName) != null;

   if (hasAlreadyFilteredAttribute) {
      if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
         doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
         return;
      }
      // Proceed without invoking this filter...
      filterChain.doFilter(request, response);
   }
   else {
      // Do invoke this filter...
      request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
      try {
         doFilterInternal(httpRequest, httpResponse, filterChain);
      }
      finally {
         // Remove the "already filtered" request attribute for this request.
         request.removeAttribute(alreadyFilteredAttributeName);
      }
   }
}
image-20220807214927002
image-20220807214927002

因此核心原理在org.springframework.session.web.http.SessionRepositoryFilter类的doFilterInternal方法

@Override
protected void doFilterInternal(HttpServletRequest request,
      HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
   request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

   SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
         request, response, this.servletContext);
   SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
         wrappedRequest, response);

   try {
      filterChain.doFilter(wrappedRequest, wrappedResponse);
   }
   finally {
      wrappedRequest.commitSession();
   }
}
image-20220807214711076
image-20220807214711076

向session中存放key为org.springframework.session.SessionRepositoryRedisOperationsSessionRepository对象

request.setAttribute是在同一个请求共享数据的,把this.sessionRepository放到当前请求,当前的同一次请求,都使用的是同一个sessionRepository(全系统就一个)

request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

把原生的请求对象request, response, this.servletContext传给SessionRepositoryRequestWrapper进行包装(包装原始请求对象)(装饰者模式)(SessionRepositoryRequestWrapper类为org.springframework.session.web.http.SessionRepositoryFilter类的内部类)

SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
         request, response, this.servletContext);

然后又把包装的SessionRepositoryRequestWrapperresponse传给SessionRepositoryResponseWrapper进行包装(包装原始响应对象)(SessionRepositoryResponseWrapper类为org.springframework.session.web.http.SessionRepositoryFilter类的内部类)

SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
         wrappedRequest, response);

将通过包装后的SessionRepositoryRequestWrapperSessionRepositoryResponseWrapper 放行给filter链,filter链执行完后就会放到Controller(包装后的对象应用到了后面的整个执行链)

filterChain.doFilter(wrappedRequest, wrappedResponse);
image-20220808090432071
image-20220808090432071

doFilterInternal方法把原生的requestresponse包装成了wrappedRequestwrappedResponse,执行request.getSession()相当于执行wrappedRequest.getSession()

SessionRepositoryRequestWrapper类重写了getSession

原生的javax.servlet.http.HttpServletRequest

/**
 * Returns the current session associated with this request, or if the
 * request does not have a session, creates one.
 *
 * @return the <code>HttpSession</code> associated with this request
 * @see #getSession(boolean)
 */
public HttpSession getSession();
image-20221226112612769
image-20221226112612769

📌在一个Web应用程序中可以注册多个Filter程序,每个Filter程序都可以针对某一个URL进行拦截。如果多个Filter程序都对同一个URL进行拦截,那么这些Filter就会组成一个Filter链(也叫过滤器链) 内部类org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper重写后的getSession();方法

@Override
public HttpSessionWrapper getSession(boolean create) {
   HttpSessionWrapper currentSession = getCurrentSession();
   if (currentSession != null) {
      return currentSession;
   }
   S requestedSession = getRequestedSession();
   if (requestedSession != null) {
      if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
         requestedSession.setLastAccessedTime(Instant.now());
         this.requestedSessionIdValid = true;
         currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
         currentSession.setNew(false);
         setCurrentSession(currentSession);
         return currentSession;
      }
   }
   else {
      // This is an invalid session id. No need to ask again if
      // request.getSession is invoked for the duration of this request
      if (SESSION_LOGGER.isDebugEnabled()) {
         SESSION_LOGGER.debug(
               "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
      }
      setAttribute(INVALID_SESSION_ID_ATTR, "true");
   }
   if (!create) {
      return null;
   }
   if (SESSION_LOGGER.isDebugEnabled()) {
      SESSION_LOGGER.debug(
            "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
                  + SESSION_LOGGER_NAME,
            new RuntimeException(
                  "For debugging purposes only (not an error)"));
   }
   S session = SessionRepositoryFilter.this.sessionRepository.createSession();
   session.setLastAccessedTime(Instant.now());
   currentSession = new HttpSessionWrapper(session, getServletContext());
   setCurrentSession(currentSession);
   return currentSession;
}
image-20220808094820279
image-20220808094820279

首先调用本内部类的getCurrentSession()方法,查询SESSION_REPOSITORY_ATTR

@SuppressWarnings("unchecked")
private HttpSessionWrapper getCurrentSession() {
   return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
}
image-20220808095133989
image-20220808095133989

如果不等于空了就返回当前Session

@Override
public HttpSessionWrapper getSession(boolean create) {
   HttpSessionWrapper currentSession = getCurrentSession();
   if (currentSession != null) {
      return currentSession;
   }
   S requestedSession = getRequestedSession();
   ...
}
image-20220808095247834
image-20220808095247834

如果为空了就在sessionRepository里重新获取

private S getRequestedSession() {
   if (!this.requestedSessionCached) {
      List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
            .resolveSessionIds(this);
      for (String sessionId : sessionIds) {
         if (this.requestedSessionId == null) {
            this.requestedSessionId = sessionId;
         }
         S session = SessionRepositoryFilter.this.sessionRepository
               .findById(sessionId);
         if (session != null) {
            this.requestedSession = session;
            this.requestedSessionId = sessionId;
            break;
         }
      }
      this.requestedSessionCached = true;
   }
   return this.requestedSession;
}
image-20220808095507267
image-20220808095507267

如果还没有并且本getSession(boolean create)方法传过来的create为true的话,还会在redis中创建session。不过最开始设置的create为false,直接return了

S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
image-20221226115035822
image-20221226115035822

而这个SessionRepository就是以前放的RedisOperationsSessionRepository,所以对session的操作全在redis

private final SessionRepository<S> sessionRepository;
image-20220808095717747
image-20220808095717747

SessionRepository里定义了增删改查的方法,并且也有redis的实现

public interface SessionRepository<S extends Session> {

	/**
	 * Creates a new {@link Session} that is capable of being persisted by this
	 * {@link SessionRepository}.
	 *
	 * <p>
	 * This allows optimizations and customizations in how the {@link Session} is
	 * persisted. For example, the implementation returned might keep track of the changes
	 * ensuring that only the delta needs to be persisted on a save.
	 * </p>
	 *
	 * @return a new {@link Session} that is capable of being persisted by this
	 * {@link SessionRepository}
	 */
	S createSession();

	/**
	 * Ensures the {@link Session} created by
	 * {@link org.springframework.session.SessionRepository#createSession()} is saved.
	 *
	 * <p>
	 * Some implementations may choose to save as the {@link Session} is updated by
	 * returning a {@link Session} that immediately persists any changes. In this case,
	 * this method may not actually do anything.
	 * </p>
	 *
	 * @param session the {@link Session} to save
	 */
	void save(S session);

	/**
	 * Gets the {@link Session} by the {@link Session#getId()} or null if no
	 * {@link Session} is found.
	 *
	 * @param id the {@link org.springframework.session.Session#getId()} to lookup
	 * @return the {@link Session} by the {@link Session#getId()} or null if no
	 * {@link Session} is found.
	 */
	S findById(String id);

	/**
	 * Deletes the {@link Session} with the given {@link Session#getId()} or does nothing
	 * if the {@link Session} is not found.
	 * @param id the {@link org.springframework.session.Session#getId()} to delete
	 */
	void deleteById(String id);
}
image-20220808100406889
image-20220808100406889

返回null以后,进到了org.springframework.web.servlet.support.SessionFlashMapManager类的retrieveFlashMaps方法,由于调用的request.getSession(false);返回了null,因此session为null,直接返回null了

public class SessionFlashMapManager extends AbstractFlashMapManager {

   /**
    * Retrieves saved FlashMap instances from the HTTP session, if any.
    */
   @Override
   @SuppressWarnings("unchecked")
   @Nullable
   protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) {
      HttpSession session = request.getSession(false);
      return (session != null ? (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null);
   }
   ...
}
image-20221226115342407
image-20221226115342407

由于retrieveFlashMaps(request)返回了null,因此org.springframework.web.servlet.support.AbstractFlashMapManager类的retrieveAndUpdate方法也返回null

public abstract class AbstractFlashMapManager implements FlashMapManager {

   @Override
   @Nullable
   public final FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response) {
      List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
      if (CollectionUtils.isEmpty(allFlashMaps)) {
         return null;
      }
      ...
   }
   ...
}
image-20221226115742369
image-20221226115742369

然后就执行了org.springframework.web.servlet.DispatcherServlet类的doDispatch方法(如果看过spring mvc源码应该知道该方法,所有请求是由doDispatch方法处理的)

public class DispatcherServlet extends FrameworkServlet {
	@Override
	protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
		logRequest(request);
		...
		if (this.flashMapManager != null) {
			FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
			if (inputFlashMap != null) {
				request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
			}
			request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
			request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
		}

		try {
			doDispatch(request, response);
		}
		finally {
			if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
				// Restore the original attribute snapshot, in case of an include.
				if (attributesSnapshot != null) {
					restoreAttributesAfterInclude(request, attributesSnapshot);
				}
			}
		}
	}
image-20221226115914228
image-20221226115914228

再次点击步过按钮,就会发现getSession(boolean create)createtrue了,此时就会创建了

image-20221226120335389
image-20221226120335389

通过栈轨迹可以看到是doDispatch方法的mv = ha.handle(processedRequest, response, mappedHandler.getHandler());这行代码设置的

image-20221226120556560
image-20221226120556560

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());这个方法就是返回模型和视图的

image-20221226120733656
image-20221226120733656

并且可以自动续期,默认30分钟过期

GIF 2022-8-8 10-11-13
GIF 2022-8-8 10-11-13

filter过滤链:Filter链是如何构建的? (itcast.cn)open in new window

在一个Web应用程序中可以注册多个Filter程序,每个Filter程序都可以针对某一个URL进行拦截。如果多个Filter程序都对同一个URL进行拦截,那么这些Filter就会组成一个Filter链(也叫过滤器链)。Filter链用FilterChain对象来表示,FilterChain对象中有一个doFilter()方法,该方法作用就是让Filter链上的当前过滤器放行,请求进入下一个Filter,接下来通过一个图例来描述Filter链的拦截过程,如图1所示。

filter链
filter链

当浏览器访问Web服务器中的资源时需要经过两个过滤器Filter1和Filter2,首先Filter1会对这个请求进行拦截,在Filter1过滤器中处理好请求后,通过调用Filter1的doFilter()方法将请求传递给Filter2,Filter2将用户请求处理后同样调用doFilter()方法,最终将请求发送给目标资源。当Web服务器对这个请求做出响应时,也会被过滤器拦截,这个拦截顺序与之前相反,最终将响应结果发送给客户端。

5、使用session存放登录数据

1、向session存放的数据

gulimall-common模块的com.atguigu.common.constant.auth.AuthServerConstant类里添加字段,作为登录成功后,向session存放数据的key

/**
 * 登录成功后,向session存放的数据
 */
public static final String LOGIN_USER = "loginUser";
image-20220808105019374
image-20220808105019374

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.OAuth2Controller类的giteeRegister方法

@GetMapping("/gitee/success")
public String giteeRegister(@RequestParam String code, HttpSession session){

    try {
        MemberEntityTo memberEntityTo = oAuth2Service.giteeRegister(code);
        session.setAttribute(AuthServerConstant.LOGIN_USER,memberEntityTo);
        //Cookie cookie = new Cookie("data",memberEntityTo.toString());
        //cookie.setPath("/");
        //response.addCookie(cookie);
        return "redirect:http://gulimall.com";
    }catch (Exception e){
        log.error("第三方登录失败 :{}",e.getMessage());
        return "redirect:http://auth.gulimall.com/login.html";
    }
}
image-20220808105126361
image-20220808105126361

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginControllerlogin方法里添加代码

/**
 * 页面传递 k,v不加 @RequestBody
 * @param vo
 * @return
 */
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session){
    R r = memberFeignService.login(vo);
    if (r.getCode()==0){
        Object data = r.get("data");
        String json = JSON.toJSONString(data);
        MemberEntityTo memberEntityTo = JSON.parseObject(json, MemberEntityTo.class);
        session.setAttribute(AuthServerConstant.LOGIN_USER,memberEntityTo);
        return "redirect:http://gulimall.com";
    }else {
        Map<String, String> errors = new HashMap<>();
        errors.put("msg",r.getMsg());
        redirectAttributes.addFlashAttribute("errors",errors);
        return "redirect:http://auth.gulimall.com/login.html";
    }
}
image-20220808105605057
image-20220808105605057

修改gulimall-product模块src/main/resources/templates/index.html文件的你好,请登录周围代码,展示用户信息

<ul>
  <li>
    <a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser?.nickname}]]</a>
    <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}">你好,请登录</a>
  </li>
  <li>
    <a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser==null}" class="li_2">免费注册</a>
  </li>
  <span>|</span>
  <li>
    <a href="#">我的订单</a>
  </li>
</ul>
image-20220808110530695
image-20220808110530695

重启GulimallProductApplicationGulimallAuthServerApplicationGulimallMemberApplication服务

http://auth.gulimall.com/login.html页面登陆后会跳转到http://gulimall.com/页面,再次访问http://auth.gulimall.com/login.html应该跳转到http://gulimall.com/页面

GIF 2022-8-8 11-05-53
GIF 2022-8-8 11-05-53
2、登陆后访问登录页跳转到首页

去掉gulimall-auth-server模块的com.atguigu.gulimall.auth.config.GulimallWebConfig类的addViewControllers方法的registry.addViewController("/login.html").setViewName("login");这行代码

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    //registry.addViewController("/login.html").setViewName("login");
    registry.addViewController("/reg.html").setViewName("reg");
}
image-20220808110902908
image-20220808110902908

gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类里添加loginPage方法

@GetMapping("/login.html")
public String loginPage(HttpSession session){
    Object hasLogin = session.getAttribute(AuthServerConstant.LOGIN_USER);
    if (hasLogin!=null){
        return "redirect:http://gulimall.com";
    }
    return "login";
}
image-20220808111407538
image-20220808111407538

重启gulimall-auth-server模块和gulimall-product模块,如果已登录,访问 http://auth.gulimall.com/login.html 会重定向到 http://gulimall.com/ 页面

GIF 2022-8-8 11-14-19
GIF 2022-8-8 11-14-19

由于注册时没有昵称,所以 http://gulimall.com/ 页面没有显示昵称

GIF 2022-8-8 11-19-32
GIF 2022-8-8 11-19-32
3、显示昵称

gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberServiceImpl类的regist方法里添加memberEntity.setNickname(vo.getUsername());,设置用户名就是昵称

@Override
public void regist(MemberRegistVo vo) {
    MemberDao baseMapper = this.baseMapper;
    MemberEntity memberEntity = new MemberEntity();

    MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
    memberEntity.setLevelId(memberLevelEntity.getId());

    //检查手机号和用户名是否唯一,使用异常机制
    checkPhoneUnique(vo.getPhone());
    checkUsernameUnique(vo.getUsername());

    memberEntity.setMobile(vo.getPhone());

    memberEntity.setUsername(vo.getUsername());
    //默认用户名也是昵称
    memberEntity.setNickname(vo.getUsername());
    //盐值加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode = bCryptPasswordEncoder.encode(vo.getPassword());
    memberEntity.setPassword(encode);

    baseMapper.insert(memberEntity);
}
image-20220808112252595
image-20220808112252595

修改gulimall_ums数据库的ums_member表的usernametest01nicknametest01

image-20220808113149618
image-20220808113149618

修改gulimall-product模块src/main/resources/templates/item.html文件你好,请登录周围代码

<li style="border: 0;">
   <a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser?.nickname}]]</a>
   <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}" class="aa">你好,请登录</a>
</li>
<li><a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser==null}" style="color: red;">免费注册</a> |</li>
<li><a href="javascript:;" class="aa">我的订单</a> |</li>
<li class="jingdong"><a href="javascript:;">我的京东</a><span class="glyphicon glyphicon-menu-down">|</span>
image-20220808113051591
image-20220808113051591

重启gulimall-member模块和gulimall-product模块,此时就显示昵称了

image-20220808145909993
image-20220808145909993

在 http://item.gulimall.com/9.html 页面里,选择logo所在的元素,将其src修改为本项目的logo

image-20220808150733517
image-20220808150733517

我们项目的logo是在linux的 /mydata/nginx/html/static/item/image/logo1.jpg位置

/mydata/nginx/html/static/item/image/logo1.jpg
image-20220808150750988
image-20220808150750988

修改gulimall-product模块的src/main/resources/templates/item.html文件的logo的src

<div class="nav_top_one"><a href="http://gulimall.com"><img src="/static/item/image/logo1.jpg"/></a></div>
image-20221226133616234
image-20221226133616234

重启gulimall-product模块,访问 http://item.gulimall.com/9.html 页面,可以看到logo显示正确

GIF 2022-8-8 15-09-51
GIF 2022-8-8 15-09-51

6、gulimall-search整合Spring Session

gulimall-search模块的pom.xml文件里添加如下依赖

<!--操作redis的客户端-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--用redis存session-->
<dependency>
   <groupId>org.springframework.session</groupId>
   <artifactId>spring-session-data-redis</artifactId>
</dependency>
image-20220808151427429
image-20220808151427429

gulimall-search模块的src/main/resources/application.yml文件里配置redis的连接信息和session的存储类型

spring:
  redis:
    host: 192.168.56.10
    port: 6379
  session:
    store-type: redis
image-20220808151631945
image-20220808151631945

gulimall-search模块的com.atguigu.gulimall.search.GulimallSearchApplication类上添加@EnableRedisHttpSession注解

@EnableRedisHttpSession
image-20220808151736196
image-20220808151736196

复制一份gulimall-auth-server模块的com.atguigu.gulimall.auth.config.GulimallSessionConfig,粘贴到gulimall-search模块的com.atguigu.gulimall.search.config包下

package com.atguigu.gulimall.search.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;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        // We customize the name of the cookie to be JSESSIONID.
        serializer.setCookieName("GULIMALL_JSESSIONID");
        serializer.setDomainName("gulimall.com");
        ////We customize the path of the cookie to be / (rather than the default of the context root).
        //serializer.setCookiePath("/");
        ////If the regular expression matches, the first grouping is used as the domain.
        ////This means that a request to https://child.example.com sets the domain to example.com.
        ////However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and,
        //// thus, still works in development without any changes being necessary for production.
        ////亲测不生效
        //serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }


}
image-20220808151859307
image-20220808151859307

修改gulimall-search模块的src/main/resources/templates/list.html文件的你好,请登录周围代码

<li>
    <a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser?.nickname}]]</a>
    <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}" class="li_2">你好,请登录</a>
</li>
<li>
    <a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser==null}">免费注册</a>
</li>
image-20220808152108212
image-20220808152108212

重启gulimall-search模块,访问 http://search.gulimall.com/list.html?catalog3Id=225 页面,这里就显示昵称了

image-20220808152237367
image-20220808152237367

5.7.7、单点登录

1、单点登录系统

参考链接: https://blog.csdn.net/MyNAMS/article/details/123855044

1、早期单一服务器,用户认证

缺点:单点性能压力,无法扩展

2、WEB应用集群,session共享模式

解决了单点性能瓶颈。

问题:

  • 1、 多业务分布式数据独立管理,不适合统一维护一份session数据。
  • 2、 分布式按业务功能切分,用户、认证解耦出来单独统一管理。
  • 3、 cookie中使用jsessionId 容易被篡改、盗取。
  • 4、 跨顶级域名无法访问。
3、分布式,SSO(single sign on)模式

解决 : 用户身份信息独立管理,更好的分布式管理。 可以自己扩展安全策略 跨域不是问题

缺点: 认证服务器访问压力较大。

4、业务流程图
5、生成token

JWTopen in new window工具

  • JWT(Json Web Token) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
  • JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
  • JWT 最重要的作用就是对 token信息的防伪作用。
  • JWT的原理, 一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。
  1. 公共部分 主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。

  2. 私有部分 用户自定义的内容,根据实际需要真正要封装的信息。

  3. 签名部分 根据用户信息+盐值+密钥生成的签名。如果想知道JWT是否是真实的只要把JWT的信息取出来,加上盐值和服务器中的密钥就可以验证真伪。所以不管由谁保存JWT,只要没有密钥就无法伪造。

  4. base64编码open in new window,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以吧base64编码解成明文,所以不要在JWT中放入涉及私密的信息,因为实际上JWT并不是加密信息。

2、使用开源框架

多系统-单点登录业务,适用于多系统的父域名不同

image-20220808152551728
image-20220808152551728
1、下载

在 https://gitee.com/ 里搜索xxl,选择 许雪里open in new window / xxl-ssoopen in new window

https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search

GIF 2022-8-8 15-34-35
GIF 2022-8-8 15-34-35

目录结构

doc
xxl-sso-core     核心包
xxl-sso-samples  简单的例子
xxl-sso-server   服务器
.gitignore
LICENSE
pom.xml
README.md
image-20220808154616794
image-20220808154616794
2、启动登录服务

hosts文件里添加如下3个域名,ssoserver.com用作登录服务,client1.comclient2.com表示其他系统

127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
image-20220808163921294
image-20220808163921294

xxl-sso\xxl-sso-server\src\main\resourcesapplication.properties配置文件里,修改redis的连接地址

### web
server.port=8080
server.context-path=/xxl-sso-server

### resources
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/

### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########

### xxl-sso
xxl.sso.redis.address=redis://192.168.56.10:6379
xxl.sso.redis.expire.minute=1440
image-20220808154834016
image-20220808154834016

xxl-sso文件夹下执行如下命名,打包该项目

mvn clean package -Dmaven.skip.test=true
image-20220808160221190
image-20220808160221190
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for xxl-sso 1.1.1-SNAPSHOT:
[INFO]
[INFO] xxl-sso ............................................ SUCCESS [  0.147 s]
[INFO] xxl-sso-core ....................................... SUCCESS [  5.561 s]
[INFO] xxl-sso-server ..................................... SUCCESS [ 33.224 s]
[INFO] xxl-sso-samples .................................... SUCCESS [  0.009 s]
[INFO] xxl-sso-web-sample-springboot ...................... SUCCESS [  0.720 s]
[INFO] xxl-sso-token-sample-springboot .................... SUCCESS [  1.147 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  45.392 s
[INFO] Finished at: 2022-08-08T15:59:35+08:00
[INFO] ------------------------------------------------------------------------
image-20220808160223642
image-20220808160223642

xxl-sso\xxl-sso-server\target文件夹里执行如下命令,启动xxl-sso-server-1.1.1-SNAPSHOT.jar

java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar
image-20220808160440611
image-20220808160440611

打开 http://ssoserver.com:8080/xxl-sso-server/login 网页,这里用于登录

image-20220808161012786
image-20220808161012786
3、启动其他服务

xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\src\main\resources文件夹的application.properties配置文件里,修改xxl.sso.serverxxl.sso.redis.address

### web
server.port=8081
server.context-path=/xxl-sso-web-sample-springboot

### freemarker
spring.freemarker.request-context-attribute=request
spring.freemarker.cache=false

### resource (default: /**  + classpath:/static/ )
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/

### xxl-sso
xxl.sso.server=http://ssoserver.com:8080/xxl-sso-server
xxl.sso.logout.path=/logout
xxl-sso.excluded.paths=
xxl.sso.redis.address=redis://192.168.56.10:6379
image-20220808160926998
image-20220808160926998

进入xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot文件夹,执行如下命令

mvn clean package -Dmaven.skip.test=true

这里打包失败了

A:\桌面\xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot>mvn clean package -Dmaven.skip.test=true
[INFO] Scanning for projects...
[WARNING]
[WARNING] Some problems were encountered while building the effective model for com.xuxueli:xxl-sso-web-sample-springboot:jar:1.1.1-SNAPSHOT
[WARNING] The expression ${parent.version} is deprecated. Please use ${project.parent.version} instead.
[WARNING]
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING]
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING]
[INFO]
[INFO] -------------< com.xuxueli:xxl-sso-web-sample-springboot >--------------
[INFO] Building xxl-sso-web-sample-springboot 1.1.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[WARNING] The POM for com.xuxueli:xxl-sso-core:jar:1.1.1-SNAPSHOT is missing, no dependency information available
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.599 s
[INFO] Finished at: 2022-08-08T16:12:04+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project xxl-sso-web-sample-springboot: Could not resolve dependencies for project com.xuxueli:xxl-sso-web-sample-springboot:jar:1.1.1-SNAPSHOT: Could not find artifact com.xuxueli:xxl-sso-core:jar:1.1.1-SNAPSHOT -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException
image-20220808161239078
image-20220808161239078

进入到xxl-sso\xxl-sso-core文件夹,执行如下命令

mvn install
image-20220808161633658
image-20220808161633658

再进入xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot文件夹,执行如下命令,执行还是失败了

mvn clean package -Dmaven.skip.test=true
image-20220808161845005
image-20220808161845005

把刚刚启动的xxl-sso-server关了,重新在xxl-sso里打包

mvn clean package -Dmaven.skip.test=true
image-20220808162150355
image-20220808162150355

这次就全部成功了

image-20220808162152659
image-20220808162152659

xxl-sso\xxl-sso-server\target文件夹里执行如下命令,启动xxl-sso-server-1.1.1-SNAPSHOT.jar

java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar
image-20220808162309292
image-20220808162309292

xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target文件夹里使用如下命令,在8081端口启动一个其他系统

java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8081
image-20220808162611834
image-20220808162611834

xxl-sso\xxl-sso-samples\xxl-sso-web-sample-springboot\target文件夹里使用如下命令,再在8082端口启动一个

java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8082
image-20220808162822238
image-20220808162822238

一次打开如下网址

ssoserver.com:8080/xxl-sso-server
client1.com:8081/xxl-sso-web-sample-springboot
client2.com:8082/xxl-sso-web-sample-springboot

可以看到当一个系统登陆后各个系统都登录了,当一个系统退出登陆后,各个系统都退出登录了

GIF 2022-8-8 16-36-19
GIF 2022-8-8 16-36-19

原理:

/xxl-sso-server 登录服务器 8080 ssoserver.com

/xxl-sso-web-sample-springboot 项目1 8081 client1.com
/xxl-sso-web-sample-springboot 项目2 8082 client2.com

127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com

核心:三个系统即使域名不一样,想办法给三个系统同步同一个用户的票据;
1)、中央认证服务器;ssoserver.com
2)、其他系统,想要登录去ssoserver.com登录,登录成功跳转回来  
3)、只要有一个登录,其他都不用登录
4)、全系统统一一个sso-sessionid;所有系统可能域名都不相同

3、自己实现单点登录

1、新建登录服务

新建一个模块,Group输入com.atguiguArtifact输入gulimall-test-sso-serverType选择MavenLanguage选择JavaPackaging选择JarJava Version选择8Version不用管,Name输入gulimall-test-sso-serverDespcription输入单点登录的认证服务器Package输入com.atguigu.gulimall.ssoserver,然后点击Next

com.atguigu
gulimall-test-sso-server
单点登录的认证服务器
com.atguigu.gulimall.ssoserver
image-20220808164835763
image-20220808164835763

选择LomboxSpring Web,然后点击Next

image-20220808164915849
image-20220808164915849

点击Finish

image-20220808164933481
image-20220808164933481

如果刚刚新建的gulimall-test-sso-server模块的pom.xml文件为赤橙色,可以选中pom.xml文件,然后右键,选择Add as Maven Project即可

image-20220808165028477
image-20220808165028477

以下内容不替换

<groupId>com.atguigu</groupId>
<artifactId>gulimall-test-sso-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-test-sso-server</name>
<description>单点登录的认证服务器</description>
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</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><build><parent>等都要替换

image-20220808165509451
image-20220808165509451

点击查看完整的gulimall-test-sso-server模块的pom.xml文件

修改gulimall-test-sso-server模块的src/test/java/com/atguigu/gulimall/ssoserver/GulimallTestSsoServerApplicationTests.java测试类为junit5相关的

package com.atguigu.gulimall.ssoserver;


import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallTestSsoServerApplicationTests {
   @Test
   public void contextLoads() {
   }
}
image-20220808165630258
image-20220808165630258

gulimall-test-sso-server模块的pom.xml文件里添加如下依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
image-20220808171653464
image-20220808171653464
2、新建其他系统

新建一个模块,Group输入com.atguiguArtifact输入gulimall-test-sso-clientType选择MavenLanguage选择JavaPackaging选择JarJava Version选择8Version不用管,Name输入gulimall-test-sso-clientDespcription输入单点登录的客户端Package输入com.atguigu.gulimall.ssoclient,然后点击Next

com.atguigu
gulimall-test-sso-client
单点登录的客户端
com.atguigu.gulimall.ssoclient
image-20220808170027100
image-20220808170027100

选择LomboxSpring WebThymeleaf

image-20220808170030445
image-20220808170030445

点击Finish

image-20220808170032561
image-20220808170032561

如果刚刚新建的gulimall-test-sso-client模块的pom.xml文件为赤橙色,可以选中pom.xml文件,然后右键,选择Add as Maven Project即可

image-20220808170214945
image-20220808170214945

同样的为了版本统一,以下内容不替换,<properties><build><parent>等都要替换

<groupId>com.atguigu</groupId>
<artifactId>gulimall-test-sso-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-test-sso-client</name>
<description>单点登录的客户端</description>

<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.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>
image-20220808170659953
image-20220808170659953

点击查看完整的gulimall-test-sso-client模块的pom.xml文件

修改gulimall-test-sso-client模块的src/test/java/com/atguigu/gulimall/ssoclient/GulimallTestSsoClientApplicationTests.java测试类

package com.atguigu.gulimall.ssoclient;


import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallTestSsoClientApplicationTests {
   @Test
   public void contextLoads() {
   }
}
image-20220808170935951
image-20220808170935951
3、其他系统添加接口

gulimall-test-sso-client模块的com.atguigu.gulimall.ssoclient包下新建controller文件夹,在controller文件夹下新建HelloController

package com.atguigu.gulimall.ssoclient.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 无名氏
 * @date 2022/8/8
 * @Description:
 */
@Controller
public class HelloController {

    /**
     * 不登录就可以访问
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    /**
     * 必须登录才可访问
     * @param model
     * @return
     */
    @GetMapping("/employees")
    public String employees(Model model){
        List<String> emps = new ArrayList<>();
        emps.add("张三");
        emps.add("李四");
        model.addAttribute("emps",emps);
        return "list";
    }
}
image-20220808185455496
image-20220808185455496

gulimall-test-sso-client模块的src/main/resources文件夹下新建templates文件夹,在templates文件夹下新建list.html文件

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>员工列表</title>
</head>
<body>
    <h1>欢迎: []</h1>

    <ul>
        <li th:each="emp: ${emps}">姓名: [[${emp}]]</li>
    </ul>
</body>
</html>
image-20220808192145135
image-20220808192145135

gulimall-test-sso-client模块的src/main/resources/application.properties文件里添加如下配置,将端口号改为8081

server.port=8081
image-20220808185548331
image-20220808185548331

(我第一次访问的时候用chrome浏览器访问client1.com:8081/hellolocalhost:8081/hello127.0.0.1:8081/hello都访问不了,最后换用Edge浏览器可以访问,再换回chrome浏览器也可以访问了,就很奇怪)

http://client1.com:8081/employees

image-20220808192355709
image-20220808192355709

http://client1.com:8081/hello

image-20220808192358289
image-20220808192358289

可以看到都可以访问

4、登录系统添加接口

gulimall-test-sso-server模块的src/main/resources/application.properties配置文件里添加如下配置,将端口号修改为8080

server.port=8080
image-20220808193333216
image-20220808193333216

gulimall-test-sso-server模块的src/main/resources文件夹下新建templates文件夹,在templates文件夹下新建login.html文件

<!DOCTYPE html>
<html lang="en">
<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="submit" value="登录">
</form>
</body>
</html>
image-20220808193648749
image-20220808193648749
5、其他系统添加配置

gulimall-test-sso-client模块的src/main/resources/application.properties配置文件里添加如下配置,指定登录服务的url

sso.server.url=http://ssoserver.com:8080/login.html
image-20220808194725231
image-20220808194725231

(不引用spring-session-data-redis,这里没少步骤,后面还会用其他方法的)

修改gulimall-test-sso-client模块的com.atguigu.gulimall.ssoclient.controller.HelloController类的employees方法

/**
 * 必须登录才可访问
 * @param model
 * @return
 */
@GetMapping("/employees")
public String employees(Model model, HttpSession session){
    Object loginUser = session.getAttribute("loginUser");
    if (loginUser==null){
        //如果没登录,就跳转到登录服务器进行登录
        return "redirect:" + serverUrl;
    }
    List<String> emps = new ArrayList<>();
    emps.add("张三");
    emps.add("李四");
    model.addAttribute("emps",emps);
    return "list";
}
image-20220808194719427
image-20220808194719427

只启动GulimallTestSsoClientApplication服务,不启动GulimallTestSsoServerApplication服务

可以看到在http://client1.com:8081/employees页面,自动跳转到了http://ssoserver.com:8080/login.html

General里的Request URL:http://client1.com:8081/employeesResponse Headers里的Location:http://ssoserver.com:8080/login.html

GIF 2022-8-8 19-58-02
GIF 2022-8-8 19-58-02
6、登录系统添加接口并测试

gulimall-test-sso-server模块的com.atguigu.gulimall.ssoserver.controller.LoginController类里添加 loginPage方法,跳转到登录页

@GetMapping("/login.html")
public String loginPage() {
    return "login";
}
image-20220808195103772
image-20220808195103772

启动GulimallTestSsoServerApplication服务

访问http://client1.com/8081/employees页面,可以看到可以正确跳转到http://ssoserver.com:8080/login.html

GIF 2022-8-8 20-29-31
GIF 2022-8-8 20-29-31

我发现一个很玄学的事情,明明url是对的,但就是访问不了,只有上一个访问成功了才能访问的到

GIF 2022-8-8 20-11-42
GIF 2022-8-8 20-11-42

😰

GIF 2022-8-8 20-18-38
GIF 2022-8-8 20-18-38
7、登录成功后返回

可以像这样,重定向的时候指定redirect_url告诉登录服务登录成功后要跳转的url,这样登录服务器就知道要返回到哪个地址了

http://ssoserver.com:8080/login.html?redirect_url=http://client1.com/8081/employees
image-20220808202029647
image-20220808202029647

修改gulimall-test-sso-client模块的src/main/java/com/atguigu/gulimall/ssoclient/controller/HelloController.java类的employees方法

/**
 * 必须登录才可访问
 * @param model
 * @return
 */
@GetMapping("/employees")
public String employees(Model model, HttpSession session, HttpServletRequest request){
    Object loginUser = session.getAttribute("loginUser");
    if (loginUser==null){
        //通过该方法获取到的是: http://127.0.0.1:8081/employees
        System.out.println(request.getRequestURL());
        //如果没登录,就跳转到登录服务器进行登录
        return "redirect:" + serverUrl+"?redirect_url=http://client1.com:8081/employees";
    }
    List<String> emps = new ArrayList<>();
    emps.add("张三");
    emps.add("李四");
    model.addAttribute("emps",emps);
    return "list";
}
image-20220808205501228
image-20220808205501228

修改gulimall-test-sso-server模块src/main/java/com/atguigu/gulimall/ssoserver/controller/LoginController.java类的loginPage方法,向model里添加url

@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url, Model model) {
    model.addAttribute("url",url);
    return "login";
}
image-20220808204153711
image-20220808204153711

gulimall-test-sso-server模块的src/main/resources/templates/login.html文件里引入thymeleaf,添加隐藏的<input>框,指定登陆成功跳转的url

<!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>
image-20220808204329424
image-20220808204329424

gulimall-test-sso-server模块的com.atguigu.gulimall.ssoserver.controller.LoginController类里,添加"/doLogin方法,用于处理表单登录请求

@PostMapping("/doLogin")
public String doLogin(String username,String password,String url) {
    if (StringUtils.hasText(username) && StringUtils.hasText(password) && StringUtils.hasText(url)){
        //如果username、password、url都不为空,就返回之前页
        return "redirect:" + url;
    }
    //如果其中一个为空,就跳转到登录页
    return "login";
}
image-20220808204955764
image-20220808204955764

重启gulimall-test-sso-client模块和gulimall-test-sso-server模块,打开如下两个页面,进行测试,发现输入用户名和密码后,貌似没有跳转成功

http://127.0.0.1:8081/employees
http://ssoserver.com:8080/login.html?redirect_url=http://client1.com:8081/employees
GIF 2022-8-8 20-56-34
GIF 2022-8-8 20-56-34

debug的方式启动gulimall-test-sso-server模块,可以看到跳转成功了,只不过判断session里面没有,又跳转过来了

GIF 2022-8-8 21-02-06
GIF 2022-8-8 21-02-06
8、引入redis

可以在gulimall-test-sso-server模块的pom.xml文件里引入redis

<!--引入redis-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
image-20220808210826601
image-20220808210826601

gulimall-test-sso-server模块的src/main/resources/application.properties配置文件里指定redis的host

spring.redis.host=192.168.56.10
image-20220808210931644
image-20220808210931644

修改gulimall-test-sso-server模块的com.atguigu.gulimall.ssoserver.controller.LoginController类的doLogin方法

@Autowired
StringRedisTemplate stringRedisTemplate;

@PostMapping("/doLogin")
public String doLogin(String username, String password, String url) {
    if (StringUtils.hasText(username) && StringUtils.hasText(password) && StringUtils.hasText(url)) {
        //如果username、password、url都不为空,就返回之前页
        String uuid = UUID.randomUUID().toString().replace("-", "");
        stringRedisTemplate.opsForValue().set(uuid,username);
        return "redirect:" + url+"?token="+uuid;
    }
    //如果其中一个为空,就跳转到登录页
    return "login";
}
image-20220808211702719
image-20220808211702719

修改gulimall-test-sso-client模块com.atguigu.gulimall.ssoclient.controller.HelloController类的 employees方法

/**
 * 必须登录才可访问
 * 只要带了token,就认为这次是在ssoserver登录成功跳回来的。(当然应该在redis里查一下,这里就不做了)
 * @param model
 * @return
 */
@GetMapping("/employees")
public String employees(Model model, HttpSession session,
                        @RequestParam(value = "token",required = false)String token){
    Object loginUser = session.getAttribute("loginUser");
    if (StringUtils.hasText(token)){
        //去ssoserver登录成功跳回来就会带.上
        //TODO 1、去ssoserver获取当前token真正对应的用户信息
        session.setAttribute("loginUser","张三");
        loginUser = "张三";
    }
    if (loginUser==null){
        //通过该方法获取到的是: http://127.0.0.1:8081/employees
        //HttpServletRequest request=>System.out.println(request.getRequestURL());
        //如果没登录,就跳转到登录服务器进行登录
        return "redirect:" + serverUrl+"?redirect_url=http://client1.com:8081/employees";
    }
    List<String> emps = new ArrayList<>();
    emps.add("张三");
    emps.add("李四");
    model.addAttribute("emps",emps);
    return "list";
}
image-20220808213356872
image-20220808213356872

清空cookie 访问 http://client1.com:8081/employees 自动跳转到 http://ssoserver.com:8080/login.html?redirect_url=http://client1.com:8081/employees 页面,登陆成功后返回到了 http://client1.com:8081/employees 页面,并携带了token

GIF 2022-8-8 21-35-59
GIF 2022-8-8 21-35-59

但是这样只能在当前域名下session有效,在另一个域名下还需要再次登录

9、多域名登录

复制一个gulimall-test-sso-client模块,起名为gulimall-test-sso-client2,和gulimall-test-sso-client模块差不多,就artifactIdname不一样

<groupId>com.atguigu</groupId>
<artifactId>gulimall-test-sso-client2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-test-sso-client2</name>
<description>单点登录的客户端</description>
image-20220808214604391
image-20220808214604391

修改gulimall-test-sso-client2模块com.atguigu.gulimall.ssoclient.controller.HelloController类的employees,将映射地址修改为/boss,修改重定向地址redirect_url参数

/**
 * 必须登录才可访问
 * 只要带了token,就认为这次是在ssoserver登录成功跳回来的。(当然应该在redis里查一下,这里就不做了)
 * @param model
 * @return
 */
@GetMapping("/boss")
public String employees(Model model, HttpSession session,
                        @RequestParam(value = "token",required = false)String token){
    Object loginUser = session.getAttribute("loginUser");
    if (StringUtils.hasText(token)){
        //去ssoserver登录成功跳回来就会带.上
        //TODO 1、去ssoserver获取当前token真正对应的用户信息
        session.setAttribute("loginUser","张三");
        loginUser = "张三";
    }
    if (loginUser==null){
        //通过该方法获取到的是: http://127.0.0.1:8081/employees
        //HttpServletRequest request=>System.out.println(request.getRequestURL());
        //如果没登录,就跳转到登录服务器进行登录
        return "redirect:" + serverUrl+"?redirect_url=http://client2.com:8082/boss";
    }
    List<String> emps = new ArrayList<>();
    emps.add("张三");
    emps.add("李四");
    model.addAttribute("emps",emps);
    return "list";
}
image-20220809091826538
image-20220809091826538

修改gulimall-test-sso-client2模块的src/main/resources/application.properties配置文件,让其在8082端口启动

server.port=8082
image-20220808214722475
image-20220808214722475

gulimall-test-sso-client2模块的启动类从GulimallTestSsoClientApplication修改为GulimallTestSsoClient2Application

image-20220808214909403
image-20220808214909403

修改gulimall-test-sso-server模块com.atguigu.gulimall.ssoserver.controller.LoginController类的doLogin方法,只要有人登录了,就给gulimall-test-sso-server服务留一个cookie

@PostMapping("/doLogin")
public String doLogin(String username, String password, String url, HttpServletResponse response) {
    if (StringUtils.hasText(username) && StringUtils.hasText(password) && StringUtils.hasText(url)) {
        //如果username、password、url都不为空,就返回之前页
        String uuid = UUID.randomUUID().toString().replace("-", "");
        stringRedisTemplate.opsForValue().set(uuid,username);
        Cookie cookie = new Cookie("sso_token", uuid);
        response.addCookie(cookie);
        return "redirect:" + url+"?token="+uuid;
    }
    //如果其中一个为空,就跳转到登录页
    return "login";
}
image-20220808215406835
image-20220808215406835

可以看到Request URL:请求的urlhttp://ssoserver.com:8080/doLogin,重定向Location:到了http://client1.com:8081/employees?token=9b5fa7861a55409d9d8c6247edcf66a3,还Set-Cookie:了一个sso_token=9b5fa7861a55409d9d8c6247edcf66a3。即在http://ssoserver.com域名下,保存了一个叫sso_tokencookie

GIF 2022-8-9 9-09-10
GIF 2022-8-9 9-09-10

在访问http://client2.com:8082/boss的时候,重定向到了http://ssoserver.com:8080/login.html?redirect_url=http://client2.com:8082/employees,在请求http://ssoserver.com:8080/login.html?redirect_url=http://client2.com:8082/employees的时候还带上了cookiesso_token=4560fbf660304f49b66182214c3ebbd2

那就说明在别的系统下这个用户已经登陆了

gulimall-test-sso-server模块的com.atguigu.gulimall.ssoserver.controller.LoginController类的loginPage方法里添加判断,如果有token了,说明已经登陆了,直接重定向到别的系统即可

@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url, Model model,
                        @CookieValue(value = "sso_token",required = false) String ssoToken) {
    if (StringUtils.hasText(ssoToken)){
        //说明之前有人登录过,浏览器留下了痕迹
        return "redirect:" + url+"?token="+ssoToken;
    }
    model.addAttribute("url", url);
    return "login";
}
image-20220809093719928
image-20220809093719928

可以看到,当http://client1.com:8081/employees登录后,http://client2.com:8082/boss就不再需要登陆了

GIF 2022-8-9 9-50-27
GIF 2022-8-9 9-50-27

gulimall-test-sso-client模块的src/main/resources/templates/list.html文件里添加欢迎信息

<h1>欢迎: [[${session.loginUser}]]</h1>
image-20220809100314882
image-20220809100314882

gulimall-test-sso-server模块的src/main/java/com/atguigu/gulimall/ssoserver/controller/LoginController.java类里添加如下接口

@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token){
    return stringRedisTemplate.opsForValue().get(token);
}
image-20220809100343807
image-20220809100343807

gulimall-test-sso-client模块的com.atguigu.gulimall.ssoclient.controller.HelloController类里修改employees方法,向gulimall-test-sso-server登录服务根据token查询用户信息

/**
 * 必须登录才可访问
 * 只要带了token,就认为这次是在ssoserver登录成功跳回来的。(当然应该在redis里查一下,这里就不做了)
 * @param model
 * @return
 */
@GetMapping("/employees")
public String employees(Model model, HttpSession session,
                        @RequestParam(value = "token",required = false)String token){
    Object loginUser = session.getAttribute("loginUser");
    if (StringUtils.hasText(token)){
        //去ssoserver登录成功跳回来就会带.上
        //TODO 1、去ssoserver获取当前token真正对应的用户信息
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> entity = restTemplate.getForEntity(
                "http://ssoserver.com:8080/userInfo?token={token}", String.class, token);
        String body = entity.getBody();
        session.setAttribute("loginUser",body);
        loginUser = "张三";
    }
    if (loginUser==null){
        //通过该方法获取到的是: http://127.0.0.1:8081/employees
        //HttpServletRequest request=>System.out.println(request.getRequestURL());
        //如果没登录,就跳转到登录服务器进行登录
        return "redirect:" + serverUrl+"?redirect_url=http://client1.com:8081/employees";
    }
    List<String> emps = new ArrayList<>();
    emps.add("张三");
    emps.add("李四");
    model.addAttribute("emps",emps);
    return "list";
}
image-20220809101133644
image-20220809101133644

gulimall-test-sso-client2模块src/main/resources/templates/list.html文件里添加欢迎信息

<h1>欢迎: [[${session.loginUser}]]</h1>
image-20220809101348952
image-20220809101348952

gulimall-test-sso-client2模块的com.atguigu.gulimall.ssoclient.controller.HelloController类里修改employees方法,向gulimall-test-sso-server登录服务根据token查询用户信息

/**
 * 必须登录才可访问
 * 只要带了token,就认为这次是在ssoserver登录成功跳回来的。(当然应该在redis里查一下,这里就不做了)
 * @param model
 * @return
 */
@GetMapping("/boss")
public String employees(Model model, HttpSession session,
                        @RequestParam(value = "token",required = false)String token){
    Object loginUser = session.getAttribute("loginUser");
    if (StringUtils.hasText(token)){
        //去ssoserver登录成功跳回来就会带.上
        //TODO 1、去ssoserver获取当前token真正对应的用户信息
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> entity = restTemplate.getForEntity(
                "http://ssoserver.com:8080/userInfo?token={token}", String.class, token);
        String body = entity.getBody();
        session.setAttribute("loginUser",body);
        loginUser = "张三";
    }
    if (loginUser==null){
        //通过该方法获取到的是: http://127.0.0.1:8081/employees
        //HttpServletRequest request=>System.out.println(request.getRequestURL());
        //如果没登录,就跳转到登录服务器进行登录
        return "redirect:" + serverUrl+"?redirect_url=http://client2.com:8082/boss";
    }
    List<String> emps = new ArrayList<>();
    emps.add("张三");
    emps.add("李四");
    model.addAttribute("emps",emps);
    return "list";
}
image-20220809101301993
image-20220809101301993

http://client1.com:8081/employees网址下登录后

ssoserver.com域名留下了一个键为sso_token、值为80d06b51f1fb4fce9e93e2f50e256b38cookie

访问http://client2.com:8082/boss也无需登录http://client2.com:8082/boss?token=80d06b51f1fb4fce9e93e2f50e256b38

GIF 2022-8-9 10-15-23
GIF 2022-8-9 10-15-23

至此就完成了简单的多系统的单点登录功能

5.7.8、购物车模块

1、初始化购物车模块

1、新建购物车模块

新建一个模块,Group输入com.atguiguArtifact输入gulimall-cartType选择MavenLanguage选择JavaPackaging选择JarJava Version选择8Version不用管,Name输入gulimall-cartDespcription输入购物车Package输入com.atguigu.gulimall.cart,然后点击Next

com.atguigu
gulimall-cart
购物车
com.atguigu.gulimall.cart
image-20220809102536538
image-20220809102536538

选择Spring Boot DevToolsSpring WebThymeleafOpenFeign,然后点击Next

image-20220809102707386
image-20220809102707386

最后点击Finish

image-20220809102724824
image-20220809102724824
2、修改代码

同样的,以下内容不替换(properties里面的要替换)(最好先替换,再把它作为maven项目添加到项目,要不然替换的时候上方的文件名会显示pom.xm(<artifactId>里的内容)(并不是pom.xml(模块名)),如果你复制的是gulimall-product模块的,有可能当前模块(gulimall-cart模块)显示为pom.xml(gulimall-product),然后你就怀疑是不是错把gulimall-product模块的pom.xml文件替换掉了)

<groupId>com.atguigu</groupId>
<artifactId>gulimall-cart</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-cart</name>
<description>购物车</description>
<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.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>
image-20220809103832773
image-20220809103832773

修改gulimall-cart模块com.atguigu.gulimall.cart.GulimallCartApplicationTests测试类的代码,并测试看是否报错

package com.atguigu.gulimall.cart;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallCartApplicationTests {

   @Test
   public void contextLoads() {
      System.out.println("hello");
   }

}
image-20220809104027237
image-20220809104027237
3、添加配置

添加cart.gulimall.com域名,将cart.gulimall.com域名的ip设置为虚拟机的ip

# gulimall
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
192.168.56.10 auth.gulimall.com
192.168.56.10 cart.gulimall.com
image-20220809104147788
image-20220809104147788

2.分布式高级篇(微服务架构篇)\资料源码\代码\html\购物车里的cartList.htmlsuccess.html复制到gulimall-cart模块的src/main/resources/templates里面

image-20220809104535897
image-20220809104535897

linux虚拟机/mydata/nginx/html/static目录下新建cart目录,把2.分布式高级篇(微服务架构篇)\资料源码\代码\html\购物车里的文件夹复制到/mydata/nginx/html/static/cart目录下

GIF 2022-8-9 10-47-05
GIF 2022-8-9 10-47-05
4、修改页面

修改gulimall-cart模块的src/main/resources/templates/cartList.html文件,将href="./全部替换为href="/static/cart/,将src="./全部替换为src="/static/cart/

href="./
href="/static/cart/

src="./
src="/static/cart/

点击查看完整页面

image-20220809105323489
image-20220809105323489

修改gulimall-cart模块的src/main/resources/templates/success.html文件,将href="./全部替换为href="/static/cart/,将src="./全部替换为src="/static/cart/

href="./
href="/static/cart/

src="./
src="/static/cart/

点击查看完整页面

image-20220809105425860
image-20220809105425860

2、添加配置并完善页面

1、添加配置

修改gulimall-cart模块的pom.xml文件,添加gulimall-common依赖

<dependency>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
image-20220809105640009
image-20220809105640009

gulimall-cart模块的com.atguigu.gulimall.cart.GulimallCartApplication类的@SpringBootApplication注解后面添加(exclude = DataSourceAutoConfiguration.class),排除数据源的自动配置

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
image-20220809105800301
image-20220809105800301

gulimall-cart模块的src/main/resources/application.properties配置文件里添加如下配置

server.port=30000

spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
image-20220809110042486
image-20220809110042486

gulimall-cart模块的com.atguigu.gulimall.cart.GulimallCartApplication类上添加如下注解,开启远程调用和服务发现

@EnableFeignClients
@EnableDiscoveryClient
image-20220809110006433
image-20220809110006433

gulimall-gateway模块的src/main/resources/application.yml配置文件里添加负载均衡到gulimall-cart模块的配置

spring:
  cloud:
    gateway:
      routes:
        - id: gulimall_cart_route
          uri: lb://gulimall-cart
          predicates:
            - Host=cart.gulimall.com
image-20220809110440063
image-20220809110440063
2、完善页面

gulimall-cart模块将src/main/resources/templates/success.html文件里的

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

修改为

<html xmlns="http://www.w3.org/1999/xhtml">

删去thymeleaf,然后把src/main/resources/templates/success.html文件重命名为index.html

image-20220809111431235
image-20220809111431235

启动GulimallGatewayApplication服务和GulimallCartApplication服务,已经可以成功访问

http://cart.gulimall.com/

image-20220809110921527
image-20220809110921527

在 http://cart.gulimall.com/ 页面里,打开控制台,选择谷粒商城首页所在的<a>标签

image-20220809111724257
image-20220809111724257

gulimall-cart模块的src/main/resources/templates/index.html文件里搜索谷粒商城首页,将该<a>标签的href的值修改为http://gulimall.com

<a href="http://gulimall.com">谷粒商城首页</a>
image-20220809111752124
image-20220809111752124

在 http://cart.gulimall.com/ 页面里,打开控制台,选择logo所在的<img>标签,复制/static/cart/img/logo1.jpg

image-20220809111822473
image-20220809111822473

gulimall-cart模块的src/main/resources/templates/index.html文件里搜索/static/cart/img/logo1.jpg,将该<img>标签外面的<a>标签的href的值修改为http://gulimall.com

<a href="http://gulimall.com"><img src="/static/cart/img/logo1.jpg"  style="height: 60px;width:180px;"  /></a>
image-20220809111906922
image-20220809111906922

重启gulimall-cart模块后,在 http://cart.gulimall.com/ 页面里,点击谷粒商城首页logo都能跳转到首页

http://cart.gulimall.com/
http://gulimall.com/
GIF 2022-8-9 11-21-44
GIF 2022-8-9 11-21-44

3、购物车需求分析

1、购物车需求

1) 、需求描述:

  • 用户在登录状态

    • 将商品添加到购物车**【用户购物车/在线购物车】**

    • 放入数据库

    • mongodb

    • 放入redis(采用)

    • 登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车;

  • 用户在未登录状态

    • 将商品添加到购物车**【游客购物车/离线购物车/临时购物车】**

    • 放入 localstorage(客户端存储,后台不存)

    • cookie

    • WebSQL

    • 放入 redis(采用)

    • 浏览器即使关闭,下次进入,临时购物车数据都在

  • 其他功能

    • 用户可以使用购物车一起结算下单

    • 给购物车添加商品

    • 用户可以查询自己的购物车

    • 用户可以在购物车中修改购买商品的数量

    • 用户可以在购物车中删除商品

    • 选中/不选中商品

    • 在购物车中展示商品优惠信息

    • 提示购物车商品价格变化

2、购物车数据结构

分析:每一个购物项信息,都是一个对象,基本字段包括

{
    skuId: 2131241,
    check: true,
    title: "Apple iphone",
    defaultImage: "	",
    price: 4999,
    count: 1,
    totalPrice: 4999, 
    skuSaleVO: {...}
}

另外,购物车中不止一条数据,因此最终会是对象的数组。即:

[
{...},{...},{...}
]

类似于如下结构:

[
    {
        skuId: 2131241,
        check: true,
        title: "Apple iphone",
        defaultImage: "	",
        price: 4999,
        count: 1,
        totalPrice: 4999, 
        skuSaleVO: {...}
    },
    {
    	.....
	}
]

Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?

首先不同用户应该有独立的购物车,因此购物车应该以用户的作为key来存储,Value是用户的所有购物车信息。这样看来基本的k-v结构就可以了。

  • 但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是k-v结构,key 是商品idvalue才是这个商品的购物车信息。

综上所述,我们的购物车结构是一个双层 Map:Map<String,Map<String,String>>

  • 第一层 Map,Key 是用户 id

  • 第二层 Map,Key 是购物车中商品 id,值是购物项数据

java中,相当于如下结构

Map<String k1,Map<String k2,CartItemInfo>> 
k1:标识每一个用户的购物车
k2:购物项的商品id

在redis中

key:用户标识
value:Hash(k:商品id,v:购物项详情)
image-20220809115021313
image-20220809115021313

4、代码实现

1、添加vo

gulimall-cart模块的com.atguigu.gulimall.cart包里新建vo文件夹,在vo文件夹里新建CartItemVo类,用于存储购物车车里每一个商品项的数据

package com.atguigu.gulimall.cart.vo;

import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

/**
 * @author 无名氏
 * @date 2022/8/9
 * @Description: 购物车里的商品项
 */
@Data
public class CartItemVo {
    /**
     * sku的id
     */
    private Long skuId;
    /**
     * 是否选中(默认选中)
     */
    private Boolean check = true;
    /**
     * 商品的标题
     */
    private String title;
    /**
     * 商品的图片
     */
    private String image;
    /**
     * sku的属性(选中的 颜色、内存容量 等)
     */
    private List<String> skuAttr;
    /**
     * 商品的价格
     */
    private BigDecimal price;
    /**
     * 商品的数量
     */
    private Integer count;
    /**
     * 总价(商品价格*商品数量)
     */
    private BigDecimal totalPrice;

    /**
     * 计算总价
     * @return
     */
    public BigDecimal getTotalPrice() {
        return price.multiply(new BigDecimal(count));
    }
}
image-20220809155425051
image-20220809155425051

gulimall-cart模块的com.atguigu.gulimall.cart.vo包里新建CartVo类,用于存放购物车数据

package com.atguigu.gulimall.cart.vo;

import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.List;

/**
 * @author 无名氏
 * @date 2022/8/9
 * @Description: 购物车
 */
public class CartVo {
    /**
     * 商品项
     */
    private List<CartItemVo> items;
    /**
     * 商品数量(所有商品的count相加)
     */
    private Integer countNum;
    /**
     * 商品有几种类型(有几种不同的商品)
     */
    private Integer countType;
    /**
     * 商品总价(所有商品总价加起来)
     */
    private BigDecimal totalAmount;
    /**
     * 减免的价格
     */
    private BigDecimal reduce = BigDecimal.ZERO;

    public List<CartItemVo> getItems() {
        return items;
    }

    public void setItems(List<CartItemVo> items) {
        this.items = items;
    }

    public Integer getCountNum() {
        int countNum = 0;
        if (!CollectionUtils.isEmpty(items)){
            for (CartItemVo item : items) {
                if (item.getCheck()){
                    countNum+=item.getCount();
                }
            }
        }
        return countNum;
    }

    public Integer getCountType() {
        int countType = 0;
        if (!CollectionUtils.isEmpty(items)){
            for (CartItemVo item : items) {
                if (item.getCheck()){
                    countType++;
                }
            }
        }
        return countType;
    }

    public BigDecimal getTotalAmount() {
        //购物总价
        BigDecimal totalAmount = BigDecimal.ZERO;
        if (!CollectionUtils.isEmpty(items)){
            for (CartItemVo item : items) {
                if (item.getCheck()){
                    totalAmount = totalAmount.add(item.getPrice());
                }
            }
        }
        //减去优惠
        totalAmount = totalAmount.subtract(getReduce());
        return totalAmount;
    }

    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}
image-20220809155447597
image-20220809155447597
2、添加redis依赖

gulimall-cart模块的pom.xml文件里添加redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
image-20220809155645296
image-20220809155645296

gulimall-cart模块的src/main/resources/application.properties文件里指定redis的host

spring.redis.host=192.168.56.10
image-20220809155727842
image-20220809155727842

gulimall-cart模块的com.atguigu.gulimall.cart包下新建service文件夹,在service文件夹下新建CartService接口

image-20220809155822937
image-20220809155822937
3、添加Spring Session依赖

gulimall-cart模块的pom.xml文件里添加Spring Session依赖

<!--引入SpringSession-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
image-20220809160358392
image-20220809160358392

gulimall-cart模块的com.atguigu.gulimall.cart包下新建config文件夹,复制gulimall-product模块的com.atguigu.gulimall.product.config.GulimallSessionConfig文件,到gulimall-cart模块的com.atguigu.gulimall.cart.config包下

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;

/**
 * @author 无名氏
 * @date 2022/8/7
 * @Description:
 */
@Configuration
public class GulimallSessionConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        // We customize the name of the cookie to be JSESSIONID.
        serializer.setCookieName("GULIMALL_JSESSIONID");
        serializer.setDomainName("gulimall.com");
        ////We customize the path of the cookie to be / (rather than the default of the context root).
        //serializer.setCookiePath("/");
        ////If the regular expression matches, the first grouping is used as the domain.
        ////This means that a request to https://child.example.com sets the domain to example.com.
        ////However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and,
        //// thus, still works in development without any changes being necessary for production.
        ////亲测不生效
        //serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }


}
image-20220809160809319
image-20220809160809319

gulimall-cart模块的com.atguigu.gulimall.cart.config.GulimallSessionConfig类上添加如下注解,开启Spring Session功能

@EnableRedisHttpSession
image-20220809161216130
image-20220809161216130
4、分析功能

功能描述:

  • 浏览器有一个cookie; user-key; 标识用户身份,一个月后过期;

  • 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;

  • 浏览器以后保存,每次访问都会带上这个cookie;

  • 登录: session有用户数据

  • 没登录:按照cookie里面带来user-key来做。

  • 第一次:如果没有临时用户,帮忙创建一个临时用户。

京东会给一个user-keycookie,有效期为一个月

image-20220809162152106
image-20220809162152106

登陆后还是会存在user-key

image-20220809162541563
image-20220809162541563
5、添加vo

gulimall-cart模块的com.atguigu.gulimall.cart包下新建controller文件夹,在controller文件夹下新建CartController

package com.atguigu.gulimall.cart.controller;

import com.atguigu.common.constant.auth.AuthServerConstant;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpSession;

/**
 * @author 无名氏
 * @date 2022/8/9
 * @Description:
 */
@Controller
public class CartController {

    /**
     * 浏览器有一个cookie; user-key; 标识用户身份,一个月后过期;
     * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
     * 浏览器以后保存,每次访问都会带上这个cookie;
     * 登录: session有用户
     * 没登录:按照cookie里面带来user-key来做。
     * 第一次:如果没有临时用户,帮忙创建一个临时用户。
     *
     * 去登录页的请求
     * @return
     */
    @GetMapping("/cart.html")
    public String cartListPage(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute==null){
            //获取临时购物车
        }else {
            //获取登录过的购物车
        }
        return "cartList";
    }
}
image-20220809163741299
image-20220809163741299

由于每次都要判断等没登录,所以可以使用拦截器

img
img

gulimall-common模块的com.atguigu.common.to包下新建UserInfoTo

package com.atguigu.common.to;

import lombok.Data;

/**
 * @author 无名氏
 * @date 2022/8/9
 * @Description:
 */
@Data
public class UserInfoTo {
    /**
     * 用户的id
     */
    private Long userId;
    /**
     * 用户的标识
     */
    private String userKey;
}
image-20220809190800927
image-20220809190800927

gulimall-common模块的com.atguigu.common.constant包下新建cart文件夹,在cart文件夹里新建CartConstant

package com.atguigu.common.constant.cart;

/**
 * @author 无名氏
 * @date 2022/8/9
 * @Description:
 */
public class CartConstant {
    /**
     * 临时或已登录用户的cookie名
     */
    public static final String TEMP_USER_COOKIE_NAME = "user-key";
}
image-20220809194222197
image-20220809194222197

5、ThreadLocal

1、添加拦截器

ThreadLocal可以在同一个线程之间共享数据

image-20220809195832706
image-20220809195832706

gulimall-cart模块的com.atguigu.gulimall.cart包先新建interceptor文件夹,在interceptor文件夹下新建CartInterceptor

package com.atguigu.gulimall.cart.interceptor;

import com.atguigu.common.constant.auth.AuthServerConstant;
import com.atguigu.common.constant.cart.CartConstant;
import com.atguigu.common.to.MemberEntityTo;
import com.atguigu.common.to.UserInfoTo;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;

/**
 * @author 无名氏
 * @date 2022/8/9
 * @Description:
 */
public class CartInterceptor implements HandlerInterceptor {

    /**
     * 把UserInfoTo的信息放到ThreadLocal里
     */
    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    /**
     * 在执行目标方法之前,判断用户的登录状态。并封装传递给controller目标请求
     * @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();
        //根据session判断当前用户是否登录,如果登录了就把用户id赋值给userInfoTo.userId
        HttpSession session = request.getSession();
        MemberEntityTo member =(MemberEntityTo) session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (member != null) {
            //用户登录
            userInfoTo.setUserId(member.getId());
        }
        //在cookies里寻找key为user-key的cookie,把该cookie的value放到userInfoTo.userKey里
        Cookie[] cookies = request.getCookies();
        if (cookies!=null && cookies.length>0){
            for (Cookie cookie : cookies) {
                if (CartConstant.TEMP_USER_COOKIE_NAME.equals(cookie.getName())) {
                    userInfoTo.setUserKey(cookie.getValue());
                    break;
                }
            }
        }
        //如果在cookies里没有找到key为user-key的cookie,就证明是第一次来到系统,或删除了cookie
        //如果没有临时用户,分配一个临时用户
        if (StringUtils.isEmpty(userInfoTo.getUserKey())){
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        //目标方法执行之前
        threadLocal.set(userInfoTo);
        return true;
    }
}
image-20220809201759825
image-20220809201759825

gulimall-cart模块的com.atguigu.gulimall.cart.config包下新建GulimallWebConfig配置类,指定该拦截器的拦截路径

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;

/**
 * @author 无名氏
 * @date 2022/8/9
 * @Description:
 */
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}
image-20220809200741968
image-20220809200741968

修改gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类的cartListPage方法

/**
 * 浏览器有一个cookie; user-key; 标识用户身份,一个月后过期;
 * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
 * 浏览器以后保存,每次访问都会带上这个cookie;
 * 登录: session有用户
 * 没登录:按照cookie里面带来user-key来做。
 * 第一次:如果没有临时用户,帮忙创建一个临时用户。
 *
 * 去登录页的请求
 * @return
 */
@GetMapping("/cart.html")
public String cartListPage(){

    //从ThreadLocal里得到用户信息
    UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    System.out.println(userInfoTo);
    return "cartList";
}
image-20220809210807657
image-20220809210807657
2、添加Cookie

gulimall-common模块的com.atguigu.common.constant.cart.CartConstant类里添加

如果没有keyuser-key的临时用户,还需要再向浏览器返回之前存放一个Cookie,并且指定过期时间TEMP_USER_COOKIE_TIMEOUT字段

/**
 * 临时或已登录用户的cookie的过期时间(30天)
 */
public static final int TEMP_USER_COOKIE_TIMEOUT = 60*60*24*30;
image-20220809202235176
image-20220809202235176

双击Shift,搜索Cookie,可以看到javax.servlet.http.Cookie里的Cookie的过期时间以为单位

/**
 * Sets the maximum age of the cookie in seconds.
 * <p>
 * A positive value indicates that the cookie will expire after that many
 * seconds have passed. Note that the value is the <i>maximum</i> age when
 * the cookie will expire, not the cookie's current age.
 * <p>
 * A negative value means that the cookie is not stored persistently and
 * will be deleted when the Web browser exits. A zero value causes the
 * cookie to be deleted.
 *
 * @param expiry
 *            an integer specifying the maximum age of the cookie in
 *            seconds; if negative, means the cookie is not stored; if zero,
 *            deletes the cookie
 * @see #getMaxAge
 */
public void setMaxAge(int expiry) {
    maxAge = expiry;
}
image-20220809202547741
image-20220809202547741

gulimall-common模块的com.atguigu.common.to.UserInfoTo类里添加tempUserCookie字段

/**
 * 是否有临时用户的cookie
 */
private boolean tempUserCookie = false;
image-20220809203431621
image-20220809203431621

gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的preHandle方法里的breake;上面添加userInfoTo.setTempUserCookie(true);

/**
 * 在执行目标方法之前,判断用户的登录状态。并封装传递给controller目标请求
 *
 * @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();
    //根据session判断当前用户是否登录,如果登录了就把用户id赋值给userInfoTo.userId
    HttpSession session = request.getSession();
    MemberEntityTo member = (MemberEntityTo) session.getAttribute(AuthServerConstant.LOGIN_USER);
    if (member != null) {
        //用户登录
        userInfoTo.setUserId(member.getId());
    }
    //在cookies里寻找key为user-key的cookie,把该cookie的value放到userInfoTo.userKey里
    Cookie[] cookies = request.getCookies();
    if (cookies != null && cookies.length > 0) {
        for (Cookie cookie : cookies) {
            if (CartConstant.TEMP_USER_COOKIE_NAME.equals(cookie.getName())) {
                userInfoTo.setUserKey(cookie.getValue());
                userInfoTo.setTempUserCookie(true);
                break;
            }
        }
    }
    //如果在cookies里没有找到key为user-key的cookie,就证明是第一次来到系统,或删除了cookie
    //如果没有临时用户,分配一个临时用户
    if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
        String uuid = UUID.randomUUID().toString();
        userInfoTo.setUserKey(uuid);
    }
    //目标方法执行之前
    threadLocal.set(userInfoTo);
    return true;
}
image-20220809204124231
image-20220809204124231

gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类里添加postHandle方法,向浏览器端写cookie

/**
 * 业务执行完后
 *
 * @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.isTempUserCookie()) {
        Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
        cookie.setPath("gulimall.com");
        cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
        response.addCookie(cookie);
    }
}
image-20220809204043771
image-20220809204043771

gulimall-common模块的com.atguigu.common.to.UserInfoTo类里tempUserCookie字段名总感觉那里怪怪的,按shift + f6重新改名为hasTempUserCookie

/**
 * 是否有临时用户的cookie
 */
private boolean hasTempUserCookie = false;
image-20220809204611574
image-20220809204611574

修改gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类里获取和修改tempUserCookie字段的代码,这样看就舒服多了

userInfoTo.setHasTempUserCookie(true);
userInfoTo.isHasTempUserCookie()
image-20220809204732994
image-20220809204732994

gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类里修改postHandle方法,在该方法的最后添加threadLocal.remove();,用于删除ThreadLocal,防止线程复用,获取到别的用户信息

/**
 * 业务执行完后,如果当前用户的cookies里没有user-key为键的cookie,就存放该cookie
 *
 * @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.isHasTempUserCookie()) {
        Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
        cookie.setPath("gulimall.com");
        cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
        response.addCookie(cookie);
    }
    //删除ThreadLocal,防止线程复用,获取到别的用户信息
    threadLocal.remove();
}
image-20220809205425490
image-20220809205425490
3、修改页面

在 http://item.gulimall.com/9.html 页面,打开控制台定位到立即预约 所在的<a>标签

image-20220809210002209
image-20220809210002209

gulimall-product模块的src/main/resources/templates/item.html文件里搜索立即预约,找到对应的<a>标签,将<a>标签的href属性的值修改为http://cart.gulimall.com/addToCart。将立即预约修改为加入购物车

<div class="box-btns-two">
   <a href="http://cart.gulimall.com/addToCart">
      <!--立即预约-->
      加入购物车
   </a>
</div>
image-20220809210342092
image-20220809210342092

gulimall-cart模块的src/main/resources/templates/index.html文件重新改名为success.html

image-20220809210503541
image-20220809210503541

gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类里添加/addToCart接口,用于添加到购物车

/**
 * 添加到购物车
 * @return
 */
@GetMapping("/addToCart")
public String addToCart(){
    return "success";
}
image-20220809211130318
image-20220809211130318

在 http://gulimall.com/ 页面,打开控制台,选择我的购物车对应的<a>标签,复制我的购物车

image-20220809211248869
image-20220809211248869

gulimall-product模块的src/main/resources/templates/index.html文件里,搜索我的购物车,将<a>标签的href的值修改为http://cart.gulimall.com/cart.html

<div class="header_gw">
  <img src="/static/index/img/img_15.png" />
  <span><a href="http://cart.gulimall.com/cart.html">我的购物车</a></span>
  <span>0</span>
</div>
image-20220809211431472
image-20220809211431472

启动GulimallMemberApplication服务、GulimallSearchApplication服务、GulimallGatewayApplication服务、GulimallProductApplication服务、GulimallAuthServerApplication服务、GulimallCartApplication服务

可以看到当浏览器中没有keyuser-keycookie时,会向浏览器写一个cookie(但是要放行多次)

GIF 2022-8-9 21-36-28
GIF 2022-8-9 21-36-28

可以看到当浏览器中有keyuser-keycookie时,不会替换掉原来的keyuser-keycookie(但是要放行多次)

GIF 2022-8-9 21-42-07
GIF 2022-8-9 21-42-07

放行多次多次就证明有多个请求,应该是拦截器拦截了不该拦截的静态资源请求引起的

image-20220809214722050
image-20220809214722050
4、修改代码

gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的postHandle方法的开头添加如下代码,如果不是拦截器直接返回

if (!(handler instanceof HandlerMethod)){
    return;
}
image-20220809215021156
image-20220809215021156

gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的preHandle方法的开头也添加类似代码,如果不是拦截器直接放行

if (!(handler instanceof HandlerMethod)){
    return true;
}
image-20220809215057482
image-20220809215057482

可以看到还是判断了多次,没有找到这些静态资源,触发了BasicErrorController(这里不用管,后面也用不到)

//执行了我们的Controller
public java.lang.String com.atguigu.gulimall.cart.controller.CartController.cartListPage()
//执行了错误视图的Controller
public org.springframework.http.ResponseEntity org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
GIF 2022-8-9 21-55-39
GIF 2022-8-9 21-55-39
5、修改页面

在 http://cart.gulimall.com/cart.html 页面里,打开控制台,选择购物车左边的<img>,复制img/logo1.jpg

image-20220810092737297
image-20220810092737297

gulimall-cart模块的src/main/resources/templates/cartList.html文件里,搜索img/logo1.jpg,找到对应的<img>标签,将<img>标签的src修改为/static/cart/img/logo1.jpg,将<img>标签外面的<a>标签的href属性的值修改为http://gulimall.com

<div class="one_top_left">
   <a href="http://gulimall.com" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a>
   <a href="#" class="one_left_link">购物车</a>
</div>
image-20220810093315148
image-20220810093315148

在 http://cart.gulimall.com/cart.html 页面里,打开控制台,选择首页对应的<a>,复制首页

image-20220810093426039
image-20220810093426039

gulimall-cart模块的src/main/resources/templates/cartList.html文件里,搜索首页,将首页对应的<a>标签的 href属性的值修改为http://gulimall.com

<ul class="header-left">
   <li>
      <a href="http://gulimall.com">首页</a>
   </li>

</ul>
image-20220810092839055
image-20220810092839055

gulimall-cart模块的src/main/resources/templates/cartList.html文件里,修改你好,请登录周围代码

<ul class="header-right">
   <li>
      <a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser?.nickname}]]</a>
      <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}">你好,请登录</a>
   </li>
   <li>
      <a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser==null}" class="li_2">免费注册</a>
   </li>
   <li class="spacer"></li>
   <li><a href="">我的订单</a></li>
   <li class="spacer"></li>
</ul>
image-20220810093624698
image-20220810093624698

gulimall-cart模块的src/main/resources/templates/success.html文件里,修改你好,请登录周围代码

<ul class="hd_wrap_right">
    <li>
        <a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser?.nickname}]]</a>
        <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}">你好,请登录</a>
    </li>
    <li>
        <a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser==null}" class="li_2">免费注册</a>
    </li>
    <li class="spacer"></li>
    <li>
        <a href="/javascript:;">我的订单</a>
    </li>
</ul>
image-20220810093815833
image-20220810093815833

重启gulimall-cart模块,可以看到cartList.html页面跳转没有什么问题

GIF 2022-8-10 9-42-31
GIF 2022-8-10 9-42-31

success.html页面除了您还没有登录!登录后购物车的商品将保存到您账号中 立即登录有问题外,其他的也没什么问题

GIF 2022-8-10 9-40-18
GIF 2022-8-10 9-40-18

在 http://cart.gulimall.com/addToCart 页面里,打开控制台,选择去购物车结算,复制对应<a>标签的id的值GotoShoppingCart

image-20220810094852763
image-20220810094852763

gulimall-cart模块的src/main/resources/templates/success.html文件里搜索GotoShoppingCart,将该该<a>标签的href的值修改为http://cart.gulimall.com/cart.html

<a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"
   id="GotoShoppingCart"><b></b>去购物车结算</a>
image-20220810094915539
image-20220810094915539

重启gulimall-cart模块,访问 http://cart.gulimall.com/addToCart 页面,点击去购物车结算,成功跳转到了 http://cart.gulimall.com/cart.html 页面

GIF 2022-8-10 9-50-55
GIF 2022-8-10 9-50-55

gulimall-cart模块的src/main/resources/templates/success.html页面里的th:href="'http://item.gmall.com:8084/'+${skuInfo?.id}+'.html'"修改为http://item.gulimall.com/9.html,先让其访问固定的商品

<div class="bg_shop">
    <a class="btn-tobback"
       href="http://item.gulimall.com/9.html">查看商品详情</a>
    <a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"
       id="GotoShoppingCart"><b></b>去购物车结算</a>
</div>
image-20220810095409271
image-20220810095409271

重启gulimall-cart模块,访问 http://cart.gulimall.com/addToCart 页面,点击查看商品详情

GIF 2022-8-10 9-54-54
GIF 2022-8-10 9-54-54

5.7.9、加入购物车

1、点击加入购物车进行跳转

1、前端跳转

gulimall-product模块的src/main/resources/templates/item.html文件里,将立即抢购外面的<a href="http://cart.gulimall.com/addToCart">修改为<a href="#" id="addToCart">

<div class="box-btns-two">
   <a href="#" id="addToCart">
      <!--立即预约-->
      加入购物车
   </a>
</div>
image-20220810095941269
image-20220810095941269

在 http://item.gulimall.com/9.html 页面里,选中加入购物车左边的数量输入框,复制value="1"

image-20220810100406207
image-20220810100406207

gulimall-product模块的src/main/resources/templates/item.html文件里,搜索value="1",将<input>输入框的id的值修改为numInput

<input type="text" name="" id="numInput" value="1" />
image-20220810100534991
image-20220810100534991

gulimall-product模块的src/main/resources/templates/item.html文件里,搜索加入购物车,将外面的<a>标签添加自定义属性th:attr="skuId=${item.info.skuId}"用于获取skuId

<div class="box-btns-two">
   <a href="#" id="addToCart" th:attr="skuId=${item.info.skuId}">
      <!--立即预约-->
      加入购物车
   </a>
</div>
image-20220810101119082
image-20220810101119082

gulimall-product模块的src/main/resources/templates/item.html文件里的<script>标签里添加如下方法,用于点击加入购物车跳转到购物车对应的页面

$("#addToCart").click(function () {
   var skuId = $(this).attr("skuId");
   var num = $("#numInput").val();
   location.href = "http://cart.gulimall.com/addToCart?skuId="+skuId+"&num=" + num
   //禁用默认行为
   return false;
})
image-20220810101406205
image-20220810101406205

重启gulimall-product模块,在http://item.gulimall.com/9.html页面,可以看到已经拼装好数据到http://cart.gulimall.com/addToCart?skuId=9&num=4页面了

GIF 2022-8-10 10-14-28
GIF 2022-8-10 10-14-28

修改gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类的addToCart方法

@Autowired
CartService cartService;

/**
 * 添加到购物车
 *
 * @return
 */
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
                        @RequestParam("num") Integer num,
                        Model model) {

    CartItemVo cartItemVo = cartService.addToCart(skuId, num);
    model.addAttribute("item",cartItemVo);
    return "success";
}
image-20220810101957590
image-20220810101957590

gulimall-cart模块的src/main/resources/templates/success.html文件里,重新添加thymeleaf

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
image-20220810102332592
image-20220810102332592

gulimall-cart模块的src/main/resources/templates/success.html文件里,将th:替换为空,一定要选中W图标(表名这是一个单词,防止把width:60px里的th:替换掉了),然后点击Replace all替换所有

image-20220810102826028
image-20220810102826028

gulimall-cart模块的src/main/resources/templates/success.html文件里,修改商品已成功加入购物车下面的代码

<div class="mc success-cont">
    <div class="success-lcol">
        <div class="success-top"><b class="succ-icon"></b>
            <h3 class="ftx-02">商品已成功加入购物车</h3></div>
        <div class="p-item">
            <div class="p-img">
                <a href="/javascript:;" target="_blank"><img style="height: 60px;width:60px;"
                                                             th:src="${item?.image}"
                                                             ></a>
            </div>
            <div class="p-info">
                <div class="p-name">
                    <a th:href="'http://item.gulimall.com/'+${item?.skuId}+'.html'"
                       th:text="${item?.title}">TCL 55A950C 55英寸32核人工智能 HDR曲面超薄4K电视金属机身(枪色)</a>
                </div>
                <div class="p-extra"><span class="txt" th:text="'数量:'+${item.count}">  数量:1</span></div>
            </div>
            <div class="clr"></div>
        </div>
    </div>
    <div class="success-btns success-btns-new">
        <div class="success-ad">
            <a href="/#none"></a>
        </div>
        <div class="clr"></div>
        <div class="bg_shop">
            <a class="btn-tobback"
               th:href="'http://item.gulimall.com/'+${item?.skuId}+'.html'">查看商品详情</a>
            <a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"
               id="GotoShoppingCart"><b></b>去购物车结算</a>
        </div>
    </div>
</div>
image-20220810103431907
image-20220810103431907
2、后端实现功能

gulimall-cart模块的com.atguigu.gulimall.cart.service.CartService接口里添加addToCart方法

CartItemVo addToCart(Long skuId, Integer num);
image-20220810103644997
image-20220810103644997

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里实现addToCart方法

@Autowired
StringRedisTemplate stringRedisTemplate;

private static final String CART_PREFIX = "gulimall:cart:";

@Override
public CartItemVo addToCart(Long skuId, Integer num) {
    CartItemVo vo = new CartItemVo();
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    //获取sku基本信息

    return vo;
}

/**
 * 获取要操作的购物车
 * @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 {
        // gulimall:cart:6a642344-003e-4ac1-bea1-27260c5c75c3
        cartKey = CART_PREFIX + userInfoTo.getUserKey();
    }
    //stringRedisTemplate.opsForHash().get(cartKey,"1");
    return stringRedisTemplate.boundHashOps(cartKey);
}
image-20220810110241522
image-20220810110241522

gulimall-product模块的com.atguigu.gulimall.product.controller.SkuInfoController#info方法可以获取sku的详细信息

  /**
   * 信息
   */
  @RequestMapping("/info/{skuId}")
      public R info(@PathVariable("skuId") Long skuId){
SkuInfoEntity skuInfo = skuInfoService.getById(skuId);

      return R.ok().put("skuInfo", skuInfo);
  }
image-20220810110023970
image-20220810110023970

gulimall-cart模块com.atguigu.gulimall.cart包下新建feign文件夹,在feign文件夹里新建ProductFeignService类,用于调用远程的商品模块

package com.atguigu.gulimall.cart.feign;

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author 无名氏
 * @date 2022/8/10
 * @Description:
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {

    @RequestMapping("/product/skuinfo/info/{skuId}")
    public R info(@PathVariable("skuId") Long skuId);
}
image-20220810110443729
image-20220810110443729

gulimall-common模块com.atguigu.common.to包下新建SkuInfoEntityTo

复制gulimall-product模块的com.atguigu.gulimall.product.entity.SkuInfoEntity类的字段,粘贴到SkuInfoEntityTo类里,并实现Serializable、在类上添加@Data注解

package com.atguigu.common.to;

import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;

@Data
public class SkuInfoEntityTo implements Serializable {
   /**
    * skuId
    */
   private Long skuId;
   /**
    * spuId
    */
   private Long spuId;
   /**
    * sku名称
    */
   private String skuName;
   /**
    * sku介绍描述
    */
   private String skuDesc;
   /**
    * 所属分类id
    */
   private Long catalogId;
   /**
    * 品牌id
    */
   private Long brandId;
   /**
    * 默认图片
    */
   private String skuDefaultImg;
   /**
    * 标题
    */
   private String skuTitle;
   /**
    * 副标题
    */
   private String skuSubtitle;
   /**
    * 价格
    */
   private BigDecimal price;
   /**
    * 销量
    */
   private Long saleCount;

}
image-20220810110959180
image-20220810110959180
3、使用线程池

由于要查sku基本信息sku组合信息,因此可以使用线程池加快执行速度

复制gulimall-product模块的com.atguigu.gulimall.product.config.MyThreadConfig类和com.atguigu.gulimall.product.config.ThreadPollConfigProperties类,粘贴到gulimall-cart模块的com.atguigu.gulimall.cart.config

MyThreadConfig类的代码如下:

image-20220810112208046
image-20220810112208046

ThreadPollConfigProperties类的代码如下

image-20220810112215617
image-20220810112215617

复制gulimall-product模块的src/main/resources/application.properties配置文件里关于线程池的配置,粘贴到gulimall-cart模块的src/main/resources/application.properties

gulimall.thread.core-pool-size=20
gulimall.thread.maximum-pool-size=200
gulimall.thread.keep-alive-time=10
image-20220810112211009
image-20220810112211009

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里修改addToCart方法

@Autowired
ThreadPoolExecutor executor;

@Override
public CartItemVo addToCart(Long skuId, Integer num) {
    CartItemVo cartItemVo = new CartItemVo();
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    //获取sku基本信息
    CompletableFuture<Void> skuInfoFuture = CompletableFuture.runAsync(() -> {
        R r = productFeignService.info(skuId);
        Object info = r.get("skuInfo");
        SkuInfoEntityTo infoEntityTo = new SkuInfoEntityTo();
        BeanUtils.copyProperties(info, infoEntityTo);
        //第一次添加,默认选中
        cartItemVo.setCheck(true);
        //第一次添加,数量都为1
        cartItemVo.setCount(num);
        cartItemVo.setImage(infoEntityTo.getSkuDefaultImg());
        cartItemVo.setTitle(infoEntityTo.getSkuTitle());
        cartItemVo.setSkuId(infoEntityTo.getSkuId());
        cartItemVo.setPrice(infoEntityTo.getPrice());
    },executor);

    //远程查询sku组合信息

    return cartItemVo;
}
image-20220810144737624
image-20220810144737624
4、获取销售属性值

测试将商品模块sku销售属性值使用符号连接起来的sql

select concat(attr_name,":",attr_value) from pms_sku_sale_attr_value where sku_id = 1
image-20220810114148564
image-20220810114148564

gulimall-product模块com.atguigu.gulimall.product.controller.SkuSaleAttrValueController类里新建getSkuSaleAttrValues方法

@GetMapping("/stringlist/{skuId}")
public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId) {
      return  skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
}
image-20220810114330547
image-20220810114330547

gulimall-product模块的com.atguigu.gulimall.product.service.SkuSaleAttrValueService接口里添加getSkuSaleAttrValuesAsStringList抽象方法

List<String> getSkuSaleAttrValuesAsStringList(Long skuId);
image-20220810114401389
image-20220810114401389

gulimall-product模块的com.atguigu.gulimall.product.service.impl.SkuSaleAttrValueServiceImpl类里实现getSkuSaleAttrValuesAsStringList方法

@Override
public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {
    return this.baseMapper.getSkuSaleAttrValuesAsStringList(skuId);
}
image-20220810114559675
image-20220810114559675

gulimall-product模块的com.atguigu.gulimall.product.dao.SkuSaleAttrValueDao接口里添加getSkuSaleAttrValuesAsStringList抽象方法

List<String> getSkuSaleAttrValuesAsStringList(Long skuId);
image-20220810114658076
image-20220810114658076

gulimall-product模块的src/main/resources/mapper/product/SkuSaleAttrValueDao.xml文件里添加查询销售属性的sql

<select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
    select concat(attr_name,":",attr_value) from gulimall_pms.pms_sku_sale_attr_value where sku_id = #{skuId}
</select>
image-20220810114914847
image-20220810114914847

gulimall-cart模块的com.atguigu.gulimall.cart.feign.ProductFeignService接口里添加方法,远程获取销售属性值

@GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
image-20220810115041334
image-20220810115041334

修改gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的addToCart方法,在后面添加如下代码

//远程查询sku组合信息
CompletableFuture<Void> getSkuSaleAttrValuesFuture = CompletableFuture.runAsync(() -> {
    List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
    cartItemVo.setSkuAttr(skuSaleAttrValues);
}, executor);
CompletableFuture.allOf(skuInfoFuture,getSkuSaleAttrValuesFuture).get();

cartOps.put(skuId.toString(), JSON.toJSON(cartItemVo));
image-20220810145118295
image-20220810145118295

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的addToCart方法,gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类的addToCart方法声明可能会抛出的异常

GIF 2022-8-10 14-54-00
GIF 2022-8-10 14-54-00
5、解决bug

将9号商品添加到购物车 来到http://cart.gulimall.com/addToCart?skuId=9&num=1页面,发现报了json转换失败的异常

image-20220810150601590
image-20220810150601590

查看GulimallCartApplication服务的控制台可以看到报了空指针异常,这是因为price有可能没有

java.lang.NullPointerException: null
	at com.atguigu.gulimall.cart.vo.CartItemVo.getTotalPrice(CartItemVo.java:53) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_301]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_301]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_301]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_301]
	at com.alibaba.fastjson.util.FieldInfo.get(FieldInfo.java:484) ~[fastjson-1.2.47.jar:na]
	at com.alibaba.fastjson.serializer.FieldSerializer.getPropertyValue(FieldSerializer.java:148) ~[fastjson-1.2.47.jar:na]
image-20220810150312584
image-20220810150312584

修改gulimall-cart模块的com.atguigu.gulimall.cart.vo.CartItemVo类的getTotalPrice方法,当price为空时,返回0

/**
 * 计算总价
 * @return
 */
public BigDecimal getTotalPrice() {
    return price==null ? BigDecimal.ZERO : price.multiply(new BigDecimal(count));
}
image-20220810150416117
image-20220810150416117

重启gulimall-cart模块,刷新 http://cart.gulimall.com/addToCart?skuId=9&num=1 页面,json工具类又报了不能强转为字符串的异常

image-20220810150805090
image-20220810150805090

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的addToCart方法里的JSON.toJSON(cartItemVo)修改为JSON.toJSONString(cartItemVo)

image-20220810150840904
image-20220810150840904
6、封装数据

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的addToCart方法的Object info = r.get("skuInfo");cartItemVo.setSkuAttr(skuSaleAttrValues);return cartItemVo;上打断点

image-20220810152402432
image-20220810152402432

可以看到BeanUtils.copyProperties(info, infoEntityTo);属性对拷,没有拷过来,这是因为info的类型为LinkedHashMap,存放的是键值对,而不是属性,所以没有拷过来

image-20220810151431796
image-20220810151431796

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的addToCart方法的

SkuInfoEntityTo infoEntityTo = new SkuInfoEntityTo();
BeanUtils.copyProperties(info, infoEntityTo);

修改为

String jsonString = JSON.toJSONString(info);
SkuInfoEntityTo infoEntityTo = JSON.parseObject(jsonString, SkuInfoEntityTo.class);

重启gulimall-cart模块,可以看到此时已经把属性拷过来了

image-20220810152042784
image-20220810152042784

可以看到gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的addToCart方法返回的cartItemVo对象的数据已经全部封装成功了

image-20220810152851473
image-20220810152851473

查看redis,临时用户保存的keygulimall:cart:6a642344-003e-4ac1-bea1-27260c5c75c3

在 http://item.gulimall.com/9.html 页面点击加入购物车,再在redis中查看,gulimall:cart里查看,可以发现刚刚加入到购物车的数据已经保存到redis了

GIF 2022-8-10 15-33-04
GIF 2022-8-10 15-33-04

登录账号,将一个商品添加到购物车,可以看到保存的keygulimall:cart:7,也就是用户的id

GIF 2022-8-10 15-40-05
GIF 2022-8-10 15-40-05

这是购物车没有此商品的情况,还需要在redis里判断购物车是否有此商品,如果有此商品,只需修改其数量就行了

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里的addToCart方法里,

BoundHashOperations<String, Object, Object> cartOps = getCartOps();这一行下面添加

String s = (String) cartOps.get(skuId.toString());
if (StringUtils.hasText(s)){
    CartItemVo cartItemVo = JSON.parseObject(s, CartItemVo.class);
    cartItemVo.setCount(cartItemVo.getCount()+num);
    //修改count后,重新计算totalPrice(总价)
    cartItemVo.setTotalPrice(cartItemVo.getTotalPrice());
    cartOps.put(skuId.toString(), JSON.toJSONString(cartItemVo));
    return cartItemVo;
}

在把BoundHashOperations<String, Object, Object> cartOps = getCartOps();上面的CartItemVo cartItemVo = new CartItemVo();修改到添加的这几行代码的下面

image-20220810155647023
image-20220810155647023

重启gulimall-cart模块,在redis里查看,可以看到当一个商品已经加入过购物车后不会再次添加该商品,只会将数量价格都增加

GIF 2022-8-10 16-01-57
GIF 2022-8-10 16-01-57

2、向redis里添加购物车数据

1、防止用户频繁添加商品到购物车

为了防止不断刷新购物车,一直增加商品,可以处理完请求后,重定向到购物车页面(不在url上显示加入购物车的接口),这样刷新页面就不会一直增加商品。(当然,如果你把加入购物车的接口复制出来,一直访问这个接口,还是会一直增加商品)

修改gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类的addToCart方法,并添加addToCartSuccessPage方法

/**
 * 添加到购物车
 *
 * @return
 */
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
                        @RequestParam("num") Integer num,
                        RedirectAttributes attributes) throws ExecutionException, InterruptedException {

    cartService.addToCart(skuId, num);
    attributes.addAttribute("skuId",skuId);
    return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}

@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId, Model model){
    //重定向到成功页面。再次查询购物车数据即可
    CartItemVo item = cartService.getCartItem(skuId);
    model.addAttribute("item",item);
    return "success";
}
image-20220810163354908
image-20220810163354908

gulimall-cart模块的com.atguigu.gulimall.cart.service.CartService接口里的getCartItem抽象方法

/**
 * 获取购物车中某个购物项
 * @param skuId
 * @return
 */
CartItemVo getCartItem(Long skuId);
image-20220810162620312
image-20220810162620312

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里实现getCartItem抽象方法

@Override
public CartItemVo getCartItem(Long skuId) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    String s = (String) cartOps.get(skuId.toString());
    return JSON.parseObject(s,CartItemVo.class);
}
image-20220810162829969
image-20220810162829969

redis里的数据清空,将一个商品添加到购物车,可以看到再次刷新购物车页面( http://cart.gulimall.com/addToCartSuccess.html?skuId=5 ) 就已经不能添加商品了

GIF 2022-8-10 16-38-14
GIF 2022-8-10 16-38-14
2、解决bug

但是,如果没有输入没有的skuId,比如http://cart.gulimall.com/addToCartSuccess.html?skuId=10

就会报thymeleaf相关的错误

image-20220810164038312
image-20220810164038312

gulimall-cart模块的src/main/resources/templates/success.html文件的class="mc success-cont"<div>上添加th:if="${item!=null}"属性,当item不为空时才显示,并添加上item==null时要显示的数据

<div class="m succeed-box">
    <div th:if="${item!=null}" class="mc success-cont">
        <div class="success-lcol">
            <div class="success-top"><b class="succ-icon"></b>
                <h3 class="ftx-02">商品已成功加入购物车</h3></div>
            <div class="p-item">
                <div class="p-img">
                    <a href="/javascript:;" target="_blank"><img style="height: 60px;width:60px;"
                                                                 th:src="${item?.image}"
                                                                 ></a>
                </div>
                <div class="p-info">
                    <div class="p-name">
                        <a th:href="'http://item.gulimall.com/'+${item?.skuId}+'.html'"
                           th:text="${item?.title}">TCL 55A950C 55英寸32核人工智能 HDR曲面超薄4K电视金属机身(枪色)</a>
                    </div>
                    <div class="p-extra"><span class="txt" th:text="'数量:'+${item.count}">  数量:1</span></div>
                </div>
                <div class="clr"></div>
            </div>
        </div>
        <div class="success-btns success-btns-new">
            <div class="success-ad">
                <a href="/#none"></a>
            </div>
            <div class="clr"></div>
            <div class="bg_shop">
                <a class="btn-tobback"
                   th:href="'http://item.gulimall.com/'+${item?.skuId}+'.html'">查看商品详情</a>
                <a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"
                   id="GotoShoppingCart"><b></b>去购物车结算</a>
            </div>
        </div>
    </div>
    <div th:if="${item==null}" class="mc success-cont">
        <h2>购物车竟然是空的</h2>
        <a href="http://gulimall.com/">去购物</a>
    </div>
</div>
image-20220810164807495
image-20220810164807495

重启gulimall-cart模块,再次访问http://cart.gulimall.com/addToCartSuccess.html?skuId=10页面,此时就会显示购物车竟然是空的

image-20220810164833622
image-20220810164833622
3、获取整个购物车

修改gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类的cartListPage方法

/**
 * 浏览器有一个cookie; user-key; 标识用户身份,一个月后过期;
 * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份;
 * 浏览器以后保存,每次访问都会带上这个cookie;
 * 登录: session有用户
 * 没登录:按照cookie里面带来user-key来做。
 * 第一次:如果没有临时用户,帮忙创建一个临时用户。
 * <p>
 * 去登录页的请求
 *
 * @return
 */
@GetMapping("/cart.html")
public String cartListPage(Model model) {

    //从ThreadLocal里得到用户信息
    //UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    //System.out.println(userInfoTo);
    CartVo cartVo = cartService.getCart();
    model.addAttribute("cart",cartVo);
    return "cartList";
}
image-20220810165336649
image-20220810165336649

gulimall-cart模块com.atguigu.gulimall.cart.service.CartService接口里添加getCart抽象方法

/**
 * 获取整个购物车
 * @return
 */
CartVo getCart();
image-20220810165422521
image-20220810165422521

gulimall-cart模块com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里实现getCart方法

@Override
public CartVo getCart() {
    CartVo cartVo = new CartVo();
    UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
    String cartKey = "";
    if (userInfoTo.getUserId() != null) {
        //已登录
        //先判断临时购物车有没有数据
        String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
        List<CartItemVo> tempCartItems = getCartItems(tempCartKey);
        if (!CollectionUtils.isEmpty(tempCartItems)){
            //临时购物车有数据(合并到用户账户中)
            for (CartItemVo tempCartItem : tempCartItems) {
                addToCart(tempCartItem.getSkuId(),tempCartItem.getCount());
            }
            //删除redis里临时购物车的数据
            stringRedisTemplate.delete(tempCartKey);
        }
        // gulimall:cart:1
        cartKey = CART_PREFIX + userInfoTo.getUserId();
        List<CartItemVo> cartItems = getCartItems(cartKey);
        cartVo.setItems(cartItems);
    }else {
        //没登录
        // gulimall:cart:6a642344-003e-4ac1-bea1-27260c5c75c3
        cartKey = CART_PREFIX + userInfoTo.getUserKey();
        //获取临时购物车的所有购物项
        cartVo.setItems(getCartItems(cartKey));

    }

    return cartVo;
}

private List<CartItemVo> getCartItems(String cartKey) {
    BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps(cartKey);
    List<Object> values = hashOps.values();
    if (!CollectionUtils.isEmpty(values)){
        List<CartItemVo> items = values.stream().map(obj -> {
            String str = (String) obj;
             return JSON.parseObject(str, CartItemVo.class);
        }).collect(Collectors.toList());

        return items;
    }
    return null;
}
image-20220810190503779
image-20220810190503779

gulimall-cart模块com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里getCart方法,gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类的cartListPage方法 声明要抛出的异常

GIF 2022-8-10 19-02-31
GIF 2022-8-10 19-02-31

修改gulimall-cart模块的src/main/resources/templates/success.html文件里你好,请登录周围的代码

<ul class="hd_wrap_right">
    <li>
        <a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser?.nickname}]]</a>
        <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}">你好,请登录</a>
    </li>
    <li>
        <a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser==null}" class="li_2">免费注册</a>
    </li>
    <li class="spacer"></li>
    <li>
        <a href="/javascript:;">我的订单</a>
    </li>
</ul>
image-20220810185536347
image-20220810185536347

重启gulimall-cart模块,清空redis里的数据,登录后随便将一个商品添加到购物车,此时就可以看到已登录用户redis里购物车的数据了

GIF 2022-8-10 19-12-25
GIF 2022-8-10 19-12-25

清空 http://gulimall.com/ 里的cookie ,未登录的情况下随便将一个商品添加到购物车,此时就可以看到未登录的用户redis里也有购物车的数据

GIF 2022-8-10 19-13-38
GIF 2022-8-10 19-13-38

未登录的情况下再将一个不同的商品添加到购物车,此时就可以看到未登录的用户redis里购物车的数据增加了

GIF 2022-8-10 19-19-48
GIF 2022-8-10 19-19-48

此时再进行登录,再次查看redis里的数据,可以看到此时已经将临时购物车里的商品合并到用户的购物车里了

GIF 2022-8-10 19-22-53
GIF 2022-8-10 19-22-53
4、显示购物车中的数据

修改gulimall-cart模块的src/main/resources/templates/cartList.html文件

点击查看完整代码

image-20220810195414544
image-20220810195414544

重启gulimall-cart模块,登录后访问 http://cart.gulimall.com/cart.html 购物车页面,此时购物车数据已经显示出来了

image-20220810195212654
image-20220810195212654

3、增删查改商品

1、勾选/取消勾选商品

gulimall-cart模块的src/main/resources/templates/cartList.html文件里选没选中的<input>标签的class="check"修改为 class="itemCheck",并添加自定义属性th:attr="skuId=${item.skuId}"

<li><input type="checkbox" th:attr="skuId=${item.skuId}"  class="itemCheck" th:checked="${item.check}"></li>
image-20220810195948921
image-20220810195948921

gulimall-cart模块的src/main/resources/templates/cartList.html文件的<script>标签里添加方法,用于勾选或取消勾选商品

$(".itemCheck").click(function () {
   var skuId = $(this).attr("skuId");
   //用prop获取checked属性返回的是true或false
    var check = $(this).prop("checked");
    location.href = "http://cart.gulimall.com/checkItem?skuId=" + skuId +"&check=" + (check?1:0);
})
image-20220810204135752
image-20220810204135752
2、添加checkItem方法

gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类里添加checkItem方法

@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";
}
image-20220810203708568
image-20220810203708568

gulimall-cart模块的com.atguigu.gulimall.cart.service.CartService接口里添加checkItem抽象方法

/**
 * 勾选购物项
 * @param skuId
 * @param check
 */
void checkItem(Long skuId, Integer check);
image-20220810201847437
image-20220810201847437

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里实现checkItem方法

/**
 * 修改购物车中商品的选中状态
 * @param skuId
 * @param check
 */
@Override
public void checkItem(Long skuId, Integer check) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    CartItemVo cartItem = getCartItem(skuId);
    cartItem.setCheck(check == 1);
    //存放到redis
    cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}
image-20220810203451501
image-20220810203451501

重启gulimall-cart模块,取消选中购物车中的商品,此时redis里对应用户的购物车里该商品的check属性变为了false,再次选中该购物车的该商品,此时redis里对应用户的购物车里该商品的check属性又变为了false

GIF 2022-8-10 20-42-49
GIF 2022-8-10 20-42-49
3、修改商品数量

gulimall-cart模块的src/main/resources/templates/cartList.html文件里,修改购物车里购物项的数量按钮对应的代码

<li>
    <p style="width: 80px" th:attr="skuId = ${item.skuId}">
        <span class="countOpsBtn">-</span>
        <span class="countOpsNum" th:text="${item.count}">5</span>
        <span class="countOpsBtn">+</span>
    </p>
</li>
image-20220810204946862
image-20220810204946862

gulimall-cart模块的src/main/resources/templates/cartList.html文件的<script>标签里添加如下代码

$(".countOpsBtn").click(function () {
   //$(this).parent():父元素
   var skuId = $(this).parent().attr("skuId");
   //find(".countOpsNum"):查找子元素
   var num = $(this).parent().find(".countOpsNum").text();
   location.href = "http://cart.gulimall.com/countItem?skuId=" + skuId +"&num=" + num;
})
image-20220810205713537
image-20220810205713537

gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类里添加countItem方法

@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";
}
image-20220810205938886
image-20220810205938886

gulimall-cart模块的com.atguigu.gulimall.cart.service.CartService接口里添加changeItemCount抽象方法

/**
 * 修改购物项(购物车里的商品)数量
 * @param skuId
 * @param num
 */
void changeItemCount(Long skuId, Integer num);
image-20220810210100509
image-20220810210100509

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里实现changeItemCount方法

@Override
public void changeItemCount(Long skuId, Integer num) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    CartItemVo cartItem = getCartItem(skuId);
    cartItem.setCount(num);
    //存放到redis
    cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
}
image-20220810210507186
image-20220810210507186

重启gulimall-cart模块,先查看购物车中第一件商品的数量,然后在 http://cart.gulimall.com/cart.html 页面里点击该商品数量的+号增加该商品数量,此时查看redis就可以看到该商品的数量增加了

GIF 2022-8-10 21-05-45
GIF 2022-8-10 21-05-45
4、删除商品

在 http://cart.gulimall.com/cart.html 页面里点击一个商品操作中的删除,此时会弹出一个删除商品对话框,,打开控制台,定位到删除商品对话框的删除,复制<button type="button">删除</button>

image-20220810210959478
image-20220810210959478

gulimall-cart模块的src/main/resources/templates/cartList.html文件里搜索<button type="button">删除</button>,找到对应的对话框的删除标签,给这个<button>标签添加onclick="deleteItem()"点击事件

<div>
   <button type="button" onclick="deleteItem()">删除</button>

</div>
image-20220810211209293
image-20220810211209293

gulimall-cart模块的src/main/resources/templates/cartList.html文件里搜索删除,找到对应的点击该删除弹出删除对话框的标签,修改为如下代码

<li>
    <p class="deleteItemBtn" th:attr="skuId=${item.skuId}">删除</p>
</li>
image-20220810211355262
image-20220810211355262

gulimall-cart模块的src/main/resources/templates/cartList.html文件里的<script>里添加如下代码,用于删除购物项

var deleteId = 0;
//删除购物项
function deleteItem() {
   location.href = "http://cart.gulimall.com/deleteItem?skuId=" + deleteId;
}

$(".deleteItemBtn").click(function (){
   deleteId = $(this).attr("skuId");
})
image-20220810232920265
image-20220810232920265

gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类里添加deleteItem方法

@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId){
    cartService.deleteItem(skuId);

    return "redirect:http://cart.gulimall.com/cart.html";
}
image-20220810232413642
image-20220810232413642

gulimall-cart模块的com.atguigu.gulimall.cart.service.CartService接口里添加deleteItem抽象方法

/**
 * 删除购物项(删除购物车里的一个商品)
 * @param skuId
 */
void deleteItem(Long skuId);
image-20220810232550529
image-20220810232550529

gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里实现deleteItem方法

@Override
public void deleteItem(Long skuId) {
    BoundHashOperations<String, Object, Object> cartOps = getCartOps();
    cartOps.delete(skuId.toString());
}
image-20220810232708946
image-20220810232708946

重启gulimall-cart模块,先在redis里查看购物车的数据,再在 http://cart.gulimall.com/cart.html 页面里删除一个商品,再次查看redis,此时刚刚删除的商品已经没有了

GIF 2022-8-11 15-32-03
GIF 2022-8-11 15-32-03

4、

redisson的无看门狗

springcache的加锁

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.0.0-alpha.8