3.生成验证码 + 开发登录、退出功能 + 显示登录信息

目录

1.生成验证码

2.开发登录、退出功能

2.1 开发数据访问层

2.2 开发业务层:实现登录功能

2.3 编写表线程逻辑

2.4 退出

3.显示登录信息

3.1  请求开始获得 ticket,利用 ticket 查找对应的 user

3.2 在模板引擎之前将 User 存储到 Model中

3.3 配置拦截器


1.生成验证码

使用 kaptcha 工具:

  • 导入 jar 包
  • 编写 Kaptcha 配置类
  • 生成随机字符、生成图片

pom.xml:

<dependency>
	<groupId>com.github.penggle</groupId>
	<artifactId>kaptcha</artifactId>
	<version>2.3.2</version>
</dependency>

新建 config 包,新建  KaptchaConfig 配置类:

  • 配置类需要加注解 @Configuration
  • 使用 @Bean 注解 :实例化接口
  • kaptcha 核心对象是接口
package com.example.demo.config;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;

@Configuration
public class KaptchaConfig {

    @Bean
    public Producer kaptchaProducer() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "100");
        properties.setProperty("kaptcha.image.height", "40");
        properties.setProperty("kaptcha.textproducer.font.size", "32");
        properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");

        DefaultKaptcha kaptcha = new DefaultKaptcha();
        Config config = new Config(properties);
        kaptcha.setConfig(config);
        return kaptcha;
    }
}

在 controller 包下的 LoginController 类中添加一个生成验证码的方法:

  • 返回 void:向浏览器输出一个特殊东西,是一张图片
@Controller
public class LoginController {

    //日志
    private static final Logger logger =LoggerFactory.getLogger(LoginController.class);

    @Autowired
    private Producer kaptchaProducer;

    //生成验证码的方法
    @RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
    public void getKaptcha(HttpServletResponse response, HttpSession session) {
        // 生成验证码
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);

        // 将验证码存入session
        session.setAttribute("kaptcha", text);

        // 将图片输出给浏览器
        response.setContentType("image/png");
        try {
            OutputStream os = response.getOutputStream();
            ImageIO.write(image, "png", os);
        } catch (IOException e) {
            logger.error("响应验证码失败:" + e.getMessage());
        }
    }
}

2.开发登录、退出功能

  • 访问登录页面:点击顶部区域内的链接,打开登录页面
  • 登录:验证账号、密码、验证码;成功时,生成登录凭证,发放给客户端;失败时,跳转回登录页
  • 退出:将登录凭证修改为失效状态;跳转至网站首页

登录凭证表:

2.1 开发数据访问层

在 entity 包下创建 LoginTicket 实体类,用来封装上述数据

package com.example.demo.entity;

import java.util.Date;

public class LoginTicket {

    private int id;
    private int userId;
    private String ticket;//字符串
    private int status;//状态
    private Date expired;//到期期

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getTicket() {
        return ticket;
    }

    public void setTicket(String ticket) {
        this.ticket = ticket;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Date getExpired() {
        return expired;
    }

    public void setExpired(Date expired) {
        this.expired = expired;
    }

    @Override
    public String toString() {
        return "LoginTicket{" +
                "id=" + id +
                ", userId=" + userId +
                ", ticket='" + ticket + '\'' +
                ", status=" + status +
                ", expired=" + expired +
                '}';
    }
}

接下来实现数据访问逻辑,在 dao 包下新建 LoginTicketMapper接口:

  • 添加注解 @Mapper,表明是数据访问对象需要容器管理
  • 业务方法:增加数据、选择凭证、修改状态
  • 在 Mapper 接口中添加注解实现对应的SQL
package com.example.demo.dao;
import com.example.demo.entity.LoginTicket;
import org.apache.ibatis.annotations.*;

@Mapper
public interface LoginTicketMapper {

    @Insert({
            "insert into login_ticket(user_id,ticket,status,expired) ",
            "values(#{userId},#{ticket},#{status},#{expired})"
    })
    //自动生成主键
    @Options(useGeneratedKeys = true, keyProperty = "id")
    //增加
    int insertLoginTicket(LoginTicket loginTicket);

    @Select({
            "select id,user_id,ticket,status,expired ",
            "from login_ticket where ticket=#{ticket}"
    })
    //查询:根据 ticket 进行查询
    LoginTicket selectByTicket(String ticket);

    @Update({
            //动态 sql:添加 <script> 标签,然后使用 if 标签
            //xml 中使用 if 标签,不需要加 <script> 标签
            "<script>",
            "update login_ticket set status=#{status} where ticket=#{ticket} ",
            "<if test=\"ticket!=null\"> ",
            "and 1=1 ",
            "</if>",
            "</script>"
    })
    //修改
    int updateStatus(String ticket, int status);
}

需要注意的是,sql 语句一定要写正确,因为没有提示,接下来我们可以做一个测试:

插入测试:

public class MailTests {
    @Autowired
    private LoginTicketMapper loginTicketMapper;

    @Test
    public void testInsertLoginTicket() {
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(101);
        loginTicket.setTicket("abc");
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10));

        loginTicketMapper.insertLoginTicket(loginTicket);
    }

}

查询修改测试:

    //查询凭证、修改状态
    @Test
    public void testSelectLoginTicket() {
        LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc");
        System.out.println(loginTicket);

        loginTicketMapper.updateStatus("abc", 1);
        loginTicket = loginTicketMapper.selectByTicket("abc");
        System.out.println(loginTicket);
    }


2.2 开发业务层:实现登录功能

在 service 包下的 UserService 中新增代码:

  • 注入 LoginTicketMapper
  • 实现登录功能:成功、失败、不存在等等情况,返回数据的情况很多,可以使用 map 封装多种返回结果
  • 登录需要传入用户、密码、凭证有限时间
  • 处理空值操作(用户、密码)
  • 验证账号是否注册:使用输入的 username 去库里查询是否存在
  • 验证账号是否处于激活状态
  • 验证密码是否正确
  • 库里的密码是加密过的,判断密码是否相同,需要给用户的密码也进行加密
  • 如果上述验证都没有问题,则登录成功,生成登录凭证
@Service
public class UserService implements CommunityConstant {
    @Autowired
    private LoginTicketMapper loginTicketMapper;

    /**
     * 实现登录功能
     */
    //实现登录功能:成功、失败、不存在等等情况,返回数据的情况很多,可以使用 map 封装多种返回结果
    //登录需要传入用户、密码、凭证有限时间
    public Map<String, Object> login(String username, String password, int expiredSeconds) {
        Map<String, Object> map = new HashMap<>();

        // 空值处理
        if (StringUtils.isBlank(username)) {
            map.put("usernameMsg", "账号不能为空!");
            return map;
        }
        if (StringUtils.isBlank(password)) {
            map.put("passwordMsg", "密码不能为空!");
            return map;
        }

        // 验证账号
        User user = userMapper.selectByName(username);
        if (user == null) {
            map.put("usernameMsg", "该账号不存在!");
            return map;
        }

        // 验证状态
        if (user.getStatus() == 0) {
            map.put("usernameMsg", "该账号未激活!");
            return map;
        }

        // 验证密码
        password = CommunityUtil.md5(password + user.getSalt());//加密后的密码
        if (!user.getPassword().equals(password)) {
            map.put("passwordMsg", "密码不正确!");
            return map;
        }

        // 生成登录凭证
        LoginTicket loginTicket = new LoginTicket();//创建实体往库里存
        loginTicket.setUserId(user.getId());
        loginTicket.setTicket(CommunityUtil.generateUUID());//生成不重复随机字符串
        loginTicket.setStatus(0);
        loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
        loginTicketMapper.insertLoginTicket(loginTicket);

        map.put("ticket", loginTicket.getTicket());
        return map;
    }
}

2.3 编写表线程逻辑

在 controller 中写方法处理页面请求,登录页面传入表单(账号、密码、验证码),收到这三个信息提交给 UserService 处理,成功时,发放给客户端;失败时,跳转回登录页,在 controller 包下的 LoginController 中新增登陆方法

  • 首先添加访问路径:处理表单提交的数据使用 POST 请求
  • 表单中传入 用户、密码、验证码、记住我(boolean 类型)、Model(返回数据)、HttpSession(页面传入验证码和之前的验证码进行对比)、 HttpServletResponse (登录成功,要把 ticket 发送给客户端使用 cookie 保存,创建 cookie 使用 HttpServletResponse 对象)
  • 首先判断验证码是否正确
  • 判断账号密码是否正确:没有勾选记住我,存入库中的时间比较短;勾选记住我,存入库中的时间比较长:定义两个常量放入 CommunityConstant 接口中
  • package com.nowcoder.community.util;
    
    public interface CommunityConstant {
    
        /**
         * 默认状态的登录凭证的超时时间
         */
        int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
    
        /**
         * 记住状态的登录凭证超时时间
         */
        int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;
    
    }
  • 如果勾选记住我,使用记住状态时间;如果不勾选,则使用默认的
  • 如果登陆成功(判断 ticket),取出 ticket 发送 cookie 给客户端,重定向首页
  • 如果登录失败,返回登陆页面
@Controller
public class LoginController {

    //注入整个项目的参数路径
    @Value("${server.servlet.context-path}")
    private String contextPath;

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    //表单中传入 用户、密码、验证码、记住我(boolean 类型)、Model(返回数据)、HttpSession(页面传入验证码和之前的验证码进行对比)
    // 、 HttpServletResponse (登录成功,要把 ticket 发送给客户端使用 cookie 保存,创建 cookie 使用 HttpServletResponse 对象)
    public String login(String username, String password, String code, boolean remember, Model model
            , HttpSession session, HttpServletResponse response) {
        //检查验证码
        String kaptcha = (String) session.getAttribute("kaptcha");
        if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
            model.addAttribute("codeMsg", "验证码不正确");
            return "/site/login";
        }

        //检查账号、密码:判断账号密码是否正确:没有勾选记住我,存入库中的时间比较短;勾选记住我,存入库中的时间比较长
        // 定义两个常量放入 CommunityConstant 接口中:如果勾选记住我,使用记住状态时间;如果不勾选,则使用默认的
        int expiredSeconds = remember ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
        Map<String, Object> map = userService.login(username, password, expiredSeconds);
        //成功:取出 ticket 发送 cookie 给客户端,重定向首页
        if (map.containsKey("ticket")) {
            Cookie cookie = new Cookie("ticket", map.get("ticket").toString());//map 中拿到的是对象需要转化为字符串
            cookie.setPath(contextPath);//有效路径:整个项目但是不要写死,写参数即可
            cookie.setMaxAge(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";
        }

    }

}

最后进行页面处理

2.4 退出

将登陆凭证修改为失效状态(在数据访问层已经实现),跳转到网站首页

在 service 包下 userService 中新增退出业务方法:

  • 退出的时候把凭证传入,根据凭证找到用户进行退出,最后改变凭证状态
//退出业务方法
public void logout(String ticket) {
    loginTicketMapper.updateStatus(ticket, 1);
}

在 controller 包下 LoginController 中添加一个处理页面的请求:

  • 退出只是一个普通请求,不需要传表单,用 GET 请求即可
  • 使用注解 @CookieValue 要求 spring mvc 注入 ticket:得到浏览器存的 cookie 
  • 最后重定向登陆页面
//退出业务方法
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
    userService.logout(ticket);
    return "redirect:/login";//默认 GET 请求
}

在前端配置 退出登录 链接:

<a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a>

3.显示登录信息

  • 使用拦截器:在请求开始查询登录用户、在本次请求中持有用户数据、在模板视图上显示用户数据、在请求结束时清理用户数据

3.1  请求开始获得 ticket,利用 ticket 查找对应的 user

这个过程是每次访问都需要的操作,我们可以使用一个拦截器:在 controller 包下创建一个 interceptor 包,在创建 LoginTicketInterceptor 类,实现 preHandle(执行具体方法之前的预处理)方法

  • 添加注解 @Component,并且实现 HandlerInterceptor 接口
  • 实现 preHandle(执行具体方法之前的预处理)方法:在请求开始获得 ticket,利用 ticket 查找对应的 user
  • public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  }
  • 从 request 中取cookie,首先封装 cookie,在 util 包下新建 CookieUtil 类:
  • package com.example.demo.util;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    
    public class CookieUtil {
    
        //返回 cookie 中的值
        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) {
                    if (cookie.getName().equals(name)) {
                        return cookie.getValue();
                    }
                }
            }
            return null;
        }
    }
  • 通过 cookie 得到 ticket:判断 ticket 是否为空
  • 当 ticket 不等于空,表示可以登录,登录就需要查询 ticket,调用 service,在 Userservice 类中添加 查询凭证代码
  • //添加 查询凭证代码
    public LoginTicket findLoginTicket(String ticket) {
        return loginTicketMapper.selectByTicket(ticket);
    }
  • 当 ticket 不等于空,查询 ticket,此时需要注入 UserService ,进行查询凭证
  • 检查凭证是否有效:凭证不为空,并且状态是0,并且超时时间晚于当前时间才有效,之后根据凭证查询用户
  • 拿到用户在 模板或者 Controller 处理业务的时候调用也可能在后面的过程中随时随地使用,因此需要在本次请求中持有用户
  • 存储用户:创建一个公共类放在容器中是不可以的,因为浏览器访问服务器是多对一的方式,一个服务器能处理多个请求(并发),每个浏览器访问服务器,服务器会创建一个独立的线程解决请求,因此服务器在处理请求的时候是一个多线程环境,因此在存储线程的时候需要考虑多线程的问题 那么
  • 存储并且使并发没有问题需要考虑线程隔离问题,每个线程单独存一份,不互相干扰,因此使用 ThreadLocal
  • 将 user 存入 ThreadLocal 中,封装一下,其他地方可能会用到
  • 在 util 包下新建 HostHolder 类:添加注解 @Compent (相当于容器的作用),持有用户信息,用于代替 session 对象;首先初始化  ThreadLocal(提供了 set 和 get 方法),添加 set 方法可以使外界传入 User,并且存入 ThreadLocal 中;添加 get 方法从  ThreadLocal 中取 User;最后添加一个清理的方法(请求结束的时候,把 ThreadLocal 中的 User 清理,只存不清不太好)
  • package com.example.demo.util;
    import com.example.demo.entity.User;
    import org.springframework.stereotype.Component;
    
    /**
     * 持有用户信息,用于代替session对象.
     */
    @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();
        }
    
    }
  • 注入 HostHolder,将用户暂存在 HostHolder 中
package com.example.demo.controller.interceptor;
import com.example.demo.entity.LoginTicket;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import com.example.demo.util.CookieUtil;
import com.example.demo.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;

@Component
public class LoginTicketInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;

    @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);
            //检查凭证是否有效:凭证不为空,并且状态是0,并且超时时间晚于当前时间才有效
            if (ticket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                // 根据凭证查询用户
                User user = userService.findUserById(loginTicket.getUserId());
                // 在本次请求中持有用户
                hostHolder.setUser(user);
            }
        }
        return true;
    }
}

以上 preHandle 方法已经完成:在请求开始之初,通过凭证找到用户,并且把用户暂存到 hostHolder(线程对应的对象中)


3.2 在模板引擎之前将 User 存储到 Model中

在 LoginTicketInterceptor 类中重写 postHandle 方法(在模板之前调用)

  • 从 hostHolder 中得到当前线程持有的 User
  • 判断 User 是否为空,如果不为空并且 ModelAndView(最终结果要放在这里) 也不为空,存入登录用户
//重写 postHandle 方法(在模板之前调用):
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler
        , ModelAndView modelAndView) throws Exception {
    //从 hostHolder 中得到当前线程持有的 User
    User user = hostHolder.getUser();
    //判断 User 是否为空,如果不为空并且 ModelAndView(最终结果要放在这里) 也不为空,存入登录用户
    if (user != null && modelAndView != null) {
        modelAndView.addObject("loginUser", user);//存入登录用户,值就是用户对象
    }
}

上述方法执行完成之后,模板就开始执行的时候,Model 中已经有 User 对象了,就可以直接使用;

最后还需要清理  hostHolder 中的 User(在整个请求结束之后),重写 afterCompletion 方法

//最后还需要清理 hostHolder 中的 User(在整个请求结束之后),重写 afterCompletion 方法
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                            Object handler, Exception ex) throws Exception {
    hostHolder.clear();
}

3.3 配置拦截器

在 config 包下新建 WebMvcConfig 类:

  • 添加注解 @Configuration,表示配置类
  • 实现 WebMvcConfigurer 接口
  • 注入拦截器 LoginTicketInterceptor
  • 排除全部静态资源
package com.example.demo.config;
import com.example.demo.controller.interceptor.LoginTicketInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

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

}

处理首页逻辑功能: 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

奋斗小温

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

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

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

打赏作者

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

抵扣说明:

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

余额充值