关于登陆和注册的几个问题

众所周知,用户注册登陆时,有以下几个问题:

1. 密码在数据库中不能以明文保存,为了增强安全性,会先为password加盐(salt),然后通过加密算法,如MD5之类的算法,对加盐后的密码进行加密,保存在数据库中。

salt:可以通过UUID.randomUUID().toString().substring(0, 5)来获取随机salt,可以给每个用户分配一个salt,保存在user表中

2. 注册处理流程:

  1. 检测用户名密码合法性(前端要检验,后端service同样要再次查验。原来的公司使用validator校验,为了防止空指针异常,service层也会再度查验一下用户名密码是否为空,这样前后就有三次校验)
  2. 密码salt加密
  3. 验证码/用户邮件激活/短信激活 等用于防止机器人垃圾用户注册。

ps. 判断String str 是否为空,这里为空包括null和空串“”,应用 if (str == null || str.isEmpty()) 来判断。更严格一点,应该用StringUtils.isNotBlank(str)来判断,这样把空格和换行符也过滤掉了。

isEmpty():实际上就是判断 length() == 0,不包括null 和 “    ”

isBlank():检验所有空串,包括但不限于null,"","   ","/r/n"等

参考文章:StringUtils中isNotEmpty和isNotBlank的区别

3. 登陆处理流程:

  1. service层先判断用户名密码的合法性,用户名是否存在等。
  2. 服务器后台密码校验,如果密码正确,为用户创建一个token(ticket),该token可以取sessionId,也可以生成一个随机值存在cookie中
  3. 服务器端:数据库中token关联userId,设置token在服务器端的有效期
  4. 客户端:存储token,浏览器存储在cookie中,app存储在本地
  5. 若浏览器或app设置了“记住登陆”,则为token设置客户端的有效期
ps. 注册完成后,也应该像登陆一样下发一个token,用于立刻登陆

综上可以看出,登陆其实就是一个token的下发过程,只要下发了token,使客户端和服务器端的token可以对得上,该用户就算登陆成功了。

4. 客户端登陆成功还不算结束,如何使浏览器/app知道当前的登陆用户是谁?

思想:现在浏览器/app应该已经有了登陆用户的token,可以随时去后台查出该token关联的用户id,从而知道当前用户是谁,但是在每个类中都这么查询显然太麻烦,如何查询一次就可以在该请求的全程都可以使用到user信息呢?

  1. ThreadLocal 变量:每个线程有一份自己的value,用于存储User信息,这样每个线程访问到的是自己线程的User
  2. 拦截器PassportInterceptor:在每个请求开始之前,检查浏览器cookie中是否有token字段;如果有,去数据库中查询该字段的token,验证该token在服务器端是否过期,是否可用;如果可用,取出该token关联的user,并将user保存在ThreadLocal<User>中,便于该请求后续所有类都可以使用user。

5. 拦截器在获取当前登陆用户中的作用:

  • boolean preHandle():请求进来首先进入preHandle(),在这里获取cookie中的token,去后台查询token是否有效,并取出关联user存入ThreadLocal变量。返回true,继续流程,会进入下一个拦截器或主controller程序;返回false,则流程中断,可以通过response来产生响应。
  • postHandle():在Controller方法调用之后,DispatcherServlet进行视图渲染之前调用,有一个ModelAndView参数,在这里将ThreadLocal中的user信息存入ModelAndView中,用于在页面上显示当前登陆用户的信息。
  • afterCompletion():视图渲染结束,整个请求结束后,主要用于资源清理。在这里调用ThreadLocal的remove操作,清理ThreadLocal变量。

注:只有preHandle()返回true时,才会执行postHandle()和afterCompletion()。

6. 遇到的问题:

1. 生成cookie后,一定要设置cookie.setPath("/"),否则cookie只在请求路径中有效。

关于cookie的path,如果不设置,会自动赋默认值,规则如下:

...............其他代码................
for (String headerValue : responseHeaders.get(headerKey)) {
 try {
  List<HttpCookie> cookies = HttpCookie.parse(headerValue);
  for (HttpCookie cookie : cookies) {
     if (cookie.getPath() == null) {
         // If no path is specified, then by default
         // the path is the directory of the page/doc
         String path = uri.getPath();
         if (!path.endsWith("/")) {
            int i = path.lastIndexOf("/");
            if (i > 0) {
               path = path.substring(0, i + 1);
             } else {
               path = "/";
            }
         }
         cookie.setPath(path);
      }
      ...............其他代码................

所以,cookie不设置path时,会获取请求uri:如果uri以 / 结尾,直接将其设置为cookie的path值;如果URI不以 / 结尾,取最后一个 / 前面的路径作为path。

2. 重定位问题

例如用户访问某个页面,该页面要求用户登陆,会先跳转到登陆页面,待登陆成功后,直接跳转回之前要访问的页面,这种应该怎么实现呢?

很简单,添加第二个拦截器——“验证用户是否登陆”的LoginRequired拦截器,在需要登陆权限的页面上使用。因为之前所有controller都已经添加获取当前登陆用户的拦截器PassportInterceptor,所以如果当前用户是登陆用户,ThreadLocal对象中会保存当前用户的对象。在LoginRequiredInterceptor中取ThreadLocal对象,如果存在,则说明是登陆用户,preHandle()返回true,程序继续往下走即可。如果没有登陆, 则取不到ThreadLocal中的对象,这时就不能按流程往下走了,preHandle()要返回false,通过response产生响应。该响应要做什么呢?发送重定位请求至登陆页面。这里为了记住最初请求的路径,会在重定位信息中添加一个next作为请求参数:

httpServletResponse.sendRedirect("/reglogin?next=" + httpServletRequest.getRequestURI());

在注册登陆的controller请求中解析出这个参数,登陆成功后不再直接跳转首页,而是跳转到指定页面:“redirect:”+ next

注:重定位:可以在controller中通过“redirect”开头的返回值实现,整个字符串像模板名一样返回。return "redirect:/" 实现重定位至首页。


猜你喜欢

转载自blog.csdn.net/blacktal/article/details/79414695