疫情防控交流社区平台——2.2 开发社区登录注册模块

🌕开发社区登录注册模块

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 测试

未登录:
在这里插入图片描述
登录:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

11_1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值