当前位置: 首页 > 科技观察

三个注解,微服务认证的优雅实现

时间:2023-03-20 15:06:32 科技观察

实现思路陈老师在之前的文章中,把鉴权和鉴权放在了网关层面。架构如下:微服务中的鉴权还有另外一种思路:将鉴权交给下游的各个微服务,网关层面只做路由和转发。这个想法其实实现起来很简单。网关级认证的代码可以修改如下:实用干货!SpringCloudGateway集成OAuth2.0实现分布式统一认证授权!1.去掉网关中的认证管理器统一认证实际上依赖于认证管理器ReactiveAuthorizationManager,所有的请求都需要经过认证管理器来认证登录用户的权限。这个认证管理器在网关认证一文中也有介绍。Chen的《Spring Cloud Alibaba 实战》中配置拦截也很简单,如下:除了配置的白名单,其他所有请求都必须被网关的认证管理器拦截。拦截鉴权,只有通过鉴权才能放行路由转发给下游服务。看到这里思路不是很清楚,如果想把认证交给下游服务,只需要在网关层直接发布即可,不需要经过认证管理器,代码如下:http....//白名单直接发布。pathMatchers(ArrayUtil.toArray(whiteUrls.getUrls(),String.class)).permitAll()//其他请求直接放行。anyExchange().permitAll().....2.通过第一个Step1定义三个注解,鉴权已经委托给下游服务,那么如何对下游服务进行拦截鉴权呢?其实SpringSecurity提供了3个用于控制权限的注解,如下:@Secured@PreAuthorize@PostAuthorize关于这三个注解,这里就不详细介绍了,有兴趣的可以查看官方文档。Chen不打算使用内置的三个注解实现,而是自定义了三个注解,如下:1)@RequiresLogin众所周知,只有用户登录才能发布,代码如下:/***@author公众号:码猿技术专栏*@url:www.java-family.cn*@description登录认证注意事项,在controller方法上标注,必须是登录后才能访问的接口*/@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD,ElementType.TYPE})public@interfaceRequiresLogin{}2)@RequiresPermissions众所周知,只有指定权限才能释放,代码如下:/***@author公众号:码猿技术专栏*@url:www.java-family.cn*@description标注在controller方法上,确保你有指定权限访问接口*/@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD,ElementType.TYPE})public@interfaceRequiresPermissions{/***待验证权限码*/字符串[]值()默认{};/***验证方式:AND|OR,defaultAND*/Logicallogical()defaultLogical.AND;}3.@RequiresRoles看名字就知道意思,只能??释放指定的角色,代码如下:/***@author公众号:码猿技术专栏*@url:www.java-family.cn*@description标注在controller方法上,确保你有指定角色访问接口*/@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD,ElementType.TYPE})public@interfaceRequiresRoles{/***需要验证的角色ID,默认超级管理员和管理员*/String[]value()default{OAuthConstant.ROLE_ROOT_CODE,OAuthConstant.ROLE_ADMIN_CODE};/***验证逻辑:AND|OR,defaultAND*/Logicallogical()defaultLogical.AND;}上面三个注解的意思想必很容易理解,这里就不多解释了……3.注解段定义注解都有,所以如何拦截他们?这里Chen定义了一个切面进行拦截,关键代码如下:/***@author公众号:码猿技术专栏*@url:www.java-family.cn*@description@RequiresLogin,@RequiresPermissions,@RequiresRoles注解方面*/@Aspect@ComponentpublicclassPreAuthorizeAspect{/***构造*/publicPreAuthorizeAspect(){}/***定义AOP签名(使用认证注解切入所有方法)*/publicstaticfinalStringPOINTCUT_SIGN="@annotation(com.mugu.blog.common.annotation.RequiresLogin)||"+"@annotation(com.mugu.blog.common.annotation.RequiresPermissions)||"+"@annotation(com.mugu.blog.common.annotation.RequiresRoles)";/***声明AOP签名*/@Pointcut(POINTCUT_SIGN)publicvoidpointcut(){}/***环绕切割**@paramjoinPoint切面对象*@return底层方法执行后的返回值*@throws底层方法抛出的Throwable异常*/@Around("pointcut()")publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{//annotationAuthenticationMethodSignature签名=(MethodSignature)joinPoint.getSignature();checkMethodAnnotation(signature.getMethod());try{//执行原来的逻辑Objectobj=joinPoint.proceed();返回对象;}catch(Throwablee){抛出e;}}/***Method对象的注解检查*/publicvoidcheckMethodAnnotation(Methodmethod){//检查@RequiresLogin注解if(requiresLogin!=null){doCheckLogin();}//检查@RequiresRoles注解RequiresRolesrequiresRoles=method.getAnnotation(RequiresRoles.class);如果(requiresRoles!=null){doCheckRole(需要角色);}//检查@RequiresPermissions注解RequiresPermissionsrequiresPermissions=method.getAnnotation(RequiresPermissions.class);如果(requiresPermissions!=null){doCheckPermissions(requiresPermissions);}}/***检查是否有登录*/privatevoiddoCheckLogin(){LoginValloginVal=SecurityContextHolder.get();如果(Objects.isNull(loginVal))抛出新的ServiceException(ResultCode.INVALID_TOKEN.getCode(),ResultCode.INVALID_TOKEN.getMsg());}/***Collat??ion检查是否有对应的角色*/privatevoiddoCheckRole(RequiresRolesrequiresRoles){String[]roles=requiresRoles.value();LoginValloginVal=OauthUtils.getCurrentUser();//登录用户对应的角色String[]authorities=loginVal.getAuthorities();布尔匹配=假;//和逻辑if(requiresRoles.logical()==Logical.AND){match=Arrays.stream(authorities).filter(StrUtil::isNotBlank).allMatch(item->CollectionUtil.contains(Arrays.asList(roles),item));}else{//OR逻辑匹配=Arrays.stream(authorities).filter(StrUtil::isNotBlank).anyMatch(item->CollectionUtil.contains(Arrays.asList(roles),item));}if(!match)thrownewServiceException(ResultCode.NO_PERMISSION.getCode(),ResultCode.NO_PERMISSION.getMsg());}/***TODO自己实现,因为你没有集成前端菜单权限,根据业务需要自己实现*/privatevoiddoCheckPermissions(RequiresPermissionsrequiresPermissions){}}其实逻辑在中间很简单,就是解析出来的Token中的权限和角色,然后在注解中@RequiresPermissions注解的逻辑还没有实现,他根据业务模仿了一下。这是一个思考的问题...4。注解的使用比如《Spring Cloud Alibaba 实战》项目中有一个添加文章的接口,只能添加supermanagement和administrator角色,那么可以使用@RequiresRoles注解来标记,如下:@RequiresRoles@AvoidRepeatableCommit@ApiOperation("AddArticle")@PostMapping("/add")publicResultMsgadd(@RequestBody@ValidArticleAddReqreq){......}效果这里就不演示了。实际效果:非supervisor和administrator角色用户的登录访问,会被直接拦截,未经权限返回。注意:这里只是解决了下游服务认证的问题,那么feigncalling是不是也适用?当然是适用的。这里使用了aspect方法。Feign其实内部使用的是http方法,接口也是适用的。比如《Spring Cloud Alibaba 实战》项目中获取文章列表的接口,会调用评论服务中的接口,通过feign获取文章总评论数。一旦在这里添加了@RequiresRoles,调用就会失败。代码如下:@RequiresRoles@ApiOperation(value="批量获取文章总数")@PostMapping(value="/list/total")publicResultMsg>listTotal(@RequestBody@ValidListparam){....}结束本文主要介绍微服务中如何将认证委托给微服务。也是为了解决读者的疑惑。在实际生产中,除非业务需要,否则陈先生建议在网关中统一进行鉴权。