Skip to content

权限设计

本文档详细介绍 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);

下一步

基于 MIT 许可发布