权限设计
本文档详细介绍 youlai-boot 的权限系统设计,包括认证、授权和权限控制的实现方案。
权限模型
采用经典的 RBAC(Role-Based Access Control) 权限模型:
用户(User) → 角色(Role) → 菜单(Menu) → 权限(Permission)数据库表设计
sql
-- 用户表
sys_user (id, username, password, ...)
-- 角色表
sys_role (id, code, name, ...)
-- 菜单表
sys_menu (id, name, path, perms, ...)
-- 用户角色关联表
sys_user_role (user_id, role_id)
-- 角色菜单关联表
sys_role_menu (role_id, menu_id)权限层级
用户 admin
├─ 角色: 超级管理员 (ADMIN)
│ ├─ 菜单: 用户管理
│ │ ├─ 权限: sys:user:query (查询)
│ │ ├─ 权限: sys:user:add (新增)
│ │ ├─ 权限: sys:user:edit (编辑)
│ │ └─ 权限: sys:user:delete (删除)
│ └─ 菜单: 角色管理
│ └─ 权限: sys:role:*
└─ ...认证流程
1. 用户登录
java
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtUtils jwtUtils;
@Override
public LoginVO login(LoginDTO loginDTO) {
// 1. 用户认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginDTO.getUsername(),
loginDTO.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 2. 获取用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 3. 生成 JWT Token
String accessToken = jwtUtils.createToken(userDetails);
// 4. 返回登录结果
LoginVO loginVO = new LoginVO();
loginVO.setAccessToken(accessToken);
loginVO.setTokenType("Bearer");
loginVO.setExpires(jwtUtils.getExpiration());
return loginVO;
}
}2. JWT Token 生成
java
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 生成 Token
*/
public String createToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
claims.put("authorities", userDetails.getAuthorities());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 验证 Token
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
}3. JWT 认证过滤器
java
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// 1. 从请求头获取 Token
String token = getTokenFromRequest(request);
// 2. 验证 Token
if (token != null && jwtUtils.validateToken(token)) {
// 3. 解析 Token 获取用户名
Claims claims = jwtUtils.parseToken(token);
String username = claims.getSubject();
// 4. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 5. 设置认证信息
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}授权流程
1. 加载用户权限
java
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) {
// 1. 查询用户信息
SysUser user = userMapper.selectOne(
new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username)
);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 查询用户权限
List<String> perms = menuMapper.listPermsByUserId(user.getId());
// 3. 转换为 Spring Security 的权限
List<GrantedAuthority> authorities = perms.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 4. 构建 UserDetails
return new User(
user.getUsername(),
user.getPassword(),
user.getStatus() == 1, // enabled
true, // accountNonExpired
true, // credentialsNonExpired
true, // accountNonLocked
authorities
);
}
}2. 权限查询 SQL
xml
<!-- MenuMapper.xml -->
<select id="listPermsByUserId" resultType="string">
SELECT DISTINCT
m.perms
FROM sys_menu m
LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id
LEFT JOIN sys_user_role ur ON rm.role_id = ur.role_id
WHERE
ur.user_id = #{userId}
AND m.perms IS NOT NULL
AND m.perms != ''
AND m.deleted = 0
</select>3. 方法级权限控制
java
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
/**
* 新增用户 - 需要 sys:user:add 权限
*/
@PreAuthorize("hasAuthority('sys:user:add')")
@PostMapping
public Result<Void> addUser(@RequestBody UserForm form) {
// ...
}
/**
* 编辑用户 - 需要 sys:user:edit 权限
*/
@PreAuthorize("hasAuthority('sys:user:edit')")
@PutMapping("/{id}")
public Result<Void> updateUser(@PathVariable Long id, @RequestBody UserForm form) {
// ...
}
/**
* 删除用户 - 需要 sys:user:delete 权限
*/
@PreAuthorize("hasAuthority('sys:user:delete')")
@DeleteMapping("/{ids}")
public Result<Void> deleteUser(@PathVariable String ids) {
// ...
}
/**
* 查询用户 - 需要任意一个权限
*/
@PreAuthorize("hasAnyAuthority('sys:user:query', 'sys:user:list')")
@GetMapping("/page")
public Result<PageResult<UserVO>> getUserPage(UserPageQuery query) {
// ...
}
}数据权限
1. 数据权限范围
java
public enum DataScopeEnum {
/**
* 全部数据权限
*/
ALL(0, "全部数据"),
/**
* 本部门及以下数据权限
*/
DEPT_AND_CHILD(1, "本部门及以下"),
/**
* 本部门数据权限
*/
DEPT(2, "本部门"),
/**
* 仅本人数据权限
*/
SELF(3, "仅本人");
private final Integer value;
private final String label;
}2. 数据权限过滤
java
@Service
public class DataPermissionService {
/**
* 获取数据权限 SQL
*/
public String getDataPermissionSQL(String deptAlias, String userAlias) {
// 获取当前用户
SysUserDetails currentUser = SecurityUtils.getCurrentUser();
// 超级管理员拥有所有数据权限
if (SecurityUtils.isAdmin()) {
return "";
}
// 获取用户角色的数据权限
Integer dataScope = currentUser.getDataScope();
StringBuilder sql = new StringBuilder();
switch (dataScope) {
case 1: // 本部门及以下
sql.append(" AND ").append(deptAlias).append(".id IN (")
.append("SELECT id FROM sys_dept ")
.append("WHERE id = ").append(currentUser.getDeptId())
.append(" OR FIND_IN_SET(").append(currentUser.getDeptId())
.append(", ancestors))");
break;
case 2: // 本部门
sql.append(" AND ").append(deptAlias).append(".id = ")
.append(currentUser.getDeptId());
break;
case 3: // 仅本人
sql.append(" AND ").append(userAlias).append(".id = ")
.append(currentUser.getUserId());
break;
default: // 全部数据
break;
}
return sql.toString();
}
}3. MyBatis 拦截器实现
java
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
@Component
public class DataPermissionInterceptor implements Interceptor {
@Autowired
private DataPermissionService dataPermissionService;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取 MappedStatement
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
// 获取 SQL
BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
String sql = boundSql.getSql();
// 注入数据权限 SQL
String dataPermissionSQL = dataPermissionService.getDataPermissionSQL("d", "u");
if (StringUtils.hasText(dataPermissionSQL)) {
sql = sql + dataPermissionSQL;
// 重新设置 SQL
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, sql);
}
return invocation.proceed();
}
}安全配置
Spring Security 配置
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF
.csrf(AbstractHttpConfigurer::disable)
// 无状态 Session
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置请求权限
.authorizeHttpRequests(auth -> auth
// 放行的接口
.requestMatchers(
"/api/v1/auth/**",
"/doc.html",
"/webjars/**",
"/v3/**"
).permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
// 异常处理
.exceptionHandling(exception -> exception
.authenticationEntryPoint(restAuthenticationEntryPoint)
.accessDeniedHandler(restfulAccessDeniedHandler)
)
// 添加 JWT 过滤器
.addFilterBefore(
jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
}常用工具类
SecurityUtils - 安全工具类
java
public class SecurityUtils {
/**
* 获取当前用户
*/
public static SysUserDetails getCurrentUser() {
Authentication authentication = SecurityContextHolder
.getContext()
.getAuthentication();
if (authentication == null) {
return null;
}
return (SysUserDetails) authentication.getPrincipal();
}
/**
* 获取当前用户ID
*/
public static Long getUserId() {
SysUserDetails user = getCurrentUser();
return user != null ? user.getUserId() : null;
}
/**
* 获取当前用户名
*/
public static String getUsername() {
SysUserDetails user = getCurrentUser();
return user != null ? user.getUsername() : null;
}
/**
* 判断是否为管理员
*/
public static boolean isAdmin() {
SysUserDetails user = getCurrentUser();
return user != null && "admin".equals(user.getUsername());
}
/**
* 判断是否有权限
*/
public static boolean hasPermission(String permission) {
Collection<? extends GrantedAuthority> authorities =
getCurrentUser().getAuthorities();
return authorities.stream()
.anyMatch(auth -> auth.getAuthority().equals(permission));
}
}最佳实践
1. 权限命名规范
格式:模块:功能:操作
示例:
sys:user:query- 查询用户sys:user:add- 新增用户sys:user:edit- 编辑用户sys:user:delete- 删除用户sys:role:*- 角色的所有操作
2. Token 刷新策略
java
// 方式一:滑动过期(推荐)
// Token 每次使用后自动延长有效期
// 方式二:双 Token
// Access Token(短期)+ Refresh Token(长期)
// 方式三:Token 黑名单
// 将已过期或已登出的 Token 加入黑名单3. 密码加密
java
@Autowired
private PasswordEncoder passwordEncoder;
// 注册时加密密码
String encodedPassword = passwordEncoder.encode(rawPassword);
// 登录时验证密码
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);