0%

仿牛客论坛项目03-登录功能

仿牛客论坛项目03-登录功能,内容记录。

会话管理,cookie,session,验证码,hostHolder,拦截器

会话管理

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
@ResponseBody
public String setCookie(HttpServletResponse response) {
// 创建cookie
Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
// 设置cookie生效路径
cookie.setPath(("/community/alpha"));
// 设置cookie生效时间
cookie.setMaxAge(600);
// 将cookie放入response响应头中
response.addCookie(cookie);
return "set cookie";
}
  1. 请求映射:当一个GET请求发送到服务器的”/cookie/set”路径时,Spring框架会根据@RequestMapping注解找到这个setCookie方法来处理请求。
  2. 创建Cookie:在setCookie方法内部,首先创建了一个Cookie对象。CommunityUtil.generateUUID()方法(这个类和方法没有在代码中定义,可能是自定义的)被用来生成一个唯一的标识符(UUID),作为Cookie的值。
  3. 设置Cookie属性
    • cookie.setPath("/community/alpha"):设置了Cookie的路径,这意味着Cookie只会在域名下的”/community/alpha”路径及其子路径中有效。
    • cookie.setMaxAge(600):设置了Cookie的最大年龄(以秒为单位)。在这个例子中,Cookie将在600秒(10分钟)后过期。
  4. 添加到响应:通过response.addCookie(cookie)方法,将创建的Cookie添加到HTTP响应中。这样,当响应发送回浏览器时,Cookie也会一起发送。
  5. 响应体:函数返回了一个字符串”set cookie”,这将作为HTTP响应的响应体发送回客户端。客户端浏览器会显示这个字符串,但更重要的是,浏览器会存储下发的Cookie。
  6. 客户端存储Cookie:浏览器接收到响应后,会根据响应头中的Set-Cookie指令存储Cookie。由于设置了路径,浏览器只会在访问指定路径时发送这个Cookie。
  7. 浏览器显示结果:浏览器会向用户显示返回的字符串”set cookie”,表示Cookie设置操作已经完成。
  8. Cookie的使用:从这一刻起,只要用户在浏览器中访问了指定路径,浏览器就会自动携带这个Cookie。服务器可以通过检查请求中的Cookie来识别用户或会话。

Session

存在服务器中,比Cookie更安全,响应时通过Cookie将session的id传给服务器,浏览器下次请求时会带上session的id。

缺点是会增加服务端的压力。

1
2
3
4
5
6
7
@RequestMapping(path = "/session/set", method = RequestMethod.GET)
@ResponseBody
public String setSession(HttpSession session) {
session.setAttribute("id", 1);
session.setAttribute("name", "test");
return "set session";
}

问题:若使用分布式服务器,中间件使用nginx负载均衡服务器,若浏览器发送了两次请求,第二次发送的请求,第二次请求的服务器上没有存这个session了

2

解决方法:粘性session (固定的id传给同一个服务器处理,缺点是负载不均衡)、同步session(当一个服务器创建了一个session就会同步给别的服务器,缺点是同步影响服务器性能、服务器之间耦合影响部署)、共享session(单独一台服务器存储session,缺点是这台服务器崩溃后其他服务器也不能用了)

3

主流方法:不存session了,尽量存cookie,敏感数据存在数据库(nosql数据库如Redis)中

生成验证码

引入Kaptcha包用于生成验证码

1
2
3
4
5
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>

逻辑是:用户发送一个请求,服务端生成一个验证码,用session保存文本,将图片响应给浏览器。

登录

刷新验证码

用JS写,因为用户刷新验证码不需要刷新整个页面,体验更好

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="col-sm-4">
<img th:src="@{/kaptcha}" style="width:100px;height:40px;" class="mr-2"/>
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>

<script>
function refresh_kaptcha() {
<!-- 防止浏览器不更新链接 -->
var path = CONTEXT_PATH + "/kaptcha?p" + Math.random();
<!-- id选择器获取更新验证码的标签 -->
$("kaptcha").attr("src", path);
}
</script>

登录、退出

实现逻辑:

  1. 获取登录页面
  2. 验证账号、密码、验证码(成功,生成登录凭证给客户端,跳转到首页;失败,跳转到登录页面显示提示信息)
  3. 退出(登录凭证设置为失效,跳转到首页)

生成登录凭证:

  • Dao层:

    login_ticket数据库交互,查询、增加、更新,写完后在Test类中进行测试

  • Service层:

    用户登录不成功有多种情况,账号不存在、账号为激活、账号为空、密码为空、密码不正确

    登录成功后要把生成的ticket传给客户端

  • Controller层:

    需要的参数有表单中提交的参数用户名、账号、验证码、是否记住账号,还需要Session从中获取生成的验证码。登录成功后要把登录凭证ticket传给cookie,因此还需要HttpServletResponse并将cookie保存到响应体reponse

    判断是否勾选记住密码,选择对于的登录凭证超时时间。

  • login.html页面:

    • 表单上需要有name属性,提交给服务器才能获取
    • 账号密码不正确需要回到这个页面上还有账号密码信息,th:value="${param.username}"作用是从request参数中取参数

显示登录信息

功能:用户登录后,要显示用户的头像以及个人信息,头部显示首页和消息,不显示注册和登录。

拦截器

HandlerInterceptor类有三阶段:

  1. preHandle: 在Controller之前执行
  2. postHandle: 在Controller之后执行
  3. afterCompletion: 在TemplateEngine之后执行

拦截器实现后,需要在WebMvcConfigurer实现类中添加该拦截器,并设置排除对静态资源的拦截,以及需要拦截的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Autowired
private AlphaInterceptor alphaInterceptor;

@Autowired
private LoginTicketInterceptor loginTicketInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 静态资源可以访问
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");

registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}

实现HostHolder对象

从线程中保存user、获取user,在线程结束时进行清理

线程在服务器处理完本次浏览器的请求后,线程结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();

public void setUser(User user) {
users.set(user);
}

public User getUser() {
return users.get();
}

public void clear() {
users.remove();
}
}

利用拦截器显示用户信息

  1. 在请求开始时,通过凭证找到用户,将用户存入HostHolder

  2. 在模板引擎执行之前,获取用户

  3. 在模板引擎执行之后,清理数据

  4. WebMvcConfigurer实现类中添加拦截器

  5. 修改页面,未登录时看不到消息,没登陆时显示注册、登录(登录后不显示)

上传头像(文件)

上传文件:

  • 必须是 POST 请求
  • 表单要加上 enctype="multipart/form-data"
  • Spring MVC: 提供 MultipartFile 处理上传文件

开发步骤:

  1. 访问账号设置页面
  2. 处理表单,上传头像,存储图像(本地服务器)
  3. 增加一个请求:获取头像
  • Service层:增加更新用户头像的方法
  • Controller层
    上传流程: 用户选择并上传文件,后端验证文件格式,生成随机文件名并保存文件到服务器,更新数据库中头像的 URL。
    获取流程: 用户请求头像文件,后端根据文件名读取服务器上的文件,设置合适的响应类型,并将图片文件传递给客户端。

更改密码(作业,自己写的)

开发步骤:

  1. 访问账号设置页面
  2. 处理表单,更改密码
  • Service层:增加更新用户密码的方法、校验旧密码是否正确
    • Controller层
      获取当前用户的原密码 ->
      校验用户输入的原密码是否与当前密码一致。如果不一致,添加错误信息并返回错误页面。 ->
      如果新密码和确认密码为空,添加错误信息。如果新密码和确认密码不一致,添加错误信息。确保新密码至少为 8 个字符长。如果不满足条件,添加错误信息。 ->
      校验完毕后,更新用户的密码。跳转登录页面。

检查登录状态

没登录前,在浏览器里输入一些的页面路径,要用拦截器拦截,不能访问。
使用拦截器方法:

  • 在方法钱标注自定义注解
  • 拦截所有请求,只处理带有该注解的方法

自定义注解:

  • 常用元注解:
    @Target(声明自定义注解可以作用在哪些类或方法上)
    @Retention(声明自定义注解有效时间)
    @Document(声明自定义注解在生成文档时是否要带)
    @Inherited(指定子类是否要继承父类的注解)
    举例:

    1
    2
    3
    4
    @Target(ElementType.METHOD)  
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LoginRequired {
    }
  • 如何读取注解
    用到反射
    Method.getDeclaredAnnotations()
    Method.getAnnotation(Class<T> annotationClass)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component  
public class LoginRequiredInterceptor implements HandlerInterceptor {

@Autowired
private HostHolder hostHolder;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断handle是否是方法
if(handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 判断方法是否有这个注解
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if(loginRequired != null && hostHolder.getUser() == null) {
// 需要登录但是没登录
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
}
return true;
}
}

问题

  1. 为什么要把登录凭证保存到cookie

    获取Cookie中的登录凭证,在每次请求时显示登录信息

    1

  2. 为什么表单上需要有name属性,提交给服务器才能获取

    • name属性用于标识表单控件,使浏览器在提交表单时能够正确地传递用户输入的数据。
    • 服务器端依赖这些键值对来解析和处理表单数据。
    • 如果表单控件没有name属性,控件的值不会被提交,导致服务器无法获取用户的输入数据。
  3. 根据cookie中的登录凭证获取到用户后,怎么存?

    不能存在session中,因为用户信息是隐私信息。也不能存在类中,因为浏览器有很多请求,一个服务器并发处理多个请求,需要隔离存储。

    使用一个线程本地存储(ThreadLocal),用于存储当前线程的用户信息。在一次请求处理中,它的作用流程如下:

    • 存储用户信息: 在请求开始时,从cookie中获取登录凭证,根据凭证查找用户信息,并将用户信息存储在HostHolder中。
    • 访问用户信息: 在整个请求处理过程中,业务逻辑可以随时从HostHolder中获取当前用户的信息,而无需再次查找或验证。
    • 清理用户信息: 请求处理完毕后,清理HostHolder中的用户信息,防止内存泄漏和数据污染。
  4. 为什么要使用拦截器来设置HostHolder

    使用拦截器是为了在请求特定的阶段执行逻辑操作,拦截器有三个阶段,preHandlepostHandleafterCompletion

说说登录模块

• 用户点击首页上的 登录按钮,服务端响应 登录页面。
• 用户在 登录页面 上输入 用户名、密码、验证码,然后点击登录按钮,服务端接收到 POST 请求,验证账号和密码的正确性。
• 若 验证码错误,返回错误信息,提示 验证码不正确,跳转回登录页面。
• 若 账号不存在或密码错误,返回相关错误信息,跳转回登录页面。
• 若 账号和密码正确,则:
• 生成 登录凭证(ticket),存入数据库,并返回给客户端(Cookie 存储)。
• 若用户勾选 “记住我”,设置 较长的过期时间,否则使用默认的短时间登录状态。
• 服务端返回 首页,用户登录成功。