0%

tomcat学习笔记CH10_安全认证

配置信息的加载

  从《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状态码,则会显示验证失败。