项目源码与所需资料
链接:https://pan.baidu.com/s/1azwRyyFwXz5elhQL0BhkCA?pwd=8z59
提取码:8z59
文章目录
- demo14-注册、登录
-
- 1.用户登录业务介绍(单点登录)
- 2.整合JWT
- 3.开通阿里云短信服务
- 4.整合阿里云短信服务
- 5.用户登录注册(后端)
- 6.根据token获取用户信息(后端)
- 7.用户登录注册(前端)
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-ui
和npm 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"> </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"> </em>
</a>
<q class="red-point" style="display: none"> </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.自行测试,我的没问题