2、登录——验证码功能以及保存登录状态

本文详细介绍了如何在Spring Boot项目中使用Spring Security实现登录界面的验证码功能,包括生成与验证验证码的步骤。同时,文章讲解了登录状态的保存,包括默认12小时和记住我状态下的100天超时设置。最后,通过Cookie和ThreadLocal实现自动登录,包括在每次请求前获取用户信息、在模板视图中显示用户数据以及在请求结束后清理用户数据的过程。

在登录界面,主要实现两个功能,刷新验证码和记住我功能
在这里插入图片描述

验证码

产生验证码

使用kaptcha生成验证码,导入jar包,编写对应的config配置类,然后在controller中生成验证码。新建cookie,在cookie中放入验证码的uuid,将cookie作为key存入redis,把text作为value,在添加过期时间。
通过response.addCookie(cookie)把cookie发送给前端,OutputStream os = response.getOutputStream(); ImageIO.write(image,“png”,os);将图片输出到浏览器。

@GetMapping("/kaptcha")
    public void getKaptcha(HttpServletResponse response){
        // 生成验证码
        String text = kaptchaProducer.createText();
        BufferedImage image = kaptchaProducer.createImage(text);

        // 验证码归属
        String kaptchaOwner = CommunityUtil.generateUUID();
        Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
        cookie.setMaxAge(60);
        cookie.setPath(contextPath);
        response.addCookie(cookie);

        // 存入Redis
        String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
        redisTemplate.opsForValue().set(redisKey,text,60, TimeUnit.SECONDS);

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

String kaptcha = null;
// cookie不为空,通过redis的key得到value
if(StringUtils.isNoneBlank(kaptchaOwner)){
    String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
    kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
}
// 验证码错误等情况
if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){
    model.addAttribute("codeMsg","验证码不正确!");
    return "site/login";
}

保存登录状态

默认状态的登录凭证的超时时间 12小时,记住状态下的登录凭证超时时间 100天。

service层:

// 生成登录凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));

String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
// loginTicket会序列化为json
redisTemplate.opsForValue().set(redisKey, loginTicket);

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

controller层:

// 是否记住我
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;

Map<String, Object> map = userService.login(username, password, expiredSeconds);
String key = "ticket";
if(map.containsKey(key)){
	// 新建cookie,配置cookie
    Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
    cookie.setPath(contextPath);
    cookie.setMaxAge(expiredSeconds);
    response.addCookie(cookie);
    return "redirect:/";
} else{
	// 返回错误信息
    model.addAttribute("usernameMsg",map.get("usernameMsg"));
    model.addAttribute("passwordMsg",map.get("passwordMsg"));
    return "site/login";
}

自动登录实现

先封装两个小工具类
要从cookie中拿到ticket的值,先封装一个cookieUtil:

public class CookieUtil {
    public static String getValue(HttpServletRequest request, String name) {
        if (request == null || name == null) {
            throw new IllegalArgumentException("参数为空!");
        }
        Cookie[] cookies = request.getCookies();
        // 遍历cookie,找到我们想要的那个
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(name)) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

浏览器同时处理多个用户请求,对每个用户的user对象要做一个存储,可以使用session,但是session依赖servlet api,我们想要在方法里随用随取,为了解决这个问题,我们就要采取一种新的方法来存储用户信息——ThreadLocal。

ThreadLocal,顾名思义,就是本地线程,可是这个名字实在容易让人误解,因为其实它是本地线程局部变量的意思,首先我们要知道,我们每个请求都会对应一个线程,这个ThreadLocal就是这个线程使用过程中的一个变量,该变量为其所属线程所有,各个线程互不影响。

我们封装一个工具HostHolder,用来持有用户信息,代替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();
    }
}

首先重写preHandler方法,利用前面封装的cookieUtil工具得到ticket,查到用户,并用hostHolder持有用户。

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 从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);
                // 构建用户认证的结果,并存入SecurityContext,以便Security进行授权
                Authentication authentication =
                        new UsernamePasswordAuthenticationToken(
                                user, user.getPassword(), userService.getAuthorities(user.getId())
                        );
                SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
            }
        }
        return true;
    }
在模板视图上显示用户数据

然后重写postHandle方法,在modelAndView上加上user。

@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);
    }
}
在请求结束时清理用户数据
@Override
public void afterCompletion(
        HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    hostHolder.clear();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值