文章目录一览
🌕开发社区登录注册模块
4.开发登录、退出功能
步骤:
重点说明:
生成登录凭证,最终发送一个key给客户端,让它记录。下次再提交给服务端能够
根据key
登录凭证来识别
你。
但是登录凭证中国包含了一些敏感数据,包括用户的id,用户名,密码;这些数据不能发送给客户端,要存在服务端,可以用session或者数据库来存,我这存到数据库里。以后,对它进行一个重构,存到Redis里。
查看数据库表login_ticket
最重要的就是ticket
字段,也是整个表的核心数据
。
ticket是一个凭证
,根据凭证(核心数据)查询,最终把ticket这个字符串发送给浏览器让它保存,客户端再次访问浏览器时就把ticket给我们,我们就能通过ticket查到到全部信息,就知道哪个用户正在登录;
4.1 登录实现
4.1.1 LoginTicket
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class LoginTicket {
private int id;
private int userId;
private String ticket; //凭证,就是一个随机的字符串,不重复的——唯一标识
private int status; //0:有效 1:无效
private Date expired; //凭证过期时间
}
4.1.2 LoginTicketMapper
通过注解方式写sql,而不是xml方式。
@Mapper
public interface LoginTicketMapper {
/*
注解方式写sql好处就是:少写一个xml文件,但是缺点是sql一旦复杂,就不方便编写。
一般情况下,用xml方式,这里是为了熟悉使用注解写sql。
*/
//1.插入一个凭证
@Insert({
"insert into login_ticket (user_id,ticket,status,expired)",
"values(#{userId},#{ticket},#{status},#{expired})"
})
@Options(useGeneratedKeys = true,keyProperty = "id") //声明主键自动生成,并将值自动注入给id
public int insertLoginTicket(LoginTicket loginTicket);
//2.根据凭证(核心数据)查询,最终把ticket这个字符串发送给浏览器让它保存,
//客户端再次访问浏览器时就把ticket给我们,我们就能查到信息,知道哪个用户正在登录;
@Select({
"select user_id,ticket,status,expired ",
"from login_ticket where ticket = #{ticket}"
})
public LoginTicket selectByTicket(String ticket);
//3.修改状态————0:有效 1:无效 因为凭证是有时间限制的,过了就无效
@Update({
"update login_ticket set status = #{status} where ticket = #{ticket}"
})
public int updateStatus(String ticket,int status);
}
4.1.2.1 MapperTest进行测试
@Resource
private LoginTicketMapper loginTicketMapper;
@Test
//1.测试插入功能
public void testInsertLoginTicket(){
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(101);
loginTicket.setTicket("abc");//注入凭证数据
loginTicket.setStatus(0); //0有效 1无效
loginTicket.setExpired(new Date(System.currentTimeMillis()+60*1000*10)); //传入凭证过期时间,即当前时间的十分钟之后
int i = loginTicketMapper.insertLoginTicket(loginTicket);
if (i <= 0){
System.out.println("数据添加失败!");
}
//这里输出当前的时间 以及 凭证过期时间。判断凭证过期时间是不是当前时间的十分钟后
System.out.println(new Date()+" 数据添加成功!"+ loginTicket.getExpired());
}
@Test
//2.根据凭证(核心数据)查询
public void testSelectByTicket(){
LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc");
if (loginTicket == null){
System.out.println("未查到对应的用户!无效凭证");
}
System.out.println(loginTicket);
}
@Test
//3.修改状态————0:有效 1:无效
public void testUpdateStatus(){
int i = loginTicketMapper.updateStatus("abc", 1);
System.out.println(i+" :数据修改成功!");
}
结果:
1.测试插入功能
数据库表的时间也和12:01:15一样。成功!但是一旦手动修改数据,expired的未来时间立马变成当前时间,为什么我也不知道。
2.查询
3.更新
这个贼简单,懂都懂,浪费时间!
⭐️4.1.3 UserService
@Resource
private LoginTicketMapper loginTicketMapper;
//※登录的方法
public Map<String,Object> login(String username,String password,Long expiredSeconds){
HashMap<String, Object> map = new HashMap<>();
//1.参数空值处理
if (StringUtils.isBlank(username)){
map.put("usernameMsg","用户名不能为空!");
return map;
}
if (StringUtils.isBlank(password)){
map.put("passwordMsg","密码不能为空!");
return map;
}
//2.验证账号
User user = userMapper.selectByName(username);/*By用户名查询用户全部信息*/
if (user == null){
map.put("usernameMsg","账号/用户名不存在!");
return map;
}
//3.验证账号状态
if (user.getStatus() == 0){/*0代表账号未邮箱激活,1是激活————与ticket的失效状态0.1区分开,别混淆了*/
map.put("usernameMsg","用户未激活!请及时邮箱激活!");
return map;
}
//4.验证密码
/*调用md5方法对用户输入的密码+数据库的salt一起进行加密,和数据库中的密文密码对比*/
String md5Password = CommunityUtil.md5(password + user.getSalt());
if (!user.getPassword().equals(md5Password)){
map.put("passwordMsg","密码错误!");
return map;
}
//5.如果走到这,就说明都是成功的!所以就要开始生成登录凭证给你
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());//查出的用户id设置进去
loginTicket.setTicket(CommunityUtil.generateUUID());//工具类生成的随机字符串设置进去
loginTicket.setStatus(0); /*0凭证有效 1凭证无效*/
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds*1000));
//6.进行添加操作
loginTicketMapper.insertLoginTicket(loginTicket);
map.put("ticket",loginTicket.getTicket());
return map;
}
⭐️4.1.4 LoginController
@Value("${server.servlet.context-path}")
private String contextPath;
......
//※登录操作请求
@RequestMapping(path = "/login",method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme,
Model model, HttpSession session, HttpServletResponse response){
/*参数说明:
code —— 用户前端输入的验证码,传到后端来的
boolean rememberme —— 判断前端是否勾了记住我的选项,有就把凭证时间放长一点
session —— 页面传进来验证码,我需要取到和之前生成的验证码去对比,所以从session里取出验证码
response —— 登陆成功,要把ticket发给客户端让它好保存,所以用cookie保存;要想创建cookie就得用HttpServletResponse。
*/
//检查验证码
/*将验证码从session取出*/
String kaptcha = (String) session.getAttribute("kaptcha");
/*将验证码和用户传入的code验证码相比*/
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){
/*kaptcha、code为空,不对两者不相等,不对。三个判断*/
model.addAttribute("codeMsg","验证码不正确!");
return "/site/login";
}
//检查账号,密码
/*调业务层来处理。调service的时候,不止要存传账号密码,还要传一个ticket凭证过期的时间*/
/*逻辑:如果没有勾上'记住我',依然把它存到库里,只是凭证时间短一点;勾上‘记住我’,依然存库里,时间会长
* 所以定义两个常量时间 ———— 写在CommunityConstant中*/
long expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")){
Cookie cookie = new Cookie("ticket",map.get("ticket").toString());
cookie.setPath(contextPath);//设置cookie生效路径,全项目,但是不能写死,所以将路径注入进来 —— @Value("${server.servlet.context-path}") private String contextPath;
cookie.setMaxAge((int) expiredSeconds); //设置cookie有效时间,就是凭证时间
response.addCookie(cookie);//将cookie发送给页面;响应时就会发送给浏览器
return "redirect:/index";
}else {
model.addAttribute("usernameMsg",map.get("usernameMsg"));
model.addAttribute("passwordMsg",map.get("passwordMsg"));
return "/site/login";
}
}
4.1.5 login.html页面改造
…
4.1.6 测试
验证码错误
账号或密码错误
同时数据库的login_ticket表也数据添加了
完成!
4.2 退出登录
其中凭证改为失效,数据访问层已经写好了,只需要写业务层逻辑。
4.2.1 LoginTicketMapper
4.2.2 UserService
//※退出登录
/*退出的时候,要把凭证传过来,服务端就知道是谁要退出*/
public void logout(String ticket){
int i = loginTicketMapper.updateStatus(ticket,1);
}
⭐️4.2.3 LoginController
@RequestMapping(path = "/logout",method = RequestMethod.GET)
/*退出登录*/
//先把在前端的那个cookie拿过来,然后调用方法即可
public String logout(@CookieValue("ticket") String ticket){
userService.logout(ticket);//调更新status的方法
return "redirect:/login"; //退出后重定向到我们的登录页面 —— 有两个login的方法,一个get,一个post,默认为get。
}
4.2.4 配置页面’退出登录’的链接
4.2.5 测试
点击确实回退到登录页面:
查看数据库底层逻辑,ticket由0生效变成1无效了。
5. 显示登录信息
比如用户没有登录,头部就要显示首页、登录、注册;
用户已近登录,就要显示首页、消息、用户头像、显示用户名,不用显示登录注册;
每一次请求都要通过这种方式来显示用户信息,太麻烦了;直接通过拦截器来完美实现
5.1 LoginTicketInterceptor ——拦截器类
要实现
HandlerInterceptor
接口
即:拦截器要实现HandlerInterceptor接口,而WebMvcConfigurer接口是MVC配置类要实现的接口。
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
//按照上面图的逻辑,我要在请求的一开始就去获取ticket,从而利用ticket去查找有没有对应的user,如果有的话就暂存一下,最早做这个事。
/*为什么一开始就做呢?因为我们在整个请求过程当中,可能随时随地都会用到当前用户,所以一开始就找他,保险一点。*/
//1.controller执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/*因为preHandle是HandlerInterceptor的接口,已经定义好了参数,所以不能通过@CookieValue来获取cookie,而通过request来获取
*比较麻烦,之后也会用到这个。所以将其封装一个成为工具类CookieUtil;
* */
//1.1 从cookie中获取凭证
String ticket = CookieUtil.getValue(request,"ticket");
if (ticket != null){ //不等于空,就说明已经登录了,里边有对应的数据
//查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
//查到凭证后也不能直接用,得先判断有没有效 —— 0有效和1无效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())){
//查到的数据不为空 凭证状态为0,是有效的 凭证时间在现在时间之后,说明凭证还没失效
//根据凭证中的用户id来查询用户信息,从而更显示在前端页面。
User user = userService.findByUserId(loginTicket.getUserId());
//在本次请求中持有用户
//因为可能同时多个浏览器对同一服务器请求,是个并发情况,所以得考虑线程的隔离,每个线程中存一份,之间不互相干扰(ThreadLocal),想办法把user存到ThreadLocal里,这个逻辑我就封装到工具类HostHolder,以后可能也用到
hostHolder.setUsers(user);
}
}
return true;
}
//
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUsers();
if (user != null && modelAndView != null){
modelAndView.addObject("loginUser",user);
}
}
//最后执行完,清理user即可
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
}
}
5.1.1 CookieUtil —— 获取cookie的工具类
以后有用到,需要灵活改变参数,道理都是一样的!
public class CookieUtil {
public static String getValue(HttpServletRequest request, String name){
//判断request对象,name对象 存的值是否为空
if (request == null || name == null){
throw new IllegalArgumentException("参数不能为空!");
}
//不是空,就通过request来获取cookie值,是一个数组因为可能有多个
Cookie[] cookies = request.getCookies();
if (cookies != null){
for (Cookie cookie : cookies) {
//判断每一个cookie的name是否等于传入的参数name;如果是,就返回cookie的value
if (cookie.getName().equals(name)){
return cookie.getValue();
}
}
}
return null;
}
}
5.1.2 HostHolder —— 持有用户信息,用于代替session
//持有用户信息,用于代替session对象
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUsers(User user){
users.set(user);
}
//直接从上面的users中取就行
public User getUsers(){
return users.get();
}
//清理方法,请求结束的时候,我们把ThreadLocal中的users清掉;不然每次都只存不清,开销很大
public void clear(){
users.remove();
}
}
5.1.3 UserService
5.2 WebMvcConfig —— MVC配置类
要实现WebMvcConfigurer接口
即:拦截器要实现HandlerInterceptor接口,而WebMvcConfigurer接口是MVC配置类要实现的接口。
@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"); //放行的资源
}
}
5.3 测试
未登录:
登录: