实现退出功能
- 将登录凭证修改为失效状态。
- 跳转至网站首页。
数据访问层
不用写了,已经有了updateStatus方法;
业务层
UserService
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}
Controller层
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
return "redirect:/login";
}
- 重定向默认是get请求
静态html修改index.html
<li class="nav-item ml-3 btn-group-vertical dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img src="http://images.nowcoder.com/head/1t.png" class="rounded-circle" style="width:30px;"/>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item text-center" href="site/profile.html">个人主页</a>
<a class="dropdown-item text-center" href="site/setting.html">账号设置</a>
<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>
<div class="dropdown-divider"></div>
<span class="dropdown-item text-center text-secondary">nowcoder</span>
</div>
</li>
把这里的退出登录改成th:href;
- 不明原因这里的下拉菜单没办法点开!!!先放个屁股,之后找到bug了再看。
显示登录信息
- 拦截器示例
- 定义拦截器,实现HandlerInterceptor
- 配置拦截器,为它指定拦截、排除的路径
- 拦截器应用
- 在请求开始时查询登录用户
- 在本次请求中持有用户数据
- 在模板视图上显示用户数据
- 在请求结束时清理用户数据
拦截器示例
- 拦截浏览器访问的请求,在请求开始/结束插入代码从而批量解决共有任务。
- 在controller中新建一个包Interceptor,新建AlphaInterceptor类:
@Controller
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;
}
//在请求处理之后调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle: " + handler.toString());
}
//在模板引擎之后调用
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion: " + handler.toString());
}
}
- 需要实现HandlerInterceptor接口,但该接口中方法都是Default,不一定全部都需要重写。
- 编写一个配置类配置:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired private AlphaInterceptor alphaInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
//拦截除了css,js,png,jpg,jpeg之外的所有请求
//只拦截注册和登录请求
//为什么是/**/*:static目录下所有目录下的css,js,png,jpg,jpeg文件
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login");
}
}
- 拦截除了css,js,png,jpg,jpeg之外的所有请求
- 只拦截register和login请求
- 为什么是/**/*:static目录下所有目录下的css,js,png,jpg,jpeg文件
- 测试
访问对应路径,看到以下输出:
拦截器应用
一次已登录的用户的请求过程(每次请求都有,故应该用拦截器进行复用)
- 首先封装一个util类获取对应key(ticket)的cookie:
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){//一个cookie都没有
for(Cookie cookie : cookies){
if(cookie.getName().equals(name)){
return cookie.getValue();
}
}
}
return null;
}
}
- 为UserService添加通过ticket取user的方法:
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
- 编写LoginTicketInterceptor拦截器(见下)
- 编写HostHolder确保每个用户线程独立互不干扰:
@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();
}
}
在请求开始时查询登录用户
- 重写preHandler方法:
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
//重写preHandle方法
@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());
//在本次请求中持有用户,把user暂存一下
hostHolder.setUser(user);//hostHolder相当于为当前线程的user提供一个临时的容器
}
}
return true;
}
...
}
在本次请求中持有用户数据
hostHolder.setUser(user);
在模板视图上显示用户数据
- 重写PostHandler方法:
@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);
}
}
- modelAndView.addObject(“loginUser”, user);,在之后的模版中就可以使用loginUser了。
在请求结束时清理用户数据
- 重写afterCompletion方法:
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清除用户
hostHolder.clear();
}
- 修改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" href="site/letter.html">消息<span class="badge badge-danger">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" th:if="${loginUser!=null}">
<a class="nav-link" th:href="@{/logout}">退出</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" href="site/profile.html">个人主页</a>
<a class="dropdown-item text-center" href="site/setting.html">账号设置</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>
开发账号设置
请求:必须是POST请求
- 表单:enctype=“multipart/form-data”
- Spring MVC:通过 MultipartFile类上传文件。
访问账号设置页面
controller层
设置一个新的UserController:
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(path = "/setting",method = RequestMethod.GET)
public String getSettingPage() {
return "/site/setting";
}
}
修改setting,html
- 除了静态改动态的必要设置外,
- 把index.html的链接练到/setting来。
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
<a class="nav-link" th:href="@{/user/setting}">账号设置</a>
</li>
上传头像
- 在配置文件中配置上传文件的存储路径
community.path.upload = /Users/iris/Desktop/community/upload
- 这里必须手动创建upload目录!否则之后上传的时候会不存在!
数据访问层
无。
业务层
- 更新headerUrl。
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId, headerUrl);
}
和Controller层
@RequestMapping(path = "/upload",method = RequestMethod.POST)
public String uploadHeader(MultipartFile headerImage, Model model) {
if(headerImage == null) {
logger.error("上传文件为空");
model.addAttribute("error","您还没有选择图片");
return "/site/setting";
}
//不能直接上传到服务器的文件夹中,因为服务器可能有多个用户,文件名可能重复
String fileName = headerImage.getOriginalFilename();//原始文件名
String suffix = fileName.substring(fileName.lastIndexOf("."));//文件后缀
if(suffix == null) {
logger.error("文件格式不正确");
return "redirect:/user/setting";
}
//生成随机文件名
fileName = CommunityUtil.generateUUID() + suffix;
//确定文件存放路径
java.io.File dest = new java.io.File(uploadPath + "/" + fileName);
try {
headerImage.transferTo(dest);
} catch (IOException e) {
logger.error("上传文件失败" + e.getMessage());
throw new RuntimeException("上传文件失败,服务器发生异常",e);
}
//更新当前用户的头像路径(web访问路径)
//http://localhost:8080/community/user/header/xxx.png
User user = hostHolder.getUser();
String headerUrl = domain + contextPath + "/user/header/" + fileName;
userService.updateHeader(user.getId(),headerUrl);
return "redirect:/index";
}
设置头像
真正的使用IO柳得到头像,这里注意为了解耦也在Controller里面写了:
@RequestMapping(path = "/header/{fileName}",method = RequestMethod.GET)
public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
//返回值是void:因为返回的是图片等二进制数据
//服务器存放路径
fileName = uploadPath + "/" + fileName;
//文件后缀
String suffix = fileName.substring(fileName.lastIndexOf("."));
//响应图片
response.setContentType("image/" + suffix);
try(//放在try()中的流会自动关闭
OutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(fileName);
) {
byte[] buffer = new byte[1024];
int b = 0;
while((b = fis.read(buffer)) != -1) {
os.write(buffer,0,b);
}
} catch (IOException e) {
logger.error("读取头像失败" + e.getMessage());
}
}
- 返回值是void,因此返回的是图片等二进制数据;
- 放在try括号里的流会自动关闭,java8开始;
- 要用io写入二进制数据,建立缓冲区
- 使用PathVarible注解取filename的值赋给filename变量,从而填补@RequestMapping
修改静态setting.html为动态
<form class="mt-5" method="post" enctype="multipart/form-data" th:action="@{/user/upload}">
<div class="form-group row mt-4">
<label for="head-image" class="col-sm-2 col-form-label text-right">选择头像:</label>
<div class="col-sm-10">
<div class="custom-file">
<input type="file" th:class="${'custom-file-input ' + (error!= null ? 'is-invalid' : '')}"
id="head-image" name="headerImage" lang="es" required="">
<label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label>
<div class="invalid-feedback" th:text="${error}">
请选择一张图片!
</div>
</div>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10 text-center">
<button type="submit" class="btn btn-info text-white form-control">立即上传</button>
</div>
</div>
</form>
- 这个拼接字符串永远做不对!!!
修改密码
检查登录状态
- 目的:确保用户在未登录情况下不能通过特定的url访问界面。
使用拦截器
- 在方法前标注自定义注解
- 拦截所有请求,只处理带有该注解的方法
自定义注解(重要)
- 常用的元注解:
@Target、@Retention、@Document、@Inherited - 如何读取注解:
Method.getDeclaredAnnotations() Method.getAnnotation(Class annotationClass)
- 加注解就拦截,不加注解就不拦截。
- 新建一个包annotation,在其中创建一个注解LoginRequired,使用元注解修饰:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
//什么都不用写
}
- target表示该注解只能修饰method
- retention表示注解在runtime运行时也存在;
- 创建注解的拦截器:
@Component
public class LoginRequireInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
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;
}
}
- HandlerMethod handlerMethod = (HandlerMethod) handler;传进来的handler是不是method;
- LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);:判断注解是否存在,不在返回null;
- 判断如果注解存在,用户是否登录。
- 配置拦截器,不拦截css等文件节省资源:
public void addInterceptors(InterceptorRegistry registry) {
//拦截除了css,js,png,jpg,jpeg之外的所有请求
//只拦截注册和登录请求
。。。
registry.addInterceptor(loginRequireInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
- 给需要登录的页面加上@LoginRequired注解
@LoginRequired
@RequestMapping(path = "/setting",method = RequestMethod.GET)
public String getSettingPage() {
return "/site/setting";
}
@LoginRequired
@RequestMapping(path = "/upload",method = RequestMethod.POST)
public String uploadHeader(MultipartFile headerImage, Model model) {
....
}