环境准备
当前模型
nginx服务器的作用
-
手机或者app端向nginx服务器发起请求,nginx基于七层模型走的是HTTP协议,可以实现基于Lua直接绕开tomcat访问Redis
-
nginx也可以作为静态资源服务器,轻松扛下上万并发并负载均衡到下游的tomcat服务器,利用集群支撑起整个项目
-
使用nginx部署前端项目后还可以做到动静分离,进一步降低tomcat服务的压力
企业级MySQL加上固态硬盘能够支撑的并发大概就是4000起~7000左右,对于上万的并发如果让tomcat直接访问Mysql,瞬间会让Mysql服务器的cpu和硬盘全部打满
- 在高并发场景下需要选择使用MySQL集群,同时为了进一步降低MySQL的压力增加访问的性能,一般还需要使用Redis集群
导入数据/前端/后端
执行项目所需要的SQL脚本
,MySQL的版本要采用5.7及以上版本,否则执行脚本时部分SQL语句无法执行
表 | 说明 |
---|---|
tb_user | 用户表 |
tb_user_info | 用户详情表 |
tb_shop | 商户信息表 |
tb_shop_type | 商户类型表 |
tb_blog | 用户日记表(达人探店日记) |
tb_follow | 用户关注表 |
tb_voucher | 优惠券表 |
tb_voucher_order | 优惠券的订单表 |
导入后端项目
,将项目放到你的idea工作空间,然后利用idea打开即可
- 第一步: 修改application.yaml文件中的MySQL和Reids的连接地址为自己服务所在的地址
- 第二步: 启动项目,在浏览器访问http://localhost:8081/shop-type/list,如果可以看到JSON数据则说明导入成功
导入前端工程
,将nginx的解压目录放到一个自己指定的目录(确保目录不含中文,特殊字符和空格)
- 第一步: 在ngnix所在目录打开一个CMD窗口,执行
start nginx.exe
命令启动ngnix服务 - 第二步: 打开浏览器并将页面调整为手机模式,访问http://localhost:8080/访问项目首页(页面的数据是从后端查询得到的)
server {
listen 9999;
server_name localhost;
# 指定前端项目所在的位置
location / {
root html/hmdp;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# 监听的路径
location /api {
default_type application/json;
#internal;
keepalive_timeout 30s;
keepalive_requests 1000;
#支持keep-alive
proxy_http_version 1.1;
# 去掉/api前缀的URL
rewrite /api(/.*) $1 break;
proxy_pass_request_headers on;
#more_clear_input_headers Accept-Encoding;
proxy_next_upstream error timeout;
# 反向代理的位置
proxy_pass http://127.0.0.1:8081;
#proxy_pass http://backend;
}
}
短信登录/登出
基于Session实现登录流程
实现发送短信验证码功能
用户点击发生验证码按钮
发起请求
UserController
中的sendCode
方法处理发生验证码请求,在UserServiceImpl
编写具体的业务逻辑
- 使用邮箱验证时我们还需要去数据库中修改phone的字段类型,将
varchar(11)改为varchar(100)
/**
*发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendcode(phone,session);
}
/**
* 发送手机验证码
*/
@Override
public Result sendcode(String phone, HttpSession session) {
// 1.校验手机号,RegexUtils是我们创建的工具类,里面还需要用到RegexPatterns工具类
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合生成验证码,RandomUtil是hutool-all的工具类
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到session
session.setAttribute("code",code);
// 5.发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 6.返回成功的信息
return Result.ok();
}
/**
* 发送邮箱验证码
*/
@Override
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) throws MessagingException {
// TODO 发送短信验证码并保存验证码
if (RegexUtils.isEmailInvalid(phone)) {
return Result.fail("邮箱格式不正确");
}
String code = MailUtils.achieveCode();
session.setAttribute(phone, code);
log.info("发送登录验证码:{}", code);
MailUtils.sendTestMail(phone, code);
return Result.ok();
}
实现登录功能
用户点击登录按钮
发起请求
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
return userService.login(loginForm, session);
}
// loginForm中封装了登录参数,包含手机号、验证码或者手机号、密码
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号的格式
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.将从session中获取的验证码与用户提交的验证码进行比较
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
// 从session中默认获取的对象都是Object类型,所以我们需要转化为String类型
if(cacheCode == null || !cacheCode.toString().equals(code)){
// 4.验证码不一致则报错
return Result.fail("验证码错误");
}
// 5.验证码一致则根据用户提交的手机号取数据库中查询用户
User user = query().eq("phone", phone).one();
// 判断用户是否存在
if(user == null){
//6.不存在则创建
user = createUserWithPhone(phone);
}
//7.将用户的信息保存到session中
session.setAttribute("user",user);
// 返回成功的信息
return Result.ok();
}
// 创建一个新用户
private User createUserWithPhone(String phone) {
//创建用户
User user = new User();
//设置手机号
user.setPhone(phone);
//设置昵称(默认名),一个固定前缀+随机字符串,USER_NICK_NAME_PREFIX是工具类中的系统常量
user.setNickName(USER_NICK_NAME_PREFIX+ RandomUtil.randomString(8));
//将用户信息保存到数据库
save(user);
return user;
}
实现登录验证功能(拦截器)
当我们登录成功后,前端还会发起一个user/me
的请求用于登录校验,由于访问每个Controller
都需要登录校验,所以我们可以把校验流程写在拦截器中
第一步; 定义一个工具类UserHolder
专门用来存储用户的信息,而且每个线程都对应一个自己的用户信息
public class UserHolder {
private static final ThreadLocal<User> tl = new ThreadLocal<>();
public static void saveUser(User user){
// 保存用户的信息,默认的key就是当前线程
tl.set(user);
}
public static User getUser(){
// 获取用户信息,默认的key就是当前线程
return tl.get();
}
public static void removeUser(){
// 删除用户信息,默认的key就是当前线程
tl.remove();
}
}
第二步: 创建一个LoginInterceptor
类实现HandlerInterceptor
接口,防止用户直接通过url路径
访问项目的功能,重写前置拦截器方法和完成处理方法
preHandle方法
: 用于我们登陆之前的权限校验,同时将从session中获取的用户信息保存到UserHolder的ThreadLocal中,方便以后在Controller中获取用户信息afterCompletion方法
: 用于处理登录后的信息避免内存泄露
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.用户存在则将用户信息保存到UserHolder的Threadlocal中
UserHolder.saveUser((User)user);
//6.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
第三步:编写配置类
,注册登录拦截器并设置该拦截器需要拦截的请求
@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/code",
"/user/login"
);
}
}
第四步: 获取当前线程对应的登录用户信息并响应到前端完成登录校验
@GetMapping("/me")
public Result me() {
User user = UserHolder.getUser();
return Result.ok(user);
}
隐藏用户敏感信息
在进行登录校验时将登录用户的全部信息响应给浏览器的行为是不安全的,所以我们应当在返回登录用户信息之前将用户的敏感信息进行隐藏
{
"success":true,
"data":{
"id":1010,
"phone":"1586385296",
"password":"",
"nickName":"user_i1b3ir09",
"icon":"",
"createTime":"2022-10-22T14:20:33",
"updateTime":"2022-10-22T14:20:33"
}
}
第一步: 新建一个不含用户敏感信息UserDto
对象,在进行登录校验时返回的含有用户敏感信息的User对象转化成UserDto对象
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
第二步: 修改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();
}
}
第三步: 修改login方法中,将保存到session域中的User对象转换成UserDto对象
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号的格式
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.将从session中获取的验证码与用户提交的验证码进行比较
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
// 从session中默认获取的对象都是Object类型,所以我们需要转化为String类型
if(cacheCode == null || !cacheCode.toString().equals(code)){
// 4.验证码不一致则报错
return Result.fail("验证码错误");
}
// 5.验证码一致则根据用户提交的手机号取数据库中查询用户
User user = query().eq("phone", phone).one();
// 判断用户是否存在
if(user == null){
//6.不存在则创建
user = createUserWithPhone(phone);
}
//7.将用户的信息隐藏后再存到到session中
session.setAttribute("user",user);
//UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//session.setAttribute("user", userDTO);
session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));
// 返回成功的信息
return Result.ok();
}
第四步: 修改拦截器中将从session中获取的隐藏了用户信息的UserDTO
对象保存到UserHolder类的ThreadLocal中
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中隐藏了用户信息的UserDTO类型的用户对象,并保存到UserHolder类的ThreadLocal中
UserDTO user = (UserDTO) session.getAttribute("user");
UserHolder.saveUser(user);
//3.判断用户是否存在
if(user == null){
//4.不存在则拦截当前请求并返回401状态码
response.setStatus(401);
return false;
}
//5.若存在保存用户的隐藏信息到Threadlocal
UserHolder.saveUser((User)user);
//6.放行
return true;
}
}
第五步: 修改登录校验的方法,将UserHolder中的ThreadLocal保存的UserDTO
对象返回
// 修改获取的类型为用户的隐藏信息
@GetMapping("/me")
public Result me() {
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}
第六步: 重启服务器,登录校验后返回的用户信息已经不含敏感信息
{
"success":true,
"data":{
"id":1016,
"nickName":"user_zkhf7cfv",
"icon":""
}
}
登出
用户点击退出登录
按钮时,我们需要清除ThreadLocal里存放的用户信息
- 这样前端发送的请求获取不到用户信息,登录拦截器从ThreadLocal中获取用户也为null,被拦截跳到登录界面
/**
* 登出功能
* @return 无
*/
@PostMapping("/logout")
public Result logout(){
// TODO 实现登出功能
UserHolder.removeUser();
return Result.ok();
}