基于SSM的项目登录设计流程

这里主要实现的是基于SSM框架的一个用户信息的设定,包括了用户的登录和退出,以及登录状态的密码修改还有忘记密码的问题验证修改密码等功能。

如果感兴趣可以点击内容查阅。

#登录页面底层设计

###储备知识
横向越权,纵向越权的安全漏洞

横向越权:
    攻击者尝试访问与他拥有相同权限的用户资源
纵向越权:
    低级别攻击者尝试访问高级别用户资源

高复用服务器响应对象的设计思想及抽象封装

数据库表设计

其实表设计已经在之前的设计中说过了,这里再专门拿出来。我们这一个模块主要就是面向这个数据库的一些内容设计的。

DROP TABLE IF EXISTS `mmall_user`;
CREATE TABLE `mmall_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户表id',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(50) NOT NULL COMMENT '用户密码,MD5加密',
  `email` varchar(50) DEFAULT NULL,
  `phone` varchar(20) DEFAULT NULL,
  `question` varchar(100) DEFAULT NULL COMMENT '找回密码问题',
  `answer` varchar(100) DEFAULT NULL COMMENT '找回密码答案',
  `role` int(4) NOT NULL COMMENT '角色0-管理员,1-普通用户',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '最后一次更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_name_unique` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;

接口设计

前台用户接口设计:

1.登录(post(代码需要post方式请求),开放get,方便调试)
    我们只需要写用户名和密码,如果返回1,则说明密码错误。
2.注册 
    当我们没有登录权限的时候需要注册后登录,且返回0时,校验成功。如果返回1说明用户已经存在
3.检查用户名是否有效
    我们之前就说了,用户名要唯一,不能起重复姓名。所以我们的用户名也需要检验。返回0就证明脚丫成功,失败返回1.
4.获取登录用户信息 
    主要针对后台编写,成功不做返回值,失败返回1,表示无法获取用户信息。
5.忘记密码
    输入用户名然后选取选项找到相对于的问题设置的密码。
6.提交问题答案
    用户设置的找回密码提交校验操作,进行后台的校验,如果成功返回0,失败返回1,表示问题答案错误。
7.忘记密码的重设密码
    再提交问题答案后返回对应的i西南西,0表示修改密码成功,1表示密码失败或者token失效。(token表示校验时间,用户保护信息)
8.登录中状态重置密码
    在登录状态下,如果密码修改成功就返回0,反之返回1。表示旧密码输入错误
9.登录状态更新个人信息
    更新后,当返回0表示成功,1表示用户未登录。
10.获取当前登录用户的详细信息,并强制登录
    成功后后台获取全部用户信息,失败返回10,强制登录。
11.推出登录
    返回0表示退出成功,1表示服务器异常

后台用户接口设计:

1.后台管理员登录
    成功即可获取用户的全部信息,失败就向前端发送1,表示密码错误
2.用户列表
    获取用户的信息列表,包含密码问题等等。失败返回10表示为登录,或1表示没有权限

有了以上的内容,我们现在可以设计登录的底层代码了。

首先我们在commmon包中创建一个能够满足上面要求的java类:

//根据要传输的信息我们声明三个类型
private int status;
private String msg;
private T data;

然后再创建他们的私有和公有的构造函数,以供内部和外部调用:

private ServerResponse(int status){
    this.status = status;
}
private ServerResponse(int status, T data){
    this.status = status;
    this.data = data;
}
private ServerResponse(int status, String msg, T data){
    this.status = status;
    this.msg = msg;
    this.data = data;
}
private ServerResponse(int status, String msg){
    this.status = status;
    this.msg = msg;
}

//如果成功就显示在前端页面,增加注解后不会显示在json中
@JsonIgnore
public boolean isSuccess(){
    return this.status == ResponseCode.SUCCESS.getCode();
}
public T getData(){
    return data;
}
public String getMsg(){
    return msg;
}

//正确情况下给前端传参
public static <T> ServerResponse<T> createBySuccess(){
    return new ServerResponse<T>(ResponseCode.SUCCESS.getCode());
}
public static <T> ServerResponse<T> createBySuccessMessage(String msg){
    return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg);
}
public static <T> ServerResponse<T> createBySuccess(T data){
    return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),data);
}
public static <T> ServerResponse<T> createBySuccess(String msg, T data){
    return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg,data);
}

//失败情况下传输的信息
public static <T> ServerResponse<T> createByError(){
    return new ServerResponse<T>(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getDesc());
}
public static <T> ServerResponse<T> createByErrorMessage(String errorMessage){
    return new ServerResponse<T>(ResponseCode.ERROR.getCode(),errorMessage);
}
public static <T> ServerResponse<T> createByErrorCodeMessage(int errorCode, String errorMessage){
    return new ServerResponse<T>(errorCode,errorMessage);
}

以上呢是我们针对登录信息下的成功和失败的情况做出的调用。我们拿其中的一个ResponseCode.ERROR.getDesc()举例说明一下:其实这是我们又创建的一个专门保存数据的枚举类对象,主要用于保存成功状态和失败状态的数据以及需要注册啊错误信息等。这样再传输的时候就更加直观。他包含了传输码和描述信息。

有了这些信息呢,我们就可以去设计实现了,我们呢在controller中写入相对于的实现。主要实现的就是从service->mybatis->dao的调用方式。就是我们从服务器获取信息再到mybatis实现后进入dao层在于数据对比操作。我们以控制层的登录为例:

这里呢我们传入用户名和密码以及session,如果用户名密码正确,就进入主页。

/**
 * 用户名登录
 * @param username
 * @param password
 * @param session
 * @return
 */
@RequestMapping(value = "login.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<User> login(String username, String password, HttpSession session){
    //service -> mybatis -> dao
    ServerResponse<User> response = iUserService.login(username,password);
    if(response.isSuccess()){
        session.setAttribute(Const.CURRENT_USER,response.getData());
    }
    return response;
}

其中User是pojo中自动生成的对应于数据库中的类文件。包含了数据库的相对数据。我们想要登录呢就需要传入用户名,密码以及对应的session,再通过调用服务层的login函数检验是否正确。既然讲到了service层,我们就讲一下它的接口和实现类。首先就是再接口中生成对应的功能,例如登录,再创建实现类:

检验的方法就是去数据库中查询对应的用户名然后根据用户名找到对应的密码,如果两个同时正确就返回登录成功。

@Override
public ServerResponse<User> login(String username, String password) {
    //先校验用户名存在与否
    int resultCount = userMapper.checkUserName(username);
    if (resultCount == 0) {
        return ServerResponse.createByErrorMessage("用户名不存在");
    }
    //密码登录MD5
    String md5Password = MD5Util.MD5EncodeUtf8(password);
    User user = userMapper.selectLogin(username, md5Password);
    if (user == null) {
        return ServerResponse.createByErrorMessage("密码错误");
    }

    user.setPassword(StringUtils.EMPTY);
    return ServerResponse.createBySuccess("登录成功", user);
}

这个先不用管我妈妈的MD5的实现(其实就是引用一个工具)这里校验的方法就是去查询相对于的dao包中的数据检测函数。当然只有函数没有对应的sql查询也是不可能实现的,于是我们就要去创建(或者说系统自动创建好了相对于的sql语句)。由于安装了free mybatis plugin,所以只需要在实现了的前提下点即箭头即可。于是定位到了:

在Mapping中的sql实现,我们的id名为checkUserName,返回值为int类型,输入的类型是String。sql的内容不做额外的讲解。

<select id="checkUserName" resultType="int" parameterType="String">
select count(1) from mmall_user
where username = #{username}
</select>

然后我们再去判断相对于的输出结果,最后返回一个response值给前端显示。


好了我们这样就完成了一个从数据库中获取数据再到分析数据并且传送到前端显示的简单实现。那么我们再用这个方法考虑应该如何写注册功能:

我们要写的话而不是读,就应该先要创建一个注册和接口,然后去实现这个接口:

public ServerResponse<String> register (User user);

然后再去实现这个功能:

这里呢我们主要实现注册,主要是我们先检测名字,因为名字id唯一。然后查询email是否重用,也是唯一,如果都行在输入密码即可。

public ServerResponse<String> register (User user){
    int resultCount = userMapper.checkUserName(user.getUsername());
    if (resultCount > 0){
        return ServerResponse.createByErrorMessage("用户名已存在");
    }
    resultCount = userMapper.checkEmail(user.getEmail());
    if (resultCount > 0){
        return ServerResponse.createByErrorMessage("email已存在");
    }
    user.setRole(Const.Role.ROLE_CUSTOMER);
    //MD5加密
    user.setPassword(MD5Util.MD5EncodeUtf8(user.getPassword()));
    int resultCount = userMapper.insert(user);
    if(resultCount == 0){
        return ServerResponse.createByErrorMessage("注册失败");
    }
    return ServerResponse.createBySuccessMessage("注册成功");
}

这里呢哦我们做的步骤就是先去查找一下用户名是否存在,如果存在就“报错”,查找的方法就在mapper中写入了,我们先去看看有没有这个方法没有的话我们需要自己加入(显然dao只有增删改查,于是我们写入即可):

所以我们这里需要两个sql查询名字和email。

<select id="checkUserName" resultType="int" parameterType="String">
select count(1) from mmall_user
where username = #{username}
</select>

<select id="checkEmail" resultType="int" parameterType="String">
select count(1) from mmall_user
where email = #{email}
</select>

如果都没事,那就可以注册了。我们就在controller里面加入注册的实现就可以啦:

通过实现的判断,我们这里去验证用户即可。

@RequestMapping(value="register.do",method = RequestMethod.GET)
@ResponseBody
public ServerResponse<String> register(User user){
    return iUserService.register(user);
}

可以看到的是我们在分析代码和读取代码的步骤是不同的哈。


现在写一个专门用来检验的类,然后检验用户名和邮箱是否存在。

检测类的时间主要是验证用户名是否存在,email是否已经被使用

public ServerResponse<String> checkValid(String str,String type){
    //校验空格是否有效,空格不占字符
    if(org.apache.commons.lang3.StringUtils.isNoneBlank(type)){
        //开始检验
        if(Const.USERNAME.equals(type)){
            int resultCount = userMapper.checkUserName(str);
            if (resultCount > 0){
                return ServerResponse.createByErrorMessage("用户名已存在");
            }
        }
        if(Const.EMAIL.equals(type)){
            int resultCount = userMapper.checkEmail(type);
            if (resultCount > 0){
                return ServerResponse.createByErrorMessage("email已存在");
            }
        }
    }else{
        return ServerResponse.createByErrorMessage("参数错误");
    }
    return ServerResponse.createBySuccessMessage("校验成功");
}

然后在接口处加入:

ServerResponse<String> checkValid(String str,String type);

然后再向控制层写入,我们呢传入两个值,一个用户名一个邮箱,然后访问是否有效把结果传递出去:

//检验信息是否正确
@RequestMapping(value="checkValid.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<String> checkValid(String str,String type){
    return iUserService.checkValid(str,type);
}

当然了,除了登录和注册,我们还需要实现如果用户忘记了自己的密码,就需要申请找回,找回的方式就是用设定的问题校验身份后修改密码。

public ServerResponse selectQuestion(String username){
    ServerResponse validResponse = this.checkValid(username,Const.USERNAME);
    if(validResponse.isSuccess()){
        return ServerResponse.createByErrorMessage("用户名不存在");
    }
    String question = userMapper.selectQuestionByUsername(username);
    if(org.apache.commons.lang3.StringUtils.isNoneBlank(question)){
        return ServerResponse.createBySuccess(question);
    }
    return ServerResponse.createByErrorMessage("找回密码的问题是空的");
}

我们为什么要先写这里的impl实现类呢,主要是因为有时候我们的传参问题变化,我们可以先写好实现类之后再把接口定义,这样就避免了因为临时写实现类时候的传参不同问题。

接下来我们就要去写它的Mapping层数据了,只有结合数据库的查找才能直到是否正确验证信息。

首先在接口出写,方便调用:

String selectQuestionByUsername(String username);

然后进入对应的Mapping写对应的Sql语句:

  <select id="selectQuestionByUsername" resultType="String" parameterType="String">
select question
from mmall_user
where username = #{username}
  </select>

一切都整理好之后呢我们也获得了返回值直到是否成功,然后我们可以写它的控制层的代码:

@RequestMapping(value="forget_get_question.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<String> forgetGetQuestion(String username){
    return iUserService.selectQuestion(username);
}

这样我们就把找回密码的问题逻辑写好了,然后等待数据(用户给定的问题答案),我们要做的就是检验这个答案是否正确,然后给定一个返回值给前端:

 public ServerResponse<String> checkAnswer(String username,String question,String answer){
    int resultCount = userMapper.checkAnswer(username,question,answer);
    if(resultCount>0){
        //说明问题以及问题答案是这个用户的
        String forgetToken = UUID.randomUUID().toString();
        TokenCache.setKey(TokenCache.TOKEN_PREFIX+username,forgetToken);
        return ServerResponse.createBySuccess(forgetToken);
    }
    return ServerResponse.createByErrorMessage("问题答案错误");
}

当然了我们要判断问题正确与否肯定要去数据库找到这个人的问题答案,然后验证。所以我们就要进入Mapping写对应的SQL语句:

定义接口:
int checkAnswer(@Param("username")String username,@Param("question")String question,@Param("answer")String answer);

实现查找的sql:
  <select id="checkAnswer" resultType="int" parameterType="map">
select count(1)
from mmall_user
where username=#{username}
and question=#{question}
and answer =#{answer}
  </select>

有了这个验证后,我们再进入控制层,把我们的结果答案传给实现类:

//校验回答是否正确
@RequestMapping(value="forget_check_answer.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<String> forgetCheckAnswer(String username,String question,String answer){
    return iUserService.checkAnswer(username,question,answer);
}

可想而知啊,问题的答案正确了,我们就要给他权力去修改自己的密码:

public ServerResponse<String> forgetResePassword(String username,String passwordNew,String forgetToken){
    if(org.apache.commons.lang3.StringUtils.isBlank(forgetToken)){
        return ServerResponse.createByErrorMessage("参数错误,token需要传递");
    }
    ServerResponse validResponse = this.checkValid(username,Const.USERNAME);
    if(validResponse.isSuccess()){
            return ServerResponse.createByErrorMessage("用户名不存在");
    }
    String token = TokenCache.getKey(TokenCache.TOKEN_PREFIX+username);
    if(org.apache.commons.lang3.StringUtils.isBlank(token)){
        return ServerResponse.createByErrorMessage("token无效或者过期");
    }
    if(org.apache.commons.lang3.StringUtils.equals(forgetToken,token)){
        String md5Password = MD5Util.MD5EncodeUtf8(passwordNew);
        int rowCount = userMapper.updatePasswordByUsername(username,md5Password);

        if(rowCount > 0){
            return ServerResponse.createBySuccessMessage("密码修改成功");
        }
        }else{
            return ServerResponse.createByErrorMessage("token错误,请重新获取重置密码的token");
    }
    return ServerResponse.createByErrorMessage("修改密码失败");
    }

这里呢我们还是要传递一个token,有什么作用呢,就是再给定的时间内去修改密码密码,如果时间不限定,被外部用户窃取后自行修改了就麻烦了,token的设定呢我们在初始化阶段就设置好了的,以供后续使用。然后呢我们就要去实现用户旧密码的更新操作:

给定接口:
int updatePasswordByUsername(@Param("username")String username,@Param("passwordNew")String passwordNew);
实现的SQL语句:
 <update id="updatePasswordByUsername" parameterType="map">
update mmall_user
set password = #{passwordNew},updata_time = now()
where username = #{username}
  </update>

然后呢我们把数据给了控制层,再有控制层把结果给实现类即可:

//忘记密码中的重置密码
@RequestMapping(value="forget_reset_password.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<String> forgetResetPassword(String username,String passwordNew,String forgetToken){
    return iUserService.forgetResePassword(username,passwordNew,forgetToken);
}

现在呢又来问题了,我们实现了忘记密码用问题找回,那么如果我在登录状态下也想修改密码怎么办呢?

public ServerResponse<String> resetPassword(String passwordOld,String passwordNew,User user){
//防止横向越权,要检验一下这个用户的旧密码,一定要指定是这个用户
    int resultCount = userMapper.checkPassword(MD5Util.MD5EncodeUtf8(passwordOld),user.getId());
    if(resultCount == 0){
        return ServerResponse.createByErrorMessage("旧密码错误");
    }
    user.setPassword(MD5Util.MD5EncodeUtf8(passwordNew));
    int updateCount = userMapper.updateByPrimaryKeySelective(user);
    if(updateCount > 0){
        return ServerResponse.createBySuccessMessage("密码更新成功");
    }
    return ServerResponse.createByErrorMessage("密码更新失败");
}

我们呢根据旧密码是否正确来设定新密码,旧密码是否正确就涉及到了数据库的查询,于是我们就进入设置相对应的SQL:

接口设定:
int checkPassword(@Param("password")String password,@Param("userId")Integer userId);
实现SQL:
  <select id="checkPassword" resultType="int" parameterType="map">
select count(1)
from mmall_user
where id = #{userId}
and password = #{password}
  </select>

由此然后再进入控制层:

//登录状态的重置密码
@RequestMapping(value="forget_password.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<String> resetPassword(HttpSession session,String passwordOld,String passwordNew){
    User user = (User)session.getAttribute(Const.CURRENT_USER);
    if(user == null){
        return ServerResponse.createByErrorMessage("用户未登录");
    }
    return iUserService.resetPassword(passwordOld,passwordNew,user);
}

这样就设定完了相应的在登录情况下修改密码操作。


对于用于自己来说,有时候信息变更了,也想把网站的个人信息更新一下,所以我们就要实现对于的个人信息更新的代码:

public ServerResponse<User> updateInformation(User user){
    //username是不能更新了
    //email也要进行校验,校验新的email是不是已经存在,并且存在的email如果相同的话不能是我们当前这个用户的
    int resultCount = userMapper.checkEmailByUserId(user.getEmail(),user.getId());
    if(resultCount > 0){
        return ServerResponse.createBySuccessMessage("email已经存在,请更换email重新尝试");
    }
    User updateUser = new User();
    updateUser.setId(user.getId());
    updateUser.setEmail(user.getEmail());
    updateUser.setPhone(user.getPhone());
    updateUser.setQuestion(user.getQuestion());
    updateUser.setAnswer(user.getAnswer());

    int updateCount = userMapper.updateByPrimaryKeySelective(updateUser);
    if(updateCount > 0){
        return ServerResponse.createBySuccess("更新个人信息成功",updateUser);
    }
    return ServerResponse.createByErrorMessage("更新个人信息失败");
}

然后还是老套路:

int checkEmailByUserId(@Param("email")String email,@Param("userId")Integer userId);
实现:
  <select id="checkEmailByUserId" resultType="int" parameterType="map">
select count(1)
from mmall_user
where email = #{email}
and id != #{userId}
  </select>

再是控制层代码:

//更新用户个人信息接口设置
@RequestMapping(value="update_information.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<User> update_information(HttpSession session,User user){
    User currentUser = (User)session.getAttribute(Const.CURRENT_USER);
    if(currentUser == null){
        return ServerResponse.createByErrorMessage("用户未登录");
    }
    user.setId(currentUser.getId());
    user.setUsername(currentUser.getUsername());
    ServerResponse<User> response = iUserService.updateInformation(user);
    if(response.isSuccess()){
        session.setAttribute(Const.CURRENT_USER,response.getData());
    }
    return response;
}

至此我们的用户登录基本写完了。

我们还需要介绍的呢就是“注解”功能需要事先了解一下Spring的知识。

@RequestMapping(value="",method=""):
    类级别的注解负责将一个特定(或符合某种模式)的请求路径映射到一个控制器上,
    同时通过方法级别的注解来细化映射,即根据特定的HTTP请求方法(GET、POST 方法等)、HTTP请求中是否携带特定参数等条件,将请求映射到匹配的方法上
@ResponseBody:
    将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML数据。
    需要注意的呢,在使用此注解之后不会再走试图处理器,而是直接将数据写入到输入流中,他的效果等同于通过response对象输出指定格式的数据。
      @RequestMapping("/login")
      @ResponseBody
      public User login(User user){
        return user;
      }
        -----------------------
      @RequestMapping("/login")
      public void login(User user, HttpServletResponse response){
        response.getWriter.write(JSONObject.fromObject(user).toString());
      }
    分析就是上面两行的作用是一样的。
@Controller:
    用于标记在一个类上,使用它标记的类就是一个SpringMvc Controller对象,分发处理器会扫描使用该注解的类的方法,并检测该方法是否使用了@RequestMapping注解。
    @Controller只是定义了一个控制器类,而使用@RequestMapping注解的方法才是处理请求的处理器。 
    @Controller标记在一个类上还不能真正意义上说它就是SpringMvc的控制器,应为这个时候Spring还不认识它,这个时候需要把这个控制器交给Spring来管理。
@Autowired:
    它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 通过 @Autowired的使用来消除 set ,get方法。
    当容器扫描到@Autowied时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性
@Service("iUserService") :
    括号内信息是以代码为例的,就是将iUserService的信息注入到Spring的Ioc容器中。不需要用new。
@JsonSerialize:
    主要用于数据转换,该注解作用在该属性的getter()方法上。
    前端显示和后台存储数据单位不统一,而且各有各自的理由,统一不了,那就转换吧。每次返回给前端时再转换一遍,返回给前端的json数据,在后端里定义的往往是一个对象因此用到本注解。

具体代码和整个项目可以点击:xiangzi1019.github.io

感谢您的鼓励.如果喜欢可以送我一包辣条。