基 于 角 色 权 限 模 型 与 数 据 权 限 控 制
以 小 明 的 摄 影 作 品 管 理 系 统 为 例 , 从 零 基 础 搭 建 到 完 整 认 证 鉴 权 , 逐 步 演 示 Spring Security 的 接 入 流 程 。 涵 盖 基 础 接 入 、 角 色 权 限 扩 展 、 数 据 权 限 控 制 三 大 模 块 , 每 步 标 注 自 定 义 类 与 Spring Security 扩 展 点 , 配 代 码 、 注 释 和 逻 辑 说 明 。 使 用 MyBatisPlus 作 为 ORM 框 架 。
** 第 一 部 分 : 基 础 接 入 流 程 --- 前 后 端 分 离 认 证 鉴 权 **
** 第 一 步 : 环 境 准 备 与 依 赖 引 入 **
** 目 的 **
搭 建 Spring Boot 基 础 项 目 , 引 入 Spring Security 及 前 后 端 分 离 必 需 组 件 , 使 用 MyBatisPlus 替 代 JPA 。
** 操 作 **
-
** 创 建 Spring Boot 项 目 **
通 过 start.spring.io 创 建 , 选 择 Maven 、 Java 17+ , 添 加 依 赖 Spring Web 、 Spring Security 、 MyBatis Plus 、 MySQL Driver 、 Lombok 。
-
** Pom.xml 核 心 依 赖 **
xml
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
** 第 二 步 : 数 据 库 设 计 与 实 体 类 **
** 目 的 **
设 计 用 户 、 角 色 表 , 并 让 用 户 PO 类 实 现 Spring Security 的 UserDetails 接 口 。
** 操 作 **
- ** 数 据 库 表 设 计 **
sql
CREATE TABLE sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密密码',
email VARCHAR(100) COMMENT '邮箱',
enabled BOOLEAN DEFAULT TRUE COMMENT '账户是否启用'
);
CREATE TABLE sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(50) NOT NULL UNIQUE COMMENT '角色名称(无ROLE_前缀)'
);
CREATE TABLE sys_user_role (
user_id BIGINT,
role_id BIGINT,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES sys_user(id),
FOREIGN KEY (role_id) REFERENCES sys_role(id)
);
- ** 实 体 类 UserPo 实 现 UserDetails 接 口 **
java
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
@Data
@TableName("sys_user")
public class UserPo implements UserDetails {
private Long id;
private String username;
private String password;
private String email;
private Boolean enabled = true;
private Set<RolePo> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName()))
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return enabled; }
}
- ** 角 色 实 体 类 RolePo **
java
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("sys_role")
public class RolePo {
private Long id;
private String roleName;
}
** 第 三 步 : MyBatisPlus Mapper 层 **
** 目 的 **
使 用 MyBatisPlus Mapper 操 作 数 据 库 。
** 操 作 **
- ** UserMapper 接 口 **
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Optional;
public interface UserMapper extends BaseMapper<UserPo> {
Optional<UserPo> findByUsername(@Param("username") String username);
}
- ** RoleMapper 接 口 **
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
public interface RoleMapper extends BaseMapper<RolePo> {
Optional<RolePo> findByRoleName(@Param("roleName") String roleName);
}
** 第 四 步 : Service 层 --- 实 现 UserDetailsService **
** 目 的 **
实 现 Spring Security 的 UserDetailsService 接 口 , 配 置 密 码 编 码 器 。
** 操 作 **
- ** 自 定 义 UserDetailsService 实 现 类 **
java
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserPo user = userMapper.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));
return user;
}
}
- ** 配 置 密 码 编 码 器 **
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
** 第 五 步 : Spring Security 配 置 类 **
** 目 的 **
配 置 认 证 规 则 、 授 权 规 则 、 JWT 过 滤 器 。
** 操 作 **
- ** 自 定 义 Security 配 置 类 **
java
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final JwtAuthFilter jwtAuthFilter;
private final PasswordEncoder passwordEncoder;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
** 第 六 步 : JWT 工 具 类 与 过 滤 器 **
** 目 的 **
实 现 JWT 生 成 、 验 证 、 提 取 用 户 名 , 并 通 过 过 滤 器 转 换 为 认 证 信 息 。
** 操 作 **
- ** JWT 工 具 类 JwtUtils **
java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.function.Function;
@Component
public class JwtUtils {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration}")
private long expiration;
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claimsResolver.apply(claims);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
}
- ** JWT 认 证 过 滤 器 JwtAuthFilter **
java
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateToken(jwt)) {
String username = jwtUtils.extractUsername(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("无法设置用户认证: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (headerAuth != null && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
** 第 七 步 : 登 录 接 口 与 资 源 接 口 **
** 目 的 **
编 写 登 录 接 口 认 证 并 生 成 Token , 编 写 受 保 护 资 源 接 口 。
** 操 作 **
- ** 登 录 控 制 器 AuthController **
java
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
@PostMapping("/login")
public ResponseEntity<JwtResponse> login(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String jwt = jwtUtils.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getUsername()));
}
}
** 第 二 部 分 : 权 限 表 扩 展 指 南 --- 基 于 角 色 权 限 模 型 **
** 一 、 数 据 库 扩 展 **
** 目 的 **
新 增 权 限 表 实 现 细 粒 度 权 限 控 制 。
** 操 作 **
- ** 新 增 权 限 表 结 构 **
sql
CREATE TABLE sys_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_symbol VARCHAR(100) NOT NULL UNIQUE COMMENT '权限符号(如 user:add)',
permission_name VARCHAR(100) NOT NULL COMMENT '权限名称'
);
CREATE TABLE sys_role_permission (
role_id BIGINT,
permission_id BIGINT,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES sys_role(id),
FOREIGN KEY (permission_id) REFERENCES sys_permission(id)
);
** 二 、 实 体 类 扩 展 **
** 目 的 **
修 改 角 色 与 用 户 实 体 类 关 联 权 限 。
** 操 作 **
- ** 新 增 权 限 实 体 类 PermissionPo **
java
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Set;
@Data
@TableName("sys_permission")
public class PermissionPo {
private Long id;
private String permissionSymbol;
private String permissionName;
private Set<RolePo> roles;
}
- ** 修 改 角 色 实 体 类 RolePo **
java
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Set;
@Data
@TableName("sys_role")
public class RolePo {
private Long id;
private String roleName;
private Set<PermissionPo> permissions;
private Set<UserPo> users;
}
- ** 修 改 用 户 实 体 类 UserPo 权 限 加 载 **
java
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<>();
for (RolePo role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
for (PermissionPo permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getPermissionSymbol()));
}
}
return authorities;
}
** 三 、 MyBatisPlus Mapper 扩 展 **
** 目 的 **
添 加 权 限 相 关 Mapper 接 口 。
** 操 作 **
- ** PermissionMapper **
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.Optional;
public interface PermissionMapper extends BaseMapper<PermissionPo> {
Optional<PermissionPo> findByPermissionSymbol(@Param("symbol") String symbol);
}
** 四 、 Service 层 扩 展 **
** 目 的 **
实 现 权 限 管 理 服 务 。
** 操 作 **
- ** 权 限 管 理 服 务 PermissionService **
java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class PermissionService {
private final PermissionMapper permissionMapper;
private final RoleMapper roleMapper;
@Transactional
public void assignPermissionToRole(Long roleId, String permissionSymbol) {
RolePo role = roleMapper.selectById(roleId);
PermissionPo permission = permissionMapper.findByPermissionSymbol(permissionSymbol)
.orElseGet(() -> {
PermissionPo p = new PermissionPo();
p.setPermissionSymbol(permissionSymbol);
p.setPermissionName(parsePermissionName(permissionSymbol));
permissionMapper.insert(p);
return p;
});
role.getPermissions().add(permission);
roleMapper.updateById(role);
}
private String parsePermissionName(String symbol) {
return Arrays.stream(symbol.split(":"))
.map(word -> word.substring(0, 1).toUpperCase() + word.substring(1))
.collect(Collectors.joining(" "));
}
}
** 五 、 控 制 器 层 扩 展 --- 使 用 权 限 注 解 **
** 目 的 **
通 过 @PreAuthorize 注 解 控 制 方 法 权 限 。
** 操 作 **
- ** 用 户 控 制 器 使 用 权 限 注 解 **
java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
@PreAuthorize("hasAuthority('user:add')")
public ResponseEntity<?> addUser() {
return ResponseEntity.ok("添加用户");
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('user:delete')")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
return ResponseEntity.ok("删除用户");
}
}
** 第 三 部 分 : 数 据 权 限 控 制 --- 订 单 查 询 只 查 看 自 己 创 建 的 订 单 **
** 一 、 数 据 库 扩 展 --- 订 单 表 **
** 目 的 **
设 计 订 单 表 关 联 创 建 人 。
** 操 作 **
- ** 订 单 表 结 构 **
sql
CREATE TABLE sys_order (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(50) NOT NULL COMMENT '订单编号',
amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
creator_id BIGINT NOT NULL COMMENT '创建人ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (creator_id) REFERENCES sys_user(id)
);
- ** 订 单 实 体 类 OrderPo **
java
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("sys_order")
public class OrderPo {
private Long id;
private String orderNo;
private BigDecimal amount;
private Long creatorId;
private LocalDateTime createdAt;
}
** 二 、 MyBatisPlus Mapper 与 Service **
** 目 的 **
实 现 订 单 数 据 访 问 与 权 限 过 滤 。
** 操 作 **
- ** OrderMapper **
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface OrderMapper extends BaseMapper<OrderPo> {
List<OrderPo> findByCreatorId(@Param("creatorId") Long creatorId);
}
- ** 订 单 服 务 实 现 **
java
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final UserMapper userMapper;
@Override
public List<OrderPo> findMyOrders() {
UserPo currentUser = getCurrentUser();
return orderMapper.findByCreatorId(currentUser.getId());
}
@Override
public OrderPo findOrderById(Long id) {
OrderPo order = orderMapper.selectById(id);
UserPo currentUser = getCurrentUser();
if (!order.getCreatorId().equals(currentUser.getId())) {
throw new AccessDeniedException("无权访问此订单");
}
return order;
}
private UserPo getCurrentUser() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return userMapper.findByUsername(username).orElseThrow();
}
}
** 三 、 控 制 器 层 实 现 **
** 目 的 **
编 写 订 单 相 关 接 口 , 集 成 数 据 权 限 控 制 。
** 操 作 **
- ** 订 单 控 制 器 **
java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@GetMapping
@PreAuthorize("hasAuthority('order:view')")
public ResponseEntity<List<OrderPo>> getMyOrders() {
List<OrderPo> orders = orderService.findMyOrders();
return ResponseEntity.ok(orders);
}
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('order:view')")
public ResponseEntity<OrderPo> getOrderById(@PathVariable Long id) {
OrderPo order = orderService.findOrderById(id);
return ResponseEntity.ok(order);
}
}
** 总 结 **
通 过 以 上 三 部 分 整 合 , 我 们 实 现 了 Spring Security 前 后 端 分 离 接 入 、 角 色 权 限 扩 展 、 数 据 权 限 控 制 的 完 整 流 程 。 核 心 要 点 如 下 :
- ** 基 础 接 入 ** : 实 现 用 户 认 证 与 JWT 授 权 。
- ** 权 限 扩 展 ** : 通 过 权 限 表 与 角 色 关 联 , 实 现 细 粒 度 权 限 控 制 。
- ** 数 据 权 限 ** : 在 Service 层 手 动 过 滤 数 据 , 确 保 用 户 只 能 查 看 自 己 创 建 的 订 单 。
整 个 系 统 兼 顾 了 安 全 性 与 灵 活 性 , 适 用 于 中 小 型 项 目 的 权 限 管 理 需 求 。