博客后台
AOP实现日志记录
需求
通过日志记录接口调用信息,便于后期排查
格式如下 :
实现
1.先定义注解类
/** * 自定义注解 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SystemLog { String businessName(); }
2.定义切面类
/** * 切面类 */ @Component @Aspect @Slf4j public class LogAspect { }
3.定义切点,及其通知方法
@Component @Aspect @Slf4j public class LogAspect { //确定切点 @Pointcut("@annotation(com.blog.annotation.SystemLog)") public void pt(){ } //通知方法(使用环绕通知) @Around("pt()") public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable { joinPoint.getArgs(); Object res; //得到目标方法调用的返回值 try { handleBefore(joinPoint); //目标方法的调用 res = joinPoint.proceed(); //打印响应信息 handleAfter(res); }//无论有没有异常都需要打印异常信息 finally { //换行 log.info("============End============" + System.lineSeparator()); } return res; } private void handleBefore(ProceedingJoinPoint joinPoint) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); //获取被增强方法上的注解对象 SystemLog systemLog = getSystemLog(joinPoint); log.info("============Start============"); // 打印请求 URL log.info("URL : {}",request.getRequestURL()); // 打印描述信息 log.info("BusinessName : {}",systemLog.businessName()); // 打印 Http method log.info("HTTP Method : {}",request.getMethod()); // 打印调用 controller 的全路径以及执行方法 log.info("Class Method : {}.{}",joinPoint.getSignature().getDeclaringTypeName(),((MethodSignature) joinPoint.getSignature()).getName()); // 打印请求的 IP log.info("IP : {}",request.getRemoteHost()); // 打印请求入参 log.info("Request Args : {}", JSON.toJSONString(joinPoint.getArgs()) ); } private void handleAfter(Object res) { // 打印出参 log.info("Response : {}", JSON.toJSONString(res)); } //todo 获取被增强方法上的注解对象 private SystemLog getSystemLog(ProceedingJoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); SystemLog systemLog = methodSignature.getMethod().getAnnotation(SystemLog.class); return systemLog; } }
4.在需要增强的方法上添加自定义注解
@SystemLog(businessName="更新用户信息") @GetMapping("/userInfo") public ResponseResult userInfo(){ return userService.userInfo(); }
Swagger2
依赖
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency>
基本使用
@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论") @GetMapping("/linkCommentList") public ResponseResult listCommentList(Integer pageNum , Integer pageSize){ return commentService.commentList(SystemConstants.COMMENT_TYPE_FRIEND,null,pageNum,pageSize); }
使用@ApiOperation(value = "友链评论列表",notes = "获取一页友链评论")来进行标注
配置形参
@ApiImplicitParams({ @ApiImplicitParam(name = "pageNum", value = "页号"), @ApiImplicitParam(name = "pageSize", value = "每页大小") })
**实体类接口 : **
一般一个实体类不止在一个接口中被用到,所以如果直接在实体类中添加的话就是使代码耦合,所以我们需要进行拆解
@ApiModel(description = "文章实体类") public class Article{ }
所以上面的写法是不正规的
所以我们需要使用DTO对象
【dto对象 :数据传输对象】
按照开发规范,所有的controller层需要的实体类参数,我们都需要将其转换为dto对象
//todo 添加评论 @PostMapping public ResponseResult addComment(@RequestBody AddCommentDto addCommentDto){ Comment comment = BeanCopyUtils.copyBean(addCommentDto, Comment.class); return commentService.addComment(comment); }
@ApiModel(description = "添加评论实体类") public class AddCommentDto { private Long id; //评论类型(0代表文章评论,1代表友链评论) private String type; //文章id @ApiModelProperty(notes = "文章id") private Long articleId; //根评论id //...... }
所有的dto都是需要添加的
获取所有标签
接口
实现
/** * 标签请求 */ @RestController @RequestMapping("/content/tag") public class TagController { @Resource private TagService tagService; @GetMapping("/list") public ResponseResult list(){ return ResponseResult.okResult(tagService.list()); } }
后台登录、登出
接口
登录
①自定义登录接口
调用ProviderManager的方法进行认证 如果认证成功生成jwt
把信息存入redis中
②自定义UserDetailsServic e
在这个实现类中进行查询数据库操作
注意配置密码加密BCryptPasswordCoder
校验
①自定义jwt认证过滤器
获取token
解析token获取其中的userId
从redis中获取用户信息
存入securityContextHolder
实现
@RestController @RequestMapping("/user") public class loginController { @Resource private AdminLoginService loginService; @PostMapping("/login") public ResponseResult login(@RequestBody User user){ if (!StringUtils.hasText(user.getUserName())){ //提示 要传用户名 throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME); } return loginService.login(user); } @PostMapping("/logout") public ResponseResult logout(){ return loginService.logout(); } }
/** * 后台登陆实现 */ @Service public class AdminLoginServiceImpl implements AdminLoginService { @Autowired private AuthenticationManager authenticationManager; @Resource private RedisCache redisCache; //todo 登录业务 @Override public ResponseResult login(User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //判断是否认证通过 //获取userId ,生成token //判断是否认证通过 if(Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } //获取userid 生成token LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userId); //把用户信息存入redis redisCache.setCacheObject(SystemConstants.LOGIN_KEY + userId,loginUser); //封装响应 : 把token 和userInfoVo(由user转换而成) 封装 ,然后返回 Map<String,String> map = new HashMap<>(); map.put("token",jwt); return ResponseResult.okResult(map); } @Override public ResponseResult logout() { //获取 token 解析获取 userId Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userId = loginUser.getUser().getId(); redisCache.deleteObject(SystemConstants.LOGIN_KEY + userId); return ResponseResult.okResult(); } }
后台权限控制及其动态路由
表分析
权限表
对应的页面
权限表
角色权限表
获取当前用户的权限和角色信息
接口(getInfo)
实现getInfo
最终实现结果
{ "code": 200, "data": { "permissions": [ "system:menu:list", "system:menu:query", "system:menu:add", "system:menu:edit", "system:menu:remove", "content:article:writer" ], "roles": [ "common", "link" ], "user": { "avatar": "http://rrpanx30j.hd-bkt.clouddn.com/images/91529822720e0cf3efed815e0446f21fbe09aa79.png", "email": "23412532@qq.com", "id": 4, "nickName": "红红火火恍恍惚惚", "sex": "1" } }, "msg": "操作成功" } controller
@RestController public class UserController { @Resource private RoleService roleService; @Resource private MenuService menuService; @GetMapping("/getInfo") public ResponseResult<AdminUserInfoVo> getInfo(){ //1. 查询当前登陆的用户 LoginUser loginUser = SecurityUtils.getLoginUser(); //2. 根据用户id查询权限 List<String> perms = menuService.selectPermsByUserId(loginUser.getUser().getId()); // 根据id查询角色信息 List<String> roleKeyList = roleService.selectRoleKeyByUserId(loginUser.getUser().getId()); //3. 封装 返回 User user = loginUser.getUser(); UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class); AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(perms,roleKeyList,userInfoVo); return ResponseResult.okResult(adminUserInfoVo); } }
service层两个实现类 /** * 查询角色权限信息 */ @Service public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService { /** * 根据用户id查询权限信息<br> * 如果用户id为1 代表管理员 ,menus中需要有所有菜单类型为c或者F的,状态为,未被删除的权限 * @param id 用户id * @return 返回该用户权限集合 */ @Override public List<String> selectPermsByUserId(Long id) { //管理员返回所有的权限 if(id == 1){ LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>(); wrapper.in(Menu::getMenuType, SystemConstants.MENU_TYPE_C,SystemConstants.MENU_TYPE_F); //菜单类型为C 和 F wrapper.eq(Menu::getStatus,SystemConstants.LINK_STATUS_NORMAL);//状态正常 List<Menu> menus = list(wrapper); List<String> Perms = menus.stream().map(Menu::getPerms).collect(Collectors.toList()); return Perms; } // 反之返回相对应用户所具有的权限 //1. 先查询sys_user_roles查询用户角色id //2. 查到角色id之后再到sys_roles_menu查询对应的权限id(menuId) //3. 最后通过menuId查询对应的menu信息 //4. 封装返回 return getBaseMapper().selectPermsByUserId(id); } }
/** * 查询角色信息 */ @Service public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService { @Override public List<String> selectRoleKeyByUserId(Long id) { //判断是否为管理员角色 if(id == 1){ List<String> roleKeys = new ArrayList<>(); roleKeys.add("admin"); //管理员角色 return roleKeys; } //如果不是返回对应id的角色信息(连表查询) return getBaseMapper().selectRoleKeyByUserId(id); } }
对应多表联查的xml文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.blog.mapper.RoleMapper"> <!-- selectRoleKeyByUserId--> <select id="selectRoleKeyByUserId" resultType="java.lang.String"> SELECT r.`role_key` FROM `sys_user_role` ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` WHERE ur.`user_id` = #{userId} AND r.`status` = 0 AND r.`del_flag` = 0 </select> <select id="selectRoleIdByUserId" resultType="java.lang.Long"> select r.id from sys_role r left join sys_user_role ur on ur.role_id = r.id where ur.user_id = #{userId} </select> </mapper>
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.blog.mapper.MenuMapper"> <select id="selectPermsByUserId" resultType="java.lang.String"> SELECT DISTINCT m.perms FROM `sys_user_role` ur LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE ur.`user_id` = #{userId} AND m.`menu_type` IN ('C','F') AND m.`status` = 0 AND m.`del_flag` = 0 </select> <select id="selectAllRouterMenu" resultType="com.blog.domain.entity.Menu"> SELECT DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time FROM `sys_menu` m WHERE m.`menu_type` IN ('C','M') AND m.`status` = 0 AND m.`del_flag` = 0 ORDER BY m.parent_id,m.order_num </select> <select id="selectRouterMenuTreeByUserId" resultType="com.blog.domain.entity.Menu"> SELECT DISTINCT m.id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, IFNULL(m.perms,'') AS perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time FROM `sys_user_role` ur LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE ur.`user_id` = #{userId} AND m.`menu_type` IN ('C','M') AND m.`status` = 0 AND m.`del_flag` = 0 ORDER BY m.parent_id,m.order_num </select> <select id="selectMenuListByRoleId" resultType="java.lang.Long"> select m.id from sys_menu m left join sys_role_menu rm on m.id = rm.menu_id where rm.role_id = #{roleId} order by m.parent_id, m.order_num </select> </mapper>
动态路由接口(getRouters)
响应格式
前端为了实现动态路由的效果,需要后端有接口能够返回所有的菜单数据
注意 :返回的菜单需要体现父子菜单的层级关系
如果用户id为1 代表管理员 ,menus中需要有所有菜单类型为c或者M的,状态为,未被删除的权限
{ "code": 200, "data": { "menus": [ { "children": [], "component": "content/article/write/index", "createTime": "2022-01-08 03:39:58", "icon": "build", "id": 2023, "isFrame": 1, "menuName": "写博文", "menuType": "C", "orderNum": 0, "parentId": 0, "path": "write", "perms": "content:article:writer", "status": "0", "visible": "0" }, { "children": [ { "children": [], "component": "system/menu/index", "createTime": "2021-11-12 10:46:19", "icon": "tree-table", "id": 102, "isFrame": 1, "menuName": "菜单管理", "menuType": "C", "orderNum": 3, "parentId": 1, "path": "menu", "perms": "system:menu:list", "status": "0", "visible": "0" } ], "createTime": "2021-11-12 10:46:19", "icon": "system", "id": 1, "isFrame": 1, "menuName": "系统管理", "menuType": "M", "orderNum": 1, "parentId": 0, "path": "system", "perms": "", "status": "0", "visible": "0" } ] }, "msg": "操作成功" }
实现getRouters
controller
@GetMapping("/getRouters") public ResponseResult<RoutersVo> getRouters(){ Long userId = SecurityUtils.getUserId(); //查询menu 结果是tree形状 List<Menu> menus = menuService.selectRouterMenuTreeByUserId(userId); RoutersVo routersVo = new RoutersVo(menus); //封装返回 return ResponseResult.okResult(routersVo); }
service层实现
@Service public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService { /** * 根据用户id查询相关的权限菜单信息 * @param userId 用户id * @return 返回符合要求的val */ @Override public List<Menu> selectRouterMenuTreeByUserId(Long userId) { MenuMapper menuMapper = getBaseMapper(); List<Menu> menus = null; //封装Menu //如果是管理员,返回所有的菜单 if(SecurityUtils.isAdmin()){ menus = menuMapper.selectAllRouterMenu(); } else{ //如果不是管理员 那么返回对应有权限的菜单按钮 menus = menuMapper.selectRouterMenuTreeByUserId(userId); } //通过上述得到的Menu无法得到我们需要的父子菜单管理,所以我们需要通过(buildMenuTree)来构建这种父子菜单关系 //构建Tree //先找出第一层的菜单,接着找到他们的子菜单,然后就可以设置children属性中 List<Menu> menuTree = buildMenuTree(menus,0L); return menuTree; } /** * 构建菜单的父子菜单关系 * <br> * 找到父menu对应的子menu ,然后将他们放到一个集合中,最后设置给children这个字段 * @param menus 传入的方法 * @param parentId 父菜单id * @return */ private List<Menu> buildMenuTree(List<Menu> menus, Long parentId) { List<Menu> menuList = menus.stream()//通过这样筛选就可以得到第一层级的menu .filter(menu -> menu.getParentId().equals(parentId)) /* 传入的menus是得到了第一层的menus(相当于Tree中的root节点),然后需要设置他的子菜单(left 和 right) 因为menus中有所有的菜单(父子都有), 所以我们在设置left和right时需要找到他们的子菜单 所以就调用getChildren找到left或者right的子菜单,然后得到之后再设置给他们 */ .map(menu -> menu.setChildren(getChildren(menu, menus))) .collect(Collectors.toList()); return menuList; } /** * 获取传入参数的子menu的list集合 * 在menus中找打当前传入的menu的子菜单 * @param menu * @param menus */ private List<Menu> getChildren(Menu menu, List<Menu> menus){ List<Menu> children = menus.stream() .filter(menu1 -> menu1.getParentId().equals(menu.getId())) .map(menu1 -> menu1.setChildren(getChildren(menu1,menus))) //如果有很多的子菜单,那么就可以用到这个递归 .collect(Collectors.toList()); return children; } }