0%

配置信息的加载

  从《how tomcat works》(深入剖析tomcat)第10章的例程Bootstrap1可以看到,有关安全认证的配置是作为listener载入到context中的:

1
2
LifecycleListener listener = new SimpleContextConfig();
((Lifecycle) context).addLifecycleListener(listener);

  查看SimpleContextConfig的代码,可以看到其监听了START_EVENT事件:

1
2
3
4
5
6
7
public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.START_EVENT.equals(event.getType())) {
context = (Context) event.getLifecycle();
authenticatorConfig();
context.setConfigured(true);
}
}

  当context启动时,会通知注册的监听器相关事件的发生:

1
lifecycle.fireLifecycleEvent(START_EVENT, null);

安全验证配置

  SimpleContextConfig的lifecycleEvent方法就会被调用,在之中主要的操作是调用了authenticatorConfig()方法,再来查看authenticatorConfig()的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private synchronized void authenticatorConfig() {
// Does this Context require an Authenticator?
SecurityConstraint constraints[] = context.findConstraints();
if ((constraints == null) || (constraints.length == 0))
return;
LoginConfig loginConfig = context.getLoginConfig();
if (loginConfig == null) {
loginConfig = new LoginConfig("NONE", null, null, null);
context.setLoginConfig(loginConfig);
}

// Has an authenticator been configured already?
Pipeline pipeline = ((StandardContext) context).getPipeline();
if (pipeline != null) {
Valve basic = pipeline.getBasic();
if ((basic != null) && (basic instanceof Authenticator))
return;
Valve valves[] = pipeline.getValves();
for (int i = 0; i < valves.length; i++) {
if (valves[i] instanceof Authenticator)
return;
}
}
else { // no Pipeline, cannot install authenticator valve
return;
}

// Has a Realm been configured for us to authenticate against?
if (context.getRealm() == null) {
return;
}

// Identify the class name of the Valve we should configure
String authenticatorName = "org.apache.catalina.authenticator.BasicAuthenticator";
// Instantiate and install an Authenticator of the requested class
Valve authenticator = null;
try {
Class authenticatorClass = Class.forName(authenticatorName);
authenticator = (Valve) authenticatorClass.newInstance();
((StandardContext) context).addValve(authenticator);
System.out.println("Added authenticator valve to Context");
}
catch (Throwable t) {
}
}

  可以看到其主要操作是在context的管道中添加BasicAuthenticator阀,当请求到来时,context会从管道从逐级调用各个阀,入口是invoke方法,因此去看下BasicAuthenticator的invoke方法,查看BasicAuthenticator代码会发现其没有重写父类的invoke方法,因此去查看BasicAuthenticator父类AuthenticatorBase的invoke方法:

验证过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public void invoke(Request request, Response response,
ValveContext context)
throws IOException, ServletException {
// If this is not an HTTP request, do nothing
if (!(request instanceof HttpRequest) ||
!(response instanceof HttpResponse)) {
context.invokeNext(request, response);
return;
}
if (!(request.getRequest() instanceof HttpServletRequest) ||
!(response.getResponse() instanceof HttpServletResponse)) {
context.invokeNext(request, response);
return;
}
HttpRequest hrequest = (HttpRequest) request;
HttpResponse hresponse = (HttpResponse) response;
if (debug >= 1)
log("Security checking request " +
((HttpServletRequest) request.getRequest()).getMethod() + " " +
((HttpServletRequest) request.getRequest()).getRequestURI());
LoginConfig config = this.context.getLoginConfig();
// Have we got a cached authenticated Principal to record?
if (cache) {
Principal principal =
((HttpServletRequest) request.getRequest()).getUserPrincipal();
if (principal == null) {
Session session = getSession(hrequest);
if (session != null) {
principal = session.getPrincipal();
if (principal != null) {
if (debug >= 1)
log("We have cached auth type " +
session.getAuthType() +
" for principal " +
session.getPrincipal());
hrequest.setAuthType(session.getAuthType());
hrequest.setUserPrincipal(principal);
}
}
}
}
// Special handling for form-based logins to deal with the case
// where the login form (and therefore the "j_security_check" URI
// to which it submits) might be outside the secured area
String contextPath = this.context.getPath();
String requestURI = hrequest.getDecodedRequestURI();
if (requestURI.startsWith(contextPath) &&
requestURI.endsWith(Constants.FORM_ACTION)) {
if (!authenticate(hrequest, hresponse, config)) {
if (debug >= 1)
log(" Failed authenticate() test");
return;
}
}
// Is this request URI subject to a security constraint?
SecurityConstraint constraint = findConstraint(hrequest);
if ((constraint == null) /* &&
(!Constants.FORM_METHOD.equals(config.getAuthMethod())) */ ) {
if (debug >= 1)
log(" Not subject to any constraint");
context.invokeNext(request, response);
return;
}
if ((debug >= 1) && (constraint != null))
log(" Subject to constraint " + constraint);
// Make sure that constrained resources are not cached by web proxies
// or browsers as caching can provide a security hole
if (!(((HttpServletRequest) hrequest.getRequest()).isSecure())) {
HttpServletResponse sresponse =
(HttpServletResponse) response.getResponse();
sresponse.setHeader("Pragma", "No-cache");
sresponse.setHeader("Cache-Control", "no-cache");
sresponse.setDateHeader("Expires", 1);
}
// Enforce any user data constraint for this security constraint
if (debug >= 1)
log(" Calling checkUserData()");
if (!checkUserData(hrequest, hresponse, constraint)) {
if (debug >= 1)
log(" Failed checkUserData() test");
// ASSERT: Authenticator already set the appropriate
// HTTP status code, so we do not have to do anything special
return;
}
// Authenticate based upon the specified login configuration
if (constraint.getAuthConstraint()) {
if (debug >= 1)
log(" Calling authenticate()");
if (!authenticate(hrequest, hresponse, config)) {
if (debug >= 1)
log(" Failed authenticate() test");
// ASSERT: Authenticator already set the appropriate
// HTTP status code, so we do not have to do anything special
return;
}
}
// Perform access control based on the specified role(s)
if (constraint.getAuthConstraint()) {
if (debug >= 1)
log(" Calling accessControl()");
if (!accessControl(hrequest, hresponse, constraint)) {
if (debug >= 1)
log(" Failed accessControl() test");
// ASSERT: AccessControl method has already set the appropriate
// HTTP status code, so we do not have to do anything special
return;
}
}
// Any and all specified constraints have been satisfied
if (debug >= 1)
log(" Successfully passed all security constraints");
context.invokeNext(request, response);
}

  代码略长,一开始过滤掉了非http请求的情况。如果开启cache的话会首先从session中查找是否已有验证。从context中获取LoginConfig,然后调用了authenticate(hrequest,hresponse, config)方法;然后会查找是否有constraint,没有的话就调用下一个管道,返回。有的话针对constraint做进一步的认证,包括access control,是根据constraint中的用户角色决定是否有权访问。
  上述提到的authenticate(hrequest,hresponse, config)方法在AuthenticatorBase是一个抽象方法,其实现在子类BasicAuthenticator中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public boolean authenticate(HttpRequest request,
HttpResponse response,
LoginConfig config)
throws IOException {
// Have we already authenticated someone?
Principal principal =
((HttpServletRequest) request.getRequest()).getUserPrincipal();
if (principal != null) {
if (debug >= 1)
log("Already authenticated '" + principal.getName() + "'");
return (true);
}
// Validate any credentials already included with this request
HttpServletRequest hreq =
(HttpServletRequest) request.getRequest();
HttpServletResponse hres =
(HttpServletResponse) response.getResponse();
String authorization = request.getAuthorization();
String username = parseUsername(authorization);
String password = parsePassword(authorization);
principal = context.getRealm().authenticate(username, password);
if (principal != null) {
register(request, response, principal, Constants.BASIC_METHOD,
username, password);
return (true);
}
// Send an "unauthorized" response and an appropriate challenge
String realmName = config.getRealmName();
if (realmName == null)
realmName = hreq.getServerName() + ":" + hreq.getServerPort();
// if (debug >= 1)
// log("Challenging for realm '" + realmName + "'");
hres.setHeader("WWW-Authenticate",
"Basic realm=\"" + realmName + "\"");
hres.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// hres.flushBuffer();
return (false);
}

  可以看到,验证成功则交给下一级阀处理,失败则返回UNAUTHORIZED(即HTTP 401状态码),浏览器在第一次接受到401状态码时会自动弹出验证窗口,用户填完验证信息并点击登录,浏览器会见携带验证信息再发送一次请求,如果再次接受到401状态码,则会显示验证失败。

​ 最近在看《how tomcat works》。看完第6章,对整个时序流程还是感觉有点模糊,所以画了一个时序图总结下。

​ 其中省略了一部分内容:

  • 生命周期管理,事件发生调用fireLifecycleEvent,相关listener会有动作,省略了
  • 管道的逐级调用省略了

Lifecycle

​ 生命周期时间发生时,调用fireLifecycleEvent,被注册的listener会逐个触发,在listener内部检测事件类型,进行相应的处理,fireLifecycleEvent方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void fireLifecycleEvent(String type, Object data) 

{

​ LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);

​ LifecycleListener interested[] = null;

synchronized (listeners) {

​ interested = (LifecycleListener[]) listeners.clone();

​ }

for (int i = 0; i < interested.length; i++)

​ interested[i].lifecycleEvent(event);

}

​ LifecycleListener在lifecycleEvent方法中对事件类型进行检查,并做出相应的响应。

Servlet的载入

​ Serlet的载入是在pipline的逐级调用中完成的。查看bootstrap的代码:

1
2
3
4
5
6
7
8
9
10
11
Wrapper wrapper1 = new SimpleWrapper();

wrapper1.setName("Primitive");

wrapper1.setServletClass("PrimitiveServlet");

Wrapper wrapper2 = new SimpleWrapper();

wrapper2.setName("Modern");

wrapper2.setServletClass("ModernServlet");

​ 可以看到Serlvlet跟SimpleWrapper相关,而SimpleWrapper的构造器有如下代码:

1
2
3
4
5
6
7
public SimpleWrapper() 

{

​ pipeline.setBasic(new SimpleWrapperValve());

}

​ 可以看到SimpleWrapperValve被设置为基础阀。在查看SimpleWrapperValve的invoke方法代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public void invoke(Request request, Response response, ValveContext valveContext)  throws IOException, ServletException { 

​ SimpleWrapper wrapper = (SimpleWrapper) getContainer();

​ ServletRequest sreq = request.getRequest();

​ ServletResponse sres = response.getResponse();

​ Servlet servlet = null;

​ HttpServletRequest hreq = null;

if (sreq instanceof HttpServletRequest)

​ hreq = (HttpServletRequest) sreq;

​ HttpServletResponse hres = null;

if (sres instanceof HttpServletResponse)

​ hres = (HttpServletResponse) sres;

// Allocate a servlet instance to process this request

try {

​ servlet = wrapper.allocate();

if (hres!=null && hreq!=null) {

​ servlet.service(hreq, hres);

​ } else {

​ servlet.service(sreq, sres);

​ }

​ } catch (ServletException e) {

​ }

}

​ 可以看到servlet的载入。

那么SimpleWrapperValve是怎么找到对应的Wrapper的呢?

​ 从代码

1
SimpleWrapper wrapper = (SimpleWrapper) getContainer(); 
看到是调用getContainer方法得到的。而setContainer()发生在addValve和setBasic方法中。

值得一提的是,SimpleWrapperValve在SimpleWrapper的pipeline实例中被设置为基础阀,而SimpleContext的pipeline实例中对应的基础阀则是SimpleContextValve,在SimpleContextValve的invoke方法中有获取Wrapper实例的代码,正是这样实现的从Context调用的子容器Wrapper

​ SimpleWrapperValve的invoke方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public void invoke(Request request, Response response, ValveContext valveContext)  throws IOException, ServletException {

// Validate the request and response object types

if (!(request.getRequest() instanceof HttpServletRequest)

​ || !(response.getResponse() instanceof HttpServletResponse))

​ {

return;

// NOTE - Not much else we can do generically

​ }

// Disallow any direct access to resources under WEB-INF or META-INF

​ HttpServletRequest hreq = (HttpServletRequest) request.getRequest();

​ String contextPath = hreq.getContextPath();

​ String requestURI = ((HttpRequest) request).getDecodedRequestURI();

​ String relativeURI = requestURI.substring(contextPath.length()).toUpperCase();

​ Context context = (Context) getContainer();

// Select the Wrapper to be used for this Request

​ Wrapper wrapper = null;

try {

​ wrapper = (Wrapper) context.map(request, true);

​ } catch (IllegalArgumentException e) {

​ badRequest(requestURI, (HttpServletResponse) response.getResponse());

return;

​ } if (wrapper == null)

​ {

​ notFound(requestURI, (HttpServletResponse) response.getResponse());

return;

​ }

// Ask this Wrapper to process this Request

​ response.setContext(context);

​ wrapper.invoke(request, response);

}

总结起来就是httpProcessor在process()方法中调用connector.getcontainer().invoke(),使得SimpleContext中的pipeline逐个调用阀,而SimpleContextValve这个基础阀调用了Wrapper,从而实现servlet的载入调用

在数据库中,触发器可以在INSERT、UPDATE或DELETE的时候,执行一些特定的操作,以此实现一些业务逻辑,可以大大简化应用程序设计。这一点使用过触发器的都会很有感触,这里主要提一下使用触发器需要注意的几个弊端。

  • MySQL触发器能基于行触发,MySQL触发器始终时基于表中的一条记录触发,而不是一组SQL语句。因此,如果需要变动整个数据集而数据集数据量又较大时,触发器效果会非常低。
  • 每一个表的一个事件只能定义一个触发器。
  • 由于MySQL触发器基于行触发的特性,因此对于批量操作并不适合使用触发器
  • 使用触发器实现的业务逻辑在出现问题时很难进行定位,特别是设计到多个触发器的情况
  • 协同开发时,写业务层代码如果不清楚数据库触发器的细节,容易搞不清到底触发了那些触发器
  • 大量使用触发器会导致代码结构容易被打乱,阅读源码困难

mac的shadowsocks客户端是没有像windows版那样的局域网代理的功能的,虚拟机科学上网如果重新配置shadowsocks就太麻烦了,完全没必要,可以通过mac宿主机代理实现,方法如下:

1.安装privoxy

通过homebrew安装: homebrew install privoxy

2.配置privoxy

修改 /usr/local/etc/privoxy/config 文件:

  • 将forward-socks5t / 改成 forward-socks5t / 127.0.0.1:1080 .(1080是shadowsocks默认端口,根据自己的配置改)
  • 将listen-address 127.0.0.1:8118 改成 listen-address 0.0.0.0:1081 (最后的端口号随便改,确保没被占用就好)

终端代理

mac下终端代理:

export http_proxy=”http://127.0.0.1:1081"

export https_proxy=”http://127.0.0.1:1081"

虚拟机下将ip 127.0.0.1改成宿主机相对虚拟机的IP地址

手机代理

手机也可以走mac建的http代理,只需要处于同一无线局域网,然后将wifi的http代理设置为:

http://ip:1081

ip为mac在改局域网内的ip地址,端口号1081根据自己的配置更改

ios的话更推荐下一个Shadowrocket,直接配shadowsocks代理更方便点

PAC自动代理

以上方法都是走全局代理的,上国内网站会变慢,可以配PAC自动代理使得访问国内网站时直连不走代理:

mac的shadowsocks客户端自带了PAC文件,默认是在http://127.0.0.1:8090/proxy.pac

将其下载下来,做以下修改:

把var proxy = “SOCKS5 127.0.0.1:1080; SOCKS 127.0.0.1:1080; DIRECT;”; 修改
var proxy = “PROXY IP:PORT; DIRECT;”;

IP指mac的ip,PORT指privoxy配的端口

将修改的PAC挂在mac本地服务器(nginx、tomcat等)

最后一步,虚拟机内或者手机设置自动代理:http://IP:port/proxy.pac

IP是mac的IP,port的服务器端口

有的时候需要定时启动一个任务,这时候可以利用crontab实现

命令格式

  • crontab [-u user] file
  • cronrab [-u user] {-l | -r | -e}

常用命令参数

  • -u 指定用户
  • -l 输出当前用户已有的crontab内容
  • -r 删除当前用户的crontab内容

crontab文件

可以在任何地方新建crontab文件,然后使用 crontab[-u user] file 指令加载。
文件格式如下:

* * * * * command

每个*依次是分钟、小时、日、月、星期。
command是要执行的指令。
比如需要每天8点30执行执行指令,可以这么写:

30 8 * * * command

文件编辑完成后运行指令:
crontab[-u user] file 即可加载

总结

  • 先写好crontab 文件
  • crontab [-u user] file 加载
  • crontab 也可以用在mac os x

可以在服务端的handler中将利用HashSet将session保存下来,具体做法可以是:
在sessionCreatedd是时候加入到HashSet(sessions)中

1
2
3
4
public void sessionCreated(IoSession session) throws Exception {
/**/
sessions.add(session);
}

sessionClosed方法内remove就OK。
需要主动推送消息的时候取出相应session,然后session.write(message);

mina自带的codec filter已经很强大了,但是有的时候需要建立自己的codec filter。
在mina中调用session.wirte(object)可以发送一个对象,这个对象的类可以是用户自建的,只需要建立好相关的codec就可以正常发送。

这里新建一个简单的类表示时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 public class Time implements Serializable {
private int hours;
private int minutes;
private int seconds;
public int getHours() {
return hours;
}
public void setHours(int hours) {
this.hours = hours;
}
public int getMinutes() {
return minutes;
}
public void setMinutes(int minutes) {
this.minutes = minutes;
}
public int getSeconds() {
return seconds;
}
public void setSeconds(int seconds) {
this.seconds = seconds;
}
@Override
public String toString() {
return "Time now:"+hours+":"+minutes+":"+seconds;
}
}

然后是关于Time类的decoder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 public class TimeMessageDecoder implements MessageDecoder {
@Override
public MessageDecoderResult decodable(IoSession session, IoBuffer in) {
if (in.remaining() < 1) {
return MessageDecoderResult.NEED_DATA;
}
// Return NOT_OK if not matches.
return MessageDecoderResult.OK;
}
@Override
public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
if (in.remaining() < 1) {
return MessageDecoderResult.NEED_DATA;
}
int hours = in.getInt();
int minutes = in.getInt();
int seconds = in.getInt();
Time time = new Time();
time.setHours(hours);
time.setMinutes(minutes);
time.setSeconds(seconds);
out.write(time);
return MessageDecoderResult.OK;
}
@Override
public void finishDecode(IoSession arg0, ProtocolDecoderOutput arg1) throws Exception {
// TODO Auto-generated method stub
}
}

以及encoder:

1
2
3
4
5
6
7
8
9
10
11
12
public class TimeMessageEncoder<T extends Time> implements MessageEncoder<T> {
@Override
public void encode(IoSession session, T message, ProtocolEncoderOutput out) throws Exception {
IoBuffer buffer = IoBuffer.allocate(100);
buffer.setAutoExpand(true);
buffer.putInt(message.getHours());
buffer.putInt(message.getMinutes());
buffer.putInt(message.getSeconds());
buffer.flip();
out.write(buffer);
}
}

CodecFactory:

1
2
3
4
5
6
public class TimeProtocolCodecFactory extends DemuxingProtocolCodecFactory {
public TimeProtocolCodecFactory() {
super.addMessageEncoder(Time.class, TimeMessageEncoder.class);
super.addMessageDecoder(TimeMessageDecoder.class);
}
}

在client中加入filterchain:

1
2
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new TimeProtocolCodecFactory()));

在server中相应加入:

1
2
acceptor.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new TimeProtocolCodecFactory()));

这样就可以了,在handler中发数据直接session.write(object instace of Time);