当前位置: 代码网 > it编程>数据库>Redis > 基于Redis实现登录功能思路详解(手机号+验证码)

基于Redis实现登录功能思路详解(手机号+验证码)

2026年01月08日 Redis 我要评论
本文使用的是手机号+验证码 的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用redis)重点是看思路,而不是具体的代码实现userserviceimpl实

本文使用的是 手机号+验证码 的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用redis)

重点是看思路,而不是具体的代码实现

userserviceimpl实现类

整体结构

@slf4j
@service
public class userserviceimpl extends serviceimpl<usermapper, user> implements iuserservice {
    @autowired
    private stringredistemplate stringredistemplate;
    @override
    public result sendcode(string phone, httpsession session) {
        //...
    }
    @override
    public result login(loginformdto loginform, httpsession session) {
        //...
    }
    private user createuserwithphone(string phone) {
        //...
    }
}

sendcode方法

这个是发送验证码的方法

public result sendcode(string phone, httpsession session) {
    // 1. 校验手机号
    if (regexutils.isphoneinvalid(phone)) {
        // 2. 如果不符合,返回错误信息
        return result.fail("手机号格式错误!");
    }
    // 3. 如果符合,生成验证码
    string code = randomutil.randomnumbers(6);
    // 4. 保存验证码到redis
    stringredistemplate.opsforvalue().set(redisconstants.login_code_key +phone,code,redisconstants.login_code_ttl, timeunit.minutes);
    // 5. 发送验证码
    log.debug("发送短信验证码成功,验证码:{}", code);
    // 6. 返回结果
    return result.ok();
}

注:这里的redisconstants是一个用来存放各种常量的类

public class redisconstants {
    public static final string login_code_key = "login:code:";
    public static final long login_code_ttl = 2l;
    public static final string login_user_key = "login:token:";
    public static final long login_user_ttl = 30l;
}

login方法

这里使用了mybatisplus来操作数据库(user user = query().eq("phone", phone).one();),但是这个不是重点

public result login(loginformdto loginform, httpsession session) {
    // 1. 校验手机号
    string phone = loginform.getphone();
    if (regexutils.isphoneinvalid(phone)) {
        return result.fail("手机号格式错误!");
    }
    // 2. 从redis获取验证码并校验
    string cachecode = stringredistemplate.opsforvalue().get(redisconstants.login_code_key +phone);
    string code = loginform.getcode();
    if (cachecode == null || !cachecode.equals(code)) {
        // 3. 不一致,报错
        return result.fail("验证码错误!");
    }
    // 4. 一致,根据手机号查询用户
    user user = query().eq("phone", phone).one();
    // 5. 判断用户是否存在
    if (user == null) {
        // 6. 不存在,创建新用户并保存
        user = createuserwithphone(phone);
    }
    // 7. 保存用户信息到redis
    string token= uuid.randomuuid().tostring(true);
    userdto userdto = beanutil.copyproperties(user, userdto.class);
    map<string, object> usermap = beanutil.beantomap(userdto,new hashmap<>(),
            copyoptions.create()
                    .setignorenullvalue(true)
                    .setfieldvalueeditor((fieldname, fieldvalue)->fieldvalue.tostring()));
    stringredistemplate.opsforhash().putall(redisconstants.login_user_key + token, usermap);
    stringredistemplate.expire(redisconstants.login_user_key + token, redisconstants.login_user_ttl, timeunit.minutes);
    return result.ok(token);
}

createuserwithphone方法

在login方法中调用了该方法

这里也使用了mybatisplus来操作数据库(save(user);)

private user createuserwithphone(string phone) {
    // 1. 创建用户
    user user = new user();
    user.setphone(phone);
    user.setnickname(user_nick_name_prefix + randomutil.randomstring(10));
    // 2. 保存用户
    save(user);
    return user;
}

拦截器

整体框架

其实就是实现了handlerinterceptor的两个方法

@slf4j
@component
public class logininterceptor implements handlerinterceptor {
    @autowired
    private stringredistemplate stringredistemplate;
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
        //...
    }
    @override
    public void aftercompletion(httpservletrequest request, httpservletresponse response, object handler, exception ex) throws exception {
        // 移除用户
        userholder.removeuser();
    }
}

 userholder是threadlocal 持有类

public class userholder {
    private static final threadlocal<userdto> tl = new threadlocal<>();
    public static void saveuser(userdto user){
        tl.set(user);
    }
    public static userdto getuser(){
        return tl.get();
    }
    public static void removeuser(){
        tl.remove();
    }
}

prehandle方法

public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
    // 1.获取请求头中的token
    string token = request.getheader("authorization");
    if (strutil.isblank(token)) {
        // 不存在,拦截
        response.setstatus(401);
        return false;
    }
    // 2.基于token获取redis中的用户
    string key = redisconstants.login_user_key + token;
    map<object, object> usermap = stringredistemplate.opsforhash().entries(key);
    // 3.判断用户是否存在
    if (usermap.isempty()) {
        // 4.不存在,拦截
        response.setstatus(401);
        return false;
    }
    // 5.将查询到的hash数据转换为userdto对象
    userdto userdto = beanutil.fillbeanwithmap(usermap, new userdto(), false);
    // 6.存在,保存用户信息到threadlocal
    userholder.saveuser(userdto);
    // 7.刷新token有效期
    stringredistemplate.expire(key, redisconstants.login_user_ttl, timeunit.minutes);
    // 8.放行
    return true;
}

注:authorization 是前端定义的用来传递token的key

配置类

@configuration
public class mvcconfig implements webmvcconfigurer {
    @autowired
    private logininterceptor logininterceptor;
    @override
    public void addinterceptors(interceptorregistry registry) {
        registry.addinterceptor(logininterceptor)
                .addpathpatterns("/**")
                .excludepathpatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

整体思路

flowchart td
subgraph a[发送验证码流程]
    a1["前端请求 发送验证码"] --> a2["校验手机号格式"]
    a2 -- 不合法 --> a3["返回错误  手机号格式错误"]
    a2 -- 合法 --> a4["生成6位验证码"]
    a4 --> a5["保存验证码到redis"]
    a5 --> a6["返回成功"]
end
subgraph b[登录流程]
    b1["前端请求 登录"] --> b2["校验手机号格式"]
    b2 -- 不合法 --> b3["返回错误"]
    b2 -- 合法 --> b4["从redis获取验证码"]
    b4 --> b5{"验证码是否正确"}
    b5 -- 否 --> b6["返回验证码错误"]
    b5 -- 是 --> b7["根据手机号查询用户"]
    b7 --> b8{"用户是否存在"}
    b8 -- 否 --> b9["创建新用户"]
    b8 -- 是 --> b10["使用已有用户"]
    b9 --> b11["生成token"]
    b10 --> b11
    b11 --> b12["用户信息写入redis"]
    b12 --> b13["返回token"]
end
subgraph c[请求拦截流程]
    c1["请求到达拦截器"] --> c2["从请求头获取token"]
    c2 --> c3{"token是否存在"}
    c3 -- 否 --> c4["返回401"]
    c3 -- 是 --> c5["从redis获取用户信息"]
    c5 --> c6{"用户是否存在"}
    c6 -- 否 --> c4
    c6 -- 是 --> c7["保存用户到threadlocal"]
    c7 --> c8["刷新token有效期"]
    c8 --> c9["放行请求"]
end
subgraph d[请求结束]
    d1["请求完成"] --> d2["清理threadlocal"]
end
b13 --> c1
c9 --> d1

复制到未命名绘图 - draw.io中用mermaid格式文件创建流程图

优化

目前之后访问被拦截的页面才会刷新有效期,所以这里我们需要优化一下

方式是采用拦截器链,即再加一个拦截器来拦截全部页面,以此来更新有效期

refreshtokeninterceptor

@slf4j
@component
public class refreshtokeninterceptor implements handlerinterceptor {
    @autowired
    private stringredistemplate stringredistemplate;
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
        // 1.获取请求头中的token
        string token = request.getheader("authorization");
        if (strutil.isblank(token)) {
            return true;
        }
        // 2.基于token获取redis中的用户
        string key = redisconstants.login_user_key + token;
        map<object, object> usermap = stringredistemplate.opsforhash().entries(key);
        // 3.判断用户是否存在
        if (usermap.isempty()) {
            return true;
        }
        // 5.将查询到的hash数据转换为userdto对象
        userdto userdto = beanutil.fillbeanwithmap(usermap, new userdto(), false);
        // 6.存在,保存用户信息到threadlocal
        userholder.saveuser(userdto);
        // 7.刷新token有效期
        stringredistemplate.expire(key, redisconstants.login_user_ttl, timeunit.minutes);
        // 8.放行
        return true;
    }
    @override
    public void aftercompletion(httpservletrequest request, httpservletresponse response, object handler, exception ex) throws exception {
        // 移除用户
        userholder.removeuser();
    }
}

logininterceptor

@slf4j
@component
public class logininterceptor implements handlerinterceptor {
    @override
    public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
        // 判断是否需要拦截(threadlocal中是否有用户)
        if (userholder.getuser() == null) {
            response.setstatus(401);
            return false;
        }
        // 有用户,则放行
        return true;
    }
    @override
    public void aftercompletion(httpservletrequest request, httpservletresponse response, object handler, exception ex) throws exception {
        // 移除用户
        userholder.removeuser();
    }
}

配置类

@configuration
public class mvcconfig implements webmvcconfigurer {
    @autowired
    private logininterceptor logininterceptor;
    @autowired
    private refreshtokeninterceptor refreshtokeninterceptor;
    @override
    public void addinterceptors(interceptorregistry registry) {
        // 登录拦截器
        registry.addinterceptor(logininterceptor)
                .addpathpatterns("/**")
                .excludepathpatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1);
        // 刷新token拦截器
        registry.addinterceptor(refreshtokeninterceptor)
                .addpathpatterns("/**").order(0);
    }
}

注:order方法是用来设置哪一个拦截器在前,哪一个在后;规则:数字小的在前,数字大的在后

到此这篇关于基于redis实现登录功能的文章就介绍到这了,更多相关redis登录内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com