Projek Kajian Kuda Hitam terutamanya merangkumi fungsi berikut:
Nota: Versi Mysql mengguna pakai versi 5.7 dan ke atas
2. Pemisahan bahagian hadapan dan hujung belakang 3. Import projek hujung belakang3.1 Import projek hujung belakang ke Idea 3.2 Nota: Ubah suai maklumat alamat mysql dan redis dalam fail application.yaml kepada maklumat anda sendiri 3.3 Mulakan projek Selepas memulakan projek, lawati: http://localhost:8081/shop-type/list dalam pelayar Jika anda dapat melihat data, ia membuktikan bahawa tiada masalah dengan operasi 4. Import projek bahagian hadapan 4.1 Import folder nginx Salin folder nginx ke mana-mana direktori Pastikan direktori itu tidak mengandungi aksara khas Cina dan ruang, contohnya: 4.2 Jalankan projek front-end dalam direktori tempat nginx berada, dan masukkan arahan untuk memulakan nginx:
start nginx.exe
Kemudian lawati: http://127.0.0.1:8080 dan anda akan melihat halaman: 2. Laksanakan proses log masuk berdasarkan Sesi
Kod utama:
@Slf4j @RestController @RequestMapping("/user") public class UserController { @Resource private IUserService userService; /** * 发送手机验证码 */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { // 发送短信验证码并保存验证码 return userService.sendCode(phone, session); } }
Kod utama:
UserController@Service @Slf4j public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Override public Result sendCode(String phone, HttpSession session) { // 1.使用工具类校验手机号 if (RegexUtils.isPhoneInvalid(phone)) { // 2.如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 3.符合,生成验证码 String code = RandomUtil.randomNumbers(6); // 4.保存验证码到 session session.setAttribute("code",code); // 5.模拟发送验证码 log.debug("发送短信验证码成功,验证码:{}", code); // 返回ok return Result.ok(); } }
rreee 🎜>3. Fungsi pengesahan log masuk
Untuk elakkan mengesahkan maklumat pengguna setiap kali apabila pengguna meminta setiap pengawal, anda boleh menambah pemintas
Pemintas hanya perlu mengesahkan sekali dan menyimpan maklumat pengguna apabila pengguna meminta akses kepada ThreadLocal untuk digunakan oleh urutan berikutnya
Kod utama:Tulis ThreadLocal dalam kelas alat
/** * 登录功能 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ // 实现登录功能 return userService.login(loginForm, session); }
Tulis pemintas log masuk dalam kelas alat
@Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { // 如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 2.校验验证码 Object cacheCode = session.getAttribute("code"); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.toString().equals(code)) { // 3.验证码不一致,则报错 return Result.fail("验证码错误"); } // 4.验证码一致,根据手机号查询用户 User user = query().eq("phone", phone).one(); // 5.判断用户是否存在 if (user == null) { // 6.用户不存在,则创建用户并保存 user = createUserWithPhone(phone); } // 7.保存用户信息到session中,UserDTO只包含简单的用户信息, // 而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用 session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); // 8.返回ok return Result.ok(); } private User createUserWithPhone(String phone) { // 1.创建用户 User user = new User(); user.setPhone(phone); // 随机设置昵称 user_mrkuw05lok user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用户 save(user); return user; }
Tambahkan kelas konfigurasi pemintas dalam kelas konfigurasi
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(); } }
UserController
@GetMapping("/me") public Result me(){ // 获取当前登录的用户并返回 UserDTO user = UserHolder.getUser(); return Result.ok(user); }
手机号作为key,String类型的验证码作为value
用户登录时正好会提交手机号,方便通过Redis进行校验验证码
token作为key,Hash类型的用户信息作为value
后端校验成功后,会返回token给前端,前端会将token保存到sessionStorage中(这是浏览器的存储方式),以后前端每次请求都会携带token,方便后端通过Redis校验用户信息
前端代码:将后端返回的token保存到sessionStorage中
前端每次请求时,都会通过拦截器将token设置到请求头中,赋值给变量authorization,后端通过authorization获取前端携带的token进行校验
修改之前代码,将验证码存入Redis
@Service @Slf4j public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result sendCode(String phone, HttpSession session) { // 1.使用工具类校验手机号 if (RegexUtils.isPhoneInvalid(phone)) { // 2.如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 3.符合,生成验证码 String code = RandomUtil.randomNumbers(6); // 4.保存验证码到 session // session.setAttribute("code",code); // 4.保存验证码到 redis // "login:code:"是业务前缀,以"login:code:" + 手机号为key,过期时间2分钟 stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5.模拟发送验证码 log.debug("发送短信验证码成功,验证码:{}", code); // 返回ok return Result.ok(); } }
修改之前代码,从Redis获取验证码并校验
随机生成token,保存用户信息到redis中,返回token
@Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { // 如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // // 2.校验验证码 // Object cacheCode = session.getAttribute("code"); // String code = loginForm.getCode(); // if (cacheCode == null || !cacheCode.toString().equals(code)) { // // 3.验证码不一致,则报错 // return Result.fail("验证码错误"); // } // 2.从Redis获取验证码并校验 String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.equals(code)) { // 3.验证码不一致,则报错 return Result.fail("验证码错误"); } // 4.验证码一致,根据手机号查询用户 User user = query().eq("phone", phone).one(); // 5.判断用户是否存在 if (user == null) { // 6.用户不存在,则创建用户并保存 user = createUserWithPhone(phone); } // // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用 // session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); // 7.保存用户信息到redis中 // 7.1随机生成token,作为登录令牌 // 使用hutool工具中的UUID,true表示不带“-”符号的UUID String token = UUID.randomUUID().toString(true); // 7.2将User对象转为Hash类型进行存储 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 由于使用的是stringRedisTemplate,所以存入的value中的值必须都是String类型的 // 但是UserDTO中的id是Long类型的,所以进行对象属性拷贝时,需要自定义实现转换规则 Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); // 7.3存入redis, "login:token:"是业务前缀,以 "login:token:" + token作为key stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap); // 7.4设置token有效期,有效期为30分钟 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.返回token return Result.ok(token); } private User createUserWithPhone(String phone) { // 1.创建用户 User user = new User(); user.setPhone(phone); // 随机设置昵称 user_mrkuw05lok user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用户 save(user); return user; }
token刷新问题是指,用户长时间不进行界面操作时,到了过期时间,token自动失效;但是,用户一旦进行操作,就需要给token续期,即更新token过期时间
为了解决token刷新问题,需要加2个拦截器
第一个拦截器可以拦截所有请求,只要用户有请求就刷新token,并保存用户信息到ThreadLocal中
第二个拦截器只对登录请求进行拦截,从ThreadLocal中获取用户信息进行校验
刷新token的拦截器代码:
public class RefreshTokenInterceptor implements HandlerInterceptor { // 因为LoginInterceptor不是通过Spring进行管理的Bean,所以不能再LoginInterceptor中进行注入StringRedisTemplate // 可以通过构造方法传入StringRedisTemplate private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 前置拦截 * @param request * @param response * @param handler * @return * @throws Exception */ @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.存在,保存用户信息到ThreadLocal // UserHolder.saveUser((UserDTO)user); // // 6.放行 // return true; // 1.获取请求头中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { // 不存在,则拦截,返回401状态码 response.setStatus(401); return false; } // 2.通过token获取redis中的用户 Map<Object, Object> userMap = stringRedisTemplate.opsForHash() .entries(RedisConstants.LOGIN_USER_KEY + token); // 3.判断用户是否存在 if (userMap.isEmpty()) { // 4.用户不存在,则拦截,返回401状态码 response.setStatus(401); return false; } // 5.将redis中Hash类型数据转换成UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.用户存在,保存用户信息到ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } /** * 后置拦截器 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束后移除用户,防止ThreadLocal造成内存泄漏 UserHolder.removeUser(); } }
登录拦截器的代码:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.判断是否需要拦截(ThreadLocal中是否有用户) if (UserHolder.getUser() == null) { // 没有,需要拦截,设置状态码 response.setStatus(401); // 拦截 return false; } // 有用户,则放行 return true; } }
@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; /** * 添加拦截器 * @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); // token刷新的拦截器,order越小,执行优先级越高,所以token刷新的拦截器先执行 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**") .excludePathPatterns( // RefreshTokenInterceptor拦截器也需要放行"/user/code","/user/login",不然token过期后再重新登录就会一直被拦截 "/user/code", "/user/login") .order(0); } }
Atas ialah kandungan terperinci Analisis contoh perusahaan log masuk SMS menggunakan Redis. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!