JAVA——通过自定义注解实现每次程序启动时,自动扫描被注解的方法,获取其路径及访问该路径所需的权限并写入数据库

Posted 叶不修233

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA——通过自定义注解实现每次程序启动时,自动扫描被注解的方法,获取其路径及访问该路径所需的权限并写入数据库相关的知识,希望对你有一定的参考价值。

JAVA——通过自定义注解实现每次程序启动时,自动扫描被注解的方法,获取其路径及访问该路径所需的权限并写入数据库

一、需求背景

在spring cloud微服务项目中,要想实现鉴权,有以下两个方案:

  • 方案一:和网关关系不是特别大。auth-service 进行登录认证;各个微服务自己去做鉴权。
    网关最多也就只是做一些 jwt-token 的合法性校验(判断它是否是伪造的)。

  • 方案二:和网关关系很大。auth-service 进行登录认证;gate网关进行鉴权;各个微服务就不用再做鉴权(不需要去"引" spring-security)。

这里选择方案二:
方案二的优势在于:

  • 整个认证鉴权的工作的"参与方"变少了,更利于 bug 的定位和编码;
  • 各个微服务之间的调用变简单了,不用再相互之间传递 jwt-token 了。

但是方案二的"代价":

以前某个路径需要什么权限才能访问,是在 spring-security 的配置类中配置,或者是使用注解标明。现在需要将 路径-访问权 的关系存到数据库中

那么就要求我们每写一个controller方法,都需要在 路径-访问权 表中插入一条数据,来表示访问该方法的路径所需要的权限

因此决定采用自定义注解的方法,在需要进行权限控制的controller方法上面加上注解:
用注解的value来表示访问该路径所需要的权限。
在每次微服务启动的时候自动获取这些注解,并将这些信息写入 路径-访问权 表中。

二、实现步骤

1.自定义注解

在需要扫描的微服务项目中新建一个自定义注解,因为这里定义的注解用于鉴权,因此给该注解取名叫做MyAccessControlAnnotation

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//@Retention--用于指定注解保留范围:SOURCE源码 --> CLASS字节码 --> RUNTIME运行
@Retention(RetentionPolicy.RUNTIME)
//@Target--用于标记这个注解应该是哪种 Java 成员,METHOD用于放方法头上
@Target(ElementType.METHOD)
public @interface MyAccessControlAnnotation 
    //自定义成员变量:role,默认值是""
    String role() default "";
    //自定义成员变量:permission,默认值是""
    String permission() default "";

2.使用注解

在需要进行权限控制的方法头上添加上面定义好的注解。
如图所示,这里在user-service中的查询所有用户方法上加上了权限控制
role = “ROLE_ADMIN”
即,访问这个方法需要角色ROLE_ADMIN

在根据id查询用户信息这个方法上加上了权限控制
role = “ROLE_QUERY”
即,访问这个方法需要角色ROLE_QUERY

全部代码如下:

package com.example.controller;


import com.example.anno.MyAccessControlAnnotation;
import com.example.converter.UserDtoConverter;
import com.example.dao.po.UserPo;
import com.example.http.dto.UserDto;
import com.example.repository.UserRepository;
import com.example.service.impl.UserServiceImpl;
import com.example.util.ResponseResult;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

/**
 * <p>
 * 用户表 前端控制器
 * </p>
 *
 * @author z
 * @since 2022-09-15
 */
@RestController
@RequestMapping("/user")
public class UserController 
    @Resource
    private UserServiceImpl userService;
    @Resource
    private UserDtoConverter userDtoConverter;
    @Resource
    private UserRepository userRepository;

    /**
     * 查询所有用户
     * @return
     */
    @MyAccessControlAnnotation(role = "ROLE_ADMIN")
    @GetMapping("queryAll")
    public ResponseResult<List<UserDto>> queryAllUser()
        List<UserDto> userDtoList = userDtoConverter.toDto(userService.list());
        return new ResponseResult<>(200,"ok",userDtoList);
    

    /**
     * 注册用户
     * @param user
     * @return
     */
    @PostMapping("register")
    public ResponseResult<Boolean> register(UserPo user)
       Boolean result = userService.register(user);
       return new ResponseResult<Boolean>(200,"ok",result);
    

    /**
     * 通过id查询用户信息
     * @param id
     * @return
     */
    @MyAccessControlAnnotation(role = "ROLE_QUERY")
    @GetMapping("getById")
    public ResponseResult<UserDto> getUserById(@RequestParam("id") Long id)
        UserDto userDto = userRepository.getById(id);
        return new ResponseResult<>(200,"ok",userDto);
    

    /**
     * 通过角色id查询用户列表
     * @param roleId
     * @return
     */
    @GetMapping("getListByRoleId")
    public ResponseResult<List<UserDto>> getUserListByRoleId(@RequestParam("roleId") Long roleId)
        List<UserDto> userDtoList = userRepository.getListByRoleId(roleId);
        return new ResponseResult<>(200,"ok",userDtoList);
    

    /**
     * 通过用户名查询用户信息
     * @param account
     * @return
     */
    @GetMapping("getByAccount")
    public ResponseResult<UserDto> getUserByAccount(@RequestParam("account") String account)
        UserDto userDto = userRepository.getByAccount(account);
        return new ResponseResult<>(200,"ok",userDto);
    

    /**
     * 通过id更新用户状态
     * @param id
     * @param statusId
     * @return
     */
    @PostMapping("updateStatus")
    public ResponseResult<Boolean> updateUserStatus(@RequestParam("id") Long id,
                                                @RequestParam("statusId") Long statusId)
        Boolean result = userService.updateStatus(id,statusId);
        return new ResponseResult<Boolean>(200,"ok",result);
    

    /**
     * 通过id更新用户角色
     * @param id
     * @param roleId
     * @return
     */
    @PostMapping("updateRole")
    public ResponseResult<Boolean> updateUserRole(@RequestParam("id") Long id,
                                                @RequestParam("roleId") Long roleId)
        Boolean result = userService.updateRole(id,roleId);
        return new ResponseResult<Boolean>(200,"ok",result);
    


3.利用反射获取和处理注解相关信息

定义一个方法handle()。
扫描被自定义注解修饰的方法。
将访问方法的路径和注解里设置的权限写入数据库。

MyAccessControlAnnotationHandle.java

package com.example.config.handle;

import com.example.anno.MyAccessControlAnnotation;
import com.example.http.api.AuthServiceClient;
import com.example.http.dto.ServiceUriAuthorityDto;
import com.example.util.ResponseResult;
import org.reflections.Reflections;
import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Set;

@Component
public class MyAccessControlAnnotationHandle 
    @Resource
    private AuthServiceClient authServiceClient;

    public void handle()
        //扫描这个包下所有被@RestController注解修饰的类
        Reflections reflections = new Reflections("com.example.controller");
        Set<Class<?>> restController = reflections.getTypesAnnotatedWith(RestController.class);
        //遍历这些类
        restController.forEach(aClass -> 
            //获取类中所有的方法
            Method[] methods = aClass.getDeclaredMethods();
            for (int i = 0; i < methods.length; i++) 
                //遍历类里的方法,获取被我们自定义注解修饰的方法
                if(methods[i].isAnnotationPresent(MyAccessControlAnnotation.class))
//                    System.out.println(aClass.getSimpleName()+"类中被我自定义注解修饰的方法");
//                    System.out.println(methods[i].getName());
                    //获取类和方法中的一些信息
                    String path1 = "";
                    String path2 = "";
                    String methodType = "";
                    if(aClass.isAnnotationPresent(RequestMapping.class))
                        path1=aClass.getAnnotation(RequestMapping.class).value()[0];
                    ;

                    if(methods[i].isAnnotationPresent(RequestMapping.class))
                        path2 = methods[i].getAnnotation(RequestMapping.class).value()[0];
                    ;
                    if(methods[i].isAnnotationPresent(GetMapping.class))
                        path2 = methods[i].getAnnotation(GetMapping.class).value()[0];
                        methodType = "GET";
                    ;
                    if(methods[i].isAnnotationPresent(PostMapping.class))
                        path2 = methods[i].getAnnotation(PostMapping.class).value()[0];
                        methodType = "POST";
                    ;
                    //获取到自定义注解对象
                    MyAccessControlAnnotation annotation = methods[i].getAnnotation(MyAccessControlAnnotation.class);
                    //获取自定义注解中的成员属性值
                    String role = annotation.role();
                    String permission = annotation.permission();
                    //拼接以上得到的信息,用于写入数据库
                    if(!"".equals(path1))
                        path1 = path1 +"/";
                    
                    //拼接得到的访问该方法的路径
                    String uri = "/user-service"+path1+path2;
                    //从注解的成员属性值中获取到访问该方法所需的角色或权限
                    String authority = role+","+permission;
                    if(authority.endsWith(","))
                        authority = authority.substring(0, authority.length()-1);
                    
                    //把以上信息写入数据库
                    ServiceUriAuthorityDto serviceUriAuthorityDto = new ServiceUriAuthorityDto(methodType,uri,authority);
//                    System.out.println(serviceUriAuthorityDto);
					//如果路径已存在,删除数据库里这些路径对应的数据
                    authServiceClient.deleteUriByUri(uri);
                    //写入路径对应的数据到数据库
                    ResponseResult<Boolean> booleanResponseResult = authServiceClient.addUri(serviceUriAuthorityDto);
                    if (booleanResponseResult.getData())
                        System.out.println(methods[i]+"方法的访问权限已初始化。");
                    else 
                        System.out.println(methods[i]+"方法访问权限初始化失败!");
                    
                
            
             ) ;
    


为了概念上更加便于理解,写入数据库的数据长这样子,它来自于这里:

4.设置每次微服务启动时运行方法

配置一个监听事件,用于在每次微服务启动时,自动运行上面定义好的方法。
这样,每次微服务启动,系统都会自动扫描所有的controller类下面被我们自定义的注解@MyAccessControlAnnotation修饰的方法,并将访问该方法的路径和所需权限写入数据库。

package com.example.config;

import com.example.anno.MyAccessControlAnnotation;
import com.example.config.handle.MyAccessControlAnnotationHandle;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Slf4j
@Component
public class SimpleApplicationStartedEventListener implements ApplicationListener<ApplicationStartedEvent> 

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private MyAccessControlAnnotationHandle myAccessControlAnnotationHandle;

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) 
        //在每次微服务启动的时候调用这个方法
        myAccessControlAnnotationHandle.handle();
        log.info("已写入访问路径和权限到数据库");
    


以上是关于JAVA——通过自定义注解实现每次程序启动时,自动扫描被注解的方法,获取其路径及访问该路径所需的权限并写入数据库的主要内容,如果未能解决你的问题,请参考以下文章

JAVA——通过自定义注解实现每次程序启动时,自动扫描被注解的方法,获取其路径及访问该路径所需的权限并写入数据库

java注解如何自动触发?

自定义SpringBoot Starter 通过注解启动装配

通过注解实现自定义Spring Boot Starter自动装配

如何实现自定义Java运行时注解功能

java元注解,局部注解怎么解析