SpringBoot+smm+Vue前后端分离项目用户模块基本功能设计

概述

此设计思路主要适合:后端SpringBoot+SSM,前端:Vue
且,前端项目需要实现多种情况的登录(如大型电商平台:门户网站的普通用户登录与注册,后台管理系统的员工登录)。

用户模块设计

多种登录情况:
1、数据库设计:
如后台管理的员工登录和普通用户的登录,后端代码只用一种方法实现。
设计一个loginInfo表,存放管理员表和普通用户表的登录信息。添加字段type,用于区分登录的是管理员还是用户。所有有关用户和管理员的查询都通过LoginInfo表查询。 但对管理员表和用户表的修改需要同步到LoginInfo表。(数据库设计:反范式)
2、完全舍弃cookie和session,用Redis代替
浏览器:存放token到localstorage
移动端:存放到文件
前端技术:axios前置拦截器、axios后置拦截器、Vue-router路由拦截器
后端技术:Redis、HandlerInterceptor

用户注册

后端:生成验证码

1、验证码的生成:校验(演示用简单校验)、生成验证码,将验证码存入Redis设置过期时间,将验证码返回前端。

@Service
public class VerificationCodeServiceImpl extends BaseServiceImpl<LoginInfo> implements IVerificationCodeService {
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private LoginInfoMapper loginInfoMapper;

    /**
     * 验证码设置
     *
     * @return 验证码
     */
    @Override
    public AjaxResult getVerificationCode(Map<String,String> phone) {
        String phoneNumber = phone.get("phone");

        //验证手机号
        if(StringUtils.hasLength(phoneNumber))
            AjaxResult.result().setSuccess(false).setMessage("请输入手机号码!");
        //验证用户是否已经注册
        LoginInfo loginIfo=loginInfoMapper.queryByPhoneNumber(phoneNumber);
        if (loginIfo!=null)
            AjaxResult.result().setSuccess(false).setMessage("该手机号已经被注册!");

        //查询redis是否有该手机号注册用户的验证码
        String key = "register" + phoneNumber;
        //value的值 (验证码:时间戳)
        String value= (String) redisUtil.get(key);
        //验证码
        String code="";

        //如果value为空,生成验证码
        if(value==null){
            //生成4位数验证码
            code = StrUtils.getComplexRandomString(4);
        }else {//如果value不为空,判断验证码是否过期
            String time = value.split(":")[1];
            if(System.currentTimeMillis()-Long.valueOf(time)<60*1000*1)
                return AjaxResult.result().setSuccess(false).setMessage("请不要频繁发送");
            else
                code=value.split(":")[0];
        }

        //存放到redis
        redisUtil.set(key,code+":"+System.currentTimeMillis(),3*60);

        //向用户发送短信,此处可扩展短信技术,向手机发送验证码
        //SmsUtil.sendSms(phoneNumber,"您本次注册的验证码为:"+code+"请在3分钟以内使用,请勿转告他人。");
        System.out.println(redisUtil.get(key)+" -- "+redisUtil.getExpire(key));

        return AjaxResult.result();
    }
}
后端:
保存注册用户信息
/**
     * user用户注册
     *
     * @param userDto 用户注册信息
     * @return 注册结果
     */
    @Transactional
    @Override
    public AjaxResult register(UserDto userDto) {
        //校验手机
        if (!StringUtils.hasLength(userDto.getPhone()))
            return AjaxResult.result().setSuccess(false).setMessage("手机号不能为空");
        LoginInfo ofLoginInfo=loginInfoMapper.queryByPhoneNumber(userDto.getPhone());
        if (ofLoginInfo!=null)
            return AjaxResult.result().setSuccess(false).setMessage("该手机已被注册");

        //校验短信验证码
        if (!StringUtils.hasLength(userDto.getVerificationCode()))
            return AjaxResult.result().setSuccess(false).setMessage("请输入验证码");
        String value = (String)redisUtil.get("register" + userDto.getPhone());
        if (value==null){
            return AjaxResult.result().setSuccess(false).setMessage("验证码已过期,请重新发送");
        }
        else {
            String code = value.split(":")[0];
            if (!code.equals(userDto.getVerificationCode()))
                return AjaxResult.result().setSuccess(false).setMessage("验证码错误");
        }
        //校验密码
        if (!StringUtils.hasLength(userDto.getPassword()))
            return AjaxResult.result().setSuccess(false).setMessage("请输入密码");
        if(!userDto.getPassword().equals(userDto.getRePassword()))
            return AjaxResult.result().setSuccess(false).setMessage("两次输入的密码不同");

        //保存注册信息User和Login
        LoginInfo loginInfo = userDto2LoginInfo(userDto);
        loginInfoMapper.insert(loginInfo);
        User user = loginInfo2User(loginInfo);
        userMapper.insert(user);

        return AjaxResult.result();
    }
UserDto 临时保存用户注册的信息

所有有关用户和管理员的查询都通过LoginInfo表查询。 但对员工表(t_employee)和用户表(t_user)的修改需要同步到LoginInfo表。(数据库设计:反范式)用户表和员工表都设置了外键loginInfo_id,关联到LoginInfo表。LoginInfo表新增数据的主键,作为用户表和员工表的外键。

import lombok.Data;

@Data
public class UserDto {
    private String phone;
    private String password;
    private String rePassword;
    private String verificationCode;
}
UserDto 2 LoginIfo
 /**
     * 将注册信息转换为LoginInfo实体类
     *
     * @param userDto 注册信息
     * @return LoginInfo实体类
     */
    private LoginInfo userDto2LoginInfo(UserDto userDto){
        LoginInfo loginInfo = new LoginInfo();
        //设置用户名,默认为手机号
        loginInfo.setUsername(userDto.getPhone());
        //设置电话
        loginInfo.setPhone(userDto.getPhone());
        //设置盐值
        String salt = UUID.randomUUID().toString();
        loginInfo.setSalt(salt);
        //设置密码
        String password = MD5Utils.encrypByMd5(salt + userDto.getPassword());
        loginInfo.setPassword(password);
        //0 代表管理员 1 用户
        loginInfo.setType(1);
        //0 不可以用 1 可用
        loginInfo.setDisable(1);
        return loginInfo;
    }
LoginIfo 2 User
 /**
     * 将loginInfo的信息转换为User实体类
     *
     * @param loginInfo loginInfo实体类
     * @return user实体类
     */
    public User loginInfo2User(LoginInfo loginInfo){
        User user = new User();

        //设置用户名,默认为手机号
        user.setUsername(loginInfo.getUsername());
        //邮箱
        user.setEmail(loginInfo.getEmail());
        //设置电话
        user.setPhone(loginInfo.getPhone());
        //设置盐值
        user.setSalt(loginInfo.getSalt());
        //设置密码
        user.setPassword(loginInfo.getPassword());
        //0 不可以用 1 可用
        user.setState(loginInfo.getDisable());
        //创建时间
        user.setCreatetime(new Date());
        //设置user对loginInfo的外键
        user.setLoginInfo(loginInfo);
        user.setAge(loginInfo.getAge());
        user.setHeadimg(loginInfo.getHeadImg());
        return user;
    }

用户登录

前端:登录页面代码

以前没有前后台分离,前后台页面都是在同一个服务器里面,不存在跨域问题。 用户访问服务器在服务器端产生session并存储。现在每一次请求页面,前端部署的服务器还要向后端部署的服务器发起请求,如果前端没有以cookie的方式携带jssionid到服务器。后台服务是没有办法保持会话的。 于是对于登录,完全舍弃传统的cookie和session。通过后UUID生成一个token作为key,将登录用户的信息(去除敏感信息)作为value,存放入Redis,并设置过期时间(一般为30分钟)。同时将token和登录信息发送给前台。

<script type="text/javascript">
    new Vue({
        el: "#loginFormDiv",
        data: {
            loginForm: {
                username: '',
                password: '',
                state: 1,
            }
        },
        methods: {
            login() {
                var loginParams = {
                    username: this.loginForm.username
                    , password: this.loginForm.password
                    , state: this.loginForm.state
                };
                this.$http.post("/login/account", loginParams).then(result => {
                    let {success, object, message} = result.data;
                    if (success) {
                        localStorage.setItem("uToken", object.token);
                        localStorage.setItem("uUser", JSON.stringify(object.user));
                        location.href = "index.html";
                    } else {
                        alert(message)
                    }
                }).catch(result => {
                    alert("网络繁忙!")
                })
            }
        },
    })
</script>
	在HTML5中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的。
	解决了cookie存储空间不足的问题,localStorage中一般浏览器支持的是5M大小。
	这个在不同的浏览器中localStorage会有所不同。
	localStorage:持久化存储。只要不删除,在当前浏览器永远有效

前台收到token和登录信息,存储到localStorage中。

后端:登录业务实现
/**
     * 用户名登录
     *
     * @param loginDto 登录信息
     * @return 登录结果
     */
    @Override
    public AjaxResult login(LoginDto loginDto) {
        //校验
        if(StringUtils.isEmpty(loginDto.getUsername()))
            return AjaxResult.result().setSuccess(false).setMessage("用户名不能为空");
        if (StringUtils.isEmpty(loginDto.getPassword()))
            return AjaxResult.result().setSuccess(false).setMessage("密码不能为空");

        //校验用户名是否正确username,phone,email
        LoginInfo loginInfo=loginInfoMapper.queryByLoginDto(loginDto);
        if(loginInfo==null)
            return AjaxResult.result().setSuccess(false).setMessage("账号或密码错误");

        //校验用户是否被禁用
        if (loginInfo.getDisable()!=1)
            return AjaxResult.result().setSuccess(false).setMessage("用户被禁用");

        //校验密码
        String salt = loginInfo.getSalt();
        String password = MD5Utils.encrypByMd5(salt + loginDto.getPassword());
        if (!password.equals(loginInfo.getPassword()))
            return AjaxResult.result().setSuccess(false).setMessage("账号或密码错误");

        //token代替session
        String token = UUID.randomUUID().toString();
        redisUtil.set(token,loginInfo,60*30);

        //返回前台数据,存放发到浏览器
        Map<String,Object> map=new HashMap<>();
        map.put("token",token);
        //这里返回用户信息后,不用再单独发请求获取用户信息
        loginInfo.setPassword("");//安全起见,不返回
        loginInfo.setSalt("");//安全起见,不返回
        map.put("user",loginInfo);

        return AjaxResult.result().setMessage("登录成功").setObject(map);
    }
前端:axios前置拦截器配置
//************************** axios前置(request)拦截配置 *******************************
//设置请求头信息,登录后,每次请求等能过携带token
axios.interceptors.request.use(config => {
    let token = localStorage.getItem("uToken")
    if (token) {
        config.headers["token"] = token;
    }
    return config;
}, error => {
    Promise.reject(error);
});

设置请求头信息,登录后,每次请求等能过携带token,同时后台设置拦截器,拦截所有请求。获取请求头的token,检查本次请求的token在Redis是否还存在或者已过期。若没有过期,则重新设置过期时间(相当于又变成30分钟后过期)。若没有Redis查找不到,则说明没有登录,或者登录过期。拦截器不放行该请求请求,同时给通知前端该请求被拦截。

后端:Interceptor的实现
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisUtil redisUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头里面的token的值
        String token = request.getHeader("token");
        if (!StringUtils.isEmpty(token)){
            //验证Redis是是否存在此
            Object object = redisUtil.get("token");
            //判断Redis的token是否过期
            if(object!=null) {
                LoginInfo loginInfo = (LoginInfo) object;
                //重新更新时间,放在token过期
                redisUtil.set(token,loginInfo,30*60);
                
                //@Todo 权限校验
                return true;
            }
        }

        //校验没有通过 返回一个前台能够识别错误 		{"success":false,"message":'noLogin'}
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.print("{\"success\":false,\"message\":\"noLogin\"}");
        out.flush();
        out.close();
        return false;
    }
}

后端配置拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        /*registry.addInterceptor(loginInterceptor)
                //放行登录
                .excludePathPatterns("/login/**")
                //放行dfs文件上传管理系统
                .excludePathPatterns("/dfs")
                //放行注册验证
                .excludePathPatterns("/verification/**")
                //放行注销
                .excludePathPatterns("/logout")
                //放行用户注册
                .excludePathPatterns("/user/phone/register");
               
    }
}
前端:axios后置拦截器配置

后置拦截器主要获取,后端所拦截到的请求,返回给前端的信息。信息匹配,则删除localStorage的用户信息,已经token。

//************************** axios后置(response)拦截配置 *****************************
// out.print("{\"success\":false,\"message\":\"noLogin\"}");
axios.interceptors.response.use(config => {
    if (!config.data.success && "noLogin" === config.data.message) {
        localStorage.removeItem("uToken");
        localStorage.removeItem("uUser");
        window.location.href = "index.html"
    }
    return config;
}, error => {
    Promise.reject(error)
});
前端:Vue-Router拦截器

此拦截器的作用是,对于需要登录才能访问页面,判断localStorage中是否有uUser就行

//对于需要登录才能访问页面,判断localStorage中是否有uUser就OK
var url = location.href;
if (url.indexOf("login.html") != -1 || url.indexOf("register.html") != -1
    || url.indexOf("bindPage.html") != -1 || url.indexOf("redirectPage.html") != -1) {
} else {
    var user = localStorage.getItem("uUser");
    if (!user) //没有用户,没有登录,跳转到登录页面
        location.href = "/login.html"
}
  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本文介绍了一个基于Spring Boot、Spring Cloud和Vue前后端分离项目实战。这个项目是一个简单的在线商城,包含了用户注册、登录、商品展示、购物车、订单管理等功能。通过这个项目,读者可以深入理解前后端分离的架构模式和互联网应用的开发方式。 首先,文章介绍了前后端分离基本概念和优势。前后端分离是将应用的前端和后端代码分开来开发,使得前端和后端具有独立的开发周期和技术栈,进而提高了开发效率和代码质量。同时,前后端分离还可以提供更好的用户体验和灵活性,对于互联网应用来说尤为重要。 接下来,文章介绍了项目的架构和技术栈。项目采用了Spring Boot和Spring Cloud框架来实现后端代码,采用MyBatis作为ORM框架和Redis作为缓存中间件。同时,项目还采用了Vue.js作为前端框架和Element UI组件库来实现前端页面。通过这些开源框架和组件,可以快速搭建一个前后端分离的互联网应用。 然后,文章介绍了项目的核心功能和代码实现。在用户注册和登录方面,项目采用了Spring Security框架和JWT令牌来实现用户认证和授权,保证了用户信息的安全性。在商品展示和购物车方面,项目采用了Vue.js来实现前端页面和事件处理。在订单管理方面,项目采用了MyBatis Plus来实现订单数据的持久化和分页查询。 最后,文章介绍了项目的测试和优化。通过对项目的压力测试和性能测试,文章发现项目还存在一些性能瓶颈和安全隐患,可以通过优化数据库查询、缓存配置和代码实现来提高应用的性能和安全性。 总之,这篇文章介绍了一个基于Spring Boot、Spring Cloud和Vue前后端分离项目实战,通过实现一个在线商城的功能,展示了前后端分离的开发模式和互联网应用的开发技术栈。本文可以作为前后端分离开发的入门教程,也可以作为互联网应用开发的参考文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值