(一):短信登录-导入黑马点评项目
server:
port: 8081
spring:
application:
name: hmdp
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC
username: 你的账号
password: 你的密码
redis:
host: 127.0.0.1
port: 6379
password: 你的密码
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
jackson:
default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
level:
com.hmdp: debug
启动:
(二):短信登录 - 基于session实习短信登录流程
(三)短信登录 - 发送短信验证码
controller层
@Resource
private IUserService userService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.setCode(phone, session);
}
service层
public interface IUserService extends IService<User> {
Result setCode(String phone, HttpSession session);
}
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
@Override
public Result setCode(String phone, HttpSession session) {
// 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误");
}
// 生成验证码
String code = RandomUtil.randomString(6);
session.setAttribute("code", code);
log.debug("发送验证码成功:{}", code);
return Result.ok(code);
}
}
前后端联调成功:
apipost接口测试:
(三)短信登录 - 实现短信验证码登录和注册
controller
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
service层
public interface IUserService extends IService<User> {
Result login(LoginFormDTO loginForm, HttpSession session);
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
// 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误");
}
// 校验验证码
String code = (String) session.getAttribute("code");
if(!code.equals(loginForm.getCode()) || code == null){
return Result.fail("验证码错误");
}
// 根据手机号查询用户
// QueryWrapper<User> wrapper = new QueryWrapper<>();
// wrapper.eq("phone",loginForm.getPhone());
// User user = getOne(wrapper);
User user = query().eq("phone", phone).one();
if(user == null){
// 用户不存在,创建新用户
user = createUserWithPhone(phone);
}
// 保存用户到session
session.setAttribute("user", user);
return Result.ok(user);
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName("user_" + RandomUtil.randomString(10));
// 保存用户
save(user);
return user;
}
前后端联调成功 - 但是会又跳转到登录页面
apipost测试
(四)短信登录 - 实现登录校验拦截器
拦截器filter
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取session信息
HttpSession session = request.getSession();
// 获取用户信息
Object user = session.getAttribute("user");
// 判断用户信息是否存在
if(user == null){
response.setStatus(404);
return false;
}
// 存入threadlocal中
UserHolder.saveUser((User) user);
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
mvcconfig注册
@Component
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns("/user/code",
"/user/login",
"/shop/**",
"/shoe-type/**",
"/blog/hot");
}
}
threadlocal
public class UserHolder {
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();
}
}
获取登录信息接口controller
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
User user = UserHolder.getUser();
return Result.ok(user);
}
(五)隐藏用户敏感信息
// 保存用户到session
// 将user->dto
UserDTO userDTO = new UserDTO();
BeanUtil.copyProperties(user, userDTO);
session.setAttribute("user", userDTO);
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
(六)集群session共享共享问题
(七)redis代替session的业务流程
(七)基于redis实现短信登录
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 = 36000L;
public static final Long CACHE_NULL_TTL = 2L;
public static final Long CACHE_SHOP_TTL = 30L;
public static final String CACHE_SHOP_KEY = "cache:shop:";
public static final String LOCK_SHOP_KEY = "lock:shop:";
public static final Long LOCK_SHOP_TTL = 10L;
public static final String SECKILL_STOCK_KEY = "seckill:stock:";
public static final String BLOG_LIKED_KEY = "blog:liked:";
public static final String FEED_KEY = "feed:";
public static final String SHOP_GEO_KEY = "shop:geo:";
public static final String USER_SIGN_KEY = "sign:";
}
保存验证码到redis
@Override
public Result setCode(String phone, HttpSession session) {
// 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误");
}
// 生成验证码
String code = RandomUtil.randomString(6);
session.setAttribute("code", code);
log.debug("发送验证码成功:{}", code);
// 保存到redis == set key val ex 120
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
return Result.ok(code);
}
登录逻辑
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
// 校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误");
}
// 校验验证码
String cacheCode = (String) stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY);
String code = loginForm.getCode();
if(!cacheCode.equals(code) || cacheCode == null){
return Result.fail("验证码错误");
}
User user = query().eq("phone", phone).one();
if(user == null){
// 用户不存在,创建新用户
user = createUserWithPhone(phone);
}
// 生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 将user对象转变为hashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
// 存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey + token, userMap);
// 设置有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 返回token
return Result.ok(token);
}
}
类型转换错误
// 将user对象转变为hashMap存储
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()));
RedisTemplate
默认使用 JDK 序列化 (JdkSerializationRedisSerializer),将 Java 对象转为二进制存储。
StringRedisTemplate
使用字符串序列化 (StringRedisSerializer),直接存储 UTF-8 字符串。
(八)解决状态登录刷新的问题
在一层拦截器的基础上,再加一层拦截器专门拦截一切路径做刷新token处理。
因为如果用户在一些不需要拦截的比如首页浏览,不会刷新token的时长。
@Component
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns("/user/code",
"/user/login",
"/shop/**",
"/shoe-type/**",
"/blog/hot").order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);
}
}
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate RedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 请求头获取token
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){
return true;
}
// 基于token获取用户信息
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().
entries(RedisConstants.LOGIN_USER_KEY + token);
// 判断用户信息是否存在
if(userMap.isEmpty()){
return true;
}
// 将hash转为map
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 存入threadlocal中
UserHolder.saveUser(userDTO);
// 刷新token的有效期
stringRedisTemplate.expire(RedisConstants.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 {
UserHolder.removeUser();
}
}
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断是否需要拦截
if(UserHolder.getUser() == null){
response.setStatus(401);
return false;
}
// 放行
return true;
}
}