文章目录
一、RBAC基础概念与原理
1.1 什么是RBAC?
RBAC(Role-Based Access Control,基于角色的访问控制)是一种广泛使用的权限管理模型。它的核心思想是将权限与角色关联,用户通过成为适当角色的成员而获得这些角色的权限。
通俗理解:想象一家公司,有"经理"、“普通员工”、"财务"等角色。经理可以审批请假,财务可以查看工资,普通员工只能提交请假申请。RBAC就是这样的系统,不是直接给每个人分配权限,而是通过角色来间接分配。
1.2 RBAC核心组件
组件 | 说明 | 生活化例子 |
---|---|---|
用户(User) | 系统的使用者 | 公司员工张三 |
角色(Role) | 权限的集合 | "部门经理"这个职位 |
权限(Permission) | 对资源的操作权限 | “审批请假申请”、“查看财务报表” |
资源(Resource) | 系统中被保护的对象 | 请假申请单、财务报表 |
用户-角色关联 | 用户和角色的关系 | 张三被任命为部门经理 |
角色-权限关联 | 角色和权限的关系 | 部门经理可以审批请假 |
1.3 RBAC模型分类
模型类型 | 特点 | 适用场景 |
---|---|---|
核心RBAC | 最基本的用户-角色-权限关系 | 简单系统 |
层级RBAC | 角色之间有继承关系(上级角色包含下级角色的权限) | 组织结构分明的系统 |
约束RBAC | 增加了职责分离约束(如互斥角色) | 金融、政务等高安全性系统 |
二、Spring Security基础集成
2.1 添加Spring Security依赖
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加这个依赖后,Spring Boot会自动保护所有端点,并生成一个默认用户(user)和随机密码(在启动日志中)。
2.2 基础安全配置
@Configuration
@EnableWebSecurity
public class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll() // 公共资源允许所有人访问
.anyRequest().authenticated() // 其他所有请求需要认证
.and()
.formLogin() // 启用表单登录
.loginPage("/login") // 自定义登录页
.permitAll()
.and()
.logout() // 启用注销功能
.permitAll();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
// 内存中的用户存储(仅用于演示,生产环境用数据库)
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
代码解析:
@EnableWebSecurity
启用Spring Securityconfigure(HttpSecurity http)
方法配置了哪些路径需要认证,哪些可以公开访问userDetailsService()
定义了两个内存用户(user和admin)及其角色
2.3 密码加密
重要:永远不要存储明文密码!Spring Security推荐使用BCrypt加密。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 使用BCrypt强哈希加密
}
// 使用示例
public class PasswordExample {
public static void main(String[] args) {
PasswordEncoder encoder = new BCryptPasswordEncoder();
String rawPassword = "myPassword123";
String encodedPassword = encoder.encode(rawPassword);
System.out.println("加密后的密码: " + encodedPassword);
// 验证密码
boolean matches = encoder.matches(rawPassword, encodedPassword);
System.out.println("密码匹配: " + matches);
}
}
输出示例:
加密后的密码: $2a$10$N9qo8uLOickgx2ZMRZoMy.Mrq4H3zQY1Q8qP3R2ZR6zBf1QwJQ1GW
密码匹配: true
三、数据库设计与实现
3.1 RBAC数据库表设计
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
account_non_expired BOOLEAN DEFAULT TRUE,
account_non_locked BOOLEAN DEFAULT TRUE,
credentials_non_expired BOOLEAN DEFAULT TRUE
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255)
);
-- 权限表
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
url VARCHAR(255),
method VARCHAR(10),
description VARCHAR(255)
);
-- 用户-角色关联表
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES sys_user(id),
FOREIGN KEY (role_id) REFERENCES sys_role(id)
);
-- 角色-权限关联表
CREATE TABLE sys_role_permission (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES sys_role(id),
FOREIGN KEY (permission_id) REFERENCES sys_permission(id)
);
RBAC 数据库关系图 (ER图)
3.2 JPA实体类实现
// 用户实体
@Entity
@Table(name = "sys_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
private Boolean enabled = true;
private Boolean accountNonExpired = true;
private Boolean accountNonLocked = true;
private Boolean credentialsNonExpired = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
private List<Role> roles;
// 实现UserDetails接口的方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
this.roles.forEach(role -> {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
role.getPermissions().forEach(permission -> {
authorities.add(new SimpleGrantedAuthority(permission.getName()));
});
});
return authorities;
}
// 其他UserDetails方法实现...
}
// 角色实体
@Entity
@Table(name = "sys_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private String description;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_role_permission",
joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "permission_id", referencedColumnName = "id")})
private List<Permission> permissions;
}
// 权限实体
@Entity
@Table(name = "sys_permission")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private String url;
private String method; // GET, POST, PUT, DELETE等
private String description;
}
四、自定义认证与授权
4.1 自定义UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 检查账户状态
if (!user.isEnabled()) {
throw new DisabledException("账户已禁用");
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException("账户已过期");
}
if (!user.isAccountNonLocked()) {
throw new LockedException("账户已锁定");
}
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("凭证已过期");
}
return user;
}
}
4.2 安全配置进阶
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级安全控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用CSRF(根据需求配置)
.csrf().disable()
// 授权配置
.authorizeRequests()
.antMatchers("/login", "/register", "/api/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers(HttpMethod.GET, "/api/products").hasAuthority("product:read")
.antMatchers(HttpMethod.POST, "/api/products").hasAuthority("product:write")
.anyRequest().authenticated()
.and()
// 表单登录配置
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/auth/login")
.successHandler(successHandler)
.failureHandler(failureHandler)
.permitAll()
.and()
// 记住我配置
.rememberMe()
.tokenValiditySeconds(7 * 24 * 60 * 60) // 7天
.key("uniqueAndSecret")
.userDetailsService(userDetailsService)
.and()
// 注销配置
.logout()
.logoutUrl("/auth/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "remember-me")
.permitAll()
.and()
// 异常处理
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 自定义认证成功处理器
@Bean
public AuthenticationSuccessHandler successHandler() {
return (request, response, authentication) -> {
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 200);
result.put("msg", "登录成功");
result.put("data", authentication);
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
};
}
// 自定义认证失败处理器
@Bean
public AuthenticationFailureHandler failureHandler() {
return (request, response, exception) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> result = new HashMap<>();
result.put("status", 401);
if (exception instanceof LockedException) {
result.put("msg", "账户被锁定,请联系管理员");
} else if (exception instanceof DisabledException) {
result.put("msg", "账户被禁用,请联系管理员");
} else if (exception instanceof AccountExpiredException) {
result.put("msg", "账户已过期,请联系管理员");
} else if (exception instanceof UsernameNotFoundException) {
result.put("msg", "用户名不存在");
} else if (exception instanceof BadCredentialsException) {
result.put("msg", "用户名或密码错误");
} else {
result.put("msg", "登录失败");
}
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
};
}
// 自定义权限不足处理器
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, exception) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> result = new HashMap<>();
result.put("status", 403);
result.put("msg", "权限不足,无法访问");
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
};
}
}
五、权限控制实践
5.1 方法级权限控制
Spring Security提供了多种方法级权限控制注解:
注解 | 说明 | 示例 |
---|---|---|
@PreAuthorize | 方法执行前检查权限 | @PreAuthorize(“hasRole(‘ADMIN’)”) |
@PostAuthorize | 方法执行后检查返回值权限 | @PostAuthorize(“returnObject.owner == authentication.name”) |
@Secured | 简单的角色检查 | @Secured(“ROLE_ADMIN”) |
@RolesAllowed | JSR-250标准角色检查 | @RolesAllowed(“ADMIN”) |
@PreFilter | 方法执行前过滤集合参数 | @PreFilter(“filterObject.owner == authentication.name”) |
@PostFilter | 方法执行后过滤返回值集合 | @PostFilter(“filterObject.isPublic or filterObject.owner == authentication.name”) |
使用示例:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping
@PreAuthorize("hasAuthority('product:read')")
public List<Product> getAllProducts() {
// 返回所有产品
}
@PostMapping
@PreAuthorize("hasAuthority('product:write')")
public Product createProduct(@RequestBody Product product) {
// 创建新产品
}
@GetMapping("/{id}")
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public Product getProductById(@PathVariable Long id) {
// 根据ID获取产品
}
@GetMapping("/mine")
@PostFilter("filterObject.owner == authentication.name")
public List<Product> getMyProducts() {
// 返回当前用户的产品(管理员会看到所有,但会被过滤)
}
}
5.2 动态权限控制
对于更复杂的权限需求,可以实现PermissionEvaluator
接口:
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private PermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
// 检查对特定对象的权限
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
String username = authentication.getName();
String perm = permission.toString();
return permissionService.hasPermission(username, targetDomainObject, perm);
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
// 通过ID和类型检查权限
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
String username = authentication.getName();
String perm = permission.toString();
return permissionService.hasPermission(username, targetType, targetId, perm);
}
}
// 配置自定义PermissionEvaluator
@Configuration
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private CustomPermissionEvaluator permissionEvaluator;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}
// 使用示例
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
@GetMapping("/{id}")
@PreAuthorize("hasPermission(#id, 'document', 'read')")
public Document getDocument(@PathVariable Long id) {
// 获取文档
}
@PutMapping("/{id}")
@PreAuthorize("hasPermission(#document, 'write')")
public Document updateDocument(@PathVariable Long id, @RequestBody Document document) {
// 更新文档
}
}
六、JWT集成与无状态认证
6.1 JWT基础概念
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。
JWT结构:
- Header:包含令牌类型和签名算法
- Payload:包含声明(用户信息、过期时间等)
- Signature:验证令牌完整性的签名
6.2 JWT集成实现
添加依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
JWT工具类:
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration-in-ms}")
private int jwtExpirationInMs;
// 生成令牌
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
// 从令牌获取用户ID
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
// 验证令牌
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
JWT认证过滤器:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
配置JWT安全:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtTokenProvider tokenProvider;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated();
// 添加JWT过滤器
http.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
}
七、权限缓存优化
7.1 使用Redis缓存权限数据
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // 1小时过期
.disableCachingNullValues())
.build();
}
}
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionRepository permissionRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private CacheManager cacheManager;
@Cacheable(value = "userPermissions", key = "#userId")
public Set<String> getUserPermissions(Long userId) {
// 从数据库查询用户权限
Set<String> permissions = new HashSet<>();
List<Role> roles = roleRepository.findByUsers_Id(userId);
for (Role role : roles) {
List<Permission> rolePermissions = permissionRepository.findByRoles_Id(role.getId());
for (Permission permission : rolePermissions) {
permissions.add(permission.getName());
}
}
return permissions;
}
@CacheEvict(value = "userPermissions", key = "#userId")
public void clearUserPermissionsCache(Long userId) {
// 方法体可以为空,注解会触发缓存清除
}
}
7.2 自定义权限缓存过滤器
public class PermissionCacheFilter extends OncePerRequestFilter {
@Autowired
private PermissionService permissionService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
// 获取当前用户ID
Long userId = ((UserPrincipal) authentication.getPrincipal()).getId();
// 从缓存获取权限
Set<String> permissions = permissionService.getUserPermissions(userId);
// 将权限存入请求属性
request.setAttribute("userPermissions", permissions);
}
filterChain.doFilter(request, response);
}
}
7.3 权限缓存流程图
八、权限系统监控与审计
8.1 权限操作日志
@Entity
@Table(name = "sys_audit_log")
@Data
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String operation;
private String method;
private String params;
private Long time;
private String ip;
@CreatedDate
private Date createTime;
}
@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AuditLogRepository auditLogRepository;
@Autowired
private HttpServletRequest request;
@Pointcut("@annotation(com.example.demo.annotation.RequiresPermission)")
public void permissionPointcut() {}
@AfterReturning("permissionPointcut()")
public void afterReturning(JoinPoint joinPoint) {
saveLog(joinPoint, null);
}
@AfterThrowing(pointcut = "permissionPointcut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
saveLog(joinPoint, e.getMessage());
}
private void saveLog(JoinPoint joinPoint, String errorMsg) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequiresPermission requiresPermission = method.getAnnotation(RequiresPermission.class);
AuditLog auditLog = new AuditLog();
auditLog.setOperation(requiresPermission.value());
auditLog.setMethod(method.getDeclaringClass().getName() + "." + method.getName());
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
auditLog.setUsername(authentication.getName());
}
Object[] args = joinPoint.getArgs();
try {
List<String> params = new ArrayList<>();
for (Object arg : args) {
if (arg != null && !(arg instanceof HttpServletRequest)
&& !(arg instanceof HttpServletResponse)) {
params.add(JSON.toJSONString(arg));
}
}
auditLog.setParams(String.join(", ", params));
} catch (Exception e) {
auditLog.setParams("参数解析失败");
}
auditLog.setIp(IpUtils.getIpAddr(request));
auditLog.setTime(System.currentTimeMillis());
if (errorMsg != null) {
auditLog.setOperation(auditLog.getOperation() + " 失败: " + errorMsg);
}
auditLogRepository.save(auditLog);
}
}
8.2 权限使用统计
@Service
public class PermissionStatsService {
@Autowired
private AuditLogRepository auditLogRepository;
public Map<String, Integer> getPermissionUsageStats(Date startDate, Date endDate) {
List<Object[]> stats = auditLogRepository.countByOperationBetweenDates(startDate, endDate);
Map<String, Integer> result = new HashMap<>();
for (Object[] stat : stats) {
result.put((String) stat[0], ((Number) stat[1]).intValue());
}
return result;
}
public Map<String, Map<String, Integer>> getUserPermissionStats(Date startDate, Date endDate) {
List<Object[]> stats = auditLogRepository.countByUserAndOperationBetweenDates(startDate, endDate);
Map<String, Map<String, Integer>> result = new HashMap<>();
for (Object[] stat : stats) {
String username = (String) stat[0];
String operation = (String) stat[1];
int count = ((Number) stat[2]).intValue();
result.computeIfAbsent(username, k -> new HashMap<>())
.put(operation, count);
}
return result;
}
}
九、前端权限控制
9.1 基于Vue的前端权限控制
权限指令:
// permission.js
import store from '@/store'
export default {
inserted(el, binding, vnode) {
const { value } = binding
const permissions = store.getters && store.getters.permissions
if (value && value instanceof Array && value.length > 0) {
const requiredPermissions = value
const hasPermission = permissions.some(permission => {
return requiredPermissions.includes(permission)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`需要指定权限,如 v-permission="['user:add']"`)
}
}
}
路由守卫:
// permission.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/auth-redirect']
router.beforeEach(async (to, from, next) => {
NProgress.start()
// 确定用户是否已登录
const hasToken = store.getters.token
if (hasToken) {
if (to.path === '/login') {
// 如果已登录,重定向到主页
next({ path: '/' })
NProgress.done()
} else {
// 检查用户是否已获取权限
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户信息
const { roles } = await store.dispatch('user/getInfo')
// 基于角色生成可访问路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 动态添加路由
router.addRoutes(accessRoutes)
// 确保addRoutes已完成
next({ ...to, replace: true })
} catch (error) {
// 移除token并重定向到登录页
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 未登录 */
if (whiteList.indexOf(to.path) !== -1) {
// 在白名单中直接放行
next()
} else {
// 其他没有访问权限的页面被重定向到登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
NProgress.done()
})
十、高级主题与最佳实践
10.1 多租户权限系统
@Entity
@Table(name = "sys_tenant")
@Data
public class Tenant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String code;
private Boolean enabled;
}
@Entity
@Table(name = "sys_user")
@Data
public class User implements UserDetails {
// 其他字段...
@ManyToOne
@JoinColumn(name = "tenant_id")
private Tenant tenant;
}
// 租户过滤器
public class TenantFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Long tenantId = userPrincipal.getTenantId();
// 设置当前租户上下文
TenantContext.setCurrentTenant(tenantId);
}
try {
filterChain.doFilter(request, response);
} finally {
// 清除租户上下文
TenantContext.clear();
}
}
}
// 租户上下文
public class TenantContext {
private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
public static void setCurrentTenant(Long tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static Long getCurrentTenant() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
// 数据过滤拦截器
@Interceptor
public class TenantDataInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
Long tenantId = TenantContext.getCurrentTenant();
if (tenantId != null) {
// 设置数据源路由
DynamicDataSourceContextHolder.setDataSourceKey("tenant_" + tenantId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}
10.2 权限系统性能优化建议
-
缓存策略:
- 使用Redis缓存用户权限数据
- 实现多级缓存(本地缓存+分布式缓存)
- 设置合理的过期时间
-
数据库优化:
- 为常用查询字段添加索引
- 使用连接查询替代多次单表查询
- 定期清理无用权限数据
-
权限检查优化:
- 批量检查权限而非单个检查
- 使用位运算存储简单权限标志
- 实现权限检查短路逻辑(一旦满足立即返回)
-
前端优化:
- 按需加载权限相关组件
- 预加载用户权限数据
- 实现权限数据的本地存储
-
监控与调优:
- 记录权限检查耗时
- 监控权限缓存命中率
- 定期分析权限使用模式
十一、完整示例项目结构
springboot-rbac-demo/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── demo/
│ │ │ ├── config/ # 配置类
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ ├── RedisConfig.java
│ │ │ │ └── WebMvcConfig.java
│ │ │ ├── controller/ # 控制器
│ │ │ │ ├── AuthController.java
│ │ │ │ ├── UserController.java
│ │ │ │ └── RoleController.java
│ │ │ ├── model/ # 数据模型
│ │ │ │ ├── entity/ # JPA实体
│ │ │ │ ├── dto/ # 数据传输对象
│ │ │ │ └── vo/ # 视图对象
│ │ │ ├── repository/ # 数据访问层
│ │ │ ├── service/ # 业务逻辑层
│ │ │ ├── security/ # 安全相关
│ │ │ │ ├── jwt/ # JWT相关
│ │ │ │ ├── oauth2/ # OAuth2相关
│ │ │ │ └── user/ # 用户详情
│ │ │ ├── util/ # 工具类
│ │ │ └── DemoApplication.java # 启动类
│ │ └── resources/
│ │ ├── application.yml # 应用配置
│ │ └── static/ # 静态资源
│ └── test/ # 测试代码
└── pom.xml # Maven配置
十二、常见问题与解决方案
12.1 权限缓存一致性问题
问题:当用户权限变更后,缓存中的权限数据未及时更新。
解决方案:
- 权限变更时主动清除相关缓存
- 使用发布-订阅模式通知所有节点更新缓存
- 设置较短的缓存过期时间
@Service
public class PermissionServiceImpl implements PermissionService {
@CacheEvict(value = "userPermissions", key = "#userId")
public void updateUserPermissions(Long userId, List<String> permissions) {
// 更新用户权限逻辑
}
@CachePut(value = "userPermissions", key = "#userId")
public Set<String> refreshUserPermissions(Long userId) {
// 重新加载用户权限
return getUserPermissionsFromDB(userId);
}
}
12.2 权限细粒度控制问题
问题:如何实现字段级的权限控制?
解决方案:
- 使用注解标记敏感字段
- 在DTO转换时过滤无权限字段
- 使用动态代理拦截字段访问
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface PermissionField {
String value(); // 需要的权限
}
public class UserDTO {
private String username;
@PermissionField("user:view:email")
private String email;
@PermissionField("user:view:phone")
private String phone;
}
// 字段过滤工具
public class PermissionFieldFilter {
public static <T> T filterFields(T dto, Set<String> permissions) {
try {
Class<?> clazz = dto.getClass();
T filteredDto = (T) clazz.newInstance();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
PermissionField annotation = field.getAnnotation(PermissionField.class);
if (annotation == null || permissions.contains(annotation.value())) {
field.set(filteredDto, field.get(dto));
}
}
return filteredDto;
} catch (Exception e) {
throw new RuntimeException("字段过滤失败", e);
}
}
}
关注不关注,你自己决定(但正确的决定只有一个)。
喜欢的点个关注,想了解更多的可以关注微信公众号 “Eric的技术杂货库” ,提供更多的干货以及资料下载保存!