spring cloud微服务架构中使用自定义注解实现简单的权限控制与权限开关

Posted TuskueNeko

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring cloud微服务架构中使用自定义注解实现简单的权限控制与权限开关相关的知识,希望对你有一定的参考价值。

前言

在微服务架构下开发权限控制一般的做法是,独立开发一个专门用于鉴权的服务,其它服务每次请求接口时都调用鉴权服务鉴权,这样做的好处是,代码耦合低,权限控制功能好扩展,其坏处是每次鉴权都要请求鉴权服务,增加服务器资源消耗,因此我弄了一个简单的权限验证,能满足接口级别的验证,不通过专门的鉴权服务,而是每个服务自己去验证权限。

 

权限验证开关注解

  并非每个服务都需要验证权限,因此我们可以定义一个类似@EnableDiscoery 这样的注解开关来控制:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(AuthenticationInterceptor.class)
public @interface EnableAuthentication 

关键代码是@Import,当你在代码中使用了@EnableAuthentication 注解时,spring 会自动扫描并加载Import注解中的AuthenticationInterceptor类

 

给需要鉴权的接口加上注解

先定义鉴权标记的注解,@Target(ElementType.METHOD,ElementType.TYPE) 表示此注解是用于方法上面的。


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD,ElementType.TYPE)
public @interface CheckPermission 
	/**
	 * @Description: 权限标识代码,请保持注解上的代码和数据库中代码一致 ,声明在类上表示该Controller下所有方法都要验证!
	 * @param: @return      
	 * @return: String      
	 * @throws
	 */
	OptionType[] value() default OptionType.CUSTOM;
	
	/**

	 * @Description: 如果使用自定义操作权限码,请在此配置
	 * @param: @return      
	 * @return: String      
	 * @throws
	 */
	String customPermissionCode() default "";
	
	/**
	 * @Description: 权限状态(暂时用不上)
	 * @param: @return      
	 * @return: int      
	 * @throws
	 */
	//int status() default 0; 

第二个注解是控制具体是增加、删除、修改、还是其它自定义权限,这里的枚举也可以改成字符串,其最终结果都是匹配字符串。


/**
 * 权限操作类型枚举
 * @Description: DETAIL:表示查询详情,LIST:查询列表,UPDATE:全量更新该条数据,UPDATE_SELECTIVE:非全量更新数据,
 * CUSTOM:自定义权限码 ,SKIP:跳过验证
 * controller级别控制请把CheckPermission注解加控制器类上。
 */
public enum OptionType 

	DETAIL,LIST,ADD,UPDATE,UPDATE_SELECTIVE,DELETE,CUSTOM,ALL,SKIP

第三个是用于类似于控制器类上的@RequestMapping

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface PermissionMapping 

	/**
	 * @Description: 权限码的前缀
	  *    如:USER_INFO,数据库中权限码可配置:USER_INFO:ADD,ADD是用于区分操作类型的枚举
	 * @param: @return      
	 * @return: String      
	 * @throws
	 */
	String value() default "";

 

在需要鉴权的接口上加上注解

OptionType.add标记是增加方法,在数据库配置权限码时要和枚举保持一致,两个注解加起来的字符串就成了:USER + ADD,

因此数据库中的权限码可定义为:USER:ADD ,如该用户拥有此方法的权限,可以在数据库中为该用户添加此权限码。

@PermissionMapping("USER")
@RequestMapping("user")
public class UserController

@PostMapping
@CheckPermission(OptionType.Add)
public Object addUser()

    return "add user";


 

 

鉴权拦截器

此处是首先获取该请求的token,然后根据token到redis中获取该用户的权限码,然后根据请求的接口获取该方法上配置的注解,通过两个注解来配置该用户在登陆时保存在redis的权限码,如该用户拥有权限码USER:ADD,PRODUCT:DELETE,在调用上面的方法时,通过获取注解拼接为:USER:ADD来匹配


/**
 * 用于验证权限的拦截器
 * @Description:
 */

public class AuthenticationInterceptor implements HandlerInterceptor
	@Autowired
	StringRedisTemplate redisTemplate;
	@Resource
	Map<String,Set<String>> userInfoCacheMap;
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	@SuppressWarnings("unchecked")
	@Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception 
        if(handler instanceof HandlerMethod) 
            HandlerMethod h = (HandlerMethod)handler;
 //权限码前缀
            PermissionMapping mapping = h.getBeanType().getAnnotation(PermissionMapping.class);
            //类上的权限验证注解 
            CheckPermission classPermission = h.getBeanType().getAnnotation(CheckPermission.class);
            //方法上的权限验证注解
            CheckPermission methodPermission = h.getMethodAnnotation(CheckPermission.class);
            	if(checkPermissionCode(methodPermission)) 
            		String permissionCode = null;
            		String token = null;
            		CheckPermission checkPermission = methodPermission;
            		OptionType[] optionTypes = checkPermission.value();
            		for(OptionType optionType : optionTypes) 
            			
            			if(optionType==OptionType.SKIP) 
                			return true;
                		
            			//如果自定义权限码则拼接customPermissionCode,否则使用枚举
                		permissionCode = mapping.value()+":"+(optionType==OptionType.CUSTOM?checkPermission.customPermissionCode():optionType.toString());
                		token = httpServletRequest.getHeader("Access-Token");
                		if(StringUtils.isEmpty(token)) 
                			throw new AuthenticationException("权限验证token为空!请确认header中token信息是否丢失!");
                		
                		String permissionCacheKey = token+"-permission";
                		//先从缓存中获取
                		Set<String> permissionCodes = userInfoCacheMap.get(permissionCacheKey);
                		if(permissionCodes==null)  
                			String permissionCodesJson = redisTemplate.opsForValue().get(permissionCacheKey);
                			if(permissionCodesJson==null) 
                				throw new AuthenticationException("非法的Token,无权限操作!");
                			
                			permissionCodes = JacksonUtil.readValue(permissionCodesJson, HashSet.class);
                			userInfoCacheMap.put(permissionCacheKey, permissionCodes);
                		else 
                			//TODO 清除缓存可配置化
                			//超过数量直接清除
                			if(userInfoCacheMap.size()>500) 
                				userInfoCacheMap.clear();
                				userInfoCacheMap.put(permissionCacheKey, permissionCodes);
                			
                		
                		//存在权限通过请求
                		if(permissionCodes.contains(permissionCode)) 
                			return true;
                		
            		
            		//403=没有权限,401=未认证、
            		logger.warn("该用户访问了没有权限的请求!请求:,用户信息:",permissionCode,redisTemplate.opsForValue().get(token));
            		httpServletResponse.setStatus(403);
            		return false;
            
        
        return true;
    
	
	private boolean checkPermissionCode(CheckPermission checkPermission) 
		return checkPermission!=null&&!StringUtils.isEmpty(checkPermission.value())?true:false;
	
 

权限开关注解最后的一点配置

要实现权限开关功能还需要在配置类中加一些代码,如果使用了@EnableAuthentication注解,那么在注入AuthenticationInterceptor 类时不会获取到null,此时将该拦截器类加入spring mvc拦截中

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcAutoConfiguration implements WebMvcConfigurer 

	/**
	 * 不强制注入,如果为空,表示并没有开启权限验证开关
	 */
	@Autowired(required = false)
	AuthenticationInterceptor authenticationInterceptor;

	@Override
	public void addCorsMappings(CorsRegistry registry) 
		// 设置允许跨域的路径
		registry.addMapping("/**")
				// 设置允许跨域请求的域名
				.allowedOrigins("*")
				// 是否允许证书 不再默认开启
				.allowCredentials(true)
				// 设置允许的方法
				.allowedMethods("*")
				// 跨域允许时间
				.maxAge(3600);
	

	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) 
		configurer.enable();
	

	/**
	 * 配置spring mvc拦截器
	 */
	@Override
	public void addInterceptors(InterceptorRegistry registry) 
		if (authenticationInterceptor != null) 
			registry.addInterceptor(authenticationInterceptor).addPathPatterns("/**");
		
		WebMvcConfigurer.super.addInterceptors(registry);
	

文章新地址:https://reiner.host/posts/e6208c64.html

以上是关于spring cloud微服务架构中使用自定义注解实现简单的权限控制与权限开关的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud构建微服务架构—服务消费(Feign)

构建微服务架构Spring Cloud:服务消费(Feign)

最新版Spring Cloud Alibaba微服务架构-Ribbon负载均衡篇

最新版Spring Cloud Alibaba微服务架构-Ribbon负载均衡篇

微服务架构 | 8.1 使用 Spring Cloud Stream 整合 Apache kafka #yyds干货盘点#

最新版Spring Cloud Alibaba微服务架构-Openfeign服务调用篇