文章目录
用户登录功能(任务二)
功能页面
用户登录流程
先分析一下思路:当用户输入用户名和密码将数据提交给后台数据库进行查询,如果存在对应的用户名和密码则表示登录成功,登录成功之后跳转到系统的主页就是index.html页面,跳转在前端使用jQuery来完成
1.登录-持久层
规划需要执行的SQL语句
依据用户提交的用户名来做select查询
select * from t_user where username=? and password=?这种不太好,这种相当于在查询用户名时直接判断了用户和密码是否一致了,如果持久层把判断做了那业务层就没事干了,所以这里我们只查询用户名,判断用户名和密码是否一致交给业务层做
select * from t_user where username=?
分析完以后发现这个功能模块已经被开发完成(UserMapper接口的findByUsername方法),所以就可以省略当前的开发步骤,但是这个分析过程不能省略
后续的设计接口和抽象方法,编写映射,单元测试都不再需要进行
2.登录-业务层
2.1规划异常
- 用户名对应的密码错误,即密码匹配的异常,起名PasswordNotMatchException,这个是运行时异常
public class PasswordNotMatchException extends ServiceException{
/**重写ServiceException的所有构造方法*/
}
- 用户名没有被找到的异常,起名UsernameNotFoundException,这个也是运行时异常
public class UsernameNotFoundException extends ServiceException {
/**重写ServiceException的所有构造方法*/
}
2.2设计接口和抽象方法及实现
1.在IUserService接口中编写抽象方法login(String username,String password)login(User user)也是可以的
登录成功某一个网站后,右上角会展示头像,昵称甚至电话号码等等,这些信息依赖于登陆成功后的信息,也就意味着一旦登录成功后在页面中切换到任意一个子页面写右上角都会展示这些信息.本质上就是查询出来这些信息,然后展示在右上角,但是这里实现查询不太现实:js中虽然打开一个html页面就自动发送一个请求,但这样就需要把这个查询的代码写在每一个html页面,显然不现实
这种情况下我们可以将当前登录成功的用户数据以当前用户对象的形式进行返回,然后进行状态管理:将数据保存在cookie或者session中,可以避免重复度很高的数据多次频繁操作数据库进行获取(这里我们用session存放用户名和用户id,用cookie存放用户头像,其中用户id是为因为有的页面展示依赖于id,用户头像也可以放在session中,而这里放在cookie是为了回顾一下cookie)
/**
* 用户登录功能
* @param username 用户名
* @param password 用户密码
* @return 当前匹配的用户数据,如果没有则返回null
*/
User login(String username,String password);
2.在抽象类UserServiceImpl中实现该抽象方法
@Override
public User login(String username, String password) {
//根据用户名称来查询用户的数据是否存在,不存在则抛出异常
User result = userMapper.findByUsername(username);
if (result == null) {
throw new UsernameNotFoundException("用户数据不存在");
}
/**
* 检测用户的密码是否匹配:
* 1.先获取数据库中加密之后的密码
* 2.和用户传递过来的密码进行比较
* 2.1先获取盐值
* 2.2将获取的用户密码按照相同的md5算法加密
*/
String oldPassword = result.getPassword();
String salt = result.getSalt();
String newMd5Password = getMD5Password(password, salt);
if (!newMd5Password.equals(oldPassword)) {
throw new PasswordNotMatchException("用户密码错误");
}
//判断is_delete字段的值是否为1,为1表示被标记为删除
if (result.getIsDelete() == 1) {
throw new UsernameNotFoundException("用户数据不存在");
}
//方法login返回的用户数据是为了辅助其他页面做数据展示使用(只会用到uid,username,avatar)
//所以可以new一个新的user只赋这三个变量的值,这样使层与层之间传输时数据体量变小,后台层与
// 层之间传输时数据量越小性能越高,前端也是的,数据量小了前端响应速度就变快了
User user = new User();
user.setUid(result.getUid());
user.setUsername(result.getUsername());
user.setAvatar(result.getAvatar());
return user;
}
2.3单元测试
在业务层的测试类UserServiceTests中添加测试方法:
@Test
public void login() {
//因为login方法可能抛出异常,所以应该捕获异常,但是测试时没必要写那么严谨
User user = userService.login("test02", "12");
System.out.println(user);
}
3.登录-控制层
3.1处理异常
业务层抛出的异常需要在统一异常处理类中进行统一的捕获和处理,如果该异常类型已经在统一异常类中曾经处理过则不需要重复添加
else if (e instanceof UsernameNotFoundException) {
result.setState(4001);
result.setMessage("用户数据不存在的异常");
} else if (e instanceof PasswordNotMatchException) {
result.setState(4002);
result.setMessage("用户名密码错误的异常");
}
3.2设计请求
- 请求路径:/users/login
- 请求参数:String username,String password
- 请求类型:POST
- 响应结果:JsonResult
3.3处理请求
在UserController类中编写处理请求的方法.编写完成后启动主服务验证一下
@RequestMapping("login")
public JsonResult<User> login(String username,String password) {
User data = userService.login(username, password);
return new JsonResult<User>(OK,data);
}
注意,控制层方法的参数是用来接收前端数据的,接收数据方式有两种:
请求处理方法的参数列表设置为非pojo类型:
SpringBoot会直接将请求的参数名和方法的参数名直接进行比较,如果名称相同则自动完成值的依赖注入
请求处理方法的参数列表设置为pojo类型:
SpringBoot会将前端的url地址中的参数名和pojo类的属性名进行比较,如果这两个名称相同,则将值注入到pojo类中对应的属性上
这两种方法都没有使用注解等等花里胡哨的,却能正常使用,原因是springboot是约定大于配置的,省略了大量配置以及注解的编写
4.登录-前端页面
在login.html加入script标签:
<script>
$("#btn-login").click(function () {
$.ajax({
url: "/users/login",
type: "POST",
data: $("#form-login").serialize(),
dataType: "JSON",
success: function (json) {
if (json.state == 200) {
alert("登录成功")
//跳转到系统主页index.html
//index和login在同一个目录结构下,所以可以用相对路
// 径index.html来确定跳转的页面,index.html和./ind
// ex.html完全一样,因为./就是表示当前目录
// 结构,也可以用../web/index.html
location.href = "index.html";
} else {
alert("登录失败")
}
},
error: function (xhr) {
//xhr.message可以获取未知异常的信息
alert("登录时产生未知的异常!"+xhr.message);
}
});
});
</script>
5.用session存储和获取用户数据
- 在用户登录成功后要保存下来用户的id,username,avatar,并且需要在任何类中都可以访问存储下来的数据,也就是说存储在一个全局对象中,会话session可以实现
- 把首次登录所获取的用户数据转移到session对象即可
- 获取session对象的属性值用session.getAttribute(“key”),因为session对象的属性值在很多页面都要被访问,这时用session对象调用方法获取数据就显得太麻烦了,解决办法是将获取session中数据的这种行为进行封装
- 考虑一下封装在哪里呢?放在一个干净的工具类里肯定可以,但就这个项目目录结构而言,只有可能在控制层使用session,而控制层里的类又继承BaseController,所以可以封装到BaseController里面
综上所述,该功能的实现需要两步:
1.在父类中封装两个方法:获取uid和获取username对应的两个方法(用户头像暂不考虑,将来封装到cookie中来使用)
/**
* 获取session对象中的uid
* @param session session对象
* @return 当前登录的用户uid的值
*/
public final Integer getUidFromSession(HttpSession session) {
//getAttribute返回的是Object对象,需要转换为字符串再转换为包装类
return Integer.valueOf(session.getAttribute("uid").toString());
}
public final String getUsernameFromSession(HttpSession session) {
return session.getAttribute("username").toString();
}
2.把首次登录所获取的用户数据转移到session对象:
服务器本身自动创建有session对象,已经是一个全局的session对象,所以我们需要想办法获取session对象:如果直接将HttpSession类型的对象作为请求处理方法的参数,这时springboot会自动将全局的session对象注入到请求处理方法的session形参上:
将登录模块的设计请求中的请求参数:String username,String password加上HttpSession session
将登录模块的处理请求中login方法加上参数HttpSession session并修改代码如下:
@RequestMapping("login")
public JsonResult<User> login(String username, String password, HttpSession session) {
User data = userService.login(username, password);
//向session对象中完成数据的绑定(这个session是全局的,项目的任何位置都可以访问)
session.setAttribute("uid",data.getUid());
session.setAttribute("username",data.getUsername());
//测试能否正常获取session中存储的数据
System.out.println(getUidFromSession(session));
System.out.println(getUsernameFromSession(session));
return new JsonResult<User>(OK,data);
}
6.拦截器
- 拦截器的作用是将所有的请求统一拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统设置的过滤规则,该项目统一的处理是重新去打开login.html页面(重定向和转发都可以,推荐使用重定向)
- 拦截器在springboot中本质是依靠springMVC完成的.springMVC提供了一个HandlerInterceptor接口用于表示定义一个拦截器
1.所以想要使用拦截器就要定义一个类并使其实现HandlerInterceptor接口,在store下建包interceptor,包下建类LoginInterceptor并编写代码:
/**定义一个拦截器*/
public class LoginInterceptor implements HandlerInterceptor {
/**
*检测全局session对象中是否有uid数据,如果有则放行,如果没有重定向到登录页面
* @param request 请求对象
* @param response 响应对象
* @param handler 处理器(把url和Controller映射到一块)
* @return 返回值为true放行当前请求,反之拦截当前请求
* @throws Exception
*/
@Override
//在DispatcherServlet调用所有处理请求的方法前被自动调用执行的方法
//springboot会自动把请求对象给到request,响应对象给到response,适配器给到handler
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
//通过HttpServletRequest对象来获取session对象
Object obj = request.getSession().getAttribute("uid");
if (obj == null) { //说明用户没有登录过系统,则重定向到login.html页面
//不能用相对路径,因为这里是要告诉前端访问的新页面是在哪个目录下的新
//页面,但前面的localhost:8080可以省略,因为在同一个项目下
response.sendRedirect("/web/login.html");
//结束后续的调用
return false;
}
//放行这个请求
return true;
}
//在ModelAndView对象返回给DispatcherServlet之后被自动调用的方法
// @Override
// public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// }
//在整个请求所有关联的资源被执行完毕后所执行的方法
// @Override
// public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// }
}
2.注册过滤器:
注册过滤器的技术:借助WebMvcConfigure接口将用户定义的拦截器进行注册.所以想要注册过滤器需要定义一个类使其实现WebMvcConfigure接口并在其内部添加黑名单(在用户登录的状态下才可以访问的页面资源)和白名单(哪些资源可以在不登录的情况下访问:①register.html②login.html③index.html④/users/reg⑤/users/login⑥静态资源):
WebMvcConfigure是配置信息,建议在store包下建config包,再定义类LoginInterceptorConfigure
/**拦截器的注册*/
@Configuration //自动加载当前的类并进行拦截器的注册,如果没有@Configuration就相当于没有写类LoginInterceptorConfigure
public class LoginInterceptorConfigure implements WebMvcConfigurer {
@Override
//配置拦截器
public void addInterceptors(InterceptorRegistry registry) {
//1.创建自定义的拦截器对象
HandlerInterceptor interceptor = new LoginInterceptor();
//2.配置白名单并存放在一个List集合
List<String> patterns = new ArrayList<>();
patterns.add("/bootstrap3/**");
patterns.add("/css/**");
patterns.add("/images/**");
patterns.add("/js/**");
patterns.add("/web/register.html");
patterns.add("/web/login.html");
patterns.add("/web/index.html");
patterns.add("/web/product.html");
patterns.add("/users/reg");
patterns.add("/users/login");
//registry.addInterceptor(interceptor);完成拦截
// 器的注册,后面的addPathPatterns表示拦截哪些url
//这里的参数/**表示所有请求,再后面的excludePathPatterns表
// 示有哪些是白名单,且参数是列表
registry.addInterceptor(interceptor)
.addPathPatterns("/**")
.excludePathPatterns(patterns);
}
}
修改密码
初步分析:需要用户提交原始密码和新密码,再根据当前登录的用户进行信息的修改操作
1.修改密码-持久层
1.1规划需要执行的SQL语句
根据用户的uid修改用户password值
update t_user set password=?,modified_user=?, modified_time=? WHERE uid=?
modified_user=?, modified_time=?是为了跟踪用户数据的变动,如果这条数据被错误修改了可以找到第一责任人
在执行修改密码之前,还应检查用户数据是否存在或者用户数据是否被标记为"已删除"(比如登录账号后的几分钟在和朋友聊天,没有看页面,管理员错误删除了你的账号或者错误设置is_delete为1)、并检查原密码是否正确,这些检查都可以通过查询用户数据来辅助完成:
SELECT * FROM t_user WHERE uid=?
1.2设计接口和抽象方法
UserMapper接口,将以上的两个方法的抽象定义出来,将来映射到sql语句上
/**
* 根据用户的uid来修改用户密码
* @param uid 用户的id
* @param password 用户输入的新密码
* @param modifiedUser 表示修改的执行者
* @param modifiedTime 表示修改数据的时间
* @return 返回值为受影响的行数
*/
Integer updatePasswordByUid(Integer uid,
String password,
String modifiedUser,
Date modifiedTime);
/**
* 根据用户的id查询用户的数据
* @param uid 用户的id
* @return 如果找到则返回对象,反之返回null值
*/
User findByUid(Integer uid);
1.3编写映射
配置到映射文件UserMapper.xml中
<update id="updatePasswordByUid">
update t_user set
`password`=#{password},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
where uid=#{uid}
</update>
<select id="findByUid" resultMap="UserEntityMap">
select * from t_user where uid=#{uid}
</select>
1.4单元测试
@Test
public void updatePasswordByUid(){
userMapper.updatePasswordByUid(
10,
"321",
"管理员",
new Date());
}
@Test
public void findByUid(){
System.out.println(userMapper.findByUid(10));
}
2.修改密码-业务层
2.1规划异常
- 用户的原密码错误,抛PasswordNotMatchException异常(前面已创建)
- 检测到is_delete字段为1和uid找不到都是抛出用户没有找到的异常,UsernameNotFoundException(前面已创建)
- update在更新的时候,有可能产生未知的异常,抛UpdateException异常
/**用户在更新数据时产生的未知异常*/
public class UpdateException extends ServiceException{
/**重写ServiceException的所有构造方法*/
}
2.2设计接口和抽象方法及实现
1.执行用户修改密码的核心方法:
/**
* changePassword方法需要什么参数:
* 要先看底层持久层需要什么参数:uid,password,modifiedUser,modifiedTime
* 1.修改人其实就是username,已经保存到session当中,通过控制层传递过来就行了
* 2.在更新数据之前需要先根据uid查这个数据存不存在,uid也可以通过控制层传递
* 3.新密码需要有
* 4.修改时间不需要在参数列表,直接在方法内部new Date()就可以了
* 5.旧密码
* */
void changePassword(Integer uid,
String username,
String oldPassword,
String newPassword);
2.在实现类中实现当前的抽象方法
@Override
public void changePassword(Integer uid,
String username,
String oldPassword,
String newPassword) {
User result = userMapper.findByUid(uid);
/**
* 用户没找到:比如登录账号后的几分钟在和朋友聊天,没
* 有看页面,管理员错误删除了你的账号或者错误设置is_delete为1)
*/
if (result ==null || result.getIsDelete() == 1) {
throw new UsernameNotFoundException("用户数据不存在");
}
//原始密码和数据库中密码进行比较
String oldMd5Password = getMD5Password(oldPassword,result.getSalt());
if (!result.getPassword().equals(oldMd5Password)) {
throw new PasswordNotMatchException("密码错误");
}
//将新的密码加密后设置到数据库中(只要曾经注册过就用以前的盐值)
String newMd5Password = getMD5Password(newPassword, result.getSalt());
Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, new Date());
if (rows != 1) {
throw new UpdateException("更新数据产生未知的异常");
}
}
2.3单元测试
@Test
public void changePassword() {
userService.changePassword(11,"管理员","123","321");
}
3.修改密码-控制层
3.1处理异常
UsernameNotFoundException异常和PasswordNotMatchException异常在前面的章节中已经处理过,现在只需要把UpdateException异常配置到统一的异常处理方法中
else if (e instanceof UpdateException) {
result.setState(5001);
result.setMessage("更新数据时产生未知的异常");
}
3.2设计请求
- /users/change_password
- post
- String oldPassword,String newPassword,HttpSession session(uid和username可以通过session获取到,在处理方法的内部获取就可以了)//如果参数名用的是非pojo类型,就需要和表单中的name属性值保持一致
- JsonResult
3.3处理请求
@RequestMapping("change_password")
public JsonResult<Void> changePassword(String oldPassword,
String newPassword,
HttpSession session) {
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
userService.changePassword(uid,username,oldPassword,newPassword);
return new JsonResult<>(OK);
}
启动服务,先登录账号然后在地址栏输入http://localhost:8080/users/change_password?oldPassword=321&newPassword=123看看是否成功
4.修改密码-前端页面
在password.html中添加ajax请求的处理
<script>
$("#btn-change-password").click(function () {
$.ajax({
url: "/users/change_password",
type: "POST",
data: $("#form-change-password").serialize(),
dataType: "JSON",
success: function (json) {
if (json.state == 200) {
alert("密码修改成功")
} else {
alert("密码修改失败")
}
},
error: function (xhr) {
//xhr.message可以获取未知异常的信息
alert("修改密码时产生未知的异常!"+xhr.message);
}
});
});
</script>