后端路由
配置处理:
登录的路由配置
作用:把oAuth2.0颁发的token存储到redis中
package com.powernode.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.powernode.constant.GatewayConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* 登录的路由配置
* 作用:把oAuth2.0颁发的token存储到redis中
*/
@Configuration
public class LoginRouteConfig {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 代码方式的路由存储token
*
* @param builder
* @return
*/
@Bean
public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("auth-server-route", r -> r.path("/oauth/token").filters(
f -> f.modifyResponseBody(String.class, String.class, (exchange, s) -> {
//s为响应的结果,类型为json,结构为{"access_token","expires_in"}
//将响应的json数据转换为json对象
JSONObject jsonObject = JSON.parseObject(s);
//查看是否包含access_token
if (jsonObject.containsKey("access_token")) {
//有:存放到redis中
//获取token值和过期时间
String access_token = jsonObject.getString("access_token");
Long expires_in = jsonObject.getLong("expires_in");
//将获取的值存放到redis中
stringRedisTemplate.opsForValue().set(GatewayConstant.TOKEN_PREFIX+access_token,"", Duration.ofSeconds(expires_in));
}
return Mono.just(s);
//uri是路由的目的地,(lb://auth-server是授权中心服务名称)
})).uri("lb://auth-server"))
.build();
}
}
.
前端返回响应的JSON
数据,与Redis一致:
解析部分JSON数据:
.
.
会生成domain实体类,service接口及实现类,mapper接口及实现xml文件:
.
创建一个controller
类:controller.SysMenuController
package com.powernode.controller;
import com.powernode.domain.SysMenu;
import com.powernode.service.SysMenuService;
import com.powernode.utils.AuthUtil;
import com.powernode.vo.MenuAndAuth;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Api(tags = "菜单权限接口管理")
@RequestMapping("sys/menu")
@RestController
public class SysMenuController {
@Autowired
private SysMenuService sysMenuService;
@ApiOperation("根据用户标识查询菜单和权限集合")
@GetMapping("nav")
public ResponseEntity<MenuAndAuth> loadUserMenuAndAuth() {
//获取用户标识
// String userId = SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();
String userId = AuthUtil.getLoginUserId();
//根据用户标识查询菜单和权限集合
//获取权限集合
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
List<String> auths = authorities.stream().map(Objects::toString).collect(Collectors.toList());
//根据用户id查询菜单集合
List<SysMenu> sysMenuList = sysMenuService.selectSysMenuListByUid(userId);
//成功,并没有数据返回
// return ResponseEntity.ok().build();
//成功,有数据返回
// return ResponseEntity.ok(数据);
MenuAndAuth menuAndAuth = new MenuAndAuth(sysMenuList,auths);
return ResponseEntity.ok(menuAndAuth);
}
// sys/menu/table
@ApiOperation("查询系统权限集合")
@GetMapping("table")
@PreAuthorize("hasAuthority('sys:menu:list')")
public ResponseEntity<List<SysMenu>> loadSysMenuList() {
List<SysMenu> list = sysMenuService.list();
return ResponseEntity.ok(list);
}
}
其中:
1、创建一个vo.MenuAndAuth类:
(用于返回菜单和权限的集合对象)
package com.powernode.vo;
import com.powernode.domain.SysMenu;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel("菜单和权限对象")
public class MenuAndAuth {
@ApiModelProperty("菜单集合")
private List<SysMenu> menuList;
@ApiModelProperty("权限集合")
private List<String> authorities;
}
2、前端代码,响应的数据属性名,也要和后端封装返回的菜单和权限属性名相同:
3、封装获取用户标识工具类:
package com.powernode.utils;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuthUtil {
public static String getLoginUserId() {
return SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();
}
}
4、业务层实现方法:
业务接口:
package com.powernode.service;
import com.powernode.domain.SysMenu;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
public interface SysMenuService extends IService<SysMenu>{
/**
* 根据用户id查询菜单集合
* @param userId
* @return
*/
List<SysMenu> selectSysMenuListByUid(String userId);
}
业务层实现类:
a.查询菜单和权限(权限一时半会变不了,使用redis缓存)
b.展开菜单,利用“树的深度”,采用递归方法
package com.powernode.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.powernode.constant.ManagerConstant;
import com.powernode.domain.SysMenu;
import com.powernode.mapper.SysMenuMapper;
import com.powernode.service.SysMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService{
@Autowired
private SysMenuMapper sysMenuMapper;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public List<SysMenu> selectSysMenuListByUid(String userId) {
//先从redis中查询用户的菜单集合
String jsonStr = stringRedisTemplate.opsForValue().get(ManagerConstant.PREFIX_MENU + userId); // "menu:" + userId
//判断是否有值
List<SysMenu> sysMenuList = null;
if (StringUtils.hasText(jsonStr)) {
//有:直接使用,将json格式的字符串转换为list集合
sysMenuList = JSONObject.parseArray(jsonStr, SysMenu.class);
} else {
//没有:去数据库查询,并存放到redis缓存中
//根据用户id查询菜单集合
sysMenuList = sysMenuMapper.selectSysMenuListByUid(userId);
//存放到redis缓存中(7天)
stringRedisTemplate.opsForValue().set(ManagerConstant.PREFIX_MENU + userId, JSON.toJSONString(sysMenuList), Duration.ofDays(7));
}
//将集合转换为树结构
return transformTree(sysMenuList,0L);
}
/**
* 集合转换为树结构,一般分为2种情况:
*
* 1.已知菜单深度,深度<=2
* 2.未知菜单深度:使用递归
*
* @param sysMenuList
* @param pid
* @return
*/
private List<SysMenu> transformTree(List<SysMenu> sysMenuList, Long pid) {
/*//获取菜单的根节点
List root = sysMenuList.stream()
.filter(sysMenu -> sysMenu.getParentId().equals(pid))
.collect(Collectors.toList());
//循环遍历根节点(获取每一个根节点的子节点集合)
root.forEach(r -> {
//从节点集合中来过滤出节点的父节点id与当前根节点的节点id值一致的节点集合
List child = sysMenuList.stream()
.filter(sysMenu -> sysMenu.getParentId().equals(r.getMenuId()))
.collect(Collectors.toList());
r.setList(child);
});*/
//第2种情况,菜单深度未知
//获取菜单的根节点
List<SysMenu> root = sysMenuList.stream()
.filter(sysMenu -> sysMenu.getParentId().equals(pid))
.collect(Collectors.toList());
root.forEach(r -> r.setList(transformTree(sysMenuList,r.getMenuId())));
return root;
}
}
涉及到的,1-实体类(菜单管理类):
package com.powernode.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 菜单管理
*/
@ApiModel(value="com-powernode-domain-SysMenu")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_menu")
public class SysMenu implements Serializable {
@TableId(value = "menu_id", type = IdType.AUTO)
@ApiModelProperty(value="")
private Long menuId;
/**
* 父菜单ID,一级菜单为0
*/
@TableField(value = "parent_id")
@ApiModelProperty(value="父菜单ID,一级菜单为0")
private Long parentId;
/**
* 菜单名称
*/
@TableField(value = "name")
@ApiModelProperty(value="菜单名称")
private String name;
/**
* 菜单URL
*/
@TableField(value = "url")
@ApiModelProperty(value="菜单URL")
private String url;
/**
* 授权(多个用逗号分隔,如:user:list,user:create)
*/
@TableField(value = "perms")
@ApiModelProperty(value="授权(多个用逗号分隔,如:user:list,user:create)")
private String perms;
/**
* 类型 0:目录 1:菜单 2:按钮
*/
@TableField(value = "type")
@ApiModelProperty(value="类型 0:目录 1:菜单 2:按钮")
private Integer type;
/**
* 菜单图标
*/
@TableField(value = "icon")
@ApiModelProperty(value="菜单图标")
private String icon;
/**
* 排序
*/
@TableField(value = "order_num")
@ApiModelProperty(value="排序")
private Integer orderNum;
//当前属性并没有对应表中的一个字段名
@TableField(exist = false)
@ApiModelProperty("子菜单集合")
private List<SysMenu> list;
private static final long serialVersionUID = 1L;
}
涉及到的,2-常量(redis缓存需要的常量):
package com.powernode.constant;
/**
* 读取redis缓存需要的常量
*/
public interface ManagerConstant {
String PREFIX_MENU = "menu:";
String ROLE_LIST = "'role:list'";
}
涉及到的,3-mapper(根据用户id查询菜单集合的SQL语句):
package com.powernode.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.powernode.domain.SysMenu;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface SysMenuMapper extends BaseMapper<SysMenu> {
@Select("select DISTINCT t1.* from sys_menu t1 join sys_role_menu t2 join sys_user_role t3\n" +
"on (t1.menu_id = t2.menu_id and t2.role_id = t3.role_id)\n" +
"where t3.user_id = #{userId} and (t1.type = 0 or t1.type = 1)")
List<SysMenu> selectSysMenuListByUid(String userId);
}
创建一个controller
类,关于用户的SysUserController:
创建一个controller
类,关于退出的的LogoutController,删除缓存中的token
:
package com.powernode.controller;
import com.powernode.constant.GatewayConstant;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Api(tags = "用户退出")
@RestController
public class LogoutController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ApiOperation("管理员退出")
@PostMapping("sys/logout")
public ResponseEntity<Void> logout(HttpServletRequest request) {
//获取令牌:bearer jwt
String authorization = request.getHeader(GatewayConstant.AUTHORIZATION);
//获取jwt
String jwt = authorization.replaceAll(GatewayConstant.BEARER, "");
//将缓存中的token删除
stringRedisTemplate.delete(GatewayConstant.TOKEN_PREFIX+jwt);
return ResponseEntity.ok().build();
}
}
在controller
类,关于用户的SysUserController,添加分页查询方法
(利用mybatis的page
对象,以及条件构造器LambdaQueryWrapper
的like()
和orderByDesc()
方法对分页进行模糊查询和排序):
其中: @PreAuthorize
获取权限控制(根据数据库的表中的字段上的值)
创建一个controller
类,关于用户的SysRoleController:
.
业务层实现list方法:
(开启缓存@Cacheable
(key = ManagerConstant.ROLE_LIST))
前端发出的 ajax
请求:
需要在userdomain实体类添加roleList这个属性:
.
在controller
类,关于用户的SysUserController,添加新增管理员的方法
(重写业务层的save方法):
业务层重写save
方法:
其中在Application启动类注入加密器BCryptPasswordEncoder
:
package com.powernode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootApplication
@EnableEurekaClient
@EnableCaching//开启注解式缓存(默认使用的缓存中间件是redis)
public class ManagerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ManagerServiceApplication.class,args);
}
// 注入加密器
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
.
在controller
类,关于用户的SysUserController,添加查询管理员详情的方法
(重写业务层的getById方法):
业务层重写getById
方法:
1.利用条件构造器LambdaQueryWrapper的eq
()方法查询
2.利用stream流
获取查询对象的集合
前端发出的 ajax
请求:
.
在controller
类,关于用户的SysUserController,添加修改管理员信息的方法:
业务层重写updateById
方法:
1.利用条件构造器LambdaQueryWrapper的eq
()方法查询
2.密码处理。加密器passwordEncoder.encode()
3.添加事务 @Transactional(rollbackFor = RuntimeException.class)
其中,要更改mybatis更新策略:
前端发出的 ajax
请求:
.
在controller
类,关于用户的SysUserController,添加修改管理员信息的方法:
业务层重写updateById
方法:
1.利用条件构造器LambdaQueryWrapper的in
()方法查询
2.添加事务 @Transactional(rollbackFor = RuntimeException.class)