目录
1.基于token的验证码登录功能
1.1发送验证码
1.1.1Controller
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone);
}
1.1.2Service
/**
* 生成验证码
*
* @author lichuancheng
* @date 创建时间 2024-04-17
* @since V1.0
*/
@Override
public Result sendCode(String phone) {
// 1.验证手机号码格式
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 2.生成验证码
String code = RandomUtil.randomNumbers(6);
// 3.验证码存redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code);
// 4.设置验证码有效期
stringRedisTemplate.expire(LOGIN_CODE_KEY+phone,LOGIN_CODE_TTL, TimeUnit.MINUTES);
// TODO 5.发送验证码
return Result.ok("发送成功!");
}
1.2登录
1.2.1Controller
/**
* 登录功能
*
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
// 登录功能
return userService.login(loginForm);
}
1.2.2Service
/**
* 登录
*
* @author lichuancheng
* @date 创建时间 2024-04-17
* @since V1.0
*/
@Override
public Result login(LoginFormDTO loginForm) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1、检验手机号码格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
if (StringUtil.isNullOrEmpty(code)) {
return Result.fail("验证码为空");
}
// 2、检验验证码
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (!code.equals(redisCode)) {
return Result.fail("验证码不正确");
}
// 3、查询数据库中用户
User user = query().eq("phone", phone).one();
// 3、1判断用户是否存在
if (user == null) {
// 不存在创建用户
user = createUserWithPhone(phone);
}
// 4、颁发登录凭证存入redis
String token = UUID.randomUUID().toString();
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()));
stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);
// 5、设置登录凭证有效期
stringRedisTemplate.expire(LOGIN_USER_KEY+token,30,TimeUnit.MINUTES);
// 6、返回token
return Result.ok(token);
}
1.3有用知识点
TODO 具体的验证码发送功能未实现,目前只能通过redis客户查看发送的验证码
2.双层拦截器刷新登录凭证并拦截登录请求
2.1第一层刷新登录凭证拦截器
public class RefreshTokenInterceptor implements HandlerInterceptor {
StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
};
@Override
public boolean preHandle(HttpServl etRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、获取token
String token = request.getHeader("authorization");
// 2、redis中获取登录凭证
Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
// 3、登录凭证不存在直接放行不进行下列步骤
if (userMap == null) {
return true;
}
// 4、登录凭证存在刷新登录凭证
stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);
// 5、登录凭证存在则保存用户信息进入threadlocal
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 线程结束后销毁线程变量,防止内存泄露
UserHolder.removeUser();
}
}
2.2第二层拦截登录请求拦截器
public class RefreshTokenInterceptor implements HandlerInterceptor {
StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
};
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、获取token
String token = request.getHeader("authorization");
// 2、redis中获取登录凭证
Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
// 3、登录凭证不存在直接放行不进行下列步骤
if (userMap == null) {
return true;
}
// 4、登录凭证存在刷新登录凭证
stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);
// 5、登录凭证存在则保存用户信息进入threadlocal
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 线程结束后销毁线程变量,防止内存泄露
UserHolder.removeUser();
}
}
2.3注册拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册登录凭证刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);
// 注册需登录功能拦截器
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.addPathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
}
}
2.4有用知识点
拦截器
1、实现HandlerInterceptor接口,重写preHandler、posthandler、afterCompletion方法,要明白这三个方法都是在哪个阶段执行的。preHandler在Controller方法运行之前执行;postHandler在Controller方法运行之后执行;afterCompletion在请求线程结束(页面渲染)之后执行
2、实现WebMvcConfigurer接口,重新addIntercepteors方法,调用addinterceptor方法添加一条拦截器,其中可以设置或排除拦截的路径,除此之外还可以设置拦截器的优先级
3.线程变量存储用户信息
3.1封装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();
}
}
3.2有用知识点
要理解threadlocal原理,明白内存泄漏的原因
源码解读:
threadlocal我们一般都是封装在工具类里面用,在UserHolder工具类中,我们可以看到new了一个ThreadLocal线程变量,并用static、final修饰,也就是说这个线程局部变量会随着程序启动就被加载,且只被加载一次不能被修改。
1.set方法解读:
//Threadlocal类的方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
//getMap(t)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//t.threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
从源码上可以看出,用threadlocal存线程变量的时候首先获取当前线程对象,接着获取当前线程对象的ThreadLocalMap ,然后用当前的ThreadLocal对象当做key,自己传入的值当value来保存信息。如果Threadlocal对象获取不到当前线程的ThreadLocalMap,则在当前线程上创建一个ThreadLocalMap ,然后用当前线程对象当做key,传入的值当做value来保存信息。
2.get方法解读
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
从源码上可以看出,threadlocal的get方法首先会获取当前的线程对象,然后通过线程对象获取ThreadLocalMap。如果获取到的ThreadLocalMap对象不为空,则以当前的threadlocal实例对象为key,获取里面的存储的值;如果获取的ThreadLocalMap对象为空,则初始化一个ThreadLocalMap,并将初始化的value返回
3.threadlocal结构图
讨论
1、如果在一次请求中,通过线程变量第一次存储的值和第二次存储的值不一样,会出现什么情况?以第一次存储的为准,还是使用后来存储的值,又或者是两者共存呢?
答:如果一次请求中,只是单线程,则threadlocal线程局部变量可以存储多次值,但是每次都以最后存储的值为准。
2、了解了threadlocal存储变量的原理后,思考为什么会出现内存泄漏现象?
答:
首先明白两个概念:一是当一个类没有线程去调用时,类就会被销毁,包括类的实例对象,类的静态变量、方法、代码块等;二是弱引用,当一个堆对象没有强引用指向他时,弱引用就被会被回收。
结合以上两点加线程池就会造成内存泄漏。
我们的线程不会被频繁的创建和销毁,而是由线程池管控,因此线程一旦被创建,则线程对应的ThreadLocalMap就不会被回收(一直有线程对象的强引用引用),ThreadLocalMap内部是key和value,key是指向ThreadLocal的弱引用,因此当ThreadLocal所在的类没有被线程使用就会被销毁,随之ThreadLocal的强引用回收,因此弱引用也回收,此时map中只剩下一个没有key的value,这个value会随着ThreadLocalMap继续存在,但是又没有key,因此也不能调用map的remove删除,长此以往累积就会把内存干爆。
另外,如若不使用线程池,线程频繁的创建和销毁,则不会出现内存泄漏的现象,线程销毁后线程所创建的ThreadLocalMap也会销毁。还有一种情况就是,不等threadlocal对象的强引用被销毁,接着将他复制一份当做弱引用存入ThreadLocalMap当做key,直接覆盖上一次的数据,只要不间断,则也不会出现内存泄漏的情况。