问答平台(2),显示登录信息

问题背景

每个页面头部都要显示用户头像。
如果用户没有登录,页面最上方显示的是登录按钮;如果用户登录,显示的是头像、消息等按钮。
根据是否登录,调整页面内容。

拦截器的好处

如果每个页面都调用相同的方法来显示用户信息,耦合度高。
利用Spring拦截器来解决问题:拦截浏览器访问请求,在请求的开始和结束部分插入,批量解决多个请求共有的业务,低耦合度。

拦截器的示例

定义拦截器

实现 HandlerInterceptor。
有三个方法:请求前执行、请求后执行、模版引擎执行后执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 包:controller -> interceptor
// AlphaInterceptor.java
@Component
public class AlphaInterceptor implements HandlerInterceptor {

private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);

// 在Controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle: " + handler.toString());
return true;
}

// 在Controller之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: " + handler.toString());
}

// 在TemplateEngine之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion: " + handler.toString());
}
}

配置拦截器

为它指定拦截、排除的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 包:config
// WebMvcConfig.java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

// 注入拦截器
@Autowired
private AlphaInterceptor alphaInterceptor;

// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//将拦截器加入registry对象
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");
}
}

拦截器的应用

示意图

请求过程-图示

1
2
3
4
- 在请求开始时查询登录用户
- 在本次请求中持有用户数据
- 在模板视图上显示用户数据
- 在请求结束时清理用户数据

工具类

CookieUtil: 处理每次请求得到 Cookie 中的 Ticket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// CookieUtil.java
public class CookieUtil {

public static String getValue(HttpServletRequest request, String name) {
if(request == null || name == null) {
throw new IllegalArgumentException("参数为空!");
}

Cookie[] cookies = request.getCookies();
if(cookies != null) {
for(Cookie cookie : cookies) {
// 找到数据,返回Cookie值
if(cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}

HostHolder: 由于服务器是多线程环境,如果简单的将 User 信息存入一个容器中,很有可能产生冲突。此时用到了线程私有的 ThreadLocal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// HostHolder.java
/**
* 持有用户信息,用于代替session对象.
*/
@Component
public class HostHolder {

// 通过ThreadLocal进行线程隔离
private ThreadLocal<User> users = new ThreadLocal<User>();

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

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

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

定义拦截器

登录的拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// LoginTicketInterceptor.java
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {

@Autowired
private UserService userService;

@Autowired
private HostHolder hostHolder;

// 请求开始时通过ticket获取User信息,将信息存入ThreadLocal
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从cookie中获取凭证
String ticket = CookieUtil.getValue(request, "ticket");

if(ticket != null) {
// 查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 检查凭证是否过期(有效)
if(loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户
hostHolder.setUser(user);
}
}

return true;
}

// Controller方法执行后,将User信息填充到model
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
if(user != null && modelAndView != null) {
modelAndView.addObject("loginUser", user);
}
}

// 渲染后,清空ThreadLocal里面的User信息
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
}

配置拦截器

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

// 注入拦截器
@Autowired
private AlphaInterceptor alphaInterceptor;

@Autowired
private LoginTicketInterceptor loginTicketInterceptor;

// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//将拦截器加入registry对象
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");

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

页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!-- index.html -->
<!-- 头部 -->
<!-- 功能 -->
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item ml-3 btn-group-vertical">
<a class="nav-link" th:href="@{/index}">首页</a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
<a class="nav-link position-relative" th:href="@{/letter/list}">消息
<span class="badge badge-danger"
th:text="${allUnreadCount!=0?allUnreadCount:''}">12
</span>
</a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}">
<a class="nav-link" th:href="@{/register}">注册</a>
</li>
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}">
<a class="nav-link" th:href="@{/login}">登录</a>
</li>
<li class="nav-item ml-3 btn-group-vertical dropdown" th:if="${loginUser!=null}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img th:src="${loginUser.headerUrl}" class="rounded-circle" style="width:30px;"/>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item text-center"
th:href="@{|/user/profile/${loginUser.id}|}">个人主页</a>
<a class="dropdown-item text-center" th:href="@{/user/setting}">账号设置</a>
<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
<div class="dropdown-divider"></div>
<span class="dropdown-item text-center text-secondary" th:utext="${loginUser.username}">nowcoder</span>
</div>
</li>
</ul>
<!-- 搜索 -->
<form class="form-inline my-2 my-lg-0" method="get" th:action="@{/search}">
<input class="form-control mr-sm-2" type="search" aria-label="Search" name="keyword" th:value="${keyword}"/>
<button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button>
</form>
</div>

问答平台(2),显示登录信息
https://lcf163.github.io/2020/04/30/问答平台(2),显示登录信息/
作者
乘风的小站
发布于
2020年4月30日
许可协议