探讨下几种SpringCloud权限实现方案,以zuul和shiro的思路做了实现,简写了搭建过程.
github地址https://github.com/MeloFocus/FocusCloudWork
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服务的是:
其他业务服务:只需要依赖于client。
这种思路来自于《跟我学shiro》的多项目集中权限,其实想想这种思路是可以的,shiro本质也是靠拦截器进行权限校验,虽然相当于每个服务都开启了一套shiro,但也就是容器中多了一些shiro拦截器和实例,而且可以用shiro的各种功能,开发方便。可以完成我们的三个目标。
因为shiro和cloud的细节太多,这里就不赘述,以下内容默认读者掌握shiro和springcloud基本组件。
我使用的是Finchley.BUILD-SNAPSHOT版本。
我们建两个服务Base和Notice,Base负责权限认证,用户访问Notice服务时先跳转到Base服务校验。
将服务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
@FeignClient(name = FocusMicroBaseConstants.SERVICE_APP_ID)
public interface BaseAuthorityRestService {
@GetMapping
public Message> getAuthorityByUser(String userId);
}
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;
}
}
notice加uaa依赖,但这时启动notice服务,uaa中shiro拦截器并没有注入到spring中,很明显springboot只能扫描到启动类路径下的配置注解,jar包中的注解无法直接扫描到。这里我们通过加spring.factories的方式
自动配置具体可看我这篇https://blog.csdn.net/u014203449/article/details/91890531
将notice依赖uaa、base-api。
@GetMapping("/notice1")
@RequiresPermissions("dd")
public String test1(){
return "noticetest";
}
这时有权限的用户直接访问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
我的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);
}
}
直接调用base服务或notice服务的需认证接口时可以访问成功,但通过notice服务利用feign调用base的认证接口失败了。因为cookie在feign调用没有传递过去。
我们知道shiro调用需要认证的接口时,authc,会执行form拦截器,debug isAccessAllowed(是否允许访问)方法,我们先看看直接调用base服务的接口,很明显有cookie JSESSIONID.
再通过notice feign调用base接口时,看到cookie时null。
解决问题很简单,只需要实现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
2.客户端token与网关结合
服务器无session,将用户信息存储在token,比如JWT。
了解JWThttps://www.cnblogs.com/cjsblog/p/9277677.html
3.浏览器cookie和网关结合。
和上述方案相同,区别是用户信息完全放在cookie,不用token