原创 L1296686146 冗谪 2023-06-29 14:03 发表于陕西
收录于合集#redis7个
前期准备
导入SQL
CREATE TABLE `tb_user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号码',
`password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '密码,加密存储',
`nick_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '昵称,默认是用户id',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '人物头像',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uniqe_key_phone` (`phone`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1011 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;
创建项目
导入依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
com.baomidou
mybatis-plus-boot-starter
3.4.3
mysql
mysql-connector-java
runtime
8.0.33
org.projectlombok
lombok
true
cn.hutool
hutool-all
5.7.17
编写启动类
@MapperScan("com.liang.mapper")
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}
编写配置文件
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: #配置自己的数据库url
username: #配置自己的数据库用户名
password: #配置自己的密码
编写实体类
/**
* 登录信息
*/
@Data
public class LoginFormDTO {
private String phone;
private String code;
}
/**
* 统一结果返回
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;
public static Result ok(){
return new Result(true, null, null, null);
}
public static Result ok(Object data){
return new Result(true, null, data, null);
}
public static Result ok(List> data, Long total){
return new Result(true, null, data, total);
}
public static Result fail(String errorMsg){
return new Result(false, errorMsg, null, null);
}
}
/**
* User实体类 对应数据库表tb_user
*/
@Data
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 手机号码
*/
private String phone;
/**
* 密码,加密存储
*/
private String password;
/**
* 昵称,默认是随机字符
*/
private String nickName;
/**
* 用户头像
*/
private String icon = "";
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
/**
* 存储用户非敏感信息
*/
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
编写controller层
/**
* User对象前端控制器
*/
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/**
* 发送手机验证码
* @param phone 手机号
* @param session
* @return
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone, session)?Result.ok():Result.fail("手机号码不合规");
}
/**
* 登录功能
* @param loginForm
* @param session
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
return userService.login(loginForm, session) ? Result.ok() : Result.fail("手机号或验证码错误");
}
编写service层
public interface IUserService extends IService {
boolean sendCode(String phone, HttpSession session);
boolean login(LoginFormDTO loginForm, HttpSession session);
}
@Service
public class UserServiceImpl extends ServiceImpl implements IUserService {
@Override
public boolean sendCode(String phone, HttpSession session) {
return true;
}
@Override
public boolean login(LoginFormDTO loginForm, HttpSession session) {
return true;
}
}
@Override
public boolean login(LoginFormDTO loginForm, HttpSession session) {
//获取手机号
String phone = loginForm.getPhone();
//验证手机号是否合理
boolean mobile = PhoneUtil.isMobile(phone);
//如果不合理 提示
if (!mobile){
//提示用户手机号不合理
return false;
}
//手机号合理 进行验证码验证
String code = loginForm.getCode();
String sessionCode = session.getAttribute("code").toString();
//如果验证码输入的是错误的 提示
if (!code.equals(sessionCode)){
return false;
}
//如果验证码也正确 那么通过手机号进行查询
User user = this.getOne(new LambdaQueryWrapper().eq(User::getPhone, phone));
// 数据库中没查询到用户信息
if (ObjectUtil.isNull(user)){
user = new User();
user.setPhone(phone);
user.setNickName("user_"+ RandomUtil.randomString(10));
this.save(user);
}
// 将该用户信息存入session中
// 简化user,只存储必要信息以及不重要的信息
UserDTO userDTO = BeanUtil.toBean(user, UserDTO.class);
session.setAttribute("user", userDTO);
return true;
}
基于session实现登录流程
发送验证码
校验手机号是否合法
发送验证码流程
@Override
public boolean sendCode(String phone, HttpSession session) {
//获取手机号,验证手机号是否合规
boolean mobile = PhoneUtil.isMobile(phone);
//不合规,则提示
if (!mobile){
return false;
}
//生成验证码
String code = RandomUtil.randomNumbers(6);
//将验证码保存到session中
session.setAttribute("code",code);
//发送验证码
System.out.println("验证码:" + code);
return true;
}
合法,生成验证码,并保存到session中、发送验证码给用户
不合法,提示用户手机号不合法
验证码登录、注册
校验登录状态
用户发送请求时,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中获取用户信息,
没获取到用户信息 则拦截,需要拦截器
获取到用户信息,则将用户信息保存到ThreadLocal中,再放行
校验登录状态
自定义拦截器,实现HandlerInterceptor接口
public class LoginInterceptor implements HandlerInterceptor {
/**
* preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
UserDTO user = (UserDTO) session.getAttribute("user");
//判断是否在session中获取到了用户
if (ObjectUtil.isNull(user)){
return false;
}
UserHolder.saveUser(user);
return true;
}
/**
* postHandle方法在控制层方法执行后,视图解析前执行(可以在这里修改控制层返回的视图和模型)
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
/**
* fterCompletion方法在视图解析完成后执行,多用于释放资源
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
实现WebMvcConfigurer接口,通过重写addInterceptors方法添加自定义拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加拦截器
registry.addInterceptor(new LoginInterceptor())
//放行资源
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
)
// 设置拦截器优先级
.order(1);
}
}
注意隐藏用户敏感信息
我们应当在返回用户信息之前,将用户敏感信息进行隐藏,采用的核心思路就是创建UserDTO类,该类没有用户敏感信息,在返回用户信息之前,将有用户敏感新的的User对象转换为没有敏感信息的UserDTO对象,就可以有效的避免用户信息被泄露的问题。
当单个tomcat服务器时,服务器崩溃,无法提供足够的处理能力时,系统可能不能使用,为了避免这些情况,提高系统的可用性、可伸缩性等,tomcat将会以集群的形式部署,集群部署的主要优势有: 高可用性 , 可伸缩性 , 负载均衡 , 无中断升级 。
集群部署的tomcat又面临新的问题,即session共享问题,由于每个tomcat都有一份属于自己的session,某个用户第一次访问tomcat时,把自己的信息存放到了编号01的tomcat服务器的session中,当第二次访问时,没有访问01服务器,而是访问到了其他tomcat服务器,而其他tomcat服务器没有该用户存放的session,此时整个登录拦截都会出现问题。
解决方式:
早期方案是session拷贝,即每当任意一台服务器的session修改时,都会同步到其他的tomcat服务器的session中,实现session共享。但此方式存在问题: ①session数据拷贝时,可能会出现延时;②每台服务器中都有完整的一份session数据,服务器压力较大 。
现在方案是基于redis来完成,即把session换成redis,redis数据本身就是共享的,可以避免session共享问题。而且redis中数据是 key-value方式存储 和session一样便于操作,且都默认 存储在内存 中,响应速度快。
集群模型
客户端发送请求,通过nginx负载均衡到下游的tomcat服务器(一台4核8G的tomcat服务器,在优化和处理简单业务的加持下,处理的并发量很有限),经过nginx负载均衡分流后,利用集群支撑整个项目,同时nginx在部署了前端项目后,做到了动静分离,进一步降低tomcat的压力,
如果让tomcat直接访问mysql,一般16、32核CPU、32/64G内存,并发量在4k~7K左右,在高并发场景下也是容易崩溃,所有一般会使用mysql集群,同时为了进一步降低mysql压力,增加访问性能,一般会加入redis集群,以提供更好地服务。