实现流程图:
发送手机验证码代码实现:
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)){
//2.如果不符合,返回错误信息
return Result.fail("手机号码格式不正确!");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到session
session.setAttribute("code",code);
//5.发送验证码
log.info("发送短信验证码成功,验证码:{}",code);
// SMSUtils.sendMessage("阿里云短信测试", "SMS_154950909", phone, code);
//返回ok
return Result.ok();
}
阿里云短信服务的依赖:
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.5.16version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-dysmsapiartifactId>
<version>2.1.0version>
dependency>
发送短信工具类:
package com.cdcas.common;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
*
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode, String phoneNumbers, String param) {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "自己的AccessKeyId", "secret");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\"" + param + "\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
} catch (ClientException e) {
e.printStackTrace();
}
}
}
登录功能代码实现:
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式不正确!");
}
//2.校验验证码
String sessionCode = (String) session.getAttribute("code");
String code = loginForm.getCode();
if (RegexUtils.isCodeInvalid(code) || !code.equals(sessionCode)){
//3.不一致,报错
return Result.fail("验证码不正确!");
}
//4.一致,根据手机号查询用户 select* from tb_user where phone = ?
User user = query().eq("phone", phone).one();
/*LambdaQueryWrapper lambdaQueryWrapper=new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(phone!=null,User::getPhone,phone);
User user = this.getOne(lambdaQueryWrapper);*/
//5.判断用户是否存在
if (user==null){
//6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
//7.保存用户信息到session中
session.setAttribute("user",user);
return Result.ok();
}
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;
}
登录验证功能:请求中Cookie携带sessionId
用实现拦截器对需要进行用户登录校验的请求进行实现:
这里注意是使用了ThreadLocal和springMvc中的拦截器进行实现:
代码实现思路:
拦截器LoginInterceptor的代码:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取session
HttpSession session = request.getSession();
//2.获取session中的用户
Object user = session.getAttribute("user");
//3.判断用户是否存在
if (user == null){
//4.不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//5.存在,保存用户信息到ThreadLocal
UserHolder2.saveUser((User)user);
//6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//依次用户
UserHolder.removeUser();
}
}
SpringMvc配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/login",
"/user/code"
);
}
}
UserHolder2:
package com.hmdp.utils;
import com.hmdp.entity.User;
public class UserHolder2 {
private static final ThreadLocal<User> tl = new ThreadLocal<>();
public static void saveUser(User user){
tl.set(user);
}
public static User getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
隐藏用户敏感信息,以及减少向session中存入的数据量通过Dto实现:
存入了UserDTO对象
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
集群的session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
session的替代方案应该满足:
显然redis就是完美的。
基于Redis实现共享session登录
发送短信验证码改变
//7.保存用户信息到session中
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
return Result.ok();*/
//todo 7.保存用户信息到redis中
stringRedisTemplate.opsForValue().set(phone,code);
return Result.ok();
登录验证的改变
//todo 从redis中获取验证码并校验
String sessionCode =stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
//todo 7.保存用户信息到redis中
//todo 7.1 随机生成token,作为登录令牌
String token=UUID.randomUUID().toString(true);
//todo 7.2 将User对象转为HashMap存储
UserDTO userDTO =BeanUtil.copyProperties(user,UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fileName,fieldValue) -> fieldValue.toString()));
//todo 7.3.存储
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
//todo 7.4.设置有序期
stringRedisTemplate.expire(token,30,TimeUnit.MINUTES);
以及请求拦截器的改变
/**
* 基于redis的登录验证
*/
//todo 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
//不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//todo 2.基于Token获取redis中的用户
Map<Object, Object> userMap= stringRedisTemplate.opsForHash()
.entries(LOGIN_USER_KEY + token);
//3.判断用户是否存在
if (userMap.isEmpty()){
//4.不存在,拦截,返回401状态码
response.setStatus(401);
return false;
}
//todo 5.将查询到的Hash数据转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
//todo 6.
//todo 7刷新token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);
总结:
redis代替session需要考虑的问题
解决状态登录刷新的问题:
新增一个拦截器(RefreshTokenInterceptor)去拦截所有请求,在每次请求之前刷新token
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.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中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
//3.判读用户是否存在
if (userMap.isEmpty()){
return true;
}
//5.将查询的map集合转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//7.刷新token的生命时间
stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
MvcConfig中新增拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//token刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
//登录拦截器
registry.addInterceptor(new LoginInterceptor()).order(1)
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/login",
"/user/code"
);
}
}