1. 用户登录功能
先分析一下思路:当用户输入用户名和密码将数据提交给后台数据库进行查询,如果存在对应的用户名和密码则表示登录成功,登录成功之后跳转到系统的主页就是index.html页面,跳转在前端使用jQuery来完成
2.持久层[Mapper]
规划需要执行的SQL语句
依据用户提交的用户名来做select查询
select * from t_user where username=? and password=?这种不太好,这种相当于在查询用户名时直接判断了用户和密码是否一致了,如果持久层把判断做了那业务层就没事干了,所以这里我们只查询用户名,判断用户名和密码是否一致交给业务层做
select * from t_user where username=?
分析完以后发现这个功能模块已经被开发完成(UserMapper接口的findByUsername方法),所以就可以省略当前的开发步骤,但是这个分析过程不能省略
3.业务层[Service]
1 规划异常
- 用户名对应的密码错误,即密码匹配的异常,起名PasswordNotMatchException,这个是运行时异常
/*** 密码验证失败*/
public class PasswordNotMatchException extends ServiceException {public PasswordNotMatchException() {super();}public PasswordNotMatchException(String message) {super(message);}public PasswordNotMatchException(String message, Throwable cause) {super(message, cause);}public PasswordNotMatchException(Throwable cause) {super(cause);}protected PasswordNotMatchException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}
- 用户名没有被找到的异常,起名UsernameNotFoundException,这个也是运行时异常
/*** 用户数据不存在的异常*/
public class UserNotFoundException extends ServiceException{public UserNotFoundException() {super();}public UserNotFoundException(String message) {super(message);}public UserNotFoundException(String message, Throwable cause) {super(message, cause);}public UserNotFoundException(Throwable cause) {super(cause);}protected UserNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}
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中实现该抽象方法
/*** 用户登录** @param username* @param password* @return*/@Overridepublic User login(String username, String password) {//根据用户名称来查询用户的数据是否存在,如果不存在则抛出异常User result = userMapper.findByUsername(username);if(result==null){throw new UserNotFoundException("用户数据不存在");}//检测用户的密码是否正确//1.先获取数据库中的加密之后的密码String oldPassword = result.getPassword();//2.和用户的传递过来的密码进行比较//2.1 先获取盐值,上一次在注册时候所自动生成的盐值String salt = result.getSalt();//2.2 将用户的密码按照相同的md5算法的规则进行加密String newMd5Password = getMD5Password(password, salt);//3.将密码进行比较if(!newMd5Password.equals(oldPassword)){throw new PasswordNotMatchException("用户密码错误");}//判断is_delete字段的值是否为1表示标记为删除if(result.getIsDelete()==1){throw new UserNotFoundException("用户数据不存在");}//将当前用户的数据返回,返回的数据是为了辅助其他页面做数据展示User user = new User();user.setUid(result.getUid());user.setUsername(result.getUsername());user.setAvatar(result.getAvatar());return user;}
3 单元测试
在业务层的测试类UserServiceTests中添加测试方法:
@Testpublic void login() {//因为login方法可能抛出异常,所以应该捕获异常,但是测试时没必要写那么严谨User user = userService.login("test02", "12");System.out.println(user);}
4. 控制层[Controller]
1处理异常
业务层抛出的异常需要在统一异常处理类中进行统一的捕获和处理,如果该异常类型已经在统一异常类中曾经处理过则不需要重复添加
/*** 控制层的基类*/
public class BaseController {/*** 操作成功的状态码*/public static final int OK = 200;//请求处理方法,这个方法的返回值就是需要传递给前端的数据//ServiceException.class:表示抛出这个异常才会调用这个方法//自动将异常对象传递给此方法的参数列表上//当项目中产生异常,被统一拦截到此方法中,这个方法此时就充当是请求处理方法,方法的返回值直接给到前端@ExceptionHandler(ServiceException.class)//统一处理抛出的异常public JsonResult<Void> handleException(Throwable e){JsonResult<Void> result = new JsonResult<>(e);if(e instanceof UsernameDuplicatedException){result.setState(4000);result.setMessage("用户名已经被占用");} else if(e instanceof UserNotFoundException){result.setState(5001);result.setMessage("用户数据不存在异常");}else if(e instanceof PasswordNotMatchException){result.setState(5002);result.setMessage("用户名的密码错误异常");}else if(e instanceof InsertException){result.setState(5000);result.setMessage("注册时产生未知的异常");}return result;}
}
2 设计请求
- 请求路径:/users/login
- 请求参数:String username,String password
- 请求类型:POST
- 响应结果:JsonResult<User>
3 处理请求
在UserController类中编写处理请求的方法.编写完成后启动主服务验证一下
@RequestMapping("login")public JsonResult<User> login(String username,String password) {User data = userService.login(username, password);return new JsonResult<User>(OK,data);}
4 注意点
注意,控制层方法的参数是用来接收前端数据的,接收数据方式有两种:
/*** 1.接收数据方式:请求处理方法的参数列表设置为pojo类型来接收前端的数据* SpringBoot会将前端url地址中的参数名和pojo类的属性名进行比较,如果这两个名称相同,* 则将值注入到pojo类中对于的属性上* @param user* @return*/@PostMapping("/reg")//@RequestBody//表示此方法的响应结果以json格式进行数据的1响应给前端public JsonResult<Void> reg(User user) {userService.reg(user);return new JsonResult<>(OK);}/*** 2.接收数据方式:请求处理的参数列表设置为非pojo类型* SpringBoot会直接将请求的参数名和方法的参数名直接进行比较,如果名称* 相同则自动完成值的依赖注入* @param username* @param password* @return*/@PostMapping("/login")public JsonResult<User> login(String username,String password,HttpSession session){User user = userService.login(username, password);//向session对象中完成数据的绑定(session全局的)session.setAttribute("uid", user.getUid());session.setAttribute("username",user.getUsername());//获取session中绑定的数据System.out.println(getuidFromSession(session));System.out.println(getUsernameFromSession(session));return new JsonResult<>(OK,user);}
5. 前端页面
<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.htmllocation.href = "index.html";} else {alert("登录失败")}},error: function (xhr) {//xhr.message可以获取未知异常的信息alert("登录时产生未知的异常!"+xhr.message);}});});</script>
6.用session存储和获取用户数据
6.1 封装位置
- 在用户登录成功后要保存下来用户的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);}
7.拦截器
拦截器的作用是将所有的请求统一拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统设置的过滤规则,该项目统一的处理是重新去打开login.html页面(重定向和转发都可以,推荐使用重定向)
拦截器在springboot中本质是依靠springMVC完成的.springMVC提供了一个HandlerInterceptor接口用于表示定义一个拦截器
1.所以想要使用拦截器就要定义一个类并使其实现HandlerInterceptor接口,在store下建包interceptor,包下建类LoginInterceptor并编写代码:
1.创建拦截器
/**定义一个拦截器*/
public class LoginInterceptor implements HandlerInterceptor {/***检测全局session对象中是否有uid数据,如果有则放行,如果没有重定向到登录页面* @param request 请求对象* @param response 响应对象* @param handler 处理器(把url和Controller映射到一块)* @return 返回值为true放行当前请求,反之拦截当前请求* @throws Exception*/@Override//在DispatcherServlet调用所有处理请求的方法前被自动调用执行的方法//springboot会自动把请求对象给到request,响应对象给到response,适配器给到handlerpublic 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);}
}