Springcloud的一种权限实现方案,zuul和shiro

探讨下几种SpringCloud权限实现方案,以zuul和shiro的思路做了实现,简写了搭建过程.

github地址https://github.com/MeloFocus/FocusCloudWork

Springcloud的一种权限实现方案,zuul和shiro_第1张图片

一.目标

1.外部请求统一从网关zuul进入,并且服务内部互相调用接口要校验权限

2.cloud和shiro结合,达到单点登录,和集中一个服务完成权限管理,其他业务服务不需要关注权限如何实现

3.其他服务依然可以控制权限细粒度到接口,如在接口上使用@RequirePermisson等注解,方便开发

二.思路

SpirngCloud zuul网关有两个作用,一个是分配路由,一个是过滤。zuul的过滤器作用有限,只能简单的做一些某个url是否能够访问之类的,无法像shiro一样细粒度到某个用户是否有某种权限;

shiro单体应用大家都会做,那变成微服务后,难道每个服务都要写一套shiro框架?这显然也太麻烦。这么几个思路:

1.在zuul服务里用shiro,做成动态url权限控制,就是把访问哪个url需要用什么权限,写入数据库,在过滤器读取与用户有的权限作对比;但是服务互相调用校验就行不通了,因为服务间调用不通过zuul

2.写一个服务专用于shiro认证和授权,包含用户、权限的curd,暴露出查询一个用户拥有什么权限的接口;在其他服务中,都写一个拦截器拿访问者token去授权服务拿此用户的权限,再跟请求的url对比;或者可以自定义注解用aop,注解标注的是访问此url需要什么权限,远程调用授权服务接口查询当前用户所有权限,与请求的url对比。

但是这个要自己实现拦截器。

3.第二种思路的简单版本。

server服务:专用于shiro认证和授权,包含用户、权限的curd,暴露出查询一个用户拥有什么权限的接口;

client项目:打成jar包供其他服务依赖,用shiro,client不同于server服务的是:

  • 1.在realm中只有授权方法,没有认证方法,因为不需要client认证、需要server认证。将登陆地址配置为server服务的地址。这样未登录的用户都会跳转到server服务登录,想办法保存下原路径,登录成功后再返回原服务.
  • 并且client的realm授权方法是调用server服务接口查询权限,再返回给client项目的安全管理器。同时做成session共享

其他业务服务:只需要依赖于client。

这种思路来自于《跟我学shiro》的多项目集中权限,其实想想这种思路是可以的,shiro本质也是靠拦截器进行权限校验,虽然相当于每个服务都开启了一套shiro,但也就是容器中多了一些shiro拦截器和实例,而且可以用shiro的各种功能,开发方便。可以完成我们的三个目标。

三.具体实现

因为shiro和cloud的细节太多,这里就不赘述,以下内容默认读者掌握shiro和springcloud基本组件。

我使用的是Finchley.BUILD-SNAPSHOT版本。

我们建两个服务Base和Notice,Base负责权限认证,用户访问Notice服务时先跳转到Base服务校验。

1.建立基本eureka,zuul,服务Base,服务Notice

将服务Base、Notice、zuul注册到eureka,能够通过zuul访问两个服务,不再赘述。

贴一下zuul的配置:

server:
  port: 18900

spring:
  application:
    name: focus-zuul
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/focus_cloud?useUnicode=true&characterEncoding=utf-8
    username: root
    password: pangtiemin

eureka:
  instance:
    prefer-ip-address: true
      #metadata-map:
    #zone: zone1 #此实例所处的zone
  client:
    #availability-zones: #可获得的region和其zone有哪些
    #xian: zone1
    #region: xian #此实例所处的 region
    serviceUrl:
      defaultZone: http://localhost:18800/eureka/
      #zone1: http://localhost:18800/erueka

zuul:
  #设置请求超时时间
  connect-timeout-millis: 15000 #HTTP连接超时要比Hystrix的大
  socket-timeout-millis: 60000   #socket超时
  #SendErrorFilter:
   # error:
   #   disable: true #禁用zuul默认的异常过滤器
  errorControllerUrl: /error #自定义配置,异常处理接口
  routes:
    focus-base: # 通过服务名serviceId路由,不通过具体的url
      path: /base/**
      serviceId: focus-base
      #默认敏感头是"Cookie", "Set-Cookie", "Authorization"这三项,取消这三项,向下游服务请求带上这些headers
      #Access-Control-Allow-Origin,Access-Control-Allow-Methods 解决其他服务的js向zuul发起请求的跨域问题
      sensitiveHeaders: Access-Control-Allow-Origin,Access-Control-Allow-Methods
    focus-notice: # 通过服务名serviceId路由,不通过具体的url
      path: /notice/**
      serviceId: focus-notice
      sensitiveHeaders: Access-Control-Allow-Origin,Access-Control-Allow-Methods

ribbon:
  ReadTimeout: 120000
  ConnectTimeout: 30000

2.在base服务搭建shiro

  • 关于登陆、用户、角色、权限的接口都写在base服务中。要求能在base中单独认证成功。
  • 使用redis做权限缓存,其他服务认证每次都访问base服务压力比较大,可以优先从缓存拿数据。如果你的shiro安全管理器和seesiondao同时实现了cache接口,直接将redis管理器注入安全管理器即可;或者可以用网上重写的redissessiondao,具体可看我这篇https://blog.csdn.net/u014203449/article/details/80888637
  • 用feign推荐的项目格式。maven父模块包含两个子模块。api模块写暴露出的接口,impl模块写具体的实现。

Springcloud的一种权限实现方案,zuul和shiro_第2张图片

  • 在base的接口中写一个通过用户id查询权限码的接口,用feign供其他服务调用。
@FeignClient(name = FocusMicroBaseConstants.SERVICE_APP_ID)
public interface BaseAuthorityRestService {

    @GetMapping
    public Message> getAuthorityByUser(String userId);

}

3.建立一个名为uaa项目

uaa项目依赖于base-api(要用base的接口)、open-feign(通过feign调用) 。此项目以后要打包,给notice之类的业务服务使用。

在uaa项目中也搭建shiro。与base服务的区别是,uaa的shiro配置中将登陆接口定位到base服务登陆接口。并且要从zuul入口访问,http://localhost:18900/base/loginpage,18900是zuul的端口,统一通过zuul访问登陆页面,因为base有可能是多实例的,而且如果直接访问base浏览器会暴露出base服务的地址,我们只应该暴露出zuul的地址。

页面里的js css也要从zuul/服务名访问,否则会访问失败。

@Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        //访问的是后端url的地址,这里要写base 服务的公用登录接口。
        shiroFilterFactoryBean.setLoginUrl("http://localhost:18900/base/loginpage");

        // 登录成功后要跳转的链接;现在应该没用
        //shiroFilterFactoryBean.setSuccessUrl("/index");

        // 未授权界面;可以写个公用的403页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

 
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginpage", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html#", "anon");

        filterChainDefinitionMap.put("/base/test", "authc");


        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        System.out.println("Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }

uaa的realm不写doGetAuthenticationInfo认证逻辑,只写校验权限逻辑。并且校验逻辑获取此用户的权限码,是通过步骤2中base的接口获取的。

因为我的realm不是通过spring注入到 ShiroFilterFactoryBean 的,所以无法在realm中用@Autowired直接调用feign。但当spring启动成功后,注解FeignClient的base接口实例已经注入到了spring中,我这里从spring中手动获取BaseAuthorityRestService 实例再使用。

public class ShiroClientRealm extends AuthorizingRealm {

    //这里没有直接注入实例,ShiroClientRealm被用在配置类中,直接注入报servercontext not set 的错。只能使用时从spring容器中拿
    private BaseAuthorityRestService baseAuthorityRestService;

    Boolean cachingEnabled=true;

   
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo();
        BaseUserVM user = (BaseUserVM)principals.getPrimaryPrincipal();

        if(user.getSuperman()){//超管
            simpleAuthorInfo.addStringPermission("administrator");

        }else{

            //这里没有直接注入实例,ShiroClientRealm被用在配置类中new出的,直接注入报servercontext not set 的错。只能使用时从spring容器中拿
            baseAuthorityRestService = SpringUtil.getBean(BaseAuthorityRestService.class);

            Message> message = baseAuthorityRestService.getAuthorityByUser(user.getId());
            if(message!=null&&message.getData()!=null){
                List authorityVMList = message.getData();
                for (BaseAuthorityVM authority:authorityVMList) {
                    simpleAuthorInfo.addStringPermission(authority.getAuthorityCode());
                }
            }
        }
        return simpleAuthorInfo;
    }

    /**
     * 这个方法不会被调用,会到base服务校验是否登录
     * @Author:Melo
     * @Date: 2019/6/10 0010
    **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authcToken) throws AuthenticationException {
        return null;
    }
}

 4.改造uaa为springboot自动配置,notice依赖uaa

notice加uaa依赖,但这时启动notice服务,uaa中shiro拦截器并没有注入到spring中,很明显springboot只能扫描到启动类路径下的配置注解,jar包中的注解无法直接扫描到。这里我们通过加spring.factories的方式

Springcloud的一种权限实现方案,zuul和shiro_第3张图片

自动配置具体可看我这篇https://blog.csdn.net/u014203449/article/details/91890531

将notice依赖uaa、base-api。

5.notice写需要权限接口

 @GetMapping("/notice1")
    @RequiresPermissions("dd")
    public String test1(){
        return "noticetest";
    }

6.zuul的cooike丢失问题 

这时有权限的用户直接访问notice需要认证接口也是失败的,在base拦截器中debug发现cookie是空的,因为zuul把cookie过滤了。

需要设置zuul的敏感头。#默认敏感头是"Cookie", "Set-Cookie", "Authorization"这三项,取消这三项,向下游服务请求带上这些headers。sensitiveHeaders设置为空即可解决这个问题,我设置了其他值是一会要解决其他问题的。

zuul:
  #设置请求超时时间
  connect-timeout-millis: 15000 #HTTP连接超时要比Hystrix的大
  socket-timeout-millis: 60000   #socket超时
  #SendErrorFilter:
   # error:
   #   disable: true #禁用zuul默认的异常过滤器
  errorControllerUrl: /error #自定义配置,异常处理接口
  routes:
    focus-base: # 通过服务名serviceId路由,不通过具体的url
      path: /base/**
      serviceId: focus-base
      #默认敏感头是"Cookie", "Set-Cookie", "Authorization"这三项,取消这三项,向下游服务请求带上这些headers
      #Access-Control-Allow-Origin,Access-Control-Allow-Methods 解决其他服务的js向zuul发起请求的跨域问题
      sensitiveHeaders: Access-Control-Allow-Origin,Access-Control-Allow-Methods
    focus-notice: # 通过服务名serviceId路由,不通过具体的url
      path: /notice/**
      serviceId: focus-notice
      sensitiveHeaders: Access-Control-Allow-Origin,Access-Control-Allow-Methods

7.zuul的跨域问题

我的js因为放在了base服务中,直接访问zuul接口,出现跨域问题:

这个跟springboot直接注入cors实例还不一样,参考https://segmentfault.com/a/1190000010722941解决。

多次请求的时候,会把这些header再带过来,然后请求zuul转发的接口又在写入一次,造成重复了,方案就是zuul转发的时候,过滤掉这些header,比如:

sensitiveHeaders: Access-Control-Allow-Origin,Access-Control-Allow-Methods

 同时zuul添加配置

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                .maxAge(3600)
                .allowCredentials(true);
    }
}

8.feign调用cooike丢失问题

直接调用base服务或notice服务的需认证接口时可以访问成功,但通过notice服务利用feign调用base的认证接口失败了。因为cookie在feign调用没有传递过去。

我们知道shiro调用需要认证的接口时,authc,会执行form拦截器,debug isAccessAllowed(是否允许访问)方法,我们先看看直接调用base服务的接口,很明显有cookie JSESSIONID.

Springcloud的一种权限实现方案,zuul和shiro_第4张图片.

再通过notice feign调用base接口时,看到cookie时null。

Springcloud的一种权限实现方案,zuul和shiro_第5张图片

解决问题很简单,只需要实现Feign的 FeignCookieInterceptor 接口,取当前请求头中的cookie,放到feign请求头上即可。

package com.focus.framework.feign;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 用Feign发新请求,原请求的header cookie都默认没有,需要自己设置
 */
@Component
public class FeignCookieInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        if (null == getHttpServletRequest()){
            return;
        }
        //requestTemplate 好像没有直接设置cookie对象的地方,但cookie其实是header中的
        //Cookie[] cookies = getHttpServletRequest().getCookies();

        requestTemplate.header("Cookie",getHttpServletRequest().getHeader("Cookie"));
    }

    /**
     * RequestContextHolder从当前线程中获取请求,又用了线程副本
     * @return
     */
    private HttpServletRequest getHttpServletRequest(){
        try{
            return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        }catch (Exception e){
            return null;
        }
    }
}

 FeignCookieInterceptor这个配置类也是notice等业务服务都需要用的。因为有一些公用的配置,我就写了一个common项目,里面配置了一些公用的配置类和可能用到的工具类。同时也用springboot的自动配置方式配置好。

到这里为止,已经实现了最初的三个目标,还有些页面跳转等细节会继续完善......

四.其他思路

1.分布式session

  • 用户在网关进行登录认证;如果通过,将用户信息存在第三方组件,mysql、redis;后端其他服务可以通过第三方组件拿到用户数据。
  • 这种方案值得推荐,方便扩展,但依赖于第三方组件,注意第三方组件的高可用。

2.客户端token与网关结合

服务器无session,将用户信息存储在token,比如JWT。

了解JWThttps://www.cnblogs.com/cjsblog/p/9277677.html

  • 客户端携带用jwt加密的token,访问网关,token携带了用户的信息
  • 网关对token认证和校验
  • 校验通过网关后,请求携带token到具体服务,可以校验具体的url权限
  • 如果用户信息量大,则不适合,因为都是存储在客户端的;并且token要在网关注注销
  • zuul + OATHU2+JWT

3.浏览器cookie和网关结合。

和上述方案相同,区别是用户信息完全放在cookie,不用token

你可能感兴趣的:(shiro,springcloud)