自定义注解+SpEL实现强大的权限管理

文摘   2024-10-24 08:01   新疆  

最新实战案例锦集:《Spring Boot3实战案例合集》持续更新,每天至少更新一篇文章,订阅后将赠送文章最后展示的所有MD文档(学习笔记)。

环境:Spring Boot 3.2.5



1. 简介

本篇文章我们将不记住任何其它安全框架(如:Spring Security),实现对用户请求进行细粒度的权限控制,并记录关键操作的日志。具体需求通过自定义注解集合SpEL表达式进行权限控制。我们的实现思路是通过自定义 HandlerInterceptor 进行权限控制。

首先,我们会通过JPA完成用户及权限的基本操作,如:登录获取token,查询用户对应的权限等。

其次,通过 HandlerInterceptor 拦截器,用于检查请求头中的Token并验证是否有对应的权限(这其中我们会结合SpEL表达式进行权限的验证)。如果没有足够的权限,则直接返回错误信息。

最后,在HandlerInterceptor结合SpEL表达式完成更加复杂粒度更细的权限控制。

接下来,我们将详细的一步一步实现权限的管理。

2. 实战案例

2.1 依赖管理&配置

我们主要使用的JPA作为持久层操作,AOP进行日志和性能的记录,引入jjwt生成及校验token使用。

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency>  <groupId>io.jsonwebtoken</groupId>  <artifactId>jjwt-api</artifactId>  <version>0.12.6</version></dependency><dependency>  <groupId>io.jsonwebtoken</groupId>  <artifactId>jjwt-impl</artifactId>  <version>0.12.6</version></dependency>

注:我们虽然用了AOP,但是并没有引入starter aop这是因为data-jpa中已经为我们引入了。

配置数据源及JPA相关

spring:  datasource:    driverClassName: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/ddd    username: root    password: xxxooo    type: com.zaxxer.hikari.HikariDataSource---spring:  jpa:    generateDdl: false    hibernate:      ddlAuto: none    openInView: true    show-sql: true

以上是最基本的配置。

2.2 定义实体

用户实体

@Entity@Table(name = "t_user")public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id ; private String username ; private String password ; @OneToMany(cascade = CascadeType.ALL, mappedBy = "user", fetch = FetchType.EAGER) private Set<Permission> permissions = new HashSet<>() ;  // getters, settters}

权限实体

@Entity@Table(name = "t_permission")public class Permission {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id ; private String name ; @ManyToOne(cascade = CascadeType.REFRESH) @JoinColumn(name = "uid") private User user ; // getters, settters}

为了简单起见,我们这里就不引入角色和资源了。

2.3 定义DAO & Service

@Servicepublic class UserService {
private final UserRepository userRepository ; private final JwtUtil jwtUtil ; public UserService(UserRepository userRepository, JwtUtil jwtUtil) { this.userRepository = userRepository ; this.jwtUtil = jwtUtil ; }
public String login(String username, String password) { User user = this.userRepository.findByUsernameAndPassword(username, password) ; if (user == null) { throw new RuntimeException("用户名或密码错误") ; } return jwtUtil.generateToken(user.getId()) ; }
public User queryUser(Long userId) { return this.userRepository.findById(userId).orElse(null) ; }}

该UserService完成以下功能:

  • 验证用户名&密码

  • 根据用户id生成token

  • 根据用户id查询用户完整信息

     

@Servicepublic class PermissionService {
private final PermissionRepository permissionRepository ; public PermissionService(PermissionRepository permissionRepository) { this.permissionRepository = permissionRepository ; }
public List<Permission> findPermissions(Long userId) { return this.permissionRepository.findByUser(new User(userId)) ; }}

该Service主要根据用户的id查询对应的拥有的权限。

他们对应的DAO接口,如下

public interface UserRepository extends JpaRepository<User, Long> {  User findByUsernameAndPassword(String username, String password) ; }public interface PermissionRepository extends JpaRepository<Permission, Long> {  List<Permission> findByUser(User user) ;}


2.4 JWT工具类

@Componentpublic class JwtUtil {
@Value("${jwt.secret}") private String secret ;
@Value("${jwt.expiration}") private Long expiration ;
// 生成 JWT 令牌 public String generateToken(Long userId) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, String.valueOf(userId)); }
// 从令牌中获取用户名 public Long getUserIdFromToken(String token) { return Long.valueOf(getClaimFromToken(token, Claims::getSubject));  }
// 从令牌中获取声明 private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); }
// 获取令牌中的所有声明 private Claims getAllClaimsFromToken(String token) { return (Claims) Jwts.parser() .verifyWith(Keys.hmacShaKeyFor(secret.getBytes())) .build().parse(token).getPayload() ; }
/**创建令牌*/ private String createToken(Map<String, Object> claims, String subject) { return Jwts.builder().claims() .add(claims) .subject(subject) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + expiration * 1000)) .and() .signWith(Keys.hmacShaKeyFor(secret.getBytes())).compact() ;  }}

该工具类用来管理JWT Token。

2.5 拦截器定义

@Componentpublic class TokenInterceptor implements HandlerInterceptor {
private final static String TOKEN_KEY = "X-TOKEN" ;
private final JwtUtil jwtUtil ; private final UserService userService ; public TokenInterceptor(JwtUtil jwtUtil, UserService userService) { this.jwtUtil = jwtUtil ; this.userService = userService ; }
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader(TOKEN_KEY) ; if (!StringUtils.hasLength(token)) { token = request.getParameter("token") ; } if (!StringUtils.hasLength(token)) { response.getWriter().println("Invalid token") ; return false ; } // 解析token;这里需要验证token Long userId = this.jwtUtil.getUserIdFromToken(token) ; SecurityContext.set(this.userService.queryUser(userId)) ; return true ; }}

该拦截器用来从请求中获取token信息,然后解析token获取对应用户信息,存入到当前的上下文中(ThreadLocal)。

@Componentpublic class AuthInterceptor implements HandlerInterceptor {
private final PermissionService permissionService ; public AuthInterceptor(PermissionService permissionService) { this.permissionService = permissionService ; }
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; PreAuthorize preAuthorize = handlerMethod.getMethodAnnotation(PreAuthorize.class); if (preAuthorize != null) { User user = SecurityContext.get() ; if (user == null) { response.getWriter().write("Goto login"); return false ; } List<String> allowedPermissions = Arrays.asList(preAuthorize.value()) ; List<Permission> permissions = this.permissionService.findPermissions(user.getId()) ; if (!hasAllowedPermission(allowedPermissions, permissions)) { response.getWriter().write("Access denied"); return false ; } } } return true ; }
private boolean hasAllowedPermission(List<String> allowedPermissions, List<Permission> permissions) { List<String> permissionNames = permissions.stream().map(Permission::getName).collect(Collectors.toList()); return allowedPermissions.stream().anyMatch(permissionNames::contains);  } }

该拦截器用来验证当前登录用户是否对当前请求的方法具有访问权限。这里我们仅仅是通过权限字符串匹配,在最后我们会结合SpEL表达式来完成更加复杂的权限验证。

注册拦截器

@Componentpublic class InterceptorConfig implements WebMvcConfigurer {
private final TokenInterceptor tokenInterceptor ; private final AuthInterceptor authInterceptor ; public InterceptorConfig(TokenInterceptor tokenInterceptor, AuthInterceptor authInterceptor) { this.tokenInterceptor = tokenInterceptor ; this.authInterceptor = authInterceptor ; }
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenInterceptor).order(-2).addPathPatterns("/api/**") ; registry.addInterceptor(authInterceptor).order(-1).addPathPatterns("/api/**") ; }}

这里对拦截器配置了顺序,确保Token的拦截器优先于Auth拦截器执行。

自定义注解

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface PreAuthorize {
String value() ;}

以上是关于拦截器相关类的定义。

2.6 Controller接口定义

@RestController@RequestMapping("/users")public class UserController {
private final UserService userService ; public UserController(UserService userService) { this.userService = userService ; }
@GetMapping("/login") public String login(String username, String password) { return this.userService.login(username, password) ; }}

用户登录接口,成功则返回JWT Token。

@RestController@RequestMapping("/api")public class ApiController {
  @PreAuthorize("api.save") @GetMapping("/save") public Object save() { return "save" ; }
  @PreAuthorize("api.update") @GetMapping("/update") public Object update() { return "update" ;  }}

接下来,我们添加数据,如下:

测试结果如下

直接访问,没有携带token返回该错误

登录操作

使用该token访问上面的/api/save接口

接下来,我们将结合SpEL表达式应用更加复杂的权限验证。

2.8 整合SpEL表达式

将AuthInterceptor改造如下

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  if (handler instanceof HandlerMethod hm) {    HandlerMethod handlerMethod = (HandlerMethod) handler;    PreAuthorize preAuthorize = handlerMethod.getMethodAnnotation(PreAuthorize.class);    if (preAuthorize != null) {      User user = SecurityContext.get() ;      if (user == null) {        response.getWriter().write("Goto login");        return false ;      }      String expressionString = preAuthorize.value();      MethodBasedEvaluationContext context = createContext(hm) ;      Object value = parser.parseExpression(expressionString).getValue(context) ;      // 如果是字符串类型,则自行进行权限的判断      if (value instanceof String) {        List<String> allowedPermissions = Arrays.asList(value.toString()) ;        List<Permission> permissions = this.permissionService.findPermissions(user.getId()) ;        if (!hasAllowedPermission(allowedPermissions, permissions)) {          response.getWriter().write("Access denied");          return false ;        }      }       // 如果SpEL表达式返回的是boolean      else if (value instanceof Boolean ret) {        if (!ret) {          response.getWriter().write("Access denied");          return ret ;        }        return ret ;      }    }  }  return true ;}

添加如下接口进行测试

@PreAuthorize("username eq 'admin'")@GetMapping("/delete")public Object delete() {  return "delete" ;}

该SpEL表达式:如果当前登录用户名是 admin 则返回true(放行)。

以上就完成了权限验证与SpEL表达式的结合。

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

实体与DTO如何转换?这个工具很厉害

Spring Boot自定义注解玩转Controller接口参数任意转换

提升性能!彻底玩转缓存在Spring Boot中的各种应用技巧

Spring Boot 3声明式接口,完全可以替换OpenFeign

Spring Boot3新特性@RSocketExchange轻松实现消息实时推送

虚拟线程在Spring Boot中的应用及性能对比

Spring AOP高级知识你知道多少?

Spring6.1 异步和定时任务新特性,太实用了

在SpringBoot中拦截修改请求Body的2种正确方式

RabbitMQ非常实用技巧,动态调整消息并发处理能力

请牢记SpringBoot这7个强大的隐藏Bean

Spring这个大坑要注意啦!会导致资源泄漏

你的项目中是否运用了@ResponseStatus注解?你真的会用?

Spring全家桶实战案例源码
spring, springboot, springcloud 案例开发详解
 最新文章