环境搭建
- 准备数据库表(dept,emp)
- 创建SpringBoot工程
- 引入web开发起步依赖,mybatis依赖(mybatis依赖和mysql驱动),lombok依赖
- 在application.properties中引入mybatis的配置信息
- 准备对应的实体类
- 准备对应的Mapper、Service(接口、实现类)、Controller基础结构
部门管理开发:根据接口文档阅读开发Restful风格
- 明确三层架构的功能
- Controller
- 1.接收请求
- 2.调用service层功能
- 3.响应
- Service
- 调用mapper接口功能
- Mapper
- 执行对应的SQL语句与数据库进行互动
简化开发:二者效果相同
@RequestNapping(value = "/depts", method = RequestNethod.GET)@GetMapping("/ depts")注意在接口中注入Bean对象,例如Controller中注入Service对象,Service中注入Mapper对象,达到解耦的效果
==接口测试,postman / 前后端联调==
==日志:@Slf4j==
==注意某些路径的 @PathVariable==
==注意解析json格式的前端请求参数可以使用@RequestBody来将json封装到实体类中==
==@RequestMapping(“/depts”) 公共路径抽取:完整的请求路径,应该是类上的@RequestMapping 的value属性+方法上的@RequestMapping的value属性。==
员工管理开发:
- 难点:分页查询
分页查询
需要两条SQL语句
- 显示当前页面
select * from emp limit ?, ?;- 显示所有数据
select count(*) from emp;前端传来的数据
- 当前页码
- 每页展示记录数
后端响应的数据
- 数据列表
- 总记录数
select count (*) from emp;
==目前需要返回两项数据,方法只能返回一个数据,使用实体类封装返回==
==@ResultParam(defaultvalue = ”默认值“) 参数 可以设置参数的默认值==
分页查询插件PageHelper
使用 PageHelper 时,先确保项目已引入匹配的依赖并注册分页拦截器,再在要分页的 MyBatis 查询前紧邻调用 PageHelper.startPage(page, pageSize),随后立即执行目标查询,避免夹入其他查询。分页插件负责改写分页 SQL,并可按配置执行总数查询;返回时将本页数据与总记录数封装到同一个响应对象。检查页码与每页条数的范围、排序是否稳定,以及筛选条件是否同时作用于数据和总数,不要分页之后再对结果重复做内存切片。
文件上传
文件上传请求使用 multipart/form-data,让普通字段与文件作为不同部分传给后端。接口联调时,对本文带普通参数的示例填写 username、age,将 image 字段设为文件并选择本地文件;字段名应与接收参数一致。服务端用 MultipartFile 读取文件,校验大小与实际类型后生成安全的唯一名称,再保存到本地目录或交给 OSS 工具。浏览器提交 FormData 时应让浏览器生成含 boundary 的请求头,不要手写一个缺少 boundary 的 Content-Type。
方法1:将文件保存到本地
/*@PostMapping("/upload")public Result upload(String username, Integer age, MultipartFile image) throws Exception { log.info("文件上传:{},{},{}", username, age, image);
//获取原始文件名 String originalFilename = image.getOriginalFilename();
//构造唯一的文件名(不能重复)-- uuid(通用唯一识别码) int index = originalFilename.lastIndexOf('.'); String extname = originalFilename.substring(index); String newFileName = UUID.randomUUID().toString() + extname; log.info("新的文件名:{}", newFileName);
//将接收到的文件存储在服务器的磁盘目录中 D:\image image.transferTo(new File("D:\\image\\" + newFileName));
return Result.success();}*/方法2:将文件保存到AliOSS
@PostMapping("/upload")public Result upload(MultipartFile image) throws Exception { log.info("文件上传,文件名:{}", image.getOriginalFilename());
//调用阿里云OSS工具类 String url = aliOSSUtils.upload(image); log.info("文件上传完成,文件访问的url为:{}", url);
return Result.success(url);}==修改员工需要查询回显和update语句==
==配置注入使用@Value(“#{配置中的key}”)注解来防止硬编码==
登陆校验(难点)
==若按照普通的开发区编写映射页面,通过数据库判别是否用户名与密码正确式不可取的==
如此的话,直接访问类似于 /emps /depts 等页面可以直接进入控制台面板而跳过了判断是否为管理员的步骤
因此,我们需要在 /login /depts /emps 之间建立一种联系
实现思路:
- 员工登陆成功存储一个标记
- 在每一个方法接口前判断员工是否登陆,登陆则访问,未登录则返回给前端错误信息
- 因此,需要两种技术==1.统一拦截== ==2.登陆标记缓存==
- 统一拦截
- Servlet提供的过滤器Filter
- Spring提供的拦截器Interceptor
- 用户登陆
- 会话技术
- 统一拦截
会话技术
- 会话:指的是用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
http协议是无状态协议,不会携带上一次会话的信息,因此无法数据共享
- 会话跟踪方案
- 客户端会话跟踪技术:Cookie(存储在浏览器)
- 服务端会话跟踪技术:Session(存储在服务器端)
- 令牌技术
- Cookie:浏览器第一次请求时在服务器端设置Cookie,响应时携带Cookie,而后Cookie缓存在浏览器本地,之后的每次请求都会携带Cookie与客户端进行对比,服务器自动响应Cookie,浏览器自动保存Cookie,发起请求自动携带Cookie,三个自动表名Http支持Cookie技术
请求头Cookie
响应头Set-Cookie
优点:HTTP协议支持
缺点:1.移动端无法使用 2.不安全,用户可以自己禁用 3.Cookie不能跨域(跨域:协议、IP/域名、端口不同)
Session:浏览器第一次请求时在服务器端设置Session(ID),响应时通过Cookie返回Session的ID,之后的每次请求都会携带Cookie中的Session的ID到服务器端对比
优点:存储在服务器端,安全
缺点:1.服务器集群在负载均衡后有可能无法使用,因此无法在服务器集群环境下使用 2.Cookie的缺点
令牌技术(==建议使用==):用户身份表示,登陆成功生成令牌,响应给浏览器,可以存储在Cookie或者其他存储空间中,之后的每次请求都会携带令牌,比对是否有效,而且想要共享数据只需要将数据保存在令牌中即可
优点:1.支持PC、移动端 2、解决集群环境下的认证问题 3、减轻了服务器端的存储压力
缺点:需要自己去实现
JWT(JSON Web Token)令牌
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
jwt令牌组成
- 第一部分
(头),记录令牌类型、签名算法等。例如: {“alg”:”HS256” ,”type”:”JWT”} - 第二部分
(有效载荷),携带一些自定义信息、默认信息等。例如: {“id”:” 1” ,”username”:”Tom”} - 第三部分
(签名),防止Token被篡改、确保安全性。将header、 payload,并加入指定秘钥,通过指定签名算法计算而来。
pom.xml引入
<!--JTW令牌--><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version></dependency>jwt生成
public class JwtUtils {
private static String signKey = "wangnine"; private static Long expire = 43200000L;
/** * 生成JWT令牌 * @param claims JWT第二部分负载 payload 中存储的内容 * @return */ public static String generateJwt(Map<String, Object> claims){ String jwt = Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256, signKey) .setExpiration(new Date(System.currentTimeMillis() + expire)) .compact(); return jwt; }
/** * 解析JWT令牌 * @param jwt JWT令牌 * @return JWT第二部分负载 payload 中存储的内容 */ public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey) .parseClaimsJws(jwt) .getBody(); return claims; }}统一拦截技术
Filter过滤器
JavaWeb三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
相当于抽取了各个页面的登陆校验,优点aop的思想
==如何实现过滤器==
- 1.定义Filter:定义一个类,实现 Filter接口,并重写其所有方法。
- 配置Filter: Filter类上加@WebFilter注解,配置拦截资源的路径。引导类上加@ServletComponentScan开启Servlet组件支持。
结合了jwt后的代码如下:
@Slf4j//@WebFilter(urlPatterns = "/*") //拦截所有public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; HttpServletResponse resp = (HttpServletResponse) servletResponse; //1.获取请求url String url = req.getRequestURL().toString(); log.info("请求的url:{}", url);
//2.判断请求的url中是否包含login,如果包含,说明是登录操作,直接放行 if(url.contains("login")) { log.info("登录操作,放行..."); filterChain.doFilter(servletRequest, servletResponse); return; }
//3.获取请求头中的token令牌 String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,则返回错误结果(未登录) if(!StringUtils.hasLength(jwt)) { log.info("请求头token为空,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); //手动将对象转换为json格式 String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return; }
//5.解析token,如果解析失败,则返回错误结果(未登录) try { JwtUtils.parseJWT(jwt); } catch (Exception e) { log.info("解析令牌失败,返回未登录的误信息"); Result error = Result.error("NOT_LOGIN"); //手动将对象转换为json格式 String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return; }
//6.放行 log.info("令牌合法,放行"); filterChain.doFilter(servletRequest, servletResponse); }}
//对于init 和 destroy 有默认实现,二者只调用一次Interceptor拦截器
@Slf4j@Componentpublic class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行,返回true放行,返回false不放行 public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url String url = req.getRequestURL().toString(); log.info("请求的url:{}", url);
//2.判断请求的url中是否包含login,如果包含,说明是登录操作,直接放行 if(url.contains("login")) { log.info("登录操作,放行..."); return true; }
//3.获取请求头中的token令牌 String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,则返回错误结果(未登录) if(!StringUtils.hasLength(jwt)) { log.info("请求头token为空,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); //手动将对象转换为json格式 String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return false; }
//5.解析token,如果解析失败,则返回错误结果(未登录) try { JwtUtils.parseJWT(jwt); } catch (Exception e) { log.info("解析令牌失败,返回未登录的误信息"); Result error = Result.error("NOT_LOGIN"); //手动将对象转换为json格式 String notLogin = JSONObject.toJSONString(error); resp.getWriter().write(notLogin); return false; }
//6.放行 log.info("令牌合法,放行"); return true; }
@Override //目标资源方法运行后运行 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle..."); }
@Override //视图渲染完毕后运行,最后运行 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion..."); }}异常处理
在Controller层定义一个全局异常处理器
import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;
/** * 全局异常处理器 */
@RestControllerAdvicepublic class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) //捕获所有的异常 public Result ex(Exception ex) { ex.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员"); }}@RestControllerAdvice
@ExceptionHandler(Exception.class) //捕获所有的异常