本文使用的是 手机号+验证码 的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用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登录内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论