说明:
(1)本篇博客内容:继续开发【注册/登录】接口;
● 目前位置,【注册/登录】接口开发了的内容是:先校验验证码是否OK;验证码校验OK后,先根据手机号去查查该用户是否已存在,如果用户不存在就创建这个用户;
● 本篇博客,继续完善【注册/登录】接口:注册/登录OK后,把用户会话信息(uid,utoken)保存到redis和cookie中;
(2)本篇博客特别需要注意的点:
● 设置cookie的SOP;
● 设置cookie的时候,cookie的key要前后端约定好;
(3)本篇博客一个可能遗漏的点:前端浏览的cookie,我们设置了uid和utoken;;但是,后端我们只在redis中存了utoken,却没有存uid;
(4)PS:要想更好的理解cookie、token等内容,深刻意识到前后端全栈的必要性~~~
目录
一:本篇博客合理性说明;
二:继续完善【注册/登录】接口:注册/登录OK后,把用户会话信息(uid,utoken)保存到redis和cookie中;
1.【user】用户微服务中,PassportController中的【注册/登录】接口:继续完善;
(1)完善内容,简述;
(2)用户在注册/登录成功后,如果用户是非冻结状态,就去保存用户的会话信息;
(3)使用【UUID】生成一个随机且唯一的字符,然后把这个字符作为token的值;
(4)把上面生成的token,存到(后端的)redis中;存储的方式是:【“redis_user_token:用户id”:“token值”】,比如【“redis_user_token:200628AFYM7AGWPH”:“f2eb40e3-7715-4e20-a20d-939df8e60413”】
(5.1)把上面生成的token,保存到(前端浏览器的)cookie中;同时,也要把用户id,保存到(前端浏览器的)cookie中;
(5.2)因为设置cookie的时候,需要request和response,所以我们在这个方法中注入了HttpServletResponse和HttpServletRequest;
(5.3)为此,我们在【api】接口工程的BaseController中,创建了一个设置cookie的方法setCookie()方法;
(5.4)这样一来,当前端页面请求我们这个注册/登录接口的时候,这个接口就会把用户相关的uid、utoken保存到cookie中;
(6)用户注册/登录成功后,需要删除我们先前保存在redis中的验证码;
(7)最后,根据“前后端的约定”,返回用户状态;
三:测试;
一:本篇博客合理性说明;
登录后,我们需要保存用户会话信息(以方便前后端在交互时,能够校验登录信息):
(1)以前,在后端我们会把用户信息存在session中;在前端浏览器处,用户会话信息会存在Cookies中;
● 有关cookie,session的基本内容,可以参考【Servlet与JSP进阶五:浏览器Cookie】、【Servlet与JSP进阶五:用户会话Session对象】、【Servlet与JSP进阶六:ServletContext全局对象;Java Web三大作用域对象总结;】;
(2)但是,在微服务分布式项目中,就需要使用分布式会话来做;
● 在后端:在分布式会话中,不管是分布式的、还是集群、还是微服务;;;在后端,只要把用户会话信息存在redis中,在任何节点都可以获取会话信息(只要其能连接redis服务);
● 在前端浏览器处:我们还是把用户会话信息,保存在cookie中;
● 这样一来,用户会话信息,我们既保存在了后端redis中,也保存在了前端浏览器中;;;;如此一来,前后端在用户登录校验等情况的时候,就会很容易通过用户会话信息来进行处理;
登录后,我们需要保存用户会话信息(以方便前后端在交互时,能够校验登录信息):
(1)以前,在后端我们会把用户信息存在session中;在前端浏览器处,用户会话信息会存在Cookies中;
● 有关cookie,session的基本内容,可以参考【Servlet与JSP进阶五:浏览器Cookie】、【Servlet与JSP进阶五:用户会话Session对象】、【Servlet与JSP进阶六:ServletContext全局对象;Java Web三大作用域对象总结;】;
(2)但是,在微服务分布式项目中,就需要使用分布式会话来做;
● 在后端:在分布式会话中,不管是分布式的、还是集群、还是微服务;;;在后端,只要把用户会话信息存在redis中,在任何节点都可以获取会话信息(只要其能连接redis服务);
● 在前端浏览器处:我们还是把用户会话信息,保存在cookie中;
● 这样一来,用户会话信息,我们既保存在了后端redis中,也保存在了前端浏览器中;;;;如此一来,前后端在用户登录校验等情况的时候,就会很容易通过用户会话信息来进行处理;
二:继续完善【注册/登录】接口:注册/登录OK后,把用户会话信息(uid,utoken)保存到redis和cookie中;
1.【user】用户微服务中,PassportController中的【注册/登录】接口:继续完善;
@Override
public GraceJSONResult doLogin(@Valid RegistLoginBo registLoginBo, BindingResult result,
HttpServletRequest request,
HttpServletResponse response) {
//0.判断BindingResult中是否保存了验证失败的错误信息,如果有,说明前端的输入是有问题的(手机号或者验证码,至少有一个没输入);
// 那么,我们就获取这个错误信息,并构建一个GraceJSONResult统一返回对象,返回;
if (result.hasErrors()) {
Map map = getErrorsFromBindingResult(result);
return GraceJSONResult.errorMap(map);
}
// 1.校验验证码是否匹配;
//1.1 获取用户在前端输入的手机号和验证码;
String mobile = registLoginBo.getMobile();
String smsCode = registLoginBo.getSmsCode();
//1.2 根据用户在前端输入的手机号,尝试去redis中获取对应的验证码;
String redisSMSCode = redisOperator.get(MOBILE_SMSCODE + ":" + mobile);
//1.3如果前端输入的验证码,在redis中不存在(说明:我们并没有针对【用户输入的这个手机号码】发送过验证码;
// 或者说是用户在收到验证码短信后,过了30min后才使用,redis中存的验证码过期失效了),
// 或者前端输入的验证码和redis中的不一样(说明:用户的验证码输错了);
// 那么,就返回对应的,包含错误信息的GraceJSONResult统一返回对象;
// 其中,这儿我们利用【org.apache.commons.lang3】中的StringUtils工具类的isBlank()方法来判空;
if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equals(smsCode)) {
return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
}
// 2.利用手机号,查询数据库中,该用户是否已注册;
AppUser user = userService.queryMobileIsExist(mobile);
//2.1如果用户存在但是已经冻结;直接返回对应的(包含错误信息)的GraceJSONResult统一返回对象
if ((user != null) && (user.getActiveStatus() == UserStatus.FROZEN.type)) {
return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
} else if (user == null) {//2.2如果用户为空,我们就去注册这个用户
user = userService.createUser(mobile);
}
// 3.保存用户分布式会话的相关操作;(如果能够走到这一步,说明用户要么登录了,要么注册了)
int userActiveStatus = user.getActiveStatus();//获取用户状态
//3.1如果用户状态不是冻结,就去保存
if (userActiveStatus != UserStatus.FROZEN.type) {
//用户会话,可以使用一个token;然后,token可以使用一个随机的uuid来做;
String uToken = UUID.randomUUID().toString();
//把该用户的token信息,保存在redis中;其key是【redis_user_token:200628AFYM7AGWPH】这种;
redisOperator.set(REDIS_USER_TOKEN + ":" + user.getId(), uToken);
//保存用户id和token到cookie中;
setCookie(request, response, "utoken", uToken, COOKIE_MONTH);
setCookie(request, response, "uid", user.getId(), COOKIE_MONTH);
}
//4.用户登录/注册成功后,【我们在前面用户登录/注册时,保存在redis中验证码】要删除;(因为,验证码只能使用一次)
redisOperator.del(MOBILE_SMSCODE + ":" + mobile);
//最后,返回用户的状态;;;(这个主要是,【后端和前端的约定】就是这样的)
return GraceJSONResult.ok(user.getActiveStatus());
}
说明:
(1)完善内容,简述;
……………………………………………………
(2)用户在注册/登录成功后,如果用户是非冻结状态,就去保存用户的会话信息;
……………………………………………………
(3)使用【UUID】生成一个随机且唯一的字符,然后把这个字符作为token的值;
● 有关UUID的内容,可以参考【附加:UUID,通用唯一识别码(Universally Unique Identifier)(Java的UUID.randomUUID()工具类,可以生成随机且唯一的统一标识码);】;
● 有关token的内容,可以参考【待写……】
……………………………………………………
(4)把上面生成的token,存到(后端的)redis中;存储的方式是:【“redis_user_token:用户id”:“token值”】,比如【“redis_user_token:200628AFYM7AGWPH”:“f2eb40e3-7715-4e20-a20d-939df8e60413”】
● 说明;
● 一个小收获:因为在插入数据时候,其主键id是我们自己基于雪花算法生成的字符去设置的,所以此时就不用考虑主键回填这档子事;
● 这样以后,在redis中,就保存了专门针对该用户的token信息(【“redis_user_token:用户id”:“token值”】);;;然后,其实保存到redis中,也就相当于我们在后端保存了该用户的token信息;
……………………………………………………
(5.1)把上面生成的token,保存到(前端浏览器的)cookie中;同时,也要把用户id,保存到(前端浏览器的)cookie中;
● 因为我们这是个前后端分离的项目,上面我们把用户的token信息保存到了后端;
● 那么也要把用户的token信息保存到前端浏览器;即,也要把相关会话信息,保存到前端浏览器的cookie中;;;在注册/登录成功后,前端浏览器都会保留用户的一些最基本信息;;
● 前端保存了userId和token的话,前后端在交互的时候;;;他们就可以很方便的进行用户登录的校验;
● 这儿我们的cookie保存的是【用户id】和【token】;其cookie的key我们设为了【uid】和【utoken】,这两个key不是随便写的,是需要和前端约定好的;;;;比如,我们在注册/登录页OK后,其会跳转到accountInfo.html页面;
● cookie的存活时间,我们设置为了30天;这个值,我们定义在了BaseController中;
(5.2)因为设置cookie的时候,需要request和response,所以我们在这个方法中注入了HttpServletResponse和HttpServletRequest;
(5.3)为此,我们在【api】接口工程的BaseController中,创建了一个设置cookie的方法setCookie()方法;
public void setCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, Integer maxAge) {
try {
cookieValue = URLEncoder.encode(cookieValue, "UTF-8");
setCookieValue(request, response,
cookieName, cookieValue, maxAge);
// Cookie cookie = new Cookie(cookieName, cookieValue);
// cookie.setMaxAge(maxAge);
// //设置,在哪些域名之下,可以拿到cookie信息;
// // 这样设置以后,只要域名是【imoocnews.com】,前端浏览器都可以拿到cookie信息;
// cookie.setDomain("imoocnews.com");
// cookie.setPath("/");//在哪些url下,可以包含cookie;即在【imoocnews.com】
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public void setCookieValue(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, Integer maxAge) {
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setMaxAge(maxAge);
//设置,在哪些域名之下,可以拿到cookie信息;
// 这样设置以后,只要域名是【imoocnews.com】,前端浏览器都可以拿到cookie信息;
cookie.setDomain("imoocnews.com");
cookie.setPath("/");//在哪些url下,可以包含cookie;即在【imoocnews.com】
//把cookie设置到响应中去;
response.addCookie(cookie);
}
● 我们把这个方法设置到了BaseController中,这样一来,后面其他地方需要的时候,就可以去调用;
● 把传过来的cookieValue基于UTF-8,编一下码;
● 使用cookieName和cookieValue实例化一个cookie对象;然后,设置cookie的存活时间;
● 设置cookie的域(domain);(我觉得:cookie的域,主要作用是【在“域名”这个较粗的粒度上,去控制cookie的可访问范围】)
● 设置cookie的path;(我觉得:cookie的path,主要作用是【在“域名”这个较粗的粒度确定后,在“目录”这个较细的粒度上去控制cookie的可访问范围】)
● 把cookie设置到response响应中去;
● 此外,cookie还有其他的一些内容;
(5.4)这样一来,当前端页面请求我们这个注册/登录接口的时候,这个接口就会把用户相关的uid、utoken保存到cookie中;
对于一些【需要是用户登录时才能操作的页面】,这些页面的对应的前端文件中就会尝试获取uid、utoken这些cookie信息的代码;;;如果,前端页面能够成功获取这些cookie信息,一切OK;;;如果,获取不到,前端页面自然编写了相应的逻辑,来提醒并阻止用户;
……………………………………………………
(6)用户注册/登录成功后,需要删除我们先前保存在redis中的验证码;
● 用户一旦注册/登录成功,就表示验证码已经被使用了,那么就需要删除;
……………………………………………………
(7)最后,根据“前后端的约定”,返回用户状态;
● 系统设计师在设计这个项目的时候,其对每个接口都做了严格设置;;;作为前端开发者和后端开发者来说,严格按照接口文档和系统设计的内容,去开发就行了;
1.【user】用户微服务中,PassportController中的【注册/登录】接口:继续完善;
@Override
public GraceJSONResult doLogin(@Valid RegistLoginBo registLoginBo, BindingResult result,
HttpServletRequest request,
HttpServletResponse response) {
//0.判断BindingResult中是否保存了验证失败的错误信息,如果有,说明前端的输入是有问题的(手机号或者验证码,至少有一个没输入);
// 那么,我们就获取这个错误信息,并构建一个GraceJSONResult统一返回对象,返回;
if (result.hasErrors()) {
Map map = getErrorsFromBindingResult(result);
return GraceJSONResult.errorMap(map);
}
// 1.校验验证码是否匹配;
//1.1 获取用户在前端输入的手机号和验证码;
String mobile = registLoginBo.getMobile();
String smsCode = registLoginBo.getSmsCode();
//1.2 根据用户在前端输入的手机号,尝试去redis中获取对应的验证码;
String redisSMSCode = redisOperator.get(MOBILE_SMSCODE + ":" + mobile);
//1.3如果前端输入的验证码,在redis中不存在(说明:我们并没有针对【用户输入的这个手机号码】发送过验证码;
// 或者说是用户在收到验证码短信后,过了30min后才使用,redis中存的验证码过期失效了),
// 或者前端输入的验证码和redis中的不一样(说明:用户的验证码输错了);
// 那么,就返回对应的,包含错误信息的GraceJSONResult统一返回对象;
// 其中,这儿我们利用【org.apache.commons.lang3】中的StringUtils工具类的isBlank()方法来判空;
if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equals(smsCode)) {
return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
}
// 2.利用手机号,查询数据库中,该用户是否已注册;
AppUser user = userService.queryMobileIsExist(mobile);
//2.1如果用户存在但是已经冻结;直接返回对应的(包含错误信息)的GraceJSONResult统一返回对象
if ((user != null) && (user.getActiveStatus() == UserStatus.FROZEN.type)) {
return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
} else if (user == null) {//2.2如果用户为空,我们就去注册这个用户
user = userService.createUser(mobile);
}
// 3.保存用户分布式会话的相关操作;(如果能够走到这一步,说明用户要么登录了,要么注册了)
int userActiveStatus = user.getActiveStatus();//获取用户状态
//3.1如果用户状态不是冻结,就去保存
if (userActiveStatus != UserStatus.FROZEN.type) {
//用户会话,可以使用一个token;然后,token可以使用一个随机的uuid来做;
String uToken = UUID.randomUUID().toString();
//把该用户的token信息,保存在redis中;其key是【redis_user_token:200628AFYM7AGWPH】这种;
redisOperator.set(REDIS_USER_TOKEN + ":" + user.getId(), uToken);
//保存用户id和token到cookie中;
setCookie(request, response, "utoken", uToken, COOKIE_MONTH);
setCookie(request, response, "uid", user.getId(), COOKIE_MONTH);
}
//4.用户登录/注册成功后,【我们在前面用户登录/注册时,保存在redis中验证码】要删除;(因为,验证码只能使用一次)
redisOperator.del(MOBILE_SMSCODE + ":" + mobile);
//最后,返回用户的状态;;;(这个主要是,【后端和前端的约定】就是这样的)
return GraceJSONResult.ok(user.getActiveStatus());
}
说明:
(1)完善内容,简述;
……………………………………………………
(2)用户在注册/登录成功后,如果用户是非冻结状态,就去保存用户的会话信息;
……………………………………………………
(3)使用【UUID】生成一个随机且唯一的字符,然后把这个字符作为token的值;
● 有关UUID的内容,可以参考【附加:UUID,通用唯一识别码(Universally Unique Identifier)(Java的UUID.randomUUID()工具类,可以生成随机且唯一的统一标识码);】;
● 有关token的内容,可以参考【待写……】
……………………………………………………
(4)把上面生成的token,存到(后端的)redis中;存储的方式是:【“redis_user_token:用户id”:“token值”】,比如【“redis_user_token:200628AFYM7AGWPH”:“f2eb40e3-7715-4e20-a20d-939df8e60413”】
● 说明;
● 一个小收获:因为在插入数据时候,其主键id是我们自己基于雪花算法生成的字符去设置的,所以此时就不用考虑主键回填这档子事;
● 这样以后,在redis中,就保存了专门针对该用户的token信息(【“redis_user_token:用户id”:“token值”】);;;然后,其实保存到redis中,也就相当于我们在后端保存了该用户的token信息;
……………………………………………………
(5.1)把上面生成的token,保存到(前端浏览器的)cookie中;同时,也要把用户id,保存到(前端浏览器的)cookie中;
● 因为我们这是个前后端分离的项目,上面我们把用户的token信息保存到了后端;
● 那么也要把用户的token信息保存到前端浏览器;即,也要把相关会话信息,保存到前端浏览器的cookie中;;;在注册/登录成功后,前端浏览器都会保留用户的一些最基本信息;;
● 前端保存了userId和token的话,前后端在交互的时候;;;他们就可以很方便的进行用户登录的校验;
● 这儿我们的cookie保存的是【用户id】和【token】;其cookie的key我们设为了【uid】和【utoken】,这两个key不是随便写的,是需要和前端约定好的;;;;比如,我们在注册/登录页OK后,其会跳转到accountInfo.html页面;
● cookie的存活时间,我们设置为了30天;这个值,我们定义在了BaseController中;
(5.2)因为设置cookie的时候,需要request和response,所以我们在这个方法中注入了HttpServletResponse和HttpServletRequest;
(5.3)为此,我们在【api】接口工程的BaseController中,创建了一个设置cookie的方法setCookie()方法;
public void setCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, Integer maxAge) {
try {
cookieValue = URLEncoder.encode(cookieValue, "UTF-8");
setCookieValue(request, response,
cookieName, cookieValue, maxAge);
// Cookie cookie = new Cookie(cookieName, cookieValue);
// cookie.setMaxAge(maxAge);
// //设置,在哪些域名之下,可以拿到cookie信息;
// // 这样设置以后,只要域名是【imoocnews.com】,前端浏览器都可以拿到cookie信息;
// cookie.setDomain("imoocnews.com");
// cookie.setPath("/");//在哪些url下,可以包含cookie;即在【imoocnews.com】
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public void setCookieValue(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, Integer maxAge) {
Cookie cookie = new Cookie(cookieName, cookieValue);
cookie.setMaxAge(maxAge);
//设置,在哪些域名之下,可以拿到cookie信息;
// 这样设置以后,只要域名是【imoocnews.com】,前端浏览器都可以拿到cookie信息;
cookie.setDomain("imoocnews.com");
cookie.setPath("/");//在哪些url下,可以包含cookie;即在【imoocnews.com】
//把cookie设置到响应中去;
response.addCookie(cookie);
}
● 我们把这个方法设置到了BaseController中,这样一来,后面其他地方需要的时候,就可以去调用;
● 把传过来的cookieValue基于UTF-8,编一下码;
● 使用cookieName和cookieValue实例化一个cookie对象;然后,设置cookie的存活时间;
● 设置cookie的域(domain);(我觉得:cookie的域,主要作用是【在“域名”这个较粗的粒度上,去控制cookie的可访问范围】)
● 设置cookie的path;(我觉得:cookie的path,主要作用是【在“域名”这个较粗的粒度确定后,在“目录”这个较细的粒度上去控制cookie的可访问范围】)
● 把cookie设置到response响应中去;
● 此外,cookie还有其他的一些内容;
(5.4)这样一来,当前端页面请求我们这个注册/登录接口的时候,这个接口就会把用户相关的uid、utoken保存到cookie中;
对于一些【需要是用户登录时才能操作的页面】,这些页面的对应的前端文件中就会尝试获取uid、utoken这些cookie信息的代码;;;如果,前端页面能够成功获取这些cookie信息,一切OK;;;如果,获取不到,前端页面自然编写了相应的逻辑,来提醒并阻止用户;
……………………………………………………
(6)用户注册/登录成功后,需要删除我们先前保存在redis中的验证码;
● 用户一旦注册/登录成功,就表示验证码已经被使用了,那么就需要删除;
……………………………………………………
(7)最后,根据“前后端的约定”,返回用户状态;
● 系统设计师在设计这个项目的时候,其对每个接口都做了严格设置;;;作为前端开发者和后端开发者来说,严格按照接口文档和系统设计的内容,去开发就行了;
三:测试;
1.先全局install一下整个项目;
2.然后,运行【user】用户微服务的主启动类;
● 如果在这个过程中,发现端口被占用的情况,对于Windows系统,可以采取下面的解决策略;
3.然后,访问注册/登录页;
附加,比如自己登录CSDN后,在浏览器中就保存了CSDN设置进cookie中一些信息;
● 所以呐,感觉【向cookie中设置值】就是一个很好的手段;;;通过这个手段,前后端可以很好的校验和控制用户会话信息;
● 一开始的时候,我们把【uid】和【utoken】都保存在了【前端的浏览器】和【后端的redis】中;;;那么,以后后面前端界面在访问后端接口的时候,请求中就会附带【uid】和【utoken】信息;;;后端接口在拿到请求的时候,就会根据其中的【uid】和【utoken】信息的具体值,来和存在redis中的值进行对比,以确认该用户的登录是否是OK的;
1.先全局install一下整个项目;
2.然后,运行【user】用户微服务的主启动类;
● 如果在这个过程中,发现端口被占用的情况,对于Windows系统,可以采取下面的解决策略;
3.然后,访问注册/登录页;
附加,比如自己登录CSDN后,在浏览器中就保存了CSDN设置进cookie中一些信息;
● 所以呐,感觉【向cookie中设置值】就是一个很好的手段;;;通过这个手段,前后端可以很好的校验和控制用户会话信息;
● 一开始的时候,我们把【uid】和【utoken】都保存在了【前端的浏览器】和【后端的redis】中;;;那么,以后后面前端界面在访问后端接口的时候,请求中就会附带【uid】和【utoken】信息;;;后端接口在拿到请求的时候,就会根据其中的【uid】和【utoken】信息的具体值,来和存在redis中的值进行对比,以确认该用户的登录是否是OK的;



