项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59
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.单点登录示例:我们在百度官网的任意一个服务(比如百度翻译)登录后,再进入其它服务(如百度文库、百度百科、百度贴吧…)都不再需要登录

SSO模式和刚刚说的单点登录是一个意思,没任何区别
1.session的广播机制说通俗点就叫session复制:当用户在service_edu服务登录后,我们先使用session.setAttribute("user",user)在service_edu服务的session域对象中存入用户数据,然后将service_edu服务的session对象复制到其他服务中
2.这种方式有一个致命的缺点:如果我们一个项目中有几十个模块,那么就需要将session对象复制几十次,这对资源是一个极大的消耗
cookie的特点:是一个客户端技术,浏览器每次发送请求都会带着cookie值进行发送;redis的特点是:基于key-value存储数据
具体实现:
1.在项目的任意一个模块进行登录后,把数据放到两个地方:redis和cookie
2.用户访问项目中其它模块时,发送请求会带着这个cookie值进行发送,然后我们获取到这个cookie值,拿着获取到的cookie值去redis中根据key进行查询,如果可以查询到对应的value就说明此时用户是登录状态
token是什么:按照一定规则(规则不是固定的,我们可以根据需求制定规则)生成的字符串,这个字符串中可以包含用户信息
比如说我们制定的规则是:ip+用户名+用户年龄,假设是192.1.1.1lucy22,然后我们将这串字符进行base64编码,再做一个加密,最后就得到了token字符串
具体实现:
1.在项目的任意一个模块进行登录后,我们按照一定规则生成一个token字符串,要求这个字符串中包含用户信息,然后将这个字符串进行返回(两种方式返回:把字符串通过cookie返回、把字符串通过地址栏返回)
通过地址栏返回的示例:

2.用户访问项目中其它模块时,每次访问在地址栏都带着生成的这个token字符串,我们得到地址栏中的这个token字符串,将这个字符串解码,然后就可以获取到用户信息
我们在javaweb阶段学过,session的默认过期时间是30分钟,其实上面的第二种方式(使用cookie+redis)和第三种方式(使用token)我们也可以设置过期时间:
在"1.2.4token(令牌机制)"我们说过了,我们按照一定规则生成token字符串,JWT就是给我们制定好了规则,我们使用JWT规则可以生成token字符串,且这个字符串中包含用户信息

JWT字符串由三部分组成:
第一部分(红色):jwt头信息
第二部分(紫色):有效载荷,含有用户信息(也可以说是主体信息)
第三部分(蓝色):签名哈希,通俗说就是字符串的防伪标志,通过这个可以判断这个字符串是我们自己根据JWT规则生成的还是别人伪造的
因为jwt我们在后面做注册、登录或其它功能时会用到jwt,所以将依赖添加到common_utils模块中(别忘了刷新maven)
<dependencies>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
dependency>
dependencies>

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

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

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

public static final long EXPIRE = 1000 * 60 * 60 * 24;:我们定义一个常量,用于设置token过期时间public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";:这是一个秘钥(这个秘钥是保密的,以后公司会给我们),我们后面生成token的第三部分(签名哈希)时会用到这个秘钥.setHeaderParam("typ", "JWT")和第26行的.setHeaderParam("alg", "HS256")共同作用,目的是设置token字符串的第一部分(jwt头信息),这是规定的,我们不需要修改,也不能修改.setSubject("guli-user")、第28行的.setIssuedAt(new Date())、第29行的.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))共同作用,目的是设置token字符串的过期时间,其中第27行的参数guli-user可以随便写.claim("id", id)和第31行的.claim("nickname", nickname)共同作用,目的是设置token字符串的第二部分(主体信息),用来存储用户信息。如果getJwtToken方法有第三个参数age,那么我们就可以再加上一行.claim("age", age).signWith(SignatureAlgorithm.HS256, APP_SECRET)和第33行的.compact();共同作用,目的是设置token字符串的第三部分(签名哈希)request.getHeader("token")得到这个字符串,然后再进行判断1.在阿里云官网首页的搜索栏搜索"短信服务",然后点击"短信服务"

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

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

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

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

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

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

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

2.创建一个Maven项目

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

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 {
}

创建配置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地址,你们填你们自己的
在service_msm模块的pom.xml中添加依赖(记得刷新maven)
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
dependency>
dependencies>

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

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

在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格式并传递
在业务层接口MsmService中定义发送短信的抽象方法
//发送短信
boolean send(Map<String, Object> param, String phone);

在业务层实现类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;
}
}

if(StringUtils.isEmpty(phone)) return false;:判断手机号是否为空,如果为空就不发送短信,如果不为空就执行接下来的代码
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步添加的手机号


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需要删掉phone和第二个参数code是key-value的关系5和第四个参数TimeUnit.MINUTES共同作用,目的是:存到redis中的验证码有效时长是5分钟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中
1.在service模块上右键选择New–>Module…

2.创建一个Maven项目

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

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='会员表';

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

创建配置文件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填写自己的
在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);
}
}

1.在nginx中配置8006端口并重启nginx
location ~ /educenter/ {
proxy_pass http://localhost:8006;
}
2.将service_ucenter服务注册到注册中心,这样做的原因在"demo12-课程管理"的"4.4问题",具体步骤在"demo12-课程管理"的"4.3服务注册(service_vod)",这里不再演示,自行配置吧
在控制器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);
}

在业务层接口UcenterMemberService中定义登录的抽象方法
//登录
String login(UcenterMember member);

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)
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中本来就没有数据,所以需要抛出"没有这个手机号数据"的异常)

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;
}

在控制器UcenterMemberController中编写代码
//注册
@PostMapping("register")
public R registerUser(@RequestBody RegisterVo registerVo) {
memberService.register(registerVo);
return R.ok();
}

在业务层接口UcenterMemberService中定义注册的抽象方法
//注册
void register(RegisterVo registerVo);

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);
}

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

1.重启后端项目,启动虚拟机中的redis服务
2.先使用8005端口的swagger得到验证码并将验证码存到redis中

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

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

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

1.我们的需求是:用户成功登录后在页面的右上角可以显示用户昵称、头像,所以我们就需要根据token获取到用户信息显示
2.在工具类JwtUtils中已经给出了"根据token获取用户信息"的方法,这个方法的返回值是用户id,我们后端得到这个方法返回的用户id后去数据库中查询就可以得到用户的所有信息了

在控制器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);
}

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


修改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)

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

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


在pages目录下创建一个register.vue页面,这个页面中的代码老师已经给我们了,直接复制过来即可
代码中第90-96行的判断手机号是否合法的方法
在pages目录下创建一个login.vue页面,这个页面中的代码老师已经给我们了,直接复制过来即可
在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
})
}
}

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);
},

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()
})
},

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

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

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

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


在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对象呀?这个我们后面会传的
1.在终端使用如下命令下载js-cookie插件(有了这个插件我们后面才可以使用cookie)
npm install js-cookie

2.在login.vue页面js文件
import cookie from 'js-cookie'
import loginApi from '@/api/login'

1.登录功能实现过程中需要分四步走:
2.为什么要把token字符串放到请求头(header)中?
看我们的工具类JwtUtils的getMemberIdByJwtToken方法内部代码:request.getHeader("token")表示从请求头中获取token,所以我们要把token字符串放到请求头中

老师说了,把token放到cookie中后,不再把token放到请求头中也可以,这样的话修改一下工具类JwtUtils中的方法,方法内部改为从cookie中获取token,这样是可行的
//登录的方法
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
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.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 = "/"

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.将下面的代码复制到上一步删除的位置

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

1.在default.vue页面定义方法实现退出功能
//退出登录
logout() {
//清空cookie值
cookie.set('guli_token','',{domain: 'localhost'})
cookie.set('guli_ucenter','',{domain: 'localhost'})
//跳转到首页
window.location.href = "/"
}

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