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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值