引言
在项目中,可以通过自定义权限校验逻辑结合 @PreAuthorize
注解实现细粒度的权限控制。@PreAuthorize
允许在方法调用前执行权限校验,通过 SpEL(Spring Expression Language)
表达式调用自定义的权限校验方法。
添加依赖
在 pom.xml
配置文件中添加以下依赖:
js
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在 xiaomayi-common/xiaomayi-security
模块中已经引入此依赖,在实际使用时直接引入以下依赖即可:
js
<!-- 安全认证依赖模块 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-security</artifactId>
</dependency>
自定义权限校验服务
创建一个服务类,用于实现权限校验逻辑。例如,检查当前用户是否拥有某个权限节点。
js
package com.xiaomayi.security.service;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.security.constant.SecurityConstant;
import com.xiaomayi.security.security.LoginUser;
import com.xiaomayi.security.utils.SecurityUtils;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 自定义权限
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
// 此处给服务起别名,便于节点权限设置时使用
@Service("pms")
public class PermissionService {
/**
* 自定义节点权限
*
* @param authority 权限标识
* @return 返回结果
*/
public boolean hasAuthority(String authority) {
// 权限节点判空
if (StringUtils.isEmpty(authority)) {
return false;
}
// 获取当前用户
LoginUser user = SecurityUtils.getLoginUser();
if (StringUtils.isNull(user)) {
return false;
}
// 获取权限节点集合
List<String> permissions = user.getPermissions();
// 判断是否拥有全部权限
if (permissions.contains(SecurityConstant.ALL_PERMISSION)) {
return true;
}
// 判断是否拥有节点权限
if (permissions.contains(authority)) {
return true;
}
return false;
}
/**
* 自定义角色权限
*
* @param authority 权限标识
* @return 返回结果
*/
public boolean hasRole(String authority) {
// 权限节点判空
if (StringUtils.isEmpty(authority)) {
return false;
}
// 获取当前用户
LoginUser user = SecurityUtils.getLoginUser();
if (StringUtils.isNull(user)) {
return false;
}
// 获取角色权限集合
List<String> roles = user.getRoles();
// 判断是否拥有超管权限
if (roles.contains(SecurityConstant.SUPER_ADMIN)) {
return true;
}
// 判断是否拥有角色权限
if (roles.contains(authority)) {
return true;
}
return false;
}
}
启用方法级安全控制
在 [安全认证服务] 章节中的 SecurityConfig
规则配置类中启用方法级安全控制:
js
// 启用@PreAuthorize等注解
@EnableMethodSecurity(prePostEnabled = true)
js
package com.xiaomayi.security.config;
import com.xiaomayi.security.filter.JwtAuthenticationTokenFilter;
import com.xiaomayi.security.filter.ResponseFilter;
import com.xiaomayi.security.handler.*;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.CorsFilter;
import java.util.stream.Collectors;
/**
* <p>
* 安全认证配置类
* 特别提醒:新版本中WebSecurityConfigurerAdapter已启用,目前采用新的写法
* 注意:SpringSecurity 6 没有了需要继承类这个做法,但是需要配置注解
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableGlobalAuthentication
@AllArgsConstructor
public class SecurityConfig {
// 以下内容全部省略...
}
在方法上使用
在需要权限控制的方法上使用 @PreAuthorize
注解,调用自定义的权限校验方法,以 用户管理
模块控制器为例。
js
package com.xiaomayi.admin.controller;
import cn.hutool.core.util.RandomUtil;
import com.itextpdf.text.DocumentException;
import com.xiaomayi.core.config.AppConfig;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.excel.annotation.RequestExcel;
import com.xiaomayi.excel.annotation.ResponseExcel;
import com.xiaomayi.logger.annotation.RequestLog;
import com.xiaomayi.logger.enums.RequestType;
import com.xiaomayi.security.utils.SecurityUtils;
import com.xiaomayi.system.dto.user.*;
import com.xiaomayi.system.service.UserService;
import com.xiaomayi.system.utils.ParamResolver;
import com.xiaomayi.system.vo.user.UserExcelVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/**
* <p>
* 用户 前端控制器
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-03-23
*/
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户管理")
@AllArgsConstructor
public class UserController {
private final UserService userService;
/**
* 查询分页列表
*
* @param userPageDTO 查询条件
* @return 返回结果
*/
@Operation(summary = "查询分页列表", description = "查询分页列表")
@PreAuthorize("@pms.hasAuthority('sys:user:page')")
@GetMapping("/page")
public R page(UserPageDTO userPageDTO) {
return R.ok(userService.page(userPageDTO));
}
/**
* 查询数据列表
*
* @param userListDTO 查询条件
* @return 返回结果
*/
@Operation(summary = "查询数据列表", description = "查询数据列表")
@PreAuthorize("@pms.hasAuthority('sys:user:list')")
@GetMapping("/list")
public R getList(UserListDTO userListDTO) {
return R.ok(userService.getList(userListDTO));
}
/**
* 根据ID查询详情
*
* @param id 用户ID
* @return 返回结果
*/
@Operation(summary = "根据ID查询详情", description = "根据ID查询详情")
@PreAuthorize("@pms.hasAuthority('sys:user:detail')")
@GetMapping("/detail/{id}")
public R getDetail(@PathVariable("id") Integer id) {
return R.ok(userService.getDetail(id));
}
/**
* 添加用户
*
* @param userAddDTO 参数
* @return 返回结果
*/
@Operation(summary = "添加用户", description = "添加用户")
@RequestLog(title = "添加用户", type = RequestType.INSERT, exclude = {"password"})
@PreAuthorize("@pms.hasAuthority('sys:user:add')")
@PostMapping("/add")
public R add(@RequestBody @Validated UserAddDTO userAddDTO) {
// 登录密码
String password = userAddDTO.getPassword();
if (StringUtils.isNotEmpty(password)) {
// 加密盐
String salt = RandomUtil.randomString(10);
userAddDTO.setSalt(salt);
// 密码基于SpringSecurity加密处理
userAddDTO.setPassword(SecurityUtils.encryptPassword(salt + password));
} else {
userAddDTO.setPassword(null);
}
// 租户ID
userAddDTO.setTenantId(SecurityUtils.getTenantId());
return userService.add(userAddDTO);
}
/**
* 更新用户
*
* @param userUpdateDTO 参数
* @return 返回结果
*/
@Operation(summary = "更新用户", description = "更新用户")
@RequestLog(title = "更新用户", type = RequestType.UPDATE, exclude = {"password"})
@PreAuthorize("@pms.hasAuthority('sys:user:update')")
@PutMapping("/update")
public R update(@RequestBody @Validated UserUpdateDTO userUpdateDTO) {
// 登录密码
String password = userUpdateDTO.getPassword();
if (StringUtils.isNotEmpty(password)) {
// 加密盐
String salt = RandomUtil.randomString(10);
userUpdateDTO.setSalt(salt);
// 密码基于SpringSecurity加密处理
userUpdateDTO.setPassword(SecurityUtils.encryptPassword(salt + password));
} else {
userUpdateDTO.setPassword(null);
}
return userService.update(userUpdateDTO);
}
/**
* 删除用户
*
* @param id 记录ID
* @return 返回结果
*/
@Operation(summary = "删除用户", description = "删除用户")
@RequestLog(title = "删除用户", type = RequestType.DELETE)
@PreAuthorize("@pms.hasAuthority('sys:user:delete')")
@DeleteMapping("/delete/{id}")
public R delete(@PathVariable Integer id) {
List<Integer> idList = Collections.singletonList(id);
return userService.delete(idList);
}
/**
* 批量删除用户
*
* @param idList 记录ID
* @return 返回结果
*/
@Operation(summary = "批量删除用户", description = "批量删除用户")
@RequestLog(title = "批量删除用户", type = RequestType.BATCH_DELETE)
@PreAuthorize("@pms.hasAuthority('sys:user:batchDelete')")
@DeleteMapping("/batchDelete")
public R batchDelete(@RequestBody @Validated List<Integer> idList) {
return userService.delete(idList);
}
/**
* 重置用户密码
*
* @param userResetPwdDTO 参数
* @return 返回结果
*/
@Operation(summary = "重置用户密码", description = "重置用户密码")
@RequestLog(title = "重置用户密码", type = RequestType.RESET_PWD)
@PreAuthorize("@pms.hasAuthority('sys:user:resetPwd')")
@PutMapping("/resetPwd")
public R resetPwd(@RequestBody @Validated UserResetPwdDTO userResetPwdDTO) {
// 加密盐
String salt = RandomUtil.randomString(10);
userResetPwdDTO.setSalt(salt);
// 获取租户默认密码参数
String userPassword = ParamResolver.getParamValue("USER_DEFAULT_PASSWORD", "123456");
// 设置密码
userResetPwdDTO.setPassword(SecurityUtils.encryptPassword(salt + userPassword));
return userService.resetPwd(userResetPwdDTO);
}
/**
* 导入用户
*
* @param userExcelVOList 导入Excel
* @return 返回结果
*/
@Operation(summary = "导入用户", description = "导入用户")
@RequestLog(title = "导入用户", type = RequestType.IMPORT)
@PreAuthorize("@pms.hasAuthority('sys:user:import')")
@PostMapping("/import")
public R importExcel(@RequestExcel List<UserExcelVO> userExcelVOList) {
return userService.importExcel(userExcelVOList);
}
/**
* 导出用户
*
* @return 返回结果
*/
@Operation(summary = "导出用户", description = "导出用户")
@RequestLog(title = "导出用户", type = RequestType.EXPORT)
@PreAuthorize("@pms.hasAuthority('sys:user:export')")
@ResponseExcel(name = "用户信息", sheetName = "用户信息")
@GetMapping("/export")
public List<UserExcelVO> exportExcel() {
return userService.exportExcel();
}
/**
* 生成文档
* 特别备注:此处文档生成仅为提供以编码的方式生成PDF文件案例参考,不涉及任何业务;
* 以此举一反三,理解逻辑后可以根据实际业务生成任何你所需要的文档,包括动态追加写入数据、图片等等;
*
* @param userId 用户ID
* @return 返回结果
* @throws IOException 异常处理
* @throws DocumentException 异常处理
*/
@Operation(summary = "生成文档", description = "生成文档")
@RequestLog(title = "生成文档", type = RequestType.OTHER)
@PreAuthorize("@pms.hasAuthority('sys:user:document')")
@GetMapping("/document/{userId}")
public R generateDocument(@PathVariable("userId") Integer userId) throws IOException, DocumentException {
return userService.generateDocument(userId);
}
}
常用权限节点控制实现:
// 原生写法,需完全匹配
@PreAuthorize("hasAuthority('sys:demo:index')")
// 满足其一即可访问资源
@PreAuthorize("hasAnyAuthority('test','admin','sys:demo:index')")
// 原生写法,需完全匹配
@PreAuthorize("hasRole('admin')")
// 原生写法,满足其一即可访问资源
@PreAuthorize("hasAnyRole('test','ADMIN')")
// 自定义写法,需完全匹配
@PreAuthorize("@pms.hasAuthority('sys:demo:index')")
总结
通过以上方案,你可以在项目中实现基于 @PreAuthorize
的自定义权限节点控制,满足复杂的权限管理需求。
1. 核心功能:
通过 @PreAuthorize 注解实现方法级权限控制。
结合自定义权限校验服务,实现灵活的权限节点控制。
2. 优点:
细粒度的权限控制,适合复杂业务场景。
与 Spring Security 无缝集成,配置简单。
3. 适用场景:
需要基于权限节点控制资源访问的应用。
需要动态权限管理的系统。
4. 注意事项:
权限节点设计应清晰合理,避免过于复杂。
生产环境中建议将权限数据存储在数据库中,并结合缓存提高性能。