企业业务上云的三种架构
容器的三个视角
从运维角度
数据工程师角度
开发角度

微服务化

12 Factor
以下以搬凳子为案例进行讲解
一、愿景
1、概述:描述整个愿景及目标
晚会要求,需要1万张凳子搬到万人坑,并做好排列,按班级分配,需在1月15日至1月16号完成,此由社联宣传部门完成;
2、方向:建设方向和长远目标
团队成长和熟悉合作流程;
二、前置条件
1、组织机构:当前组织情况
社联宣传部,负责社联的宣传对外工作;
2、人力资源:当前人员配置
宣传部长1名(有一定的管理经验,入会2年)、
宣传副部长1名(有一定的管理经验,但办事拖拉)、
理事4名(无经验,入会3个月)
3、团队情况:团队内部情况,找出缺陷,进行补救,以配合目标建议
暂时无成功共事经验,理事成员对事不上心,副部能力跟不上,凝聚力不足
4、团队建设:
内部讨论(形成统一意识)
人员培训(经验交流,外部人员培训等,定期培训,坚定意识统一)
试点合作(尝试可控项目的试点管理,进行团队磨合)
4、成本管理:资源及资本情况
经费预算1万,其实包括团队建设费用2千,奖励奖金2千,其它计划支持6千;
三、计划
1、前期计划
团队磨合,风险评估
2、中期计划
正式计划执行,项目落地,过程梳理和学习
3、后期计划
团队总结和能力提升,任务交换等
四、方案
1、前期方案
组织合作培训,尝试团队磨合,增加讨论风气;
组织一次聚餐(或者小活动),进一步整合团队,加强磨合;
2、中期方案
部长联系搬运人员,并与搬家公司联系,建议反馈机制及基本执行条件;
副部跟进搬家公司沟通情况,并做好理事工作分工,做好安排有,向部长江报;
理事按副部的计划进行执行,并向副部汇报,如有紧急情况,向部长汇报;
3、后期方案
项目总结;
五、风险
1、人员资源风险
过程人员请假或者无法坚持,根据前期表现,适当增加人员,如找兼职;
2、资源偏差值风险
节点过程与计划偏差,做好预计值评估,如有异常,及时与组办方沟通;
3、人为风险
过程破坏,或者刻意破坏的情况,沟通不顺利及不愿意执行,由部长进行深入沟通,了解具体原因,解决困难;
4、费用风险
根据情况适当调整活动及资源调配,如培训邀请一般专家即可
5、不可控风险
具体情况,具体分配
五、持久
1、岗位交接
暂无;
2、流程梳理
暂无;
3、过程管理
暂无;
4、人员培养
暂无;
5、团队培养
暂无;
项目背景:springboot+thymeleaf
thymeleaf两种方式处理自定义标签:AbstractAttributeTagProcessor 和 AbstractElementTagProcessor
一、AbstractAttributeTagProcessor :
1. 定义dialog
package com.spt.im.web.config;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Value;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.processor.IProcessor;
public class CustomDialect extends AbstractProcessorDialect{
private static final String DIALECT_NAME = "staticFile";
private static final String PREFIX = "W";
public static final int PROCESSOR_PRECEDENCE = 1000;
@Value("${im.static.resources}")
private String filePath;
protected CustomDialect() {
super(DIALECT_NAME, PREFIX, PROCESSOR_PRECEDENCE);
}
@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
final Set<IProcessor> processors = new HashSet<IProcessor>();
processors.add(new SampleJsTagProcessor(dialectPrefix, filePath));
processors.add(new SampleCssTagProcessor(dialectPrefix, filePath));
processors.add(new SampleSrcTagProcessor(dialectPrefix, filePath));
return processors;
}
}
2. 定义处理器
package com.spt.im.web.config;
import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.engine.AttributeName;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.standard.expression.IStandardExpression;
import org.thymeleaf.standard.expression.IStandardExpressionParser;
import org.thymeleaf.standard.expression.StandardExpressions;
import org.thymeleaf.templatemode.TemplateMode;
public class SampleJsTagProcessor extends AbstractAttributeTagProcessor{
private static final String ATTR_NAME = "js";
private static final String ELE_NAME = "script";
private static final int PRECEDENCE = 10000;
private String filePath;
protected SampleJsTagProcessor(String dialectPrefix, String filePath) {
super(
TemplateMode.HTML,
dialectPrefix,
ELE_NAME,
false,
ATTR_NAME,
true,
PRECEDENCE,
true);
this.filePath = filePath;
}
@Override
protected void doProcess(ITemplateContext context,
IProcessableElementTag tag, AttributeName attributeName,
String attributeValue, IElementTagStructureHandler structureHandler) {
final IEngineConfiguration configuration = context.getConfiguration();
final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
final IStandardExpression expression = parser.parseExpression(context, attributeValue);
final String url = (String) expression.execute(context);
structureHandler.setAttribute("src", filePath + url);
}
}
3. 添加到配置中
package com.spt.im.web.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WorkImport {
@Bean
public CustomDialect testDialect(){
return new CustomDialect();
}
}
4. 页面中使用
<script W:js="@{/static/js/pinyin.js}" type="text/javascript"></script>
项目运行,上述标签会被替换成相应的链接。
最近项目分给我一个需求解决CAS认证登陆的内外网双IP访问的问题,当使用通用的CAS统一认证服务时,由于WEB应用工程中web.xml配置的CAS地址是固定的,而不是一个动态的地址,当将WEB应用服务器例如TOMCAT端口映射外网后,在访问应用时会自动根据在web.xml文件中去配置对应的CAS地址,而此时的地址只能是内网使用,外网自然无法找到,则无法登陆,而由于项目的本身需要,必须要同时内外网都能访问,由此看了一周源码后,提供以下解决方案。
供工具类
public class HttpConnectionUtil {
//读取配置文件信息
static Properties prop = new Properties();
static{
InputStream inStream = HttpConnectionUtil.class.getClassLoader().getResourceAsStream(“application.properties”);
try {
prop.load(inStream);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*
* @param name
* @return
*/
//在配置文件中通过键取值
public static String getByName(String name){
return prop.getProperty(name);
}
//判断是内网环境还是外网环境
public static boolean isInner(String clientIP) {
String reg = “(10|172|192|127)\\.([0-1][0-9]{0,2}|[2][0-5]{0,2}|[3-9][0-9]{0,1})\\.([0-1][0-9]{0,2}|[2][0-5]{0,2}|[3-9][0-9]{0,1})\\.([0-1][0-9]{0,2}|[2][0-5]{0,2}|[3-9][0-9]{0,1})”;
Pattern p = Pattern.compile(reg);
Matcher matcher = p.matcher(clientIP);
return matcher.find();
}
}
源码解读:
本项目是cas和shiro的整合,cas认证登陆加入到shiro的过滤连里面:
在web.xml里面配置:
<!– 单点登录end –>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!– 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean–>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
然后过滤链在spring容器里面找id为shiroFilter的bean工厂
<bean id=”casFilter” class=”org.apache.shiro.cas.CasFilter”>
<!– 配置验证错误时的失败页面 –>
<!–<property name=”failureUrl” value=”/error.jsp”/> –>
<!– <property name=”failureUrl” value=”/casFailure.jsp” /> –>
<!– <property name=”successUrl” value=”/front3/index.html”/> –>
<property name=”failureUrl” value=”${common_in.ip}/disrec” />
</bean>
<!– <bean id=”casRealm” class=”com.zonekey.disrec.service.auth.ShiroDbRealm”>
<property name=”cachingEnabled” value=”true” />
<property name=”authenticationCachingEnabled” value=”true” />
<property name=”authenticationCacheName” value=”authenticationCache” />
<property name=”authorizationCachingEnabled” value=”true” />
<property name=”authorizationCacheName” value=”authorizationCache” />
<property name=”casServerUrlPrefix” value=${login.ip}/>
客户端的回调地址设置,必须和下面的shiro-cas过滤器拦截的地址一致
<property name=”casService” value=”${common.ip}/disrec/shiro-cas”/>
</bean> –>
<bean id=”casRealm” class=”com.zonekey.disrec.service.auth.ShiroDbRealm”>
<property name=”cachingEnabled” value=”true” />
<property name=”authenticationCachingEnabled” value=”true” />
<property name=”authenticationCacheName” value=”authenticationCache” />
<property name=”authorizationCachingEnabled” value=”true” />
<property name=”authorizationCacheName” value=”authorizationCache” />
<property name=”casServerUrlPrefix” value=”${login_in.ip}” />
<!– 该地址为client1 的访问地址+ 下面配置的cas filter –>
<property name=”casService” value=”${common_in.ip}/disrec/shiro-cas” />
</bean>
<bean id=”securityManager” class=”org.apache.shiro.web.mgt.DefaultWebSecurityManager”>
<property name=”realm” ref=”casRealm”/>
<property name=”rememberMeManager” ref=”rememberMeManager” />
<property name=”subjectFactory” ref=”casSubjectFactory”/>
</bean>
<!– rememberMe管理器 如需要记住功能 可删掉相关配置<span style=”white-space:pre”> </span>
rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位) –>
<bean id=”rememberMeManager” class=”org.apache.shiro.web.mgt.CookieRememberMeManager”>
<property name=”cipherKey”
value=”#{T(org.apache.shiro.codec.Base64).decode(‘4AvVhmFLUs0KTA3Kprsdag==’)}” />
<property name=”cookie” ref=”rememberMeCookie” />
</bean>
<!– 会话ID生成器 –>
<bean id=”sessionIdGenerator” class=”org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator” />
<!– 会话Cookie模板 –>
<bean id=”sessionIdCookie” class=”org.apache.shiro.web.servlet.SimpleCookie”>
<constructor-arg value=”sid” />
<property name=”httpOnly” value=”true” />
<property name=”maxAge” value=”-1″ />
</bean>
<bean id=”rememberMeCookie” class=”org.apache.shiro.web.servlet.SimpleCookie”>
<constructor-arg value=”rememberMe” />
<property name=”httpOnly” value=”true” />
<property name=”maxAge” value=”2592000″ /><!– 30天 –>
</bean>
<!– 会话DAO –>
<bean id=”sessionDAO” class=”org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO”>
<property name=”activeSessionsCacheName” value=”shiro-activeSessionCache” />
<property name=”sessionIdGenerator” ref=”sessionIdGenerator” />
</bean>
<bean id=”logout” class=”org.apache.shiro.web.filter.authc.LogoutFilter”>
<property name=”redirectUrl” value=”${login_in.ip}/cas/logout?service=${common_in.ip}/disrec/shiro-cas/” />
</bean>
<!– 如果要实现cas的remember me的功能,需要用到下面这个bean,并设置到securityManager的subjectFactory中 –>
<bean id=”casSubjectFactory” class=”org.apache.shiro.cas.CasSubjectFactory”/>
**//这个就是我们要改源码的地方**
<bean id=”formAuthenticationFilter” class=”com.zonekey.disrec.common.utils.redirectIP.MyFormAuthenticationFilter” />
<!– 保证实现了Shiro内部lifecycle函数的bean执行 –>
<bean id=”lifecycleBeanPostProcessor” class=”org.apache.shiro.spring.LifecycleBeanPostProcessor”/>
<!– 相当于调用SecurityUtils.setSecurityManager(securityManager) –>
<bean class=”org.springframework.beans.factory.config.MethodInvokingFactoryBean”>
<property name=”staticMethod” value=”org.apache.shiro.SecurityUtils.setSecurityManager” />
<property name=”arguments” ref=”securityManager” />
</bean>
一般是不需要改动源码,只是用子类继承基类能达到改动效果
package com.zonekey.disrec.common.utils.redirectIP;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import com.zonekey.disrec.service.auth.ShiroDbRealm;
public class MyFormAuthenticationFilter extends FormAuthenticationFilter{
/**
* loginUrl地址重写
*/
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest req=(HttpServletRequest)request;
HttpServletResponse res=(HttpServletResponse)response;
//动态获取请求服务端的地址
String serverIp=req.getRequestURL().toString();
//请求资源
String serverAddr=req.getRequestURI();
//动态截取请求服务IP
String commonIp=serverIp.substring(0,serverIp.indexOf(serverAddr));
//System.out.println(“commonIP:”+commonIp);
//String common_in = ReadProperties.getByName(“common_in.ip”);
String login_in = HttpConnectionUtil.getByName(“login_in.ip”);
if(login_in==null) login_in= commonIp+”/cas”;
//String common_out = ReadProperties.getByName(“common_out.ip”);
String login_out = HttpConnectionUtil.getByName(“login_out.ip”);
if(login_out==null) login_out= commonIp+”/cas”;
//获取servletContext容器
ServletContext sc=req.getSession().getServletContext();
//获取web环境下spring容器
ApplicationContext ac=WebApplicationContextUtils.getWebApplicationContext(sc);
//ApplicationContextUtil ac=new ApplicationContextUtil();
ShiroDbRealm shiroDbRealm=(ShiroDbRealm)ac.getBean(“casRealm”);
LogoutFilter logoutFilter=(LogoutFilter)ac.getBean(“logout”);
//一一一一一一一一一一一注意是此处一一一一一一一一一一一一一一一一一一一
ShiroFilterFactoryBean shiroFilter=(ShiroFilterFactoryBean) ac.getBean(“&shiroFilter”);
String clientIP=null;
if (req.getHeader(“x-forwarded-for”) == null) {
clientIP=req.getRemoteAddr();
}else{
clientIP=req.getHeader(“x-forwarded-for”);
}
/*System.out.println(“clientIp:”+clientIP);
System.out.println(“isInner:”+HttpConnectionUtil.isInner(clientIP));*/
if(!HttpConnectionUtil.isInner(clientIP)){
shiroFilter.setLoginUrl(login_out+”/login?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
//casFilter.setFailureUrl(map.get(“common_out.ip”)+”/disrec”);
//shiroDbRealm.setCasServerUrlPrefix(login_out);
shiroDbRealm.setCasService(commonIp+req.getContextPath()+”/shiro-cas”);
logoutFilter.setRedirectUrl(login_out+”/logout?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
//二二二${common_out.ip}/sysmanagement/shiro-cas
}else{
shiroFilter.setLoginUrl(login_in+”/login?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
//casFilter.setFailureUrl(map.get(“common_in.ip”)+”/disrec”);
//shiroDbRealm.setCasServerUrlPrefix(login_in);
shiroDbRealm.setCasService(commonIp+req.getContextPath()+”/shiro-cas”);
logoutFilter.setRedirectUrl(login_in+”/logout?service=”+commonIp+req.getContextPath()+”/shiro-cas”);
}
WebUtils.issueRedirect(req, res, shiroFilter.getLoginUrl());
}
}
由此双IP问题得到了解决
整个思路:client发起请求,截取到client的请求路径判断用户IP是内网还是外网访问,然后再认证成功重定向路径动态重写登陆成功跳转的路径。
真实案例:
查看nginx日志,发现别有用心的人恶意调用API接口刷短信:
30966487 115.213.229.38 "-" [05/Jun/2018:14:37:29 +0800] 0.003 xxxxxx.com "POST /xxx/sendCheckCode HTTP/1.1" 401 200 46 xx.xx.xx.xx:0000 0.003 200 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode" 30963985 60.181.111.140 "-" [05/Jun/2018:14:37:29 +0800] 0.004 xxxxxx.com "POST /xxx/sendCheckCode HTTP/1.1" 401 200 46 xx.xx.xx.xx:0000 0.004 200 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode" 30959954 220.190.18.25 "-" [05/Jun/2018:14:37:29 +0800] 0.003 xxxxxx.com "POST /xxx/sendCheckCode HTTP/1.1" 401 200 46 xx.xx.xx.xx:0000 0.003 200 "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"
思考了几种方案,最终考虑使用ip黑名单的方式:
处理方法:
一、nginx黑名单方式:
1、过滤日志访问API接口的IP,统计每10分钟调用超过100次的IP,直接丢进nginx的访问黑名单
2、具体步骤:
编写shell脚本:
vim /shell/nginx_cutaccesslog.sh
#!/bin/bash
log_path=/xxx/nginx/logs
date=`date -d "10 min ago" +%Y%m%d-%H:%M:%S`
nginxpid=`cat ${log_path}/nginx.pid`
cd ${log_path}
#过滤access.log中正常访问API接口并在10分钟(下面是日志切割,再做个定时任务每10分钟执行一次,就可以实现了)内访问量最高的30个IP,取值如果此IP访问量大于100次,则把此IP放入黑名单
cat access.log | grep sendCheckCode | grep -v 403 | awk '{print $2}'|sort|uniq -c | sort -k1 -n | tail -30 | awk '{if($1>100) print "deny "$2";"}' > ../conf/denyip.conf
#日志切割,做定时任务,每10分钟执行一次
mv ${log_path}/access.log ${log_path}/accesslog.bak/access_${date}.log
../sbin/nginx -s reload
可自己定义时间间隔和访问量,也可取消筛选访问量最高的30个,直接取值每10分钟访问接口超过100次的
其中:”grep -v 403″ 是把已经禁止访问的IP给过滤掉,只筛选正常访问的
3、修改nginx.conf
在http模块加入:
include denyip.conf;
重新加载nginx生效。
4、添加计划任务:
*/10 * * * * /bin/bash /shell/nginx_cutaccesslog.sh > /dev/null 2>&1
5、验证:
[root@xxx logs]# ll accesslog.bak/ -rw-r--r-- 1 root root 2663901 Jun 5 15:10 access_20180605-15:00:01.log -rw-r--r-- 1 root root 13696947 Jun 5 15:20 access_20180605-15:10:01.log -rw-r--r-- 1 root root 13265509 Jun 5 15:30 access_20180605-15:20:01.log -rw-r--r-- 1 root root 13846297 Jun 5 15:40 access_20180605-15:30:01.log [root@xxx logs]# cat ../conf/denyip.conf ………… ………… deny 112.12.137.28; deny 183.167.237.229; deny 111.41.43.58; deny 115.217.117.159; deny 219.133.100.133; deny 171.221.254.115; deny 60.184.131.6; ………… …………
再查看已经禁用IP的访问日志,则会返回403错误:
[root@xxx logs]# tail -f access.log | grep "60.184.131.6" 31268622 60.184.131.6 "-" [05/Jun/2018:15:47:34 +0800] 0.000 xxxxxx.com "POST /xxxxxx/sendCheckCode HTTP/1.1" 377 403 168 - - - "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode" 31268622 60.184.131.6 "-" [05/Jun/2018:15:47:35 +0800] 0.000 xxxxxx.com "POST /xxxxxx/sendCheckCode HTTP/1.1" 377 403 168 - - - "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode" 31268622 60.184.131.6 "-" [05/Jun/2018:15:47:35 +0800] 0.000 xxxxxx.com "POST /xxxxxx/sendCheckCode HTTP/1.1" 377 403 168 - - - "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0" "https://xxxxxx/sendCheckCode"
二、限制IP请求数:
处理这种情况的方法还有一种是限制单 IP 单位时间的请求数,以及单 IP 的并发连接数
此方法没有实际运用,因为感觉这种方法会误杀正常的访问用户
写一下此方法的大概配置,http模块加入:
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=8r/s;
server {
location /search/ {
limit_req zone=one burst=5;
}
如何估算 limit_req_zone size:
一兆字节区域可以保持大约1万6064字节的状态。
那么 10M 就可以存储 16 万的 IP 统计信息, 这个对普通应用足够了,16 万每秒的 UV,已经超级厉害了。
如果 size 的大小如果设置小了, 例如设置成 1M,那么当一秒内的请求 IP 数超过 16000 的时候,超出的 IP 对应的用户看到的均为 503 Service Temporarily Unavailable 页面了。参考, 漏桶算法 Leaky Bucket。 同时,rate 的单位用 r/s 非常合适,如果换成按天,按小时计数,10M 的内存肯定不够用。
如何估算 limit_req_zone rate:
首先需要知道的是,普通浏览器的同时并发数量。按照 Dropbox 技术博客里所谈到的,目前主流浏览器限制 AJAX 对同一个子域名的并发连接数是6个。IE 6,IE 7 是两个。
大多数浏览器每个主机名都有6个并发连接的限制。