《应用拆分与平台搭建最佳实践》- 服务化的权限
服务化的权限,也就是权限接口服务化。应用拆分之后,每个应用访问都需要经过授权处理。
授权处理的方式有多种
第一种 基于请求转发的方式
第二种 基于配置权限过滤器,调用远程接口check权限
笔者这里选择第二种方式,第一种方式权限过于集中化,对负载的要求很高。第二种服务化的方式更扁平,应用可以自行决定白名单管理,也可以通过权限平台提供的白名单,仁者见仁。
笔者这里使用的架构是基于spring cloud的方案,所以需要eureka这样肥服务发现系统。
如上图,在eureka上注册的机器一共有4台,login临时充当授权系统,其他为常规子系统。
以www为例,如何使用spring cloud fegin远程调用
首先我们需要配置fegin maven依赖
<!-- 远程调用 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> <version>1.3.2.RELEASE</version> </dependency>
然后在login应用中添加权限接口
/** * * 权限远程调用接口 * * Created by xiaotian.shi on 2017/7/28. */ @Controller public class AuthFeginController { private Logger logger = LoggerFactory.getLogger(LoginController.class); @Autowired private AuthService authService; @Value("${app.server.hosts}") private String appServer; @RequestMapping(value = {"/auth"}, method={RequestMethod.POST}) @ResponseBody public boolean check( @RequestParam("loginId") String loginId, @RequestParam("target") String target ) { logger.info("账号访问{} 目标 {}", loginId, target); Map<String, List<String>> data = authService.getAuthData(); if(StringUtils.isEmpty(target)) { return false; } URL targetUrl = null; try { targetUrl = new URL(target); } catch (MalformedURLException e) { return false; } List<String> urlList = data.get(loginId); if(CollectionUtils.isEmpty(urlList)) { return false; } boolean isPass = false; for(String passUrl : urlList) { if(targetUrl.equals(passUrl)) { isPass = true; } } return isPass; } @RequestMapping(value = {"/auth/host"}, method={RequestMethod.POST}) @ResponseBody public String getAuthHost() { return appServer; } }
其中一共2个接口,一个权限check,一个获取权限系统的域名地址,当然权限系统复杂可以提供更加复杂的权限接口。
使用权限数据的一方(www)
/** * Created by xiaotian.shi on 2017/7/28. */ // 指定远程服务器名称 @FeignClient("xiaotian-login") public interface PlatAuthClient { /** * * 检查是否有访问权限 * * @param loginId 登陆账号 * @param target 访问目标 * @return */ @RequestMapping(value = "/auth", method={RequestMethod.POST}) boolean check(@RequestParam("loginId") String loginId, @RequestParam("target") String target); /** * * 获取权限服务器地址 * * @return */ @RequestMapping(value = {"/auth/host"}, method={RequestMethod.POST}) public String getAuthHost(); }
权限拦截器
** * Created by xiaotian.shi on 2017/7/15. */ public class PlatAuthInterceptor implements HandlerInterceptor { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private String loginServer; @Autowired private PlatAuthClient authClient; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 登陆的对象 LoginUser loginUser = null; // 获取会话 HttpSession session = request.getSession(); Object loginUserObject = session.getAttribute("loginUser"); // 获取请求地址 String url = request.getRequestURL().toString(); String redirectUrl = this.getLoginServer() +"?redirect=" + url ; loginUser = (LoginUser) loginUserObject; // 有当前页面权限则通过 没有权限跳转到无权限页面 boolean result = authClient.check(loginUser.getUsername(), url); if(result) { return true; } else { // 返回没有权限的信息,这里的实现, response.sendRedirect(this.getLoginServer() + "/error401"); return false; } } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } private String getLoginServer() { if(StringUtils.isEmpty(loginServer)) { loginServer = authClient.getAuthHost(); } return loginServer; } }
会话拦截器
/** * Created by xiaotian.shi on 2017/7/15. */ public class PlatSessionInterceptor implements HandlerInterceptor { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private String loginServer; @Autowired private PlatAuthClient authClient; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 登陆的对象 LoginUser loginUser = null; // 获取会话 HttpSession session = request.getSession(); Object loginUserObject = session.getAttribute("loginUser"); // 获取请求地址 String url = request.getRequestURL().toString();; String redirectUrl = this.getLoginServer() +"?redirect=" + url ; if(loginUserObject != null) { loginUser = (LoginUser) loginUserObject; } else { // 会话不存在不允许访问 response.sendRedirect(redirectUrl); return false; } // 会话是否已经登陆 if(Boolean.TRUE.equals(loginUser.isLogin())) { return true; } else { // 没有登陆 // 会话未登陆 response.sendRedirect(redirectUrl); return false; } } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } private String getLoginServer() { if(StringUtils.isEmpty(loginServer)) { loginServer = authClient.getAuthHost(); } return loginServer; } }
发了那么多大段的程序,大家肯定心里有疑问,难道每个应用接入放都要复制这么多文件么?还能自己改动逻辑。
当然不会是这样,我们需要将这些东西,做成一个maven包,通过maven依赖的方式依赖进来。如图,首先我们构建
一个模块plat。
但是问题又来了,如果maven parent不一样,怎么能安全的引入呢?所以我们需要将plat的parent修改成自己定制的公用parent。
新建maven应用,配置pom.xml如下。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>xiaotian.shi</groupId> <artifactId>deploy</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <artifactId>maven-source-plugin</artifactId> <version>2.1.2</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
然后编译进入本地的maven仓库和私服
mvn -Dmaven.test.skip=true package deploy
如果这里有失败,请忽略,只要pom.xml进了仓库就ok。
plat的pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>deploy</artifactId> <groupId>xiaotian.shi</groupId> <version>1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>plat</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <dependencies> <!-- spring boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>1.5.2.RELEASE</version> </dependency> <!-- 远程调用 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> <version>1.3.2.RELEASE</version> </dependency> <!-- 服务发现 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> <version>1.3.2.RELEASE</version> </dependency> <!-- logger --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> <version>1.5.2.RELEASE</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> <version>1.7.24</version> </dependency> </dependencies> </project>
注意parent标签的配置
然后在需要接入授权的地方,引入这个maven dependency
<dependency> <groupId>xiaotian.shi</groupId> <artifactId>plat</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
这样,这个包里的拦截器程序就可以引用了。
但是这样不算完成,有如下两个问题需要解决。
第一 开启fegin配置,配置能扫描到包内的fegin接口
第二 拦截器需要配置进应用中
在www应用中开启配置,在继承了SpringBootServletInitializer的controller中,或者单独配置一个config
增加
@EnableFeignClients(basePackages={"shi.xiaotian"}) @EnableEurekaClient
(demo中可能是top.miledao 这是我个人站的包地址)
在www应用中拦截器配置
/** * Created by xiaotian.shi on 2017/6/1. */ @Configuration public class InterceptorConfigure extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { // 会话拦截器 registry.addInterceptor(this.sessionInterceptor()).addPathPatterns("/**"); // 权限过滤器 registry.addInterceptor(this.authInterceptor()).addPathPatterns("/**"); // 全局过滤器 registry.addInterceptor(new GlobalInterceptor()).addPathPatterns("/**"); // 自定义全局工具过滤器 registry.addInterceptor(new GlobalToolsInterceptor()).addPathPatterns("/**"); } // 解决拦截器不能注入实例的问题 @Bean @Scope("singleton") PlatSessionInterceptor sessionInterceptor() { return new PlatSessionInterceptor(); } // 解决拦截器不能注入实例的问题 @Bean @Scope("singleton") PlatAuthInterceptor authInterceptor() { return new PlatAuthInterceptor(); } }
权限配置完成
项目地址 https://github.com/shixiaotian/xiaotian.shi-plat.git
demo http://www.miledao.top/
账户密码
admin admin
user1 user1
user2 user2
user3 user3