一、导入黑马点评项目
1. 数据库:
? tb_user:用户表 ? tb_user_info:用户详情表 ? tb_shop:商户信息表 ? tb_shop_type:商户类型表 ? tb_blog:用户日记表(达人探店日记) ? tb_follow:用户关注表 ? tb_voucher:优惠券表 ? tb_voucher_order:优惠券的订单表
2. 单体项目介绍:
案例是:前后端分离的单体项目,后端部署在Tomcat,前端部署在Nginx服务器上。 当移动端、客户端、app端发起Nginx请求,得到静态资源。页面再通过Nginx,向服务端发起请求,进行查询数据。查询后,返回给前端,前端在进行渲染。 开启Nginx: D:\Program Files\Nginx_Test\nginx-1.18.0> start .\nginx.exe
二、基于session实现登录
短信登录包括:
- 短信的发送
- 基于短信验证码的登录、注册
- 对登录状态的校验
1、发送短信验证码
1.理论流程
用户通过手机进行获取验证码,进行登录。 1.那么用户会根据手机号请求一个验证码 2.服务端接收到手机号后,进行验证手机号。 2.1校验失败,重新写手机号 2.2校验成功,服务端生成验证码 3.保存验证码到session 4.发送验证码
请求路径:api是表示当前的请求,是一个发向Tomcat服务的请求,会过滤掉。真正的请求路径是:/user/code 请求参数:phone=888888
2.代码操作:
controller层:
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
return userService.sendCode(phone,session);
}
----------------------sever层-----------------------
package com.hmdp.service.impl;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
if (RegexUtils.isPhoneInvalid(phone)){
log.debug("验证码错误.....");
return Result.fail("手机号错误,请重新填写");
}
String code = RandomUtil.randomNumbers(6);
session.setAttribute("code",code);
log.debug("验证码发送成功,{}",code);
return Result.ok();
}
}
---------手机号正则---------------
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
2、短信验证码登录、注册
1.理论流程
用户收到了验证码,就可以进行登录或者注册了。 1.用户将手机号和验证码进行提交。 2.服务端进行校验验证码。从session中取出验证码进行比较 2.1比较失败,拒绝登录 2.2比较成功,说明验证码填写的是正确的,但是手机号不能确定是否正确。 3.根据手机号查询用户信息。用户是否存在 3.1如果不存在说明该用户第一次登录,那么为该用户注册成新的用户。然后在进行登录。 3.2该用户以及存在了,可也进行登录 4.登录-》保存用户信息。保存到session中。
2.代码操作:
-----------controller ---------
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
return userService.login(loginForm,session);
}
------------service-------------------------
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
log.debug("手机号:{}错误",phone);
Result.fail("手机号格式错误");
}
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if (code ==null || cacheCode.toString().equals(code) ){
log.debug("验证码:{}错误",code);
Result.fail("验证码错误");
}
User user = query().eq("phone", phone).one();
if (user == null){
user = createUser(phone);
}
session.setAttribute("user",user);
return Result.ok();
}
private User createUser(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
save(user);
return user;
}
3、校验登录状态
1.理论流程
用户登录成功后,在进行访问其他页面的的时候,需要进行校验登录状态。 1.基于session进行校验。 基于cookie中的session的id,拿到session。
之前将用户信息保存到了session。session是基于cookie,每个session都有一个id,保存在浏览器的cookie当中。 当用户来访问其他页面时,那么就会携带着自己的cookie,而cookie有session的id。那么就可以基于session中的id,拿到session。从而在session中获取用户信息。
2.判断session有无用户 2.1如果没有,则拦截请求 2.2如果有,则缓存用户信息,将用户信息保存到ThreadLocal。这样其他业务就可以直接从ThreadLoad中获取用户信息。
ThreadLoad是线程域对象,在业务当中,每一个请求到达微服务,都会是一个独立的线程。 ThreadLoad会将数据保存到每一个线程的内部,在线程内部创建一个map来保存。 每一个请求来了以后,都有独立的存储空间,请求之间相互没有干扰。
3.放行。
后续其他请求,就可以从ThreadLoad中取出自己的用户信息
前端在请求接口时,会携带cookie。而登录的凭证就是session的id,而该id是存在cookie其中。 那么服务端根据这个session的id得到session。 从session中获取用户信息。 在根据该用户信息去数据库判断,有无该用户, 如果没有则拦截。 如果有,则保护信息到ThreadLoad,在进行放行;
在springmvc中,拦截器可以在所有controller执行前,进行执行。 那么所有的请求,先经过拦截器,在进行访问各个controller。 拦截器:实现用户登录的校验。 –校验后: 那么后续的业务当中【比如说订单、点赞、评论】,都是需要用户的信息 所有说各个controller也需要的到用户信息。 那么就需要将拦截器拦截后得到的信息,传给各个controller。 需要注意的是:线程安全 –解解方法:ThreadLoad 当拦截器拦截了用户信息后,将用户信息保存到ThreadLoad。 ThreadLoad是一个线程域对象,每一个进入Tomcat的请求都是一个独立的线程。 ThreadLoad就会在线程内开一个内存空间,进行保存对应的用户。这样每个用户相互不干扰 – controller需要用到用户信息时,在从ThreadLoad中取出用户
2.代码操作:
-----------------拦截器:----------
package com.hmdp.utils;
import com.hmdp.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
if (user == null ){
response.setStatus(401);
return false;
}
UserHolder.saveUser((User) user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
UserHolder.removeUser();
}
}
-----------配置拦截器的configuration-----------
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration interceptorRegistration = registry.addInterceptor(new LoginInterceptor());
interceptorRegistration.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
----------------controller类进行返回用户信息--------
@GetMapping("/me")
public Result me(){
return Result.ok(UserHolder.getUser());
}
----ThreadLoad类的信息------该信息是隐藏了用户信息累的实体类
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
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();
}
}
3.隐藏用户敏感信息
登录校验功能,返回的信息比较多。比如密码这些重要信息不应该进行返回到前端。
为什么前端收到了所有的用户信息? 从后往前推:
- controller接口:校验成功后,将用户信息返回给前端,返回的是User完整的信息。而user信息是从UserHolder中得到的
- 拦截器: 从session中获取的用户信息,就是完整的用户信息。直接通过UserHolder的save方法,将所有的用户信息,添加到了拦截器。
- 那么谁给session存的信息呢?是登录业务时:从数据库中查询了所有的用户信息。直接放到了session中。
解决方法: 4. 也就是上的第三步:将数据库查询出来的用户信息,放到新pojo类中,在放到session中。 5. 也就是第二步:获取session中的就是UserDTo了。不在是整个user用户信息了
改动的代码:
三、集群的session共享问题
session共享问题:多台Tomcat并不共享session共享空间,当请求切换到不同Tomcat服务时导致数据丢失的问题。
存储到ATomcat服务器时,在其他的Tomcat服务器中是看不到的。
四、基于redis实现共享session登录
1、发送验证码。如何将验证码存入到redis中?
- redis的数据类型:用String类型
验证码就是6位数字。 - Redis的Key:手机号
确保每一个手机号来验证时,保存的key都是不一样的。那么就用每一个手机号来作为redis的key。 当登录时,取验证码时,将前端传过来的手机号作为key,来获取验证码,进行比较登录成功。 - Redis的value:就是验证码
代码操作:
2、验证码登录。如何将用户信息保存到redis
- 数据类型:hash
保存的是用户信息,用户对象。 - redis的key:随机token为key存储用户数据。比如用UUID来生成。
key的要求:key唯一 - redis的value:保存整个用户信息
String类型:把java对象序列化为Jason字符串,保存在redis。看起来简单,把整个JSON数据变成一个字符串了。占用内存多,除了保存自身外,还有JSON串的格式,比如冒号、大括号 hash类型:value是一个hashMap类型。把java中每一个字段保存在redis。每个字段是独立的,可以针对单个字段做CRUD。占用更少的内存。只需要保存数据本身就可以了。
代码操作:
-----用户登录service层----------
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
log.debug("手机号:{}错误",phone);
Result.fail("手机号格式错误");
}
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
String code = loginForm.getCode();
if (code ==null || !cacheCode.equals(code) ){
log.debug("验证码:{}的错误",code);
return Result.fail("验证码错误");
}
User user = query().eq("phone", phone).one();
if (user == null){
user = createUser(phone);
}
String token = UUID.randomUUID().toString(true);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldVaule)->fieldVaule.toString()));
String tokenKey = LOGIN_USER_KEY+ token;
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}
private User createUser(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
save(user);
return user;
}
3、校验登录功能。获取用户信息中的token【hash的key】
将redis中token返回给前端。访问接口时就会携带这token来访问接口,服务端就会根据token,获取用户信息。在进行判断是否存在,能否访问接口等等校验功能,以及业务代码功能。
代码操作:
-------interceptor拦截器代码:-------------
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
return false;
}
String redisKey = RedisConstants.LOGIN_USER_KEY+token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(redisKey);
if (userMap.isEmpty() ){
response.setStatus(401);
return false;
}
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
stringRedisTemplate.expire(redisKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
UserHolder.removeUser();
}
}
在拦截器中:不能使用@Aotuwired、@Resurces注解,自动导入。 因为interceptor拦截器类是手动new出来的,并不是@Component、@Configuration等注解来构建的。也就是说不是由spring来构建的,就不能进行依赖注入。 所以说通过构造函数注入,谁来注入呢?那就是谁用了类,谁就来注入该类。而MVC的configuration拦截器用到了。从而mvcConfig来帮助redisTemplate来注入到spring中。
4、测试:
五、登录拦截器的优化
用户访问任何一个页面,都会进行刷新登录token的倒计时:
----------拦截所有请求,都进行刷新token----------
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
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 {
String token = request.getHeader("authorization");
String redisKey = RedisConstants.LOGIN_USER_KEY+token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(redisKey);
if (userMap.isEmpty()){
return true;
}
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
stringRedisTemplate.expire(redisKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
UserHolder.removeUser();
}
}
---------拦截部分请求,刷洗token---------
package com.hmdp.utils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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;
}
}
--------mvcConfig配置拦截器:
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration interceptorRegistration = registry.addInterceptor(new LoginInterceptor());
interceptorRegistration.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
|