这是我参与8月更文挑战的第27天,活动详情查看:8月更文挑战
退出数据返回
jwt -username token - 随机码 - redis JwtLogoutSuccessHandler
// 加入Spring 的注解
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
// 注入 jwt 的工具类
@Autowired
JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest Request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 判断是否为空 不为空的话 退出
if (authentication != null) {
new SecurityContextLogoutHandler().logout(Request,response,authentication);
}
// 响应的格式
response.setContentType("application/json;charset=UTF-8");
// 输出流
ServletOutputStream outputStream = response.getOutputStream();
// 头信息
response.setHeader(jwtUtils.getHeader(), "");
// 返回的 结果
Results result = Results.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
无权限数据返回
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 设置头信息 的编码规范
httpServletResponse.setContentType("application/json;charset=UTF-8");
// 给一个权限不足的状态吗
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 设置输出流
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
// 设置错误的异常输出结果
Results fail = Results.fail(e.getMessage());
// 写入的 格式
outputStream.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
// 刷新喝关闭流
outputStream.flush();
outputStream.close();
}
}
SpringSecurity就已经完美整合到了我们的项目中来了。
解决跨域问题
上面的调试我们都是使用的postman,如果我们和前端进行对接的时候,会出现跨域的问题 CorsConfig
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
// .allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
菜单接口开发
开发菜单的接口,因为这3个表:用户表、角色表、菜单表,才有菜单表是不需要通过其他表来获取信息的。比如用户需要关联角色,角色需要关联菜单,而菜单不需要主动关联其他表。 获取菜单导航和权限的链接是/sys/menu/nav,然后我们的菜单导航的json数据应该是这样的:
{ title:
'角色管理',
icon: 'el-icon-rank',
path: '/sys/roles',
name: 'SysRoles',
component: 'sys/Role',
children: []}
然后返回的权限数据应该是个数组:
["sys:menu:list","sys:menu:save","sys:user:list"...]
注意导航菜单那里有个children,也就是子菜单,是个树形结构,因为我们的菜单可能这样:
系统管理 - 菜单管理 - 添加菜单
这就已经有3级了菜单了。注意这个关系的关联。我们的SysMenu实体类中有个parentId,但是没有children,因此我们可以在SysMenu中添加一个children,当然了其实不添加也可以,因为我们也需要一个dto,这样我们才能按照上面json数据格式返回。 添加一个children吧: SysMenu
@Data
@EqualsAndHashCode(callSuper = true)
public class SysMenu extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 父菜单ID,一级菜单为0
*/
@NotNull(message = "上级菜单不能为空")
private Long parentId;
@NotBlank(message = "菜单名称不能为空")
private String name;
/**
* 菜单URL
*/
private String path;
/**
* 授权(多个用逗号分隔,如:user:list,user:create)
*/
@NotBlank(message = "菜单授权码不能为空")
private String perms;
private String component;
/**
* 类型 0:目录 1:菜单 2:按钮
*/
@NotNull(message = "菜单类型不能为空")
private Integer type;
/**
* 菜单图标
*/
private String icon;
/**
* 排序
*/
@TableField("orderNum")
private Integer orderNum;
@TableField(exist = false)
private List<SysMenu> children = new ArrayList<>();
}
SysMenuDto吧,知道要返回什么样的数据,我们就只需要去填充数据就好了
package com.example.demo.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
public class SysMenuDto implements Serializable {
private Long id;
private String name;
private String title;
private String icon;
private String path;
private String component;
private List<SysMenuDto> children = new ArrayList<>();
}
SysMenuController
package com.example.demo.controller;
import cn.hutool.core.map.MapUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.Result.Results;
import com.example.demo.dto.SysMenuDto;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUser;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@RestController
@RequestMapping("/sys/menu")
public class SysMenuController extends BaseController {
// 获取 菜单的 链接
@GetMapping("/nav")
// 获取 nav 的导航
public Results nav(Principal principal) {
// 获取当前登录的用户
SysUser byUsername = sysUserService.getByUsername(principal.getName());
// 获取权限信息 是 用逗号隔开的
String userAuthorityInfo = sysUserService.getUserAuthorityInfo(byUsername.getId());
// 转换成数组
String[] strings = StringUtils.tokenizeToStringArray(userAuthorityInfo, ",");
// 获取导航的信息
List<SysMenuDto> navs = sysMenuService.getCurrentUserNav();
// 返回结果
return Results.succ(MapUtil.builder()
.put("authoritys", strings)
.put("nav", navs)
.build()
);
}
// 获取用户的信息
@GetMapping("/userInfo")
public Results userInfo(Principal principal) {
SysUser sysUser = sysUserService.getByUsername(principal.getName());
return Results.succ(MapUtil.builder()
.put("id", sysUser.getId())
.put("username", sysUser.getUsername())
.put("avatar", sysUser.getAvatar())
.put("created", sysUser.getCreated())
.map()
);
}
@GetMapping("/info/{id}")
@PreAuthorize("hasAuthority('sys:menu:list')")
public Results info (@PathVariable(name = "id") Long id) {
return Results.succ(sysMenuService.getById(id));
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('sys:menu:list')")
public Results list () {
List<SysMenu> menus = sysMenuService.tree ();
return Results.succ(menus);
}
//保存
@PostMapping("/save")
@PreAuthorize("hasAuthority('sys:menu:save')")
public Results save (@Validated @RequestBody SysMenu sysMenu) {
sysMenu.setCreated(LocalDateTime.now());
sysMenuService.save(sysMenu);
return Results.succ(sysMenu);
}
// 更新
@PostMapping("/update")
@PreAuthorize("hasAuthority('sys:menu:update')")
public Results update (@Validated @RequestBody SysMenu sysMenu) {
sysMenu.setUpdated(LocalDateTime.now());
sysMenuService.updateById(sysMenu);
// 因为是更新的操作,所以需要清楚缓存
sysUserService.clearUserAuthorityInfoByMenuId(sysMenu.getId());
return Results.succ(sysMenu);
}
// 删除
@PostMapping("/delete/{id}")
@PreAuthorize("hasAuthority('sys:menu:delete')")
public Results delete (@PathVariable ("id") Long id) {
// 判断 子节点还存在不,不存在才可以删除
int parent_id = sysMenuService.count(new QueryWrapper<SysMenu>().eq("parent_id", id));
if (parent_id > 0) {
return Results.fail("请先删除子菜单");
}
// 清楚所有的缓存
sysUserService.clearUserAuthorityInfoByMenuId(id);
sysMenuService.removeById(id);
//删除关联表的数据
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("menu_id",id));
return Results.succ("success");
}
}
方法中Principal principal表示注入当前用户的信息,getName就可以获取当当前用户的用户名了。sysUserService.getUserAuthorityInfo方法我们之前已经说过了,就在我们登录完成或者身份认证时候需要返回用户权限时候编写的。然后通过StringUtils.tokenizeToStringArray把字符串通过逗号分开组成数组形式。 重点在与sysMenuService.getcurrentUserNav,获取当前用户的菜单导航, SysMenuServiceImpl
package com.example.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dto.SysMenuDto;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysUser;
import com.example.demo.mapper.SysMenuMapper;
import com.example.demo.mapper.SysUserMapper;
import com.example.demo.service.ISysMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {
@Autowired
SysUserServiceImpl sysUserService;
@Autowired
SysUserMapper sysUserMapper;
@Override
public List<SysMenuDto> getCurrentUserNav() {
// 获取 登录的用户
String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 通过用户名获取
SysUser sysUser = sysUserService.getByUsername(username);
// 通过 用户Id 获取 菜单Id
List<Long> navMenuIds = sysUserMapper.getNavMenuIds(sysUser.getId());
// 通过菜单 id 获取 菜单
List<SysMenu> sysMenus = this.listByIds(navMenuIds);
// 转换成树桩结构
List<SysMenu> menustree = buildTreeMeun(sysMenus);
// 实体类转换
return convert(menustree);
}
@Override
public List<SysMenu> tree() {
// 获取所有菜单信息
List<SysMenu> sysMenus = this.list(new QueryWrapper<SysMenu>().orderByAsc("orderNum"));
// 转成 树桩结构
List<SysMenu> menus = buildTreeMeun(sysMenus);
return menus;
}
private List<SysMenuDto> convert(List<SysMenu> menustree) {
ArrayList<SysMenuDto> menuDtos = new ArrayList<>();
menustree.forEach(m -> {
SysMenuDto dto = new SysMenuDto();
dto.setId(m.getId());
dto.setName(m.getPerms());
dto.setTitle(m.getName());
dto.setComponent(m.getComponent());
dto.setPath(m.getPath());
// 如果 长度大于就给 字节点复制
if (m.getChildren().size() > 0) {
// 子节点调用当前的方法 进行复制。
dto.setChildren(convert(m.getChildren()));
}
menuDtos.add(dto);
});
return menuDtos;
}
// 转换树桩结构
private List<SysMenu> buildTreeMeun(List<SysMenu> sysMenus) {
// 准备返回的list
ArrayList<SysMenu> finalMenus = new ArrayList<>();
// 找到各自的子节点
for (SysMenu sysMenu : sysMenus) {
for (SysMenu sysMenu1 : sysMenus) {
// 如果 id 相同说明就是自己的孩子了
if (sysMenu.getId() == sysMenu1.getParentId()) {
sysMenu.getChildren().add(sysMenu1);
}
}
// 提取出来父节点
if (sysMenu.getParentId() == 0L) {
finalMenus.add(sysMenu);
}
}
return finalMenus;
}
}
接口中sysUserMapper.getNavMenuIds我们之前就已经写过的了,通过用户id获取菜单的id,然后后面就是转成树形结构,buildTreeMenu方法的思想很简单,我们现实把菜单循环,让所有菜单先找到各自的子节点,然后我们在把最顶级的菜单获取出来,这样顶级下面有二级,二级也有自己的三级。最后就是convert把menu转成menuDto。这个比较简单,就不说了。好了,导航菜单已经开发完毕,我们来写菜单管理的增删改查,因为菜单列表也是个树形接口,这次我们就不是获取当前用户的菜单列表的,而是所有菜单然后组成树形结构,一样的思想,数据不一样而已。
删除、更新菜单的时候记得调用根据菜单id清楚用户权限缓存信息的方法哈。然后每个方法前都会带有权限注解:@PreAuthorize("hasAuthority('sys:menu:delete')"),这就要求用户有特定的操作权限才能调用这个接口,sys:menu:delete这些数据不是乱写出来的,我们必须和数据库的数据保持一致才行,然后component字段,也是要和前端进行沟通,因为这个是链接到的前端的组件页面。有了增删改查,我们就去先添加我们的所有的菜单权限数据先。效果如下:
角色接口开发
角色的增删改查其实也简单,而且字段这么少 SysRoleController
package com.example.demo.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.Result.Results;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysRole;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUserRole;
import com.example.demo.utils.Const;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* 前端控制器
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@RestController
@RequestMapping("/sys/role")
public class SysRoleController extends BaseController {
@PreAuthorize("hasAuthority('sys:role:list')")
@GetMapping("/info/{id}")
public Results info(@PathVariable("id") Long id) {
// 获取到实体类
SysRole byId = sysRoleService.getById(id);
//获取关联表的 meunid
List<SysRoleMenu> role_id = sysRoleMenuService.list(new QueryWrapper<SysRoleMenu>().eq("role_id", byId));
// 通过 流 来获取 出meunid
List<Long> collect = role_id.stream().map(p -> p.getMenuId()).collect(Collectors.toList());
byId.setMenuIds(collect);
return Results.succ(byId);
}
@PreAuthorize("hasAuthority('sys:role:list')")
@GetMapping("/list")
public Results list(String name) {
// 分页查询 ,判断有没有名字
Page page = sysRoleService.page(getPage(),
new QueryWrapper<SysRole>().like(StrUtil.isNotBlank(name), "name", name));
return Results.succ(page);
}
@PreAuthorize("hasAuthority('sys:role:save')")
@PostMapping("/save")
public Results save(@Validated @RequestBody SysRole sysRole) {
// 新增 的方法
// 获取当前的 更改的时间
sysRole.setCreated(LocalDateTime.now());
// 保存的状态
sysRole.setStatu(Const.STATUS_ON);
sysRoleService.save(sysRole);
return Results.succ(sysRole);
}
@PreAuthorize("hasAuthority('sys:role:update')")
// 更新的
@PostMapping("/update")
public Results update(@Validated @RequestBody SysRole sysRole) {
// 设置更新的时间
sysRole.setCreated(LocalDateTime.now());
//调用更新的方法
sysRoleService.updateById(sysRole);
// 删除 缓存
sysUserService.clearUserAuthorityInfoByRoleId(sysRole.getId());
return Results.succ(sysRole);
}
// 批量删除
@PreAuthorize("hasAuthority('sys:role:delete')")
@PostMapping("/delete")
//加上事务避免出现 删除失败的情况
@Transactional
public Results delete(@RequestBody Long[] RoleIds) {
// 调用方法
sysRoleService.removeByIds(Arrays.asList(RoleIds));
// 删除中间表
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().in("role_id",RoleIds));
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("role_id",RoleIds));
// 删除缓存
Arrays.stream(RoleIds).forEach(f -> {
sysUserService.clearUserAuthorityInfoByRoleId(f);
});
return Results.succ("success");
}
@PreAuthorize("hasAuthority('sys:role:perm')")
@PostMapping("/perm/{roleId}")
@Transactional
public Results info(@PathVariable("roleId") Long roleId,@RequestBody Long[] menuId) {
// 用来存放的集合
List<SysRoleMenu> sysRoleMenus = new ArrayList<>();
Arrays.stream(menuId).forEach(menuid -> {
SysRoleMenu roleMenu = new SysRoleMenu();
roleMenu.setMenuId(menuid);
roleMenu.setRoleId(roleId);
sysRoleMenus.add(roleMenu);
});
//删除记录
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("role_id",roleId));
//保存新的
sysRoleMenuService.saveBatch(sysRoleMenus);
// 删除缓存
sysUserService.clearUserAuthorityInfoByRoleId(roleId);
return Results.succ(menuId);
}
}
上面方法中:info方法获取角色信息的方法,因为我们不仅仅在编辑角色时候会用到这个方法,在回显角色关联菜单的时候也需要被调用,因此我们需要把角色关联的所有的菜单的id也一并查询出来,也就是分配权限的操作。对应到前端就是这样的,点击分配权限,会弹出出所有的菜单列表,然后根据角色已经关联的菜单的id回显勾选上已经关联过的。
用户接口开发
用户管理里面有个用户关联角色的分配角色操作,和角色关联菜单的写法差不多的
package com.example.demo.controller;
import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.Result.Results;
import com.example.demo.dto.PassDto;
import com.example.demo.entity.SysRole;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUser;
import com.example.demo.entity.SysUserRole;
import com.example.demo.utils.Const;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@RestController
@RequestMapping("/sys/user")
public class SysUserController extends BaseController {
// 注入加密的
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/info/{id}")
@PreAuthorize("hasAuthority('sys:user:list')")
public Results info(@PathVariable("id") Long id) {
SysUser sysUser = sysUserService.getById(id);
// 断言判断是不是为空
Assert.notNull(sysUser, "找不到该管理员");
List<SysRole> sysRoles = sysRoleService.listRolesByUserId(id);
sysUser.setSysRoles(sysRoles);
return Results.succ(sysUser);
}
@PreAuthorize("hasAuthority('sys:user:list')")
@GetMapping("/list")
public Results list(String username) {
Page<SysUser> page = sysUserService.page(getPage(), new QueryWrapper<SysUser>().like(StringUtils.isNotBlank(username), "username", username));
page.getRecords().forEach(p -> {
p.setSysRoles(sysRoleService.listRolesByUserId(p.getId()));
});
return Results.succ(page);
}
@PreAuthorize("hasAuthority('sys:user:save')")
@PostMapping("/save")
public Results save(@Validated @RequestBody SysUser sysUser) {
// 设置 更新时间
sysUser.setCreated(LocalDateTime.now());
// 默认的状态
sysUser.setStatu(Const.STATUS_ON);
// 设置默认的加密的密码
String password = bCryptPasswordEncoder.encode(Const.PASS_WORD);
sysUser.setPassword(password);
// 设置默认的头像
sysUser.setAvatar(Const.Avatar);
sysUserService.save(sysUser);
return Results.succ(sysUser);
}
@PreAuthorize("hasAuthority('sys:user:update')")
@PostMapping("/update")
public Results update(@Validated @RequestBody SysUser sysUser) {
// 设置更新的时间
// sysUser.setCreated(LocalDateTime.now());
sysUser.setUpdated(LocalDateTime.now());
sysUserService.updateById(sysUser);
return Results.succ(sysUser);
}
@PreAuthorize("hasAuthority('sys:user:delete')")
@PostMapping("/delete")
@Transactional
public Results delete(@RequestBody Long [] ids) {
sysUserService.removeByIds(Arrays.asList(ids));
// 删除中间的关系表
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("user_id",ids));
return Results.succ("");
}
@PreAuthorize("hasAuthority('sys:user:role')")
@PostMapping("/role/{userId}")
@Transactional
public Results rolePerm(@PathVariable Long userId,@RequestBody Long [] roleIds) {
ArrayList<SysUserRole> UserRoleList = new ArrayList<>();
Arrays.stream(roleIds).forEach(r ->{
SysUserRole userRole = new SysUserRole();
userRole.setRoleId(r);
userRole.setUserId(userId);
UserRoleList.add(userRole);
});
// 删除关联表的数据
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().eq("user_id",userId));
sysUserRoleService.saveBatch(UserRoleList);
// 删除缓存
SysUser sysUser = sysUserService.getById(userId);
sysUserService.clearUserAuthorityInfo(sysUser.getUsername());
return Results.succ("");
}
@PostMapping("/repass")
@PreAuthorize("hasAuthority('sys:user:repass')")
public Results repass(@RequestBody Long id) {
SysUser byId = sysUserService.getById(id);
byId.setPassword(bCryptPasswordEncoder.encode(Const.PASS_WORD));
byId.setCreated(LocalDateTime.now());
sysUserService.save(byId);
return Results.succ("");
}
// 个人中心修改
@PostMapping("/updatePass")
public Results updatePass(@Validated @RequestBody PassDto passDto, Principal principal) {
SysUser sysUser = sysUserService.getByUsername(principal.getName());
boolean matches = bCryptPasswordEncoder.matches(passDto.getCurrentPass(), sysUser.getPassword());
if (!matches) {
return Results.fail("密码不正确");
}
sysUser.setPassword(bCryptPasswordEncoder.encode(Const.PASS_WORD));
sysUser.setUpdated(LocalDateTime.now());
sysUserService.updateById(sysUser);
return Results.succ("");
}
}
上面用到一个sysRoleService.listRolesByUserId,通过用户id获取所有关联的角色,用到了中间表,可以写sql,这里我这样写的
@Overridepublic List<SysRole> listRolesByUserId(Long userId) { return this.list( new QueryWrapper<SysRole>() .inSql("id", "select role_id from sys_user_role where user_id = " + userId));}
userId一定要是自己数据库查出来的,千万别让前端传过来啥就直接调用这个方法,不然会可能会被攻击
结束
所有的资源放在了资源里面可以下载。
参考的up主 添加链接描述