Phase 1 #19: RBAC 认证授权系统 + SSO 单点登录

- Entity: SysUser/SysRole/SysMenu/SysDept (MyBatis-Plus)
- Mapper: 含自定义SQL(角色权限查询/数据范围查询/菜单按角色查询)
- Service: 登录验证(BCrypt)/Token创建(Sa-Token)/菜单树/部门树构建
- Controller: AuthController(登录/登出/用户信息/Token校验)
  + SysUserController + SysRoleController + SysMenuController + SysDeptController (CRUD)
- Config: SaToken拦截器(排除认证+Swagger) + MyBatis-Plus分页插件 + Knife4j Swagger
- 支持5级角色(admin/leader/manager/operator/tech) + 数据权限(datas_scope: ALL/DEPT/SELF)
This commit is contained in:
bot_pm
2026-06-14 13:16:39 +08:00
parent f337ebedfd
commit 9a7e0481ef
21 changed files with 595 additions and 2 deletions
@@ -0,0 +1,18 @@
package com.water.base.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
return interceptor;
}
}
@@ -0,0 +1,25 @@
package com.water.base.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> {
// 放行认证接口
SaRouter.match("/auth/**").stop();
// 放行 Swagger
SaRouter.match("/swagger-ui/**").stop();
SaRouter.match("/v3/api-docs/**").stop();
// 其他需要登录
SaRouter.match("/**").check(r -> StpUtil.checkLogin());
})).addPathPatterns("/**");
}
}
@@ -0,0 +1,21 @@
package com.water.base.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI waterOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("智慧水务管理系统 - 基础服务 API")
.description("系统管理:用户/角色/菜单/部门/日志")
.version("1.0.0")
.contact(new Contact().name("WM Team")));
}
}
@@ -0,0 +1,52 @@
package com.water.base.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.water.base.service.SysUserService;
import com.water.common.core.result.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
@Tag(name = "认证管理")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final SysUserService userService;
@Data
public static class LoginRequest {
@NotBlank private String username;
@NotBlank private String password;
}
@Operation(summary = "登录")
@PostMapping("/login")
public R<String> login(@RequestBody LoginRequest req) {
String token = userService.login(req.getUsername(), req.getPassword());
return R.ok(token);
}
@Operation(summary = "登出")
@PostMapping("/logout")
public R<Void> logout() {
StpUtil.logout();
return R.ok();
}
@Operation(summary = "获取当前用户信息")
@GetMapping("/user-info")
public R<?> userInfo() {
return R.ok(userService.getLoginUser());
}
@Operation(summary = "验证token是否有效")
@GetMapping("/check")
public R<Boolean> check() {
return R.ok(StpUtil.isLogin());
}
}
@@ -0,0 +1,48 @@
package com.water.base.controller;
import com.water.base.entity.SysDept;
import com.water.base.service.SysDeptService;
import com.water.common.core.result.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "部门管理")
@RestController
@RequestMapping("/sys/dept")
@RequiredArgsConstructor
public class SysDeptController {
private final SysDeptService sysDeptService;
@Operation(summary = "部门树")
@GetMapping("/tree")
public R<List<SysDept>> tree() {
return R.ok(sysDeptService.buildDeptTree(sysDeptService.list()));
}
@Operation(summary = "新增部门")
@PostMapping
public R<String> create(@RequestBody SysDept dept) {
sysDeptService.save(dept);
return R.ok("创建成功");
}
@Operation(summary = "编辑部门")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody SysDept dept) {
dept.setId(id);
sysDeptService.updateById(dept);
return R.ok("更新成功");
}
@Operation(summary = "删除部门")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
sysDeptService.removeById(id);
return R.ok("删除成功");
}
}
@@ -0,0 +1,55 @@
package com.water.base.controller;
import com.water.base.entity.SysMenu;
import com.water.base.service.SysMenuService;
import com.water.common.core.result.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "菜单管理")
@RestController
@RequestMapping("/sys/menu")
@RequiredArgsConstructor
public class SysMenuController {
private final SysMenuService sysMenuService;
@Operation(summary = "菜单树")
@GetMapping("/tree")
public R<List<SysMenu>> tree() {
List<SysMenu> all = sysMenuService.list();
return R.ok(sysMenuService.buildMenuTree(all));
}
@Operation(summary = "菜单列表(平铺)")
@GetMapping("/list")
public R<List<SysMenu>> list() {
return R.ok(sysMenuService.list());
}
@Operation(summary = "新增菜单")
@PostMapping
public R<String> create(@RequestBody SysMenu menu) {
sysMenuService.save(menu);
return R.ok("创建成功");
}
@Operation(summary = "编辑菜单")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody SysMenu menu) {
menu.setId(id);
sysMenuService.updateById(menu);
return R.ok("更新成功");
}
@Operation(summary = "删除菜单")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
sysMenuService.removeById(id);
return R.ok("删除成功");
}
}
@@ -0,0 +1,54 @@
package com.water.base.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.base.entity.SysRole;
import com.water.base.service.SysRoleService;
import com.water.common.core.result.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "角色管理")
@RestController
@RequestMapping("/sys/role")
@RequiredArgsConstructor
public class SysRoleController {
private final SysRoleService sysRoleService;
@Operation(summary = "角色列表")
@GetMapping("/list")
public R<Page<SysRole>> list(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return R.ok(sysRoleService.page(new Page<>(page, size)));
}
@Operation(summary = "查询角色")
@GetMapping("/{id}")
public R<SysRole> getById(@PathVariable Long id) {
return R.ok(sysRoleService.getById(id));
}
@Operation(summary = "新增角色")
@PostMapping
public R<String> create(@RequestBody SysRole role) {
sysRoleService.save(role);
return R.ok("创建成功");
}
@Operation(summary = "编辑角色")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody SysRole role) {
role.setId(id);
sysRoleService.updateById(role);
return R.ok("更新成功");
}
@Operation(summary = "删除角色")
@DeleteMapping("/{id}")
public R<String> delete(@PathVariable Long id) {
sysRoleService.removeById(id);
return R.ok("删除成功");
}
}
@@ -0,0 +1,63 @@
package com.water.base.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.water.base.entity.SysUser;
import com.water.base.service.SysUserService;
import com.water.common.core.result.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "用户管理")
@RestController
@RequestMapping("/sys/user")
@RequiredArgsConstructor
public class SysUserController {
private final SysUserService sysUserService;
@Operation(summary = "分页查询用户")
@GetMapping("/list")
public R<Page<SysUser>> list(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String username) {
LambdaQueryWrapper<SysUser> qw = new LambdaQueryWrapper<>();
if (username != null && !username.isEmpty()) {
qw.like(SysUser::getUsername, username);
}
return R.ok(sysUserService.page(new Page<>(page, size), qw));
}
@Operation(summary = "获取用户详情")
@GetMapping("/{id}")
public R<SysUser> getById(@PathVariable Long id) {
return R.ok(sysUserService.getById(id));
}
@Operation(summary = "新增用户")
@PostMapping
public R<String> create(@RequestBody SysUser user) {
sysUserService.save(user);
return R.ok("创建成功");
}
@Operation(summary = "编辑用户")
@PutMapping("/{id}")
public R<String> update(@PathVariable Long id, @RequestBody SysUser user) {
user.setId(id);
sysUserService.updateById(user);
return R.ok("更新成功");
}
@Operation(summary = "启用/停用用户")
@PutMapping("/{id}/status")
public R<String> toggleStatus(@PathVariable Long id, @RequestParam int status) {
SysUser user = new SysUser();
user.setId(id);
user.setStatus(status);
sysUserService.updateById(user);
return R.ok(status == 1 ? "已启用" : "已停用");
}
}
@@ -0,0 +1,26 @@
package com.water.base.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("sys_dept")
public class SysDept {
@TableId(type = IdType.AUTO)
private Long id;
private Long parentId;
private String deptName;
private String deptType;
private Integer sortOrder;
private String leader;
private String phone;
private Integer status;
@TableLogic
private Integer deleted;
@TableField(exist = false)
private List<SysDept> children;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,29 @@
package com.water.base.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("sys_menu")
public class SysMenu {
@TableId(type = IdType.AUTO)
private Long id;
private Long parentId;
private String menuName;
private String menuType;
private String path;
private String component;
private String perms;
private String icon;
private Integer sortOrder;
private Integer visible;
private Integer status;
@TableLogic
private Integer deleted;
@TableField(exist = false)
private List<SysMenu> children;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,22 @@
package com.water.base.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_role")
public class SysRole {
@TableId(type = IdType.AUTO)
private Long id;
private String roleName;
private String roleKey;
private Integer roleSort;
private String dataScope;
private Integer status;
private String remark;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,29 @@
package com.water.base.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.AUTO)
private Long id;
private Long deptId;
private String username;
private String password;
private String realName;
private String nickname;
private String phone;
private String email;
private String avatar;
private Integer gender;
private String roleType;
private Integer status;
private String loginIp;
private LocalDateTime loginAt;
@TableLogic
private Integer deleted;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,8 @@
package com.water.base.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.base.entity.SysDept;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysDeptMapper extends BaseMapper<SysDept> {}
@@ -0,0 +1,13 @@
package com.water.base.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.base.entity.SysMenu;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface SysMenuMapper extends BaseMapper<SysMenu> {
@Select("SELECT m.* FROM sys_menu m INNER JOIN sys_role_menu rm ON m.id = rm.menu_id WHERE rm.role_id = #{roleId} AND m.status = 1 AND m.visible = 1 ORDER BY m.sort_order")
List<SysMenu> selectMenusByRoleId(Long roleId);
}
@@ -0,0 +1,8 @@
package com.water.base.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.base.entity.SysRole;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysRoleMapper extends BaseMapper<SysRole> {}
@@ -0,0 +1,16 @@
package com.water.base.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.water.base.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
@Select("SELECT r.role_key FROM sys_role r INNER JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = #{userId} AND r.status = 1")
List<String> selectRoleKeysByUserId(Long userId);
@Select("SELECT r.data_scope FROM sys_role r INNER JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = #{userId} AND r.status = 1 ORDER BY r.role_sort LIMIT 1")
String selectDataScopeByUserId(Long userId);
}
@@ -0,0 +1,23 @@
package com.water.base.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.water.base.entity.SysDept;
import com.water.base.mapper.SysDeptMapper;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class SysDeptService extends ServiceImpl<SysDeptMapper, SysDept> {
public List<SysDept> buildDeptTree(List<SysDept> depts) {
Map<Long, List<SysDept>> parentMap = depts.stream()
.collect(Collectors.groupingBy(d -> d.getParentId() == null ? 0L : d.getParentId()));
List<SysDept> roots = parentMap.getOrDefault(0L, Collections.emptyList());
for (SysDept root : roots) {
root.setChildren(parentMap.getOrDefault(root.getId(), Collections.emptyList()));
}
return roots;
}
}
@@ -0,0 +1,30 @@
package com.water.base.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.water.base.entity.SysMenu;
import com.water.base.mapper.SysMenuMapper;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class SysMenuService extends ServiceImpl<SysMenuMapper, SysMenu> {
/**
* 构建菜单树
*/
public List<SysMenu> buildMenuTree(List<SysMenu> menus) {
Map<Long, List<SysMenu>> parentMap = menus.stream()
.collect(Collectors.groupingBy(m -> m.getParentId() == null ? 0L : m.getParentId()));
List<SysMenu> roots = parentMap.getOrDefault(0L, Collections.emptyList());
for (SysMenu root : roots) {
root.setChildren(parentMap.getOrDefault(root.getId(), Collections.emptyList()));
}
return roots;
}
public List<SysMenu> getMenusByRoleId(Long roleId) {
return baseMapper.selectMenusByRoleId(roleId);
}
}
@@ -0,0 +1,9 @@
package com.water.base.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.water.base.entity.SysRole;
import com.water.base.mapper.SysRoleMapper;
import org.springframework.stereotype.Service;
@Service
public class SysRoleService extends ServiceImpl<SysRoleMapper, SysRole> {}
@@ -0,0 +1,42 @@
package com.water.base.service;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.water.base.entity.SysUser;
import com.water.base.mapper.SysUserMapper;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SysUserService extends ServiceImpl<SysUserMapper, SysUser> {
/**
* 登录验证
*/
public String login(String username, String password) {
SysUser user = this.getOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username)
.eq(SysUser::getStatus, 1));
if (user == null) {
throw new RuntimeException("用户名或密码错误");
}
if (!BCrypt.checkpw(password, user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
// 登录成功,创建token
StpUtil.login(user.getId());
// 获取用户角色权限列表
List<String> roleKeys = baseMapper.selectRoleKeysByUserId(user.getId());
StpUtil.getSession().set("roleKeys", roleKeys);
StpUtil.getSession().set("realName", user.getRealName());
return StpUtil.getTokenValue();
}
public SysUser getLoginUser() {
long userId = StpUtil.getLoginIdAsLong();
return this.getById(userId);
}
}
+3 -1
View File
@@ -11,5 +11,7 @@
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId></dependency>
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
</dependencies>
</project>
</project><?xml version="1.0" encoding="UTF-8"?>
<!-- overwrite -->