谷粒学院16万字笔记+1600张配图(十四)——注册、登录

项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59

文章目录

demo14-注册、登录

1.用户登录业务介绍(单点登录)

1.1单一服务器模式

1.早期用的都是单一服务器。比如说我现在有一个程序,里面有登录在内的各种功能,然后我把这个程序部署到一台tomcat中,这种用一台服务器运行程序的方式就叫做单一服务器模式

2.单一服务器模式的缺点:单点性能压力,无法扩展

3.单一服务器模式下判断用户是否登录可以使用session对象实现:用户登录成功后,我们把用户数据放到session域对象中,这样的话判断用户是否登录时就可以从session中获取数据,如果可以获取数据那就是已登录,如果不能获取数据那就是未登录。但这种方式只适合单一服务器模式下使用,如果是分布式或集群,这样做会出问题的

4.解释一下为什么在分布式下使用session域对象会出问题:

在这里插入图片描述

用户在service_edu服务登录后,使用session.setAttribute("user",user)在service_edu服务的session域对象中存入用户数据,但是此时在线教育项目的其它项目(service_oss、service_vod、service_cms…)的session域对象中并没有存入该用户的数据,所以当这个用户明明已经在service_edu服务登录过了,但是访问在线教育项目的其它服务(service_oss、service_vod、service_cms…)时仍需登录,这就是问题

5.我们理想效果肯定是:用户在在线教育项目下的任何一个服务登录后,再访问该项目下的其它服务时都不再需要登录,这个理想效果有一个专业术语:单点登录

6.单点登录示例:我们在百度官网的任意一个服务(比如百度翻译)登录后,再进入其它服务(如百度文库、百度百科、百度贴吧…)都不再需要登录

在这里插入图片描述

1.2SSO(single sign on)模式

SSO模式和刚刚说的单点登录是一个意思,没任何区别

1.2.1单点登录三种常见方式

  • 使用session域对象的广播机制实现单点登录(现在用的不多了)
  • 使用cookie和redis实现单点登录(常用)
  • 使用token(令牌机制)实现单点登录(常用)

1.2.2session广播机制

1.session的广播机制说通俗点就叫session复制:当用户在service_edu服务登录后,我们先使用session.setAttribute("user",user)在service_edu服务的session域对象中存入用户数据,然后将service_edu服务的session对象复制到其他服务中

2.这种方式有一个致命的缺点:如果我们一个项目中有几十个模块,那么就需要将session对象复制几十次,这对资源是一个极大的消耗

1.2.3cookie+redis

cookie的特点:是一个客户端技术,浏览器每次发送请求都会带着cookie值进行发送;redis的特点是:基于key-value存储数据

具体实现:

1.在项目的任意一个模块进行登录后,把数据放到两个地方:redis和cookie

  • redis:按照一定规则(规则不是固定的,我们可以根据需求制定规则)生成唯一、随机的一个值,将这个值放到key中;将用户数据放到value中
  • cookie:把redis里面生成的key值放到cookie中

2.用户访问项目中其它模块时,发送请求会带着这个cookie值进行发送,然后我们获取到这个cookie值,拿着获取到的cookie值去redis中根据key进行查询,如果可以查询到对应的value就说明此时用户是登录状态

1.2.4token(令牌机制)

token是什么:按照一定规则(规则不是固定的,我们可以根据需求制定规则)生成的字符串,这个字符串中可以包含用户信息

比如说我们制定的规则是:ip+用户名+用户年龄,假设是192.1.1.1lucy22,然后我们将这串字符进行base64编码,再做一个加密,最后就得到了token字符串

具体实现:

1.在项目的任意一个模块进行登录后,我们按照一定规则生成一个token字符串,要求这个字符串中包含用户信息,然后将这个字符串进行返回(两种方式返回:把字符串通过cookie返回、把字符串通过地址栏返回)

通过地址栏返回的示例:

在这里插入图片描述

2.用户访问项目中其它模块时,每次访问在地址栏都带着生成的这个token字符串,我们得到地址栏中的这个token字符串,将这个字符串解码,然后就可以获取到用户信息

1.2.5过期时间

我们在javaweb阶段学过,session的默认过期时间是30分钟,其实上面的第二种方式(使用cookie+redis)和第三种方式(使用token)我们也可以设置过期时间:

  • 通过设置redis的过期时间来规定第二种方式的过期时间
  • 生成token时我们可以设置过期时间,后面会说到

2.整合JWT

2.1JWT是什么

在"1.2.4token(令牌机制)"我们说过了,我们按照一定规则生成token字符串,JWT就是给我们制定好了规则,我们使用JWT规则可以生成token字符串,且这个字符串中包含用户信息

2.2JWT的组成

在这里插入图片描述

JWT字符串由三部分组成:

第一部分(红色):jwt头信息

第二部分(紫色):有效载荷,含有用户信息(也可以说是主体信息)

第三部分(蓝色):签名哈希,通俗说就是字符串的防伪标志,通过这个可以判断这个字符串是我们自己根据JWT规则生成的还是别人伪造的

2.3引入依赖

因为jwt我们在后面做注册、登录或其它功能时会用到jwt,所以将依赖添加到common_utils模块中(别忘了刷新maven)

<dependencies>
    <!-- JWT-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>

在这里插入图片描述

2.4创建jwt工具类

1.jwt工具类不用自己会写,能够根据需求修改就可以了,我把jwt工具类放到了资料中

在这里插入图片描述

2.我们将jwt工具类复制到common_utils模块的commonutils包下

在这里插入图片描述

3.分析一下这个jwt工具类

在这里插入图片描述

  • 截图中第19行的public static final long EXPIRE = 1000 * 60 * 60 * 24;:我们定义一个常量,用于设置token过期时间
  • 截图中第20行的public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";:这是一个秘钥(这个秘钥是保密的,以后公司会给我们),我们后面生成token的第三部分(签名哈希)时会用到这个秘钥
  • 截图中第22行的getJwtToken方法:这是一个生成token字符串的方法,我们这里只给这个方法两个参数,分别是用户id和用户名称。接下来还会说这个方法内部的具体代码,理解后其实这个方法三个或四个或五个或其它任意数量参数,我们就都会修改方法内部的代码了
  • 截图中第25行的.setHeaderParam("typ", "JWT")和第26行的.setHeaderParam("alg", "HS256")共同作用,目的是设置token字符串的第一部分(jwt头信息),这是规定的,我们不需要修改,也不能修改
  • 截图中第27行的.setSubject("guli-user")、第28行的.setIssuedAt(new Date())、第29行的.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))共同作用,目的是设置token字符串的过期时间,其中第27行的参数guli-user可以随便写
  • 截图中第30行的.claim("id", id)和第31行的.claim("nickname", nickname)共同作用,目的是设置token字符串的第二部分(主体信息),用来存储用户信息。如果getJwtToken方法有第三个参数age,那么我们就可以再加上一行.claim("age", age)
  • 截图中第32行的.signWith(SignatureAlgorithm.HS256, APP_SECRET)和第33行的.compact();共同作用,目的是设置token字符串的第三部分(签名哈希)
  • 截图中第43行和第59行都是checkToken方法,这是方法重载,两个方法都是用来判断token是否存在与有效。第59行的checkToken方法:为了操作方便,我们项目中会把token字符串放到header中,所以就需要先通过request.getHeader("token")得到这个字符串,然后再进行判断
  • 截图中第76行的getMemberIdByJwtToken方法:根据token字符串获取用户信息,这个方法获取的是用户id并返回,以便我们拿着用户id后去数据库查询用户所有信息

3.开通阿里云短信服务

1.在阿里云官网首页的搜索栏搜索"短信服务",然后点击"短信服务"

在这里插入图片描述

2.点击"免费开通"即可开通阿里云短信服务

在这里插入图片描述

3.进入到短信服务的控制台,注意"快速学习"菜单下的签名名称(阿里云短信测试)和模板Code(SMS_154950909),这两个数据我们后面会用

在这里插入图片描述

4.点击"快速学习"菜单下的"绑定测试手机号"来添加一个测试手机号

在这里插入图片描述

5.在阿里云官网首页点击"试用中心"

在这里插入图片描述

6.在搜索栏搜索"短信"并按回车,可以看到有可使用的短信产品,我们点击"0元试用"

在这里插入图片描述

7.然后傻瓜式下一步,出现下图这个页面后,就说明成功了,此时我们已经有了100条免费短信(3个月后失效)

在这里插入图片描述

4.整合阿里云短信服务

4.1新建短信微服务

4.1.1创建子子模块service_msm

1.在service模块上右键选择New–>Module…

在这里插入图片描述

2.创建一个Maven项目

在这里插入图片描述

3.填写信息后点击"Finish"

在这里插入图片描述

4.1.2创建项目结构

1.在java包下创建包com.atguigu.msmservice,然后在msmservice包下创建启动类MsmApplication,并在启动类中添加代码

@ComponentScan({
    
    "com.atguigu"})
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置
public class MsmApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(MsmApplication.class, args);
    }
}

在这里插入图片描述

2.在msmservice包下创建包controller,然后在controller包下创建控制器MsmController,并在控制器上添加注解

@RestController
@RequestMapping("/edumsm/msm")
@CrossOrigin
public class MsmController {
    
    
}

在这里插入图片描述

3.在msmservice包下创建包service,然后:①在service包下创建业务层接口MsmService②在service包下创建包impl,然后在impl包下创建业务层实现类MsmServiceImpl并使其实现MsmService接口

@Service
public class MsmServiceImpl implements MsmService {
    
    
}

在这里插入图片描述

4.1.3配置application.properties

创建配置application.properties文件并编写配置

# 服务端口
server.port=8005
# 服务名
spring.application.name=service-msm

spring.redis.host=192.168.111.100
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲

#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

在这里插入图片描述

注意:截图中第6行的192.168.111.100是我linux虚拟机的ip地址,你们填你们自己的

4.2添加依赖

在service_msm模块的pom.xml中添加依赖(记得刷新maven)

<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
    </dependency>
</dependencies>

在这里插入图片描述

4.3创建随机生成数字的工具类

1.阿里云只负责给手机号发送验证码,生成验证码的过程是我们自己完成的,我们这里使用工具类来生成随机数字,工具类在资料中给出了

在这里插入图片描述

2.在service_msm模块的msmservice包下创建包utils,然后将工具类RandomUtil.java复制到utils包下

在这里插入图片描述

4.4控制层

在service_msm模块的控制器MsmController中编写代码

@Autowired
private MsmService msmService;

//发送短信的方法
@GetMapping("send/{phone}")
public R sendMsm(@PathVariable String phone) {
    
    
    //生成4位随机数字
    String code = RandomUtil.getFourBitRandom();
    Map<String,Object> param = new HashMap<>();
    param.put("code", code);

    //调用service中发送短信的方法
    boolean isSend = msmService.send(param, phone);
    if (isSend) {
    
    
        return R.ok();
    } else {
    
    
        return R.error().message("短信发送失败");
    }
}

在这里插入图片描述

为什么要把code放到Map集合中传递过去?

在后面的"4.6业务层实现类"的截图中的第39行request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));:人家阿里云规定了,当键为TemplateParam时,putQueryParameter方法的第二个参数必须传的数据格式是json格式,我们这里在控制层将code放到Map后传给业务层,那么业务层只需要使用JSONObject.toJSONString(param)就可以将Map集合转为json格式并传递

4.5业务层接口

在业务层接口MsmService中定义发送短信的抽象方法

//发送短信
boolean send(Map<String, Object> param, String phone);

在这里插入图片描述

4.6业务层实现类

在业务层实现类MsmServiceImpl中实现上一步定义的抽象方法

//发送短信
@Override
public boolean send(Map<String, Object> param, String phone) {
    
    
    if(StringUtils.isEmpty(phone)) return false;

    DefaultProfile profile = DefaultProfile.getProfile(
    "default","LTAI5tMUCkxmE6ouUc2dmbXm","0Py10jHOPVkeFp6MiIm88c9QqyykUE");
    IAcsClient client = new DefaultAcsClient(profile);

    //设置相关参数(固定的,不需要修改)
    CommonRequest request = new CommonRequest();
    request.setMethod(MethodType.POST); //提交方式
    request.setDomain("dysmsapi.aliyuncs.com"); //发送时要访问阿里云中的哪个地方
    request.setVersion("2017-05-25"); //版本号
    request.setAction("SendSms"); //请求里面的哪个方法

    //设置发送的相关参数
    request.putQueryParameter("PhoneNumbers", phone); //设置要发送的手机号
    request.putQueryParameter("SignName", "阿里云短信测试"); //在阿里云申请的签名名称
    request.putQueryParameter("TemplateCode", "SMS_154950909"); //在阿里云中申请的模板Code
    request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param)); //验证码数据

    try {
    
    
        //最终发送
        CommonResponse response = client.getCommonResponse(request);

        boolean success = response.getHttpResponse().isSuccess();
        return success;
    } catch(Exception e) {
    
    
        e.printStackTrace();
        return false;
    }
}

在这里插入图片描述

  • 截图中第22行的if(StringUtils.isEmpty(phone)) return false;:判断手机号是否为空,如果为空就不发送短信,如果不为空就执行接下来的代码
    • 注意:这里的工具类StringUtils导的是spring包下的,别导错包了
  • 截图中第24行的getProfile方法的第一个参数是地域节点,我们用默认的就可以、第二个参数和第三个参数分别是阿里云的id和秘钥,填写自己的
  • 截图中第29-33行是固定的,不需要修改,也不能修改
  • 截图中第36、37、38、39行的PhoneNumbers、SignName、TemplateCode、TemplateParam是固定的
  • 还记得在"3.开通阿里云短信服务"的第3步说的签名名称(阿里云短信测试)和模板Code(SMS_154950909)吗?截图中第37行和38行用到了

4.7测试

1.在nginx中配置8005端口

location ~ /edumsm/ {
	proxy_pass http://localhost:8005;
}

在这里插入图片描述

2.将service_msm服务注册到注册中心,这样做的原因在"demo12-课程管理"的"4.4问题",具体步骤在"demo12-课程管理"的"4.3服务注册(service_vod)",这里不再演示,自行配置吧

3.启动MsmApplication服务,使用swagger进行测试。注意:输入的手机号必须是在"3.开通阿里云短信服务"的第4步添加的手机号

在这里插入图片描述

在这里插入图片描述

4.8整合redis

1.实际场景中验证码是在一定时间内有效(比如5分钟内有效),但是阿里云只负责将验证码发给用户,并不会管理验证码的失效时间

2.解决方法:我们在后端将验证码存到redis中,并设置有效时间

3.但是这里使用redis的目的和在"demo13-搭建前台环境、首页数据显示"的"10.Redis"中的目的不一样,我们在那里使用redis是为了缓存,而我们这里使用redis是为了设置验证码的有效时间,所以我们这里换一种方式来使用redis

4.我们的业务逻辑是:发送验证码时先从redis中取,如果能从redis取到验证码,那就说明该手机号此时有一个有效的验证码,无需再次给该手机号发送验证码;如果不能从redis取到验证码,那就说明该手机号此时没有可用验证码,需要使用阿里云给该手机号发送验证码

5.SpringBoot整合redis时人家给我们封装了一个RedisTemplate对象,现在我们在控制器MsmController中注入这个对象

@Autowired
private RedisTemplate<String,String> redisTemplate;

在这里插入图片描述

6.在控制器MsmController的sendMsm方法中添加两段代码

//1.从redis中获取验证码,如果能取到就不需要使用阿里云发送验证码,我们直接返回
String code = redisTemplate.opsForValue().get(phone);
if (!StringUtils.isEmpty(code)) {
    
    
    return R.ok();
}
//2.如果不能从redis中获取到,就使用阿里云发送验证码
//发送成功,把发送成功的验证码放到redis里面并且设置有效时长
redisTemplate.opsForValue().set(phone, code,5, TimeUnit.MINUTES);

在这里插入图片描述

  • 绿方框圈起来的String需要删掉
  • 截图中第44行的set方法的第一个参数phone和第二个参数code是key-value的关系
  • 截图中第44行的set方法的第三个参数5和第四个参数TimeUnit.MINUTES共同作用,目的是:存到redis中的验证码有效时长是5分钟

4.9再次测试

1.Xshell连接上虚拟机后,先使用命令cd /usr/local/bin进入该目录,然后在该目录使用如下命令启动redis,并且启动时使用的配置文件是etc目录下的redis.conf

redis-server /etc/redis.conf

在这里插入图片描述

2.在bin目录使用redis-cli命令,目的是:在本地客户端(也就是linux虚拟机)连接linux虚拟机中的redis

在这里插入图片描述

3.重启后端项目,使用swagger进行测试

在这里插入图片描述

在这里插入图片描述

4.使用get xxx命令看一下能否获取到验证码数据(xxx是我们测试时填写的手机号)

在这里插入图片描述

可以看到能获取到验证码数据,说明我们成功将该手机号的验证码存到了redis中

5.用户登录注册(后端)

5.1新建用户微服务

5.1.1创建子子模块service_ucenter

1.在service模块上右键选择New–>Module…

在这里插入图片描述

2.创建一个Maven项目

在这里插入图片描述

3.填写信息后点击"Finish"

在这里插入图片描述

5.1.2创建数据表ucenter_member

1.创建这张表的脚本在资料的guli_ucenter.sql文件中

在这里插入图片描述

在这里插入图片描述

2.将创建ucenter_member表的脚本复制到数据库中执行

CREATE TABLE `ucenter_member` (
  `id` char(19) NOT NULL COMMENT '会员id',
  `openid` varchar(128) DEFAULT NULL COMMENT '微信openid',
  `mobile` varchar(11) DEFAULT '' COMMENT '手机号',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
  `sex` tinyint(2) unsigned DEFAULT NULL COMMENT '性别 1 女,2 男',
  `age` tinyint(3) unsigned DEFAULT NULL COMMENT '年龄',
  `avatar` varchar(255) DEFAULT NULL COMMENT '用户头像',
  `sign` varchar(100) DEFAULT NULL COMMENT '用户签名',
  `is_disabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否禁用 1(true)已禁用,  0(false)未禁用',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员表';

在这里插入图片描述

5.1.3生成代码

1.在service_ucenter模块的test包的java包下创建包demo.codedemo

在这里插入图片描述

2.将service_cms模块的代码生成器CodeGenerator复制到上一步创建的codedemo包下

在这里插入图片描述

3.修改service_ucenter模块的代码生成器中的部分代码

在这里插入图片描述

4.在run方法上右键选择"Run ‘run()’"就可以生成代码了

在这里插入图片描述

5.给生成的控制器UcenterMemberController添加注解@CrossOrigin以实现跨域,并且将请求路径/educenter/ucenter-member中的ucenter-member改为member

在这里插入图片描述

5.1.4配置application.properties

创建配置文件application.properties并编写配置

# 服务端口
server.port=8006
# 服务名
spring.application.name=service-ucenter

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

spring.redis.host=192.168.111.100
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲

#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/atguigu/educenter/mapper/xml/*.xml

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

在这里插入图片描述

注意:数据库账号密码和虚拟机ip填写自己的

5.1.5创建启动类

在educenter包下创建启动类UcenterApplication

@SpringBootApplication
@ComponentScan({
    
    "com.atguigu"}) //指定扫描位置
@MapperScan("com.atguigu.educenter.mapper")
public class UcenterApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(UcenterApplication.class, args);
    }
}

在这里插入图片描述

5.1.6配置nginx、nacos

1.在nginx中配置8006端口并重启nginx

location ~ /educenter/ {
	proxy_pass http://localhost:8006;
}

2.将service_ucenter服务注册到注册中心,这样做的原因在"demo12-课程管理"的"4.4问题",具体步骤在"demo12-课程管理"的"4.3服务注册(service_vod)",这里不再演示,自行配置吧

5.2控制层(登录功能)

在控制器UcenterMemberController中编写代码

@Autowired
private UcenterMemberService memberService;

//登录
@GetMapping("login")
public R loginUser(@RequestBody UcenterMember member) {
    
    
    //业务层的登录方法login返回一个token值
    String token = memberService.login(member);
    return R.ok().data("token", token);
}

在这里插入图片描述

5.3业务层接口(登录功能)

在业务层接口UcenterMemberService中定义登录的抽象方法

//登录
String login(UcenterMember member);

在这里插入图片描述

5.4业务层实现类(登录功能)

1.我们先去看ucenter_member表,可以看到每条用户数据中都有这三个字段:mobile、password、is_disabled,所以我们判断用户能否登录时把这三个字段都判断一下

在这里插入图片描述

2.后期做用户注册功能时我们是这样存用户密码的:先将密码进行MD5加密,将加密得到的数据作为用户密码存到数据库。将密码进行MD5加密的工具类在资料中提供了,我们将这个工具类复制到common_utils模块的commonutils包下

在这里插入图片描述

在这里插入图片描述

3.在业务层实现类UcenterMemberServiceImpl中实现刚刚定义的抽象方法

//登录
@Override
public String login(UcenterMember member) {
    
    
    //获取手机号和密码
    String mobile = member.getMobile();
    String password = member.getPassword();

    //手机号和密码非空判断
    if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
    
    
        throw new GuliException(20001, "手机号、密码为空");
    }

    //判断手机号是否正确
    QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
    wrapper.eq("mobile", mobile);
    UcenterMember mobileMember = baseMapper.selectOne(wrapper);
    //判断查询对象是否正确
    if (mobileMember == null) {
    
     //数据表中没有这个手机号
        throw new GuliException(20001, "没有这个手机号数据");
    }

    //判断密码
    //把用户输入的密码进行MD5加密,然后和数据库中的密码进行比较
    if (!MD5.encrypt(password).equals(mobileMember.getPassword())) {
    
    
        throw new GuliException(20001, "密码错误");
    }

    //判断用户是否禁用
    if (mobileMember.getIsDisabled()) {
    
    
        throw new GuliException(20001, "用户已禁用");
    }

    //登录成功,生成token字符串
    String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());

    return jwtToken;
}

在这里插入图片描述

截图中第58行生成token字符串时使用的从数据库查到的mobileMember对象而不是前端传过来的member对象,因为member对象是从前端传过来的,里面只有手机号和密码,并没有用户id和用户名称(nickname)

5.5测试(登录功能)

1.启动服务,使用swagger进行测试(注意:数据库中不需要有数据,使用swagger时参数随便写就行,因为这次测试不是最重要的,重要的是后面会引出一个问题)

可以看到,有异常,说明我们后端代码有问题

在这里插入图片描述

2.看下控制台报错信息

在这里插入图片描述

org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.atguigu.commonutils.R com.atguigu.educenter.controller.UcenterMemberController.loginUser(com.atguigu.educenter.entity.UcenterMember)

3.以后只要我们看到了Required request body is missing就先看下提交方式是否正确,可以看到我们UcenterMemberController的loginUser方法的提交方式是get方式

在这里插入图片描述

4.这里使用get提交是不对的:因为loginUser方法的参数用了注解@RequestBody,所以不能使用get提交(因为get提交没有请求体),需要使用post提交。所以我们将loginUser方法的提交方式改为post方式

在这里插入图片描述

5.重启项目,重新进行测试,可以看到不再像刚刚测试那样抛出全局异常(当然,我们数据表ucenter_member中本来就没有数据,所以需要抛出"没有这个手机号数据"的异常)

在这里插入图片描述

5.6创建VO类(注册功能)

1.用户注册时前端会传来用户的昵称、手机号、密码、验证码,但是实体类UcenterMember中并没有定义验证码变量,所以我们需创建一个VO类来封装用户的昵称、手机号、密码、验证码

2.在entity包下创建包vo,然后在vo包下创建vo类RegisterVo

@Data
public class RegisterVo {
    
    
    @ApiModelProperty(value = "昵称")
    private String nickname;
    @ApiModelProperty(value = "手机号")
    private String mobile;
    @ApiModelProperty(value = "密码")
    private String password;
    @ApiModelProperty(value = "验证码")
    private String code;
}

在这里插入图片描述

5.7控制层(注册功能)

在控制器UcenterMemberController中编写代码

//注册
@PostMapping("register")
public R registerUser(@RequestBody RegisterVo registerVo) {
    
    
    memberService.register(registerVo);
    return R.ok();
}

在这里插入图片描述

5.8业务层接口(注册功能)

在业务层接口UcenterMemberService中定义注册的抽象方法

//注册
void register(RegisterVo registerVo);

在这里插入图片描述

5.9业务层实现类(注册功能)

1.因为我们业务逻辑中有一个操作是:从redis中获取验证码,并和用户输入的验证码进行比较。所以需要在业务层实现类UcenterMemberServiceImpl中注入RedisTemplate对象

@Autowired
private RedisTemplate<String,String> redisTemplate;

在这里插入图片描述

2.在业务层实现类UcenterMemberServiceImpl中实现上一步定义的抽象方法

//注册
@Override
public void register(RegisterVo registerVo) {
    
    
    //获取注册的数据
    String nickname = registerVo.getNickname(); //昵称
    String mobile = registerVo.getMobile(); //手机号
    String password = registerVo.getPassword(); //密码
    String code = registerVo.getCode(); //验证码

    //非空判断
    if (StringUtils.isEmpty(nickname) ||
        StringUtils.isEmpty(mobile) ||
        StringUtils.isEmpty(password) ||
        StringUtils.isEmpty(code)) {
    
    
        throw new GuliException(20001, "注册失败");
    }

    //判断验证码是否正确
    //先从redis中取得验证码
    String redisCode = redisTemplate.opsForValue().get(mobile);
    if (!code.equals(redisCode)) {
    
    
        throw new GuliException(20001, "注册失败");
    }

    //判断手机号是否重复
    //如果表中存在相同的手机号,那就不允许进行添加
    QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
    wrapper.eq("mobile", mobile);
    Integer count = baseMapper.selectCount(wrapper);
    if (count > 0) {
    
     //数据表中已经有了这个手机号
        throw new GuliException(20001, "注册失败");
    }

    //将数据添加到数据库中
    UcenterMember member = new UcenterMember();
    member.setNickname(nickname);
    member.setMobile(mobile);
    member.setPassword(MD5.encrypt(password));
    member.setIsDisabled(false); //用户未禁用
    member.setAvatar("https://edu-mxy.oss-cn-hangzhou.aliyuncs.com/2022/08" +
                     "/28/e97af8298b4c481695cc7723c01c614a1243.jpg"); //用户默认头像
    baseMapper.insert(member);
}

在这里插入图片描述

5.10自动填充

给实体类UcenterMember的gmtCreate字段和gmtModified字段都添加@TableField注解以实现自动填充

在这里插入图片描述

5.11测试

1.重启后端项目,启动虚拟机中的redis服务

2.先使用8005端口的swagger得到验证码并将验证码存到redis中

在这里插入图片描述

3.再使用8006端口的swagger测试能否注册成功

在这里插入图片描述

4.去数据中可以看到我们成功添加了这条数据

在这里插入图片描述

5.再测试一下登录,可以看到登录成功并且给我们返回了token字符串

在这里插入图片描述

6.根据token获取用户信息(后端)

6.1分析

1.我们的需求是:用户成功登录后在页面的右上角可以显示用户昵称、头像,所以我们就需要根据token获取到用户信息显示

2.在工具类JwtUtils中已经给出了"根据token获取用户信息"的方法,这个方法的返回值是用户id,我们后端得到这个方法返回的用户id后去数据库中查询就可以得到用户的所有信息了

在这里插入图片描述

6.2控制层

在控制器UcenterMemberController中编写代码

//根据token获取用户信息
@GetMapping("getMemberInfo")
public R getMemberInfo(HttpServletRequest request) {
    
    
    //调用jwt工具类的方法,该方法内部:根据request对象获取请求头中的token,然后就可以返回用户id
    String memberId = JwtUtils.getMemberIdByJwtToken(request);
    //查询数据库,根据用户id得到用户信息
    UcenterMember member = memberService.getById(memberId);
    return R.ok().data("userInfo", member);
}

在这里插入图片描述

7.用户登录注册(前端)

7.1在NUXT环境中安装插件

7.1.1安装element-ui和vue-qriously

  • 我们在"demo13-搭建前台环境、首页数据显示"的"2.2NUXT目录结构"说过,NUXT环境并没有集成element-ui,如果我们想使用element-ui就需要将element-ui引入过来
  • vue-qriously暂时用不到,后面微信支付会用到(这个插件用来下载二维码),我们也先将这个插件装上

在终端中分别使用命令npm install element-uinpm install vue-qriously安装这两个插件

在这里插入图片描述

在这里插入图片描述

7.1.2在NUXT环境中使用这两个插件

修改plugins目录下的配置文件nuxt-swiper-plugin.js,使得我们可以在NUXT环境中使用这两个插件

配置文件nuxt-swiper-plugin.js中的完整代码如下:

import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
import VueQriously from 'vue-qriously'
import ElementUI from 'element-ui' //element-ui的全部组件
import 'element-ui/lib/theme-chalk/index.css'//element-ui的css
Vue.use(ElementUI) //使用elementUI
Vue.use(VueQriously)
Vue.use(VueAwesomeSwiper)

在这里插入图片描述

7.2创建布局页面

1.在layouts目录下创建布局页面sign.vue,用户登录、注册时使用

可能有朋友会问了,layouts目录下不是已经有了布局页面default.vue了,为什么还要创建一个布局页面?我们的需求是:其他页面使用default.vue的页面布局,登录、注册页面使用sign.vue的页面布局。当然,如果你想让所有页面都使用default.vue的页面布局,那就不用再创建布局页面sign.vue了

<template>
  <div class="sign">
    <!--标题-->
    <div class="logo">
      <img src="~/assets/img/logo.png" alt="logo">
    </div>
    <!--表单-->
    <nuxt/>
  </div>
</template>

在这里插入图片描述

7.3修改登录和注册的超链接地址

修改布局页面default.vue中的登录和注册的超链接地址,使得我们点击页面头部的登录、注册按钮时可以发生跳转

在这里插入图片描述

在这里插入图片描述

7.4整合注册页面

在pages目录下创建一个register.vue页面,这个页面中的代码老师已经给我们了,直接复制过来即可

<template>
  <div class="main">
    <div class="title">
      <a href="/login">登录</a>
      <span>·</span>
      <a class="active" href="/register">注册</a>
    </div>

    <div class="sign-up-container">
      <el-form ref="userForm" :model="params">

        <el-form-item class="input-prepend restyle" prop="nickname" :rules="[{ required: true, message: '请输入你的昵称', trigger: 'blur' }]">
          <div>
            <el-input type="text" placeholder="你的昵称" v-model="params.nickname"/>
            <i class="iconfont icon-user"/>
          </div>
        </el-form-item>

        <el-form-item class="input-prepend restyle no-radius" prop="mobile" :rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]">
          <div>
            <el-input type="text" placeholder="手机号" v-model="params.mobile"/>
            <i class="iconfont icon-phone"/>
          </div>
        </el-form-item>

        <el-form-item class="input-prepend restyle no-radius" prop="code" :rules="[{ required: true, message: '请输入验证码', trigger: 'blur' }]">
          <div style="width: 100%;display: block;float: left;position: relative">
            <el-input type="text" placeholder="验证码" v-model="params.code"/>
            <i class="iconfont icon-phone"/>
          </div>
          <div class="btn" style="position:absolute;right: 0;top: 6px;width: 40%;">
            <a href="javascript:" type="button" @click="getCodeFun()" :value="codeTest" style="border: none;background-color: none">{
   
   {codeTest}}</a>
          </div>
        </el-form-item>

        <el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">
          <div>
            <el-input type="password" placeholder="设置密码" v-model="params.password"/>
            <i class="iconfont icon-password"/>
          </div>
        </el-form-item>

        <div class="btn">
          <input type="button" class="sign-up-button" value="注册" @click="submitRegister()">
        </div>
        <p class="sign-up-msg">
          点击 “注册” 即表示您同意并愿意遵守简书
          <br>
          <a target="_blank" href="http://www.jianshu.com/p/c44d171298ce">用户协议</a>
          和
          <a target="_blank" href="http://www.jianshu.com/p/2ov8x3">隐私政策</a> 。
        </p>
      </el-form>
      <!-- 更多注册方式 -->
      <div class="more-sign">
        <h6>社交帐号直接注册</h6>
        <ul>
          <li><a id="weixin" class="weixin" target="_blank" href="http://huaan.free.idcfengye.com/api/ucenter/wx/login"><i
            class="iconfont icon-weixin"/></a></li>
          <li><a id="qq" class="qq" target="_blank" href="#"><i class="iconfont icon-qq"/></a></li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
  import '~/assets/css/sign.css'
  import '~/assets/css/iconfont.css'


  export default {
    layout: 'sign',
    data() {
      return {
        params: {
          mobile: '',
          code: '',
          nickname: '',
          password: ''
        },
        sending: true,      //是否发送验证码
        second: 60,        //倒计时间
        codeTest: '获取验证码'
      }
    },
    methods: {


      checkPhone (rule, value, callback) {
        //debugger
        if (!(/^1[34578]\d{9}$/.test(value))) {
          return callback(new Error('手机号码格式不正确'))
        }
        return callback()
      }
    }
  }
</script>

代码中第90-96行的判断手机号是否合法的方法

7.5整合登录页面

在pages目录下创建一个login.vue页面,这个页面中的代码老师已经给我们了,直接复制过来即可

<template>
  <div class="main">
    <div class="title">
      <a class="active" href="/login">登录</a>
      <span>·</span>
      <a href="/register">注册</a>
    </div>

    <div class="sign-up-container">
      <el-form ref="userForm" :model="user">

        <el-form-item class="input-prepend restyle" prop="mobile" :rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]">
          <div >
            <el-input type="text" placeholder="手机号" v-model="user.mobile"/>
            <i class="iconfont icon-phone" />
          </div>
        </el-form-item>

        <el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">
          <div>
            <el-input type="password" placeholder="密码" v-model="user.password"/>
            <i class="iconfont icon-password"/>
          </div>
        </el-form-item>

        <div class="btn">
          <input type="button" class="sign-in-button" value="登录" @click="submitLogin()">
        </div>
      </el-form>
      <!-- 更多登录方式 -->
      <div class="more-sign">
        <h6>社交帐号登录</h6>
        <ul>
          <li><a id="weixin" class="weixin" target="_blank" href="http://qy.free.idcfengye.com/api/ucenter/weixinLogin/login"><i class="iconfont icon-weixin"/></a></li>
          <li><a id="qq" class="qq" target="_blank" href="#"><i class="iconfont icon-qq"/></a></li>
        </ul>
      </div>
    </div>

  </div>
</template>

<script>
  import '~/assets/css/sign.css'
  import '~/assets/css/iconfont.css'
  export default {
    layout: 'sign',

    data () {
      return {
        user:{ //封装登录的手机号和密码
          mobile:'',
          password:''
        },
        loginInfo:{} //用户信息
      }
    },

    methods: {

      checkPhone (rule, value, callback) {
        //debugger
        if (!(/^1[34578]\d{9}$/.test(value))) {
          return callback(new Error('手机号码格式不正确'))
        }
        return callback()
      }
    }
  }
</script>
<style>
   .el-form-item__error{
    z-index: 9999999;
  }
</style>

7.6在api中定义方法(注册功能)

在api目录下创建register.js文件,定义方法调用后端接口

import request from '@/utils/request'
export default {
    
    
  //给手机号发送验证码
  sendCode(phone) {
    
    
    return request({
    
    
      url: `/edumsm/msm/send/${
      
      phone}`,
      method: 'get'
    })
  },
  //注册的方法
  registerMember(formItem) {
    
    
    return request({
    
    
      url: `/educenter/member/register`,
      method: 'post',
      data: formItem
    })
  }
}

在这里插入图片描述

7.7倒计时效果(注册功能)

1.实际应用场景中,发送验证码后必须等60秒后才可以再次发送验证码,这个需求的实现需要用到js中的setInterval方法,这个方法的第一个参数表示要执行的方法,第二个参数表示间隔多久执行一次第一个参数中的方法

2.在methods: {...}中定义方法,完成需求

//倒计时
timeDown() {
    
    
  let result = setInterval(() => {
    
    
    --this.second;
    this.codeTest = this.second
    if (this.second < 1) {
    
    
      clearInterval(result);
      this.sending = true;
      this.second = 60;
      this.codeTest = "获取验证码"
    }
  }, 1000);
},

在这里插入图片描述

7.8调用api中的方法(注册功能)

1.在register.vue页面引入上一步创建的js文件

import registerApi from '@/api/register'

在这里插入图片描述

2.调用api中的方法

//注册提交的方法
submitRegister() {
    
    
  registerApi.registerMember(this.params)
    .then(Response => {
    
    
      //提示注册成功
      this.$message({
    
    
        type: 'success',
        message: "注册成功"
      })
      //跳转到登录页面
      this.$router.push({
    
    path:'/login'})
    })
},
//给手机号发送验证码
getCodeFun() {
    
    
  registerApi.sendCode(this.params.mobile)
    .then(Response => {
    
    
      this.sending = false //不能再点击,需等倒计时结束才可以再点击
      //调用倒计时方法
      this.timeDown()
    })
},

在这里插入图片描述

7.9判断输入框不为空、手机号合法(注册功能)

1.当我们点击昵称输入框,但是没有输入任何东西就把光标移到别的位置,此时就会提示"请输入昵称",这个需求的实现在js中需要我们自己编写好多代码来实现,但是这个框架直接给我们封装好了,我们拿来用就行

在这里插入图片描述

2.并且这里还会判断手机号是否合法

在这里插入图片描述

7.10测试(注册功能)

1.测试之前先将我们在"5.11测试"进行测试时插入进数据库的那条数据删掉,因为我们在业务层实现类UcenterMemberServiceImpl的register方法中有一个业务逻辑是:如果数据库中已经存在这个手机号的数据,那就不允许再添加这个手机号

在这里插入图片描述

2.自行测试,我的没问题(如果报跨域问题的,看下nginx是否配置,配置后是否重启,看看路径是否正确,请求方式是否正确)

在这里插入图片描述

在这里插入图片描述

7.11在api中定义方法(登录功能)

在api目录下创建login.js文件,定义方法调用后端接口

import request from '@/utils/request'
export default {
    
    
  //登录的方法
  submitLoginUser(userInfo) {
    
    
    return request({
    
    
      url: `/educenter/member/login`,
      method: 'post',
      data: userInfo
    })
  },
  //根据token获取用户信息
  getLoginUserInfo() {
    
    
    return request({
    
    
      url: `/educenter/member/getMemberInfo`,
      method: 'get'
    })
  }
}

在这里插入图片描述

有没有同学会问:控制器UcenterMemberController的getMemberInfo方法的参数是一个HttpServletRequest对象,但是我们在api中定义的getLoginUserInfo方法调用后端接口时并没有传HttpServletRequest对象呀?这个我们后面会传的

7.12下载插件、引入js文件(登录功能)

1.在终端使用如下命令下载js-cookie插件(有了这个插件我们后面才可以使用cookie)

npm install js-cookie

在这里插入图片描述

2.在login.vue页面js文件

import cookie from 'js-cookie'
import loginApi from '@/api/login'

在这里插入图片描述

7.13分析(登录功能)

1.登录功能实现过程中需要分四步走:

  • 调用后端登录接口,将返回的token字符串放到cookie中
  • 创建前端拦截器
    • 判断cookie中是否有token字符串,如果有,就把token字符串放到请求头(header)中
  • 根据token值调用后端接口获取用户信息,并把获取到的用户信息放到cookie中
  • 从cookie中获取用户信息并在页面进行显示

2.为什么要把token字符串放到请求头(header)中?

看我们的工具类JwtUtils的getMemberIdByJwtToken方法内部代码:request.getHeader("token")表示从请求头中获取token,所以我们要把token字符串放到请求头中

在这里插入图片描述

老师说了,把token放到cookie中后,不再把token放到请求头中也可以,这样的话修改一下工具类JwtUtils中的方法,方法内部改为从cookie中获取token,这样是可行的

7.14调用后端登录接口(登录功能)

//登录的方法
submitLogin() {
    
    
  loginApi.submitLoginUser(this.user)
    .then(response => {
    
    
      //获取token字符串,将其放到cookie中
      cookie.set('guli_token',response.data.data.token,{
    
    domain: 'localhost'})
    })
},

在这里插入图片描述

截图中第68行的set方法:第一个参数是值的名称,我们可以把cookie理解为key-value的存储方式;第二个参数是值;第三个参数是作用范围,我们这里使用{domain: 'localhost'}表示只要访问的是localhost,就可以传递这个cookie

7.15创建前端拦截器(登录功能)

1.拦截器拦截的是当前的所有请求,而不是某一个请求,那怎么才能拦截所有请求呢:我们知道,api目录下的每个js文件的第一行都是使用import request from '@/utils/request'引入utils目录下的request.js文件,所以我们可以把拦截器写到utils目录下的request.js文件中(不会写没事,勉强能看懂就行)

// http request 拦截器
service.interceptors.request.use(
    config => {
    
    
        //debugger
        if (cookie.get('guli_token')) {
    
    
            //把获取到的cookie值放到header中
            config.headers['token'] = cookie.get('guli_token');
        }
        return config
    },
    err => {
    
    
        return Promise.reject(err);
    })

在这里插入图片描述

截图中第10行的service.interceptors.request.use表示每次请求中都使用这个拦截器

2.拦截器中使用了cookie,所以需要在request.js文件中引入js-cookie

import cookie from 'js-cookie'

在这里插入图片描述

3.还有一个组件,暂时用不上,不过怕后面忘了引入,所以我们这里先引入进来

import {
    
     MessageBox, Message } from 'element-ui'

在这里插入图片描述

4.还要一个拦截器,暂时用不上,等后面做了支付会用到,这里也先把代码放进来吧

// http response 拦截器
service.interceptors.response.use(
  response => {
    
    
    //debugger
    if (response.data.code == 28004) {
    
    
        console.log("response.data.resultCode是28004")
        // 返回 错误代码-1 清除ticket信息并跳转到登录页面
        //debugger
        window.location.href="/login"
        return
    }else{
    
    
      if (response.data.code !== 20000) {
    
    
        //25000:订单支付中,不做任何提示
        if(response.data.code != 25000) {
    
    
          Message({
    
    
            message: response.data.message || 'error',
            type: 'error',
            duration: 5 * 1000
          })
        }
      } else {
    
    
        return response;
      }
    }
  },
  error => {
    
    
    return Promise.reject(error.response)   // 返回接口返回的错误信息
});

在这里插入图片描述

7.16调用后端接口获取用户信息(登录功能)

我们在"7.14调用后端登录接口(登录功能)"定义了submitLogin方法,现在给这个方法添加代码

//调用接口获取用户信息,将其放到cookie中
loginApi.getLoginUserInfo()
  .then(response => {
    
    
    this.loginInfo = response.data.data.userInfo

    //将json对象转为json字符串,这样才能存到cookie中
    var jsonStr = JSON.stringify(this.loginInfo)
    cookie.set('guli_ucenter',jsonStr,{
    
    domain: 'localhost'})
  })

//跳转到首页面(这两种方式都行)
// this.$router.push({path:'/'})
window.location.href = "/"

在这里插入图片描述

7.17将用户信息在页面显示

1.在layouts目录下的default.vue页面引入js-cookie

import cookie from 'js-cookie'

在这里插入图片描述

2.在layouts目录下的default.vue页面编写如下代码

data() {
    
    
  return {
    
    
    token: '',
    loginInfo: {
    
     //封装用户信息
      id: '',
      age: '',
      avatar: '',
      mobile: '',
      nickname: '',
      sex: ''
    }
  }
},
created() {
    
    
  this.showInfo()
},
methods: {
    
    
  //在页面显示用户信息
  showInfo() {
    
    
    //从cookie中获取用户信息
    var userStr = cookie.get('guli_ucenter')
    //把json字符串转换为json对象
    if(userStr) {
    
     //先判断不为空
      this.loginInfo = JSON.parse(userStr)
    }
  }
}

在这里插入图片描述

截图中第164行是将json字符串转为json对象。为什么这里要把json字符串转为json对象才能继续接下来的操作,而我们做这个项目的前半部分时一次也没遇见过需要转为json对象才能继续接下来的操作?

因为我们这里是先将json对象转为json字符串并放到了cookie中(在"7.16调用后端接口获取用户信息(登录功能)"的截图的第76、77行),然后又从cookie中取出json字符串。所以需要先将json字符串转为json对象才能继续接下来的操作

3.将default.vue页面中下图用方框圈起来的部分删掉

在这里插入图片描述

4.将下面的代码复制到上一步删除的位置

<!-- / nav -->
<ul class="h-r-login">
    <li v-if="!loginInfo.id" id="no-login">
        <a href="/login" title="登录">
            <em class="icon18 login-icon">&nbsp;</em>
            <span class="vam ml5">登录</span>
        </a>
        |
        <a href="/register" title="注册">
            <span class="vam ml5">注册</span>
        </a>
    </li>
    <li v-if="loginInfo.id" id="is-login-one" class="mr10">
        <a id="headerMsgCountId" href="#" title="消息">
            <em class="icon18 news-icon">&nbsp;</em>
        </a>
        <q class="red-point" style="display: none">&nbsp;</q>
    </li>
    <li v-if="loginInfo.id" id="is-login-two" class="h-r-user">
        <a href="/ucenter" title>
            <img
                :src="loginInfo.avatar"
                width="30"
                height="30"
                class="vam picImg"
                alt
                >
            <span id="userName" class="vam disIb">{
   
   { loginInfo.nickname }}</span>
        </a>
        <a href="javascript:void(0);" title="退出" @click="logout()" class="ml5">退出</a>
    </li>
    <!-- /未登录显示第1 li;登录后显示第2,3 li -->
</ul>

在这里插入图片描述

截图中第

5.自行测试,我的没问题,可以正常显示

在这里插入图片描述

7.18退出登录

1.在default.vue页面定义方法实现退出功能

//退出登录
logout() {
    
    
  //清空cookie值
  cookie.set('guli_token','',{
    
    domain: 'localhost'})
  cookie.set('guli_ucenter','',{
    
    domain: 'localhost'})

  //跳转到首页
  window.location.href = "/"
}

在这里插入图片描述

2.自行测试,我的没问题

猜你喜欢

转载自blog.csdn.net/maxiangyu_/article/details/127032657