Chapter 10 covers web application security constraints for restricting access to certain contents. You will learn entities related to security such as principals, roles, login config, authenticators, etc. You will also write two applications that install an authenticator valve in the StandardContext object and uses basic authentication to authenticate users.
主体和之前的一样,新加的内容是安全相关的东西,更具体来说,是授权相关。可以根据配置的账户密码信息限制用户访问。这个功能现在应该挺鸡肋了,因为一般的 App 都是将这部分功能坐在内部的 login service 中的,哪里会通过这种方式作授权啊,除非买现成的但是不提供授权服务,这也太蠢了吧。。。
publicinterfaceRealm{ public Principal authenticate(String username, String credentials); public Principal authenticate(String username, byte[] credentials); public Principal authenticate(String username, String digest, String nonce, String nc, String cnonce, String qop, String realm, String md5a2); public Principal authenticate(X509Certificate certs[]); }
同时这个接口还包含 public boolean hasRole(Principal principal, String role); 方法。这个接口有一个抽象实现 org.apache.catalina.realm.RealmBase 还有几个具体实现都在同一个包下:JDBCRealm, JNDIRealm, MemoryRealm, and UserDatabaseRealm。默认使用的是 MemoryRealm,当 server 启动时,他会读取 tomcat-users.xml。
GenericPrincipal
java.security.Principal 代表 Principal 这个概念,具体实现为 org.apache.catalina.realm.GenericPrincipal。GenericPrincipal 必须关联一个 realm, 构造函数如下
publicGenericPrincipal(Realm realm, String name, String password, List roles){ super(); this.realm = realm; this.name = name; this.password = password; if (roles != null) { this.roles = new String[roles.size()]; this.roles = (String[]) roles.toArray(this.roles); if (this.roles.length > 0) Arrays.sort(this.roles); } }
Principal 中也包含 hasRole() 方法,你可以传入 * 作为参数检测是否包含任意 role 的意思。
LoginConfig
login config 包含 realm name 由 org.apache.catalina.deploy.LoginConfig 这个 final class 表示. LoginConfig 包含 realm 和 authentication 的信息,auth name 必须是 BASIC, DIGEST, FORM, or CLIENT-CERT。
privatesynchronizedvoidauthenticatorConfig(){ // 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) { } } }
private User getUser(String username, String password){ Iterator iterator = users.iterator(); while (iterator.hasNext()) { User user = (User) iterator.next(); if (user.username.equals(username) && user.password.equals(password)) return user; } returnnull; }
privatevoidcreateUserDatabase(){ User user1 = new User("ken", "blackcomb"); user1.addRole("manager"); user1.addRole("programmer"); User user2 = new User("cindy", "bamboo"); user2.addRole("programmer");
protected UserDatabase database = null; protectedstaticfinal String name = "SimpleUserDatabaseRealm";
protected String resourceName = "UserDatabase";
public Principal authenticate(String username, String credentials){ // Does a user with this username exist? User user = database.findUser(username); if (user == null) { return (null); }
// Do the credentials specified by the user match? // FIXME - Update all realms to support encoded passwords boolean validated = false; if (hasMessageDigest()) { // Hex hashes should be compared case-insensitive validated = (digest(credentials).equalsIgnoreCase(user.getPassword())); } else { validated = (digest(credentials).equals(user.getPassword())); } if (!validated) { returnnull; }
ArrayList combined = new ArrayList(); Iterator roles = user.getRoles(); while (roles.hasNext()) { Role role = (Role) roles.next(); String rolename = role.getRolename(); if (!combined.contains(rolename)) { combined.add(rolename); } } Iterator groups = user.getGroups(); while (groups.hasNext()) { Group group = (Group) groups.next(); roles = group.getRoles(); while (roles.hasNext()) { Role role = (Role) roles.next(); String rolename = role.getRolename(); if (!combined.contains(rolename)) { combined.add(rolename); } } } return (new GenericPrincipal(this, user.getUsername(), user.getPassword(), combined)); }
Chapter 9 discusses the manager, the component that manages sessions in session management. It explains the various types of managers and how a manager can persist session objects into a store. At the end of the chapter, you will learn how to build an application that uses a StandardManager instance to run a servlet that uses session objects to store values.
在 Class 文件格式与执行引擎这部分里,用户的程序能直接参与的内容并不多,Class 以何种格式存储,类型和施加在,如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能操作的,主要是字节码生成与类加载两个部分的功能,但仅仅在如何处理这两点上,就已经出现了许多值得欣赏借鉴的思路,这些思路后来成为很多常用功能和程序实现的基础。
9.2 案例分析
四个案例,关于类加载和字节码各两个。
9.2.1 Tomcat: 正统的类加载架构
主流的 Java Web 服务器,如 Tomcat, Jetty 等都实现了自己定义的类加载器,而且还不止一个。因为一个功能健全的 Web 服务器,都要解决如下这些问题:
部署在同一个服务器上的两个 web 应用程序所使用的 Java 类库可以实现互相隔离。两个不同应用可能依赖同一个第三方类库的不同版本,不能要求每个类库在一个服务器中只能有一份,服务器应该能够保证两个独立应用程序的类库可以互相独立使用。
部署在同一个服务器上的两个 web 应用所使用的 Java 类库可以互相共享。与前一个相反,但很常见,如用户可能有 10 个使用 Spring 的应用部署在统一台服务器,如果把 10 份 Spring 分别存在应用的隔离目录,将会很大的浪费资源。磁盘空间是其次,主要是良妃内存,很容易造成方法去过度膨胀的风险。
服务器需要尽可能保证自身的安全不受部署的 Web 应用程序的影响。一般来说,给予安全考虑,服务器所使用的类库应该与程序类库相互独立。
只是 JSP 应用的 Web 服务器,十有八九都需要支持 HotSwap 功能。JSP 由于其纯文本特性,修改几率远大于第三方类库和自己的 Class 文件。ASP,PHP 和 JSP 这些网页应用也将修改后无需重启作为优势来看待,因此,主流 Web 服务器都会支持 JSP 生成类的热替换。
由于以上问题,不是 web 应用时,单独一个 ClassPath 就不能满足要求了,所以各种 web 服务器不约而同的提供了好几个不同还以的 ClassPath 路径供用户存放第三方类库。一般这些路径都以 lib 或 classes 命名。不同路径中的类库,具备不同的访问范围和服务对象,通常每个目录都会对应一个自定义类加载器去加载防止在里面的 Java 类库。下面以 Tomcat 为例,分析其规划。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader }
if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name);
// this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
publicclassMoreBasicThreads{ publicstaticvoidmain(String[] args){ for (int i = 0; i < 5; i++) new Thread(new LiftOff()).start(); System.out.println("Waiting for LiftOff..."); } }
publicclassCallableDemo{ publicstaticvoidmain(String[] args)throws ExecutionException, InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); ArrayList<Future<String>> results = new ArrayList<>(); for (int i = 0; i < 10; i++) { results.add(exec.submit(new TaskWithResult(i))); } exec.shutdown(); for (Future<String> fs : results) { System.out.println(fs.get()); } } }
// result of TaskWithResult 0 // result of TaskWithResult 1 // result of TaskWithResult 2 // result of TaskWithResult 3 // result of TaskWithResult 4 // result of TaskWithResult 5 // result of TaskWithResult 6 // result of TaskWithResult 7 // result of TaskWithResult 8 // result of TaskWithResult 9
@Override publicvoidrun(){ Thread.currentThread().setPriority(priority); while (true) { // An expensive, interruptable operation for (int i = 0; i < 100000; i++) { d += (Math.PI + Math.E) / (double) i; if (i % 1000 == 0) Thread.yield(); } System.out.println(this); if (--countDown == 0) return; } }
publicvoidrunTask(){ if (t == null) { t = new Thread(name) { @Override publicvoidrun(){ try { while(true) { System.out.println(this); if (--countDown == 0) return; sleep(10); } } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); } } }
客户端调用
1 2 3 4 5 6 7 8 9
publicclassThreadVariations{ publicstaticvoidmain(String[] args){ new InnerThread1("InnerThread1"); new InnerThread2("InnerThread2"); new InnerRunnable1("InnerRunnable1"); new InnerRunnable2("InnerRunnable2"); new ThreadMethod("ThreadMethod").runTask(); } }
Terminology
术语解释,没发现什么有趣的点
Joining a thread
在线程 A 的执行中,如果你 call 了线程 B 的 join() 方法,那么,线程 A 会等待线程 B 结束后再执行。
上面的 join 的行为可以通过调用 B 的 interrup() 方法进行打断
示例解析:
Sleeper 继承自 Thread 通过构造函数指定线程名称和休眠时间。当被打断时输出日志。
Joiner 继承自 Thread, 通过参数指定 Thread name 和将要 join 的 thread
publicEvenChecker(IntGenerator g, int ident){ generator = g; id = ident; }
@Override publicvoidrun(){ while (!generator.isCanceled()) { int val = generator.next(); if (val % 2 != 0) { System.out.println(val + " not event..."); generator.cancel(); } } }
publicstaticvoidtest(IntGenerator gp, int count){ System.out.println("Ctrl + c to exit"); ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < count; i++) { exec.execute(new EvenChecker(gp, i)); } exec.shutdown(); }
publicstaticvoidmain(String[] args){ EvenChecker.test(new EvenGenerator()); } // } // Ctrl + c to exit // 1515 not event... // 1519 not event... // 1517 not event...
publicstaticvoidmain(String[] args)throws InterruptedException { final AttemptLocking al = new AttemptLocking(); al.untimed(); al.timed(); new Thread() { {setDaemon(true);}
The Goetz Test: If you can write a hight-perormance JVM for a modern microprocessor, then you are qualified to think about whether you can avoid synchronizing.
classCircularSet{ privateint[] array; privateint len; privateint index = 0; publicCircularSet(int size){ array = newint[size]; len = size; // Initialize to a value not produced by the SerialNumberChecker for (int i = 0; i < size; i++) { array[i] = -1; } }
publicsynchronizedvoidadd(int i){ array[index] = i; // Wrap index and write over old elements index = ++index % len; }
publicsynchronizedbooleancontains(int val){ for (int i = 0; i < len; i++) { if (array[i] == val) returntrue; } returnfalse; } } // Duplicate: 47
publicclassOrnamentalGarden{ publicstaticvoidmain(String[] args)throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { exec.execute(new Entrance(i)); } // Run for a while then stop and collect the data: TimeUnit.SECONDS.sleep(3); Entrance.cancel(); exec.shutdown(); if (!exec.awaitTermination(250, TimeUnit.MILLISECONDS)) System.out.println("Some task were not terminated!"); System.out.println("Total: " + Entrance.getTotalCount()); System.out.println("Sum of Entrances: " + Entrance.sumEntrances()); } }
classCount{ privateint count = 0; private Random rand = new Random(47);
// Remove the synchronized keyword to see counting fail: publicsynchronizedintincrement(){ int temp = count; if (rand.nextBoolean()) // Yield half the time Thread.yield(); return (count = ++temp); }
publicsynchronizedintvalue(){ return count; } }
classEntranceimplementsRunnable{ privatestatic Count count = new Count(); privatestatic List<Entrance> entrances = new ArrayList<>(); privateint number = 0; // Doesn't need synchronization to read: privatefinalint id; privatestaticvolatileboolean canceled = false;
// Atomic operation on a volatile field: publicstaticvoidcancel(){ canceled = true; }
publicEntrance(int id){ this.id = id; // Keep this task in a list. Also prevents garbage collection of dead tasks: entrances.add(this); }
publicSynchronizedBlocked(){ new Thread() { publicvoidrun(){ f(); // Lock acquired by this thread } }.start(); }
publicvoidrun(){ System.out.println("Trying to call f()"); f(); System.out.println("Exiting SynchronizedBlocked.run()"); } }
// Interrupting org.jz.c23.SleepBlocked // Interrupt sent to org.jz.c23.SleepBlocked // InterruptedException // Existing SleepBlocked.run() // Waiting for read(): // Interrupting org.jz.c23.IOBlocked // Interrupt sent to org.jz.c23.IOBlocked // Trying to call f() // Interrupting org.jz.c23.SynchronizedBlocked // Interrupt sent to org.jz.c23.SynchronizedBlocked // Aborting with System.exit(0)
publicclassInterrupting2{ publicstaticvoidmain(String[] args)throws InterruptedException { Thread t = new Thread(new Blocked2()); t.start(); TimeUnit.SECONDS.sleep(1); System.out.println("Issuing t.interrupt()"); t.interrupt(); } }
classBlockedMutex{ private Lock lock = new ReentrantLock();
publicBlockedMutex(){ // Acquire it right away, to demonstrate interruption of a task blocked on a ReentrantLock lock.lock(); }
publicvoidf(){ try { // This will never be available to a second task lock.lockInterruptibly(); System.out.println("lock acquired in f()"); } catch (InterruptedException e) { System.out.println("Interrupted from lock acquisition in f()"); } } }
classBlocked2implementsRunnable{ BlockedMutex blocked = new BlockedMutex();
@Override publicvoidrun(){ System.out.println("Waiting for f() in BlockedMutex"); blocked.f(); System.out.println("Broken out of blocked call"); } }
// Waiting for f() in BlockedMutex // Issuing t.interrupt() // Interrupted from lock acquisition in f() // Broken out of blocked call
在提示 Enter a file in which to save the key (/Users/you/.ssh/id_rsa): [Press enter] 时添加后缀指定环境, 我本地的配置情况 id_rsa_github id_rsa_github.pub id_rsa_sap id_rsa_sap.pub
The Facade Pattern provides a unified interface to a set of interfaces in a subsytem. Facade defines a higher-level interface that makes the subsystem easier to use. 提供一套更 high-level 的接口简化子系统调用
A facade not only simplifies an interface, it decouples a client from a subsystem of components. Facades and adapters may wrap multiple classes, but a facade’s intent is to simplify, while an adapter’s is to convert the interface to something different. 外观模式不仅仅是简化接口,同时他还将子系统和客户端解耦了 Facade 和 Adapter 都会在类外面包一层,但是 Facade 是为了简化,而 Adapter 是为了转换
Chapter 8 explains about loaders. A loader is an important Catalina module responsible for loading servlet and other classes that a web application uses. This chapter also shows how application reloading is achieved.
之前章节我们已经给出了一个简单的 loader 实现用于加载 servlet。这章我们将介绍 tomcat 的 standard web application loader. servlet container 必须实现自己的 loader,而不能使用系统自带的那个。因为它不信任运行的 servlets。如果它像我们之前的例子那样使用默认的类加载器,那么 servlet 将可以访问任何 JVM classpath 下的 class 和 lib,这和 security 的规则相违背。
Class clazz = Class.forName(loaderClass); WebappClassLoader classLoader = null;
if (parentClassLoader == null) { // Will cause a ClassCast is the class does not extend WCL, but // this is on purpose (the exception will be caught and rethrown) classLoader = (WebappClassLoader) clazz.newInstance(); } else { Class[] argTypes = { ClassLoader.class }; Object[] args = { parentClassLoader }; Constructor constr = clazz.getConstructor(argTypes); classLoader = (WebappClassLoader) constr.newInstance(args); }
All previously loaded classes are cached, so first check the local cache.
If not found in the local cache, check in the cache, i.e. by calling the findLoadedClass of the java.lang.ClassLoader class.
If not found in both caches, use the system’s class loader to prevent the web application from overriding J2EE class.
If SecurityManager is used, check if the class is allowed to be loaded. If the class is not allowed, throw a ClassNotFoundException.
If the delegate flag is on or if the class to be loaded belongs to the package name in the package trigger, use the parent class loader to load the class. If the parent class loader is null, use the system class loader.
Load the class from the current repositories.
If the class is not found in the current repositories, and if the delegate flag is not on, use the parent class loader. If the parent class loader is null, use the system class loader.
If the class is still not found, throw a ClassNotFoundException.
// context.addServletMapping()... // add ContextConfig. This listener is important because it configures // StandardContext (sets configured to true), otherwise StandardContext // won't start LifecycleListener listener = new SimpleContextConfig(); ((Lifecycle) context).addLifecycleListener(listener);
// here is our loader Loader loader = new WebappLoader(); // associate the loader with the Context context.setLoader(loader);
connector.setContainer(context);
try { connector.initialize(); ((Lifecycle) connector).start(); ((Lifecycle) context).start(); // now we want to know some details about WebappLoader WebappClassLoader classLoader = (WebappClassLoader) loader.getClassLoader(); System.out.println("Resources' docBase: " + ((ProxyDirContext) classLoader.getResources()).getDocBase()); String[] repositories = classLoader.findRepositories(); for (int i = 0; i < repositories.length; i++) { System.out.println(" repository: " + repositories[i]); }
// make the application wait until we press a key. System.in.read(); ((Lifecycle) context).stop(); } catch (Exception e) { e.printStackTrace(); } } }
publicclassFileLoggerextendsLoggerBaseimplementsLifecycle{ publicvoidstart()throws LifecycleException { // Validate and update our current component state if (started) thrownew LifecycleException(sm.getString("fileLogger.alreadyStarted")); lifecycle.fireLifecycleEvent(START_EVENT, null); started = true; }
publicvoidstop()throws LifecycleException { // Validate and update our current component state if (!started) thrownew LifecycleException(sm.getString("fileLogger.notStarted")); lifecycle.fireLifecycleEvent(STOP_EVENT, null); started = false; close(); } }
publicvoidlog(String msg){ // Construct the timestamp we will use, if requested Timestamp ts = new Timestamp(System.currentTimeMillis()); String tsString = ts.toString().substring(0, 19); String tsDate = tsString.substring(0, 10);
// If the date has changed, switch log files if (!date.equals(tsDate)) { synchronized (this) { if (!date.equals(tsDate)) { close(); date = tsDate; open(); } } }
// Log this message, timestamped if necessary if (writer != null) { if (timestamp) { writer.println(tsString + " " + msg); } else { writer.println(msg); } } }
通常情况下 FileLogger 会操作多个文件,在操作下一个时会把当前的关掉。
The open method
先检查目录是否村子啊,没有就建一个。然后再创建 log 文件的 PrintWriter 并返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
privatevoidopen(){
// Create the directory if necessary File dir = new File(directory); if (!dir.isAbsolute()) dir = new File(System.getProperty("catalina.base"), directory); dir.mkdirs();
// Open the current log file try { String pathname = dir.getAbsolutePath() + File.separator + prefix + date + suffix; writer = new PrintWriter(new FileWriter(pathname, true), true); } catch (IOException e) { writer = null; }
}
The close method
很简单,flush IO 流并重制变量
1 2 3 4 5 6 7 8
privatevoidclose(){ if (writer == null) return; writer.flush(); writer.close(); writer = null; date = ""; }
这个案例运行的时候大概卡了快一分钟才执行完,看了 log 原来是有 class 找不到, 应该是我用的 jar 包已经是 Catalina 了,不是 tomcat 了,版本太高
1 2 3 4 5 6 7 8 9 10 11 12 13 14
2021-08-02 10:38:13 HttpProcessor[8080][4] process.invoke java.lang.NoClassDefFoundError: org/apache/tomcat/util/log/SystemLogHandler at org.apache.catalina.connector.RequestBase.recycle(RequestBase.java:562) at org.apache.catalina.connector.HttpRequestBase.recycle(HttpRequestBase.java:417) at org.apache.catalina.connector.http.HttpRequestImpl.recycle(HttpRequestImpl.java:195) at org.apache.catalina.connector.http.HttpProcessor.process(HttpProcessor.java:1101) at org.apache.catalina.connector.http.HttpProcessor.run(HttpProcessor.java:1151) at java.lang.Thread.run(Thread.java:836) Caused by: java.lang.ClassNotFoundException: org.apache.tomcat.util.log.SystemLogHandler at java.net.URLClassLoader.findClass(URLClassLoader.java:444) at java.lang.ClassLoader.loadClass(ClassLoader.java:480) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:384) at java.lang.ClassLoader.loadClass(ClassLoader.java:413) ... 6 more