9. 用户-登录-业务层
(a) 规划异常
此时,应该穷举用户在“登录”过程中可能出现的任何“错误”,例如,可能出现“用户名不存在”,或“密码错误”,或“用户数据被标记为已删除”。
所以,需要为以上“错误”创建对应的异常类:
cn.tedu.store.service.ex.UserNotFoundException
cn.tedu.store.service.ex.PasswordNotMatchException
创建的异常类都应该继承自ServiceException
。
(b) 接口与抽象方法
在IUserService
接口中添加“登录”的抽象方法:
User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException;
© 实现抽象方法
在UserServiceImpl
实现类中重写以上抽象方法:
public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
// 根据参数username执行查询
// 判断查询结果是否为null
// 是:抛出UserNotFoundException
// 判断查询结果中的isDelete是否为1
// 是:抛出UserNotFoundException
// 从查询结果中获取盐值
// 基于参数password和盐值执行加密
// 判断以上加密结果与查询结果中的password是否不匹配
// 是:抛出PasswordNotMatchException
// 将查询结果中的password设置为null
// 将查询结果中的salt设置为null
// 将查询结果中的isDelete设置为null
// 返回查询结果
}
具体实现为:
@Override
public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
// 根据参数username执行查询
User result = userMapper.findByUsername(username);
// 判断查询结果是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"登录失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"登录失败!用户数据不存在!");
}
// 从查询结果中获取盐值
String salt = result.getSalt();
// 基于参数password和盐值执行加密
String md5Password = getMd5Password(password, salt);
// 判断以上加密结果与查询结果中的password是否不匹配
if (!md5Password.equals(result.getPassword())) {
// 是:抛出PasswordNotMatchException
throw new PasswordNotMatchException(
"登录失败!密码错误!");
}
// 将查询结果中的password设置为null
result.setPassword(null);
// 将查询结果中的salt设置为null
result.setSalt(null);
// 将查询结果中的isDelete设置为null
result.setIsDelete(null);
// 返回查询结果
return result;
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void login() {
try {
String username = "root";
String password = "1234x";
User result = service.login(username, password);
System.err.println(result);
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
##################### 测试结果为1 #####################
控制台输出登录成功信息:
User [uid=11, username=root1, password=null, salt=null, gender=null, phone=null, email=null, avatar=null, isDelete=null]
10. 用户-登录-控制器层
(a) 统一处理异常
在BaseController
的处理异常的方法中,添加更多的分支,对以上新抛出的2种异常进行处理:
@ExceptionHandler(ServiceException.class)
@ResponseBody
public JsonResult<Void> handleException(Throwable e) {
JsonResult<Void> jr = new JsonResult<>(e);
jr.setMessage(e.getMessage());
if (e instanceof UsernameDuplicateException) {
jr.setState(4000);
} else if (e instanceof UserNotFoundException) {
jr.setState(4001);
} else if (e instanceof PasswordNotMatchException) {
jr.setState(4002);
} else if (e instanceof InsertException) {
jr.setState(5000);
}
return jr;
}
(b) 设计请求
设计“用户登录”的请求方式:
请求路径:/users/login
请求参数:String username, String password, HttpSession session
请求方式:POST
响应数据:JsonResult<User>
© 处理请求
在类中添加处理请求的方法:
@RequestMapping("login")
public JsonResult<User> login(String username, String password, HttpSession session) {
// 调用业务层对象的“登录”方法,获取返回结果
// 向Session中存入用户id和用户名
// 返回
}
具体代码为:
@RequestMapping("login")
public JsonResult<User> login(String username, String password, HttpSession session) {
// 调用业务层对象的“登录”方法,获取返回结果
User data = userService.login(username, password);
// 向Session中存入用户id和用户名
session.setAttribute("uid", data.getUid());
session.setAttribute("username", data.getUsername());
// 返回
return new JsonResult<>(SUCCESS, data);
}
JsonResult中添加state和data的构造方法,以便于传参数。
完成后,可以通过http://localhost:8080/users/login?username=rootx&password=1234x
进行单元测试,完成后,将@RequestMapping
改成@PostMapping
。
对于输出的JSON结果过于复杂,且将为null的结果都已经输出为JSON数据,这种做法并不合理,存在浪费资源的问题,并且还暴露项目的特征数据,要解决该问题,可以在application.properties中添加配置,使得为null的属性将不被输出到JSON结果中:
spring.jackson.default-property-inclusion=non_null
并且在jsonRequest类中加入以下注解:
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonResult {
}
##################### 测试结果为2 #####################
登录成功:
{“state”:2000,“data”:{“uid”:10,“username”:“root”}}
用户名不存在:
{“state”:4001,“message”:“登录失败!用户数据不存在!”}
密码错误:
{“state”:4002,“message”:“登录失败!密码错误!”}
11. 用户-登录-前端界面
1.直接复制ajax
2.修改ajax中对应的参数
3.修改表单中的数据,以便于用户输入的值能传到服务端进行匹配。
12. 用户-修改密码-持久层
(a) 规划SQL语句
执行修改密码的SQL语句大致是:
update t_user set password=?, modified_user=?, modified_time=? where uid=?
在执行修改之前,还需要验证原密码是否正确,但是,不应该是:
update t_user set password=? where uid=? and password=?
之所以不将密码作为查询条件之一,是因为在SQL语句中不区分大小写,而密码应该匹分大小写,另外,“需要验证原密码”是软件开发者所设计的业务规则,应该在业务层中去体现,并不应该写在SQL语句中。
为了保证“验证原密码”的功能,还需要查询出原密码和盐值,则需要执行:
select password, salt from t_user where uid=?
另外,在查询时,还应该检查用户的is_delete是否正常,所以:
select password, salt, is_delete from t_user where uid=?
(b) 接口与抽象方法
在UserMapper.java
接口中添加抽象方法:
Integer updatePassword(
@Param("uid") Integer uid,
@Param("password") String password,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime);
User findByUid(Integer uid);
© 配置映射
在UserMapper.xml
中配置以上2个抽象方法的映射,如果不希望每次查询时都需要自定义别名,可以事先配置好<resultMap>
:
<!-- 查询结果与用户数据实体的映射 -->
<resultMap id="UserEntityMap"
type="cn.tedu.store.entity.User">
<id column="uid" property="uid"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="salt" property="salt"/>
<result column="gender" property="gender"/>
<result column="phone" property="phone"/>
<result column="email" property="email"/>
<result column="avatar" property="avatar"/>
<result column="is_delete" property="isDelete"/>
<result column="created_user" property="createdUser"/>
<result column="created_time" property="createdTime"/>
<result column="modified_user" property="modifiedUser"/>
<result column="modified_time" property="modifiedTime"/>
</resultMap>
<!-- 更新密码 -->
<!-- Integer updatePassword(
@Param("uid") Integer uid,
@Param("password") String password,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime) -->
<update id="updatePassword">
UPDATE
t_user
SET
password=#{password},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
</update>
<!-- 根据用户id查询用户数据 -->
<!-- User findByUid(Integer uid) -->
<select id="findByUid"
resultMap="UserEntityMap">
SELECT
password, salt,
is_delete
FROM
t_user
WHERE
uid=#{uid}
</select>
在UserMapperTests
中编写并执行单元测试:
@Test
public void updatePassword() {
Integer uid = 8;
String password ="1234";
String modifiedUser = "超级管理员";
Date modifiedTime = new Date();
Integer rows = mapper.updatePassword(uid, password, modifiedUser, modifiedTime);
System.err.println("rows=" + rows);
}
@Test
public void findByUid() {
Integer uid = 8;
User result = mapper.findByUid(uid);
System.err.println(result);
}
##################### 测试结果为3 #####################
1.修改密码
13. 用户-修改密码-业务层
(a) 规划异常
本次执行的主要是更新数据的操作,则可能出现UpdateException
。
在执行更新之前,还需要根据uid查询用户数据,检查数据是否存在,检查数据的is_delete,都可能出现UserNotFoundException
。
在执行更新之前,还需要验证原密码是否正确,则可能出现PasswordNotMatchException
。
则需要创建cn.tedu.store.service.ex.UpdateException
,并继承自ServiceException
。
(b) 接口与抽象方法
在IUserService
中添加“修改密码”的抽象方法:
void changePassword(Integer uid, String oldPassword,
String newPassword, String modifiedUser) throws
UserNotFoundException, PasswordNotMatchException, UpdateException;
关于抽象方法的参数的设计原则:要么是客户端提交的,要么是服务器端的控制器中提供的,并且,穷举所有参数后,足以调用持久层的相关功能!
© 实现抽象方法
在UserServiceImpl
实现类中添加新的抽象方法并实现:
public void changePassword(Integer uid, String oldPassword,
String newPassword, String modifiedUser) throws
UserNotFoundException, PasswordNotMatchException, UpdateException {
// 根据参数uid查询用户数据
// 判断查询结果是否为null:UserNotFoundException
// 判断查询结果中的isDelete是否为1:UserNotFoundException
// 从查询结果中获取盐值
// 对参数oldPassword执行加密,得到oldMd5Password
// 判断查询结果中的密码与oldMd5Password是否不匹配:PasswordNotMatchException
// 对参数newPassword执行加密,得到newMd5Passowrd
// 执行更新,获取返回值(受影响的行数)
// 判断受影响的行数是否不为1:UpdateException
}
具体实现为:
@Override
public void changePassword(Integer uid, String oldPassword, String newPassword, String modifiedUser)
throws UserNotFoundException, PasswordNotMatchException, UpdateException {
// 根据参数uid查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果是否为null:UserNotFoundException
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改密码失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1:UserNotFoundException
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改密码失败!用户数据不存在!");
}
// 从查询结果中获取盐值
String salt = result.getSalt();
// 对参数oldPassword执行加密,得到oldMd5Password
String oldMd5Password = getMd5Password(oldPassword, salt);
// 判断查询结果中的密码与oldMd5Password是否不匹配:PasswordNotMatchException
if (!result.getPassword().equals(oldMd5Password)) {
// 是:抛出PasswordNotMatchException
throw new PasswordNotMatchException(
"修改密码失败!原密码错误!");
}
// 对参数newPassword执行加密,得到newMd5Passowrd
String newMd5Password = getMd5Password(newPassword, salt);
// 执行更新,获取返回值(受影响的行数)
Integer rows = userMapper.updatePassword(uid, newMd5Password, modifiedUser, new Date());
// 判断受影响的行数是否不为1:UpdateException
if (rows != 1) {
throw new UpdateException(
"修改密码失败!更新数据时出现未知错误!");
}
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void changePassword() {
try {
Integer uid = 9;
String oldPassword = "8888";
String newPassword = "1234";
String modifiedUser = "系统管理员";
service.changePassword(uid, oldPassword, newPassword, modifiedUser);
System.err.println("OK");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
##################### 测试结果为4 #####################
控制台输出对应修改数据
14. 用户-修改密码-控制器层
(a) 统一处理异常
需要在BaseController
中添加对UpdateException
的处理。
(b) 设计请求
设计“修改密码”的请求方式:
请求路径:/users/change_password
请求参数:@RequestParam("old_password") String oldPassword,
@RequestParam("new_password") String newPassword, HttpSession session
请求方式:POST
响应数据:JsonResult<Void>
© 处理请求
在BaseController
中添加各子级控制器都可能需要执行的方法,例如获取uid的方法:
protected final Integer getUidFromSession(HttpSession session) {
return Integer.valueOf(session.getAttribute("uid").toString());
}
protected final String getUsernameFromSession(HttpSession session) {
return session.getAttribute("username").toString();
}
在UserController
中添加处理请求的方法:
@RequestMapping("change_password")
public JsonResult<Void> changePassword(
@RequestParam("old_password") String oldPassword,
@RequestParam("new_password") String newPassword,
HttpSession session) {
// 从session中获取uid
Integer uid = getUidFromSession(session);
// 从session中获取username
String username = getUsernameFromSession(session);
// 调用service对象执行修改密码
userService.changePassword(uid, oldPassword, newPassword, username);
// 响应成功
return new JsonResult<>(SUCCESS);
}
完成后,打开浏览器,先登录,然后通过http://localhost:8080/users/change_password?old_password=1234&new_password=8888
进行测试。
##################### 测试结果为5 #####################
切忌,在没有登录的情况下测试,否则会出现空指针异常。
15. 添加拦截器
由于后续的越来越多的操作都是需要事先登录的,不登录则不允许执行相关操作,例如修改密码、修改资料、上传头像、创建收货地址、操作购物车、生成订单等……所以,可以在项目中添加登录拦截器,对用户是否登录进行验证,如果没有登录,则不执行后续的请求处理,而是直接重定向到登录页,避免发生错误!
首先,创建cn.tedu.store.interceptor.LoginInterceptor
拦截器类,并定义拦截处理方式:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 判断session中是否有登录信息
if (session.getAttribute("uid") == null) {
// 没有登录信息,则重定向到登录页
response.sendRedirect("/web/login.html");
// 执行拦截
return false;
}
// 放行
return true;
}
}
然后,需要对拦截器进行配置,在SpringBoot中,需要自定义配置类,以对拦截器进行配置:所以需要新创建一个cn.tedu.blogs.config.interceptor
@Configuration
public class InterceptorConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 创建拦截器对象
HandlerInterceptor interceptor = new LoginInterceptor();
// 白名单
List<String> patterns = new ArrayList<>();
patterns.add("/bootstrap3/**");
patterns.add("/css/**");
patterns.add("/js/**");
patterns.add("/images/**");
patterns.add("/web/register.html");
patterns.add("/web/login.html");
patterns.add("/users/reg");
patterns.add("/users/login");
// 注册拦截器
registry.addInterceptor(interceptor)
.addPathPatterns("/**")
.excludePathPatterns(patterns);
}
}
##################### 测试结果为5 #####################
浏览器在没有登录的情况下,不能进行其他操作。
只有列入白名单的网页才能访问
比如以下就是没有登录,就不能访问的页面:
http://localhost:8080/web/404.html
http://localhost:8080/web/address.html
注册,登录可以
15. 用户-修改密码-前端界面
-----------------------------------------
12. 用户-修改密码-持久层
(a) 规划SQL语句
(b) 接口与抽象方法
© 配置映射
13. 用户-修改密码-业务层
(a) 规划异常
(b) 接口与抽象方法
© 实现抽象方法
14. 用户-修改密码-控制器层
(a) 统一处理异常
(b) 设计请求
设计“xxx”的请求方式:
请求路径:/users/reg
请求参数:User user
请求方式:POST
响应数据:JsonResult<Void>
© 处理请求