项目搭建
-
前期准备
-
导入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;
-
创建项目
-
导入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.33</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.17</version> </dependency> </dependencies>
-
编写启动类
@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<User> { boolean sendCode(String phone, HttpSession session); boolean login(LoginFormDTO loginForm, HttpSession session); }
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @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; } @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<User>().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实现登录
-
基于session实现登录流程
-
发送验证码
-
校验手机号是否合法
-
合法,生成验证码,并保存到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中
- 该用户信息不在数据库中,则表明该用户是注册
- 在数据库中存储用户信息
- 用户信息保存到session中
将用户信息存储在session中,主要是方便后序获取当前登录信息
- 该用户信息在数据库中,则表明该用户是登录
@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<User>().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; }
-
-
校验登录状态
-
用户发送请求时,会从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对象,就可以有效的避免用户信息被泄露的问题。
Session存在问题
-
当单个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集群,以提供更好地服务。