安全性是Java应用程序的非功能性需求的重要组成部分,如同其它的非功能性需求同样,安全性很容易被开发人员所忽略。固然,对于Java EE的开发人员来讲,安全性的话题可能没那么陌生,用户认证和受权多是绝大部分Web应用都有的功能。相似Spring Security这样的框架,也使得开发变得更加简单。本文并不会讨论Web应用的安全性,而是介绍Java安全一些底层和基本的内容。html
用户认证是应用安全性的重要组成部分,其目的是确保应用的使用者具备合法的身份。 Java安全中使用术语主体(Subject)来表示访问请求的来源。一个主体能够是任何的实体。一个主体能够有多个不一样的身份标识(Principal)。好比一个应用的用户这类主体,就能够有用户名、身份证号码和手机号码等多种身份标识。除了身份标识以外,一个主体还能够有公开或是私有的安全相关的凭证(Credential),包括密码和密钥等。java
典型的用户认证过程是经过登陆操做来完成的。在登陆成功以后,一个主体中就具有了相应的身份标识。Java提供了一个可扩展的登陆框架,使得应用开发人员能够很容易的定制和扩展与登陆相关的逻辑。登陆的过程由LoginContext启动。在建立LoginContext的时候须要指定一个登陆配置(Configuration)的名称。该登陆配置中包含了登陆所需的多个LoginModule的信息。每一个LoginModule实现了一种登陆方式。当调用LoginContext的login方法的时候,所配置的每一个LoginModule会被调用来执行登陆操做。若是整个登陆过程成功,则经过getSubject方法就能够获取到包含了身份标识信息的主体。开发人员能够实现本身的LoginModule来定制不一样的登陆逻辑。算法
每一个LoginModule的登陆方式由两个阶段组成。第一个阶段是在login方法的实现中。这个阶段用来进行必要的身份认证,可能须要获取用户的输入,以及经过数据库、网络操做或其它方式来完成认证。当认证成功以后,把必要的信息保存起来。若是认证失败,则抛出相关的异常。第二阶段是在commit或abort方法中。因为一个登陆过程可能涉及到多个LoginModule。LoginContext会根据每一个LoginModule的认证结果以及相关的配置信息来肯定本次登陆是否成功。LoginContext用来判断的依据是每一个LoginModule对整个登陆过程的必要性,分红必需、必要、充分和可选这四种状况。若是登陆成功,则每一个LoginModule的commit方法会被调用,用来把身份标识关联到主体上。若是登陆失败,则LoginModule 的abort方法会被调用,用来清除以前保存的认证相关信息。spring
在LoginModule进行认证的过程当中,若是须要获取用户的输入,能够经过CallbackHandler和对应的Callback来完成。每一个Callback能够用来进行必要的数据传递。典型的启动登陆的过程以下:数据库
1 public Subject login() throws LoginException { 2 TextInputCallbackHandler callbackHandler = new TextInputCallbackHandler(); 3 LoginContext lc = new LoginContext("SmsApp", callbackHandler); 4 lc.login(); 5 return lc.getSubject(); 6 }
这里的SmsApp是登陆配置的名称,能够在配置文件中找到。该配置文件的内容也很简单。api
1 SmsApp { 2 security.login.SmsLoginModule required; 3 };
这里声明了使用security.login.SmsLoginModule这个登陆模块,并且该模块是必需的。配置文件能够经过启动程序时的参数java.security.auth.login.config来指定,或修改JVM的默认设置。下面看看SmsLoginModule的核心方法login和commit。安全
1 public boolean login() throws LoginException { 2 TextInputCallback phoneInputCallback = new TextInputCallback("Phone number: "); 3 TextInputCallback smsInputCallback = new TextInputCallback("Code: "); 4 try { 5 handler.handle(new Callback[] {phoneInputCallback, smsInputCallback}); 6 } catch (Exception e) { 7 throw new LoginException(e.getMessage()); 8 } 9 String code = smsInputCallback.getText(); 10 boolean isValid = code.length() > 3; //此处只是简单的进行验证。 11 if (isValid) { 12 phoneNumber = phoneInputCallback.getText(); 13 } 14 return isValid; 15 } 16 public boolean commit() throws LoginException { 17 if (phoneNumber != null) { 18 subject.getPrincipals().add(new PhonePrincipal(phoneNumber)); 19 return true; 20 } 21 return false; 22 }
这里使用了两个TextInputCallback来获取用户的输入。当用户输入的编码有效的时候,就把相关的信息记录下来,此处是用户的手机号码。在commit方法中,就把该手机号码做为用户的身份标识与主体关联起来。网络
在验证了访问请求来源的合法身份以后,另外一项工做是验证其是否具备相应的权限。权限由Permission及其子类来表示。每一个权限都有一个名称,该名称的含义与权限类型相关。某些权限有与之对应的动做列表。比较典型的是文件操做权限FilePermission,它的名称是文件的路径,而它的动做列表则包括读取、写入和执行等。Permission类中最重要的是implies方法,它定义了权限之间的包含关系,是进行验证的基础。架构
权限控制包括管理和验证两个部分。管理指的是定义应用中的权限控制策略,而验证指的则是在运行时刻根据策略来判断某次请求是否合法。策略能够与主体关联,也能够没有关联。策略由Policy来表示,JDK提供了基于文件存储的基本实现。开发人员也能够提供本身的实现。在应用运行过程当中,只可能有一个Policy处于生效的状态。验证部分的具体执行者是AccessController,其中的checkPermission方法用来验证给定的权限是否被容许。在应用中执行相关的访问请求以前,都须要调用checkPermission方法来进行验证。若是验证失败的话,该方法会抛出AccessControlException异常。 JVM中内置提供了一些对访问关键部份内容的访问控制检查,不过只有在启动应用的时经过参数-Djava.security.manager启用了安全管理器以后才能生效,并与策略相配合。oracle
与访问控制相关的另一个概念是特权动做。特权动做只关心动做自己所要求的权限是否具有,而并不关心调用者是谁。好比一个写入文件的特权动做,它只要求对该文件有写入权限便可,并不关心是谁要求它执行这样的动做。特权动做根据是否抛出受检异常,分为PrivilegedAction和PrivilegedExceptionAction。这两个接口都只有一个run方法用来执行相关的动做,也能够向调用者返回结果。经过AccessController的doPrivileged方法就能够执行特权动做。
Java安全使用了保护域的概念。每一个保护域都包含一组类、身份标识和权限,其意义是在当访问请求的来源是这些身份标识的时候,这些类的实例就自动具备给定的这些权限。保护域的权限既能够是固定,也能够根据策略来动态变化。ProtectionDomain类用来表示保护域,它的两个构造方法分别用来支持静态和动态的权限。通常来讲,应用程序一般会涉及到系统保护域和应用保护域。很多的方法调用可能会跨越多个保护域的边界。所以,在AccessController进行访问控制验证的时候,须要考虑当前操做的调用上下文,主要指的是方法调用栈上不一样方法所属于的不一样保护域。这个调用上下文通常是与当前线程绑定在一块儿的。经过AccessController的getContext方法能够获取到表示调用上下文的AccessControlContext对象,至关于访问控制验证所需的调用栈的一个快照。在有些状况下,会须要传递此对象以方便在其它线程中进行访问控制验证。
考虑下面的权限验证代码:
1 Subject subject = new Subject(); 2 ViewerPrincipal principal = new ViewerPrincipal("Alex"); 3 subject.getPrincipals().add(principal); 4 Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() { 5 public Object run() { 6 new Viewer().view(); 7 return null; 8 } 9 }, null);
这里建立了一个新的Subject对象并关联上身份标识。一般来讲,这个过程是由登陆操做来完成的。经过Subject的doAsPrivileged方法就能够执行一个特权动做。Viewer对象的view方法会使用AccessController来检查是否具备相应的权限。策略配置文件的内容也比较简单,在启动程序的时候经过参数java.security.auth.policy指定文件路径便可。
1 grant Principal security.access.ViewerPrincipal "Alex" { 2 permission security.access.ViewPermission "CONFIDENTIAL"; 3 }; //这里把名称为CONFIDENTIAL的ViewPermission受权给了身份标识为Alex的主体。
构建安全的Java应用离不开加密和解密。Java的密码框架采用了常见的服务提供者架构,以提供所需的可扩展性和互操做性。该密码框架提供了一系列经常使用的服务,包括加密、数字签名和报文摘要等。这些服务都有服务提供者接口(SPI),服务的实现者只须要实现这些接口,并注册到密码框架中便可。好比加密服务Cipher的SPI接口就是CipherSpi。每一个服务均可以有不一样的算法来实现。密码框架也提供了相应的工厂方法用来获取到服务的实例。好比想使用采用MD5算法的报文摘要服务,只须要调用MessageDigest.getInstance("MD5")便可。
加密和解密过程当中并不可少的就是密钥(Key)。加密算法通常分红对称和非对称两种。对称加密算法使用同一个密钥进行加密和解密;而非对称加密算法使用一对公钥和私钥,一个加密的时候,另一个就用来解密。不一样的加密算法,有不一样的密钥。对称加密算法使用的是SecretKey,而非对称加密算法则使用PublicKey和PrivateKey。与密钥Key对应的另外一个接口是KeySpec,用来描述不一样算法的密钥的具体内容。好比一个典型的使用对称加密的方式以下:
1 KeyGenerator generator = KeyGenerator.getInstance("DES"); 2 SecretKey key = generator.generateKey(); 3 saveFile("key.data", key.getEncoded()); 4 Cipher cipher = Cipher.getInstance("DES"); 5 cipher.init(Cipher.ENCRYPT_MODE, key); 6 String text = "Hello World"; 7 byte[] encrypted = cipher.doFinal(text.getBytes()); 8 saveFile("encrypted.bin", encrypted);
加密的时候首先要生成一个密钥,再由Cipher服务来完成。能够把密钥的内容保存起来,方便传递给须要解密的程序。
1 byte[] keyData = getData("key.data"); 2 SecretKeySpec keySpec = new SecretKeySpec(keyData, "DES"); 3 Cipher cipher = Cipher.getInstance("DES"); 4 cipher.init(Cipher.DECRYPT_MODE, keySpec); 5 byte[] data = getData("encrypted.bin"); 6 byte[] result = cipher.doFinal(data);
解密的时候先从保存的文件中获得密钥编码以后的内容,再经过SecretKeySpec获取到密钥自己的内容,再进行解密。
报文摘要的目的在于防止信息被有意或无心的修改。经过对原始数据应用某些算法,能够获得一个校验码。当收到数据以后,只须要应用一样的算法,再比较校验码是否一致,就能够判断数据是否被修改过。相对原始数据来讲,校验码长度更小,更容易进行比较。消息认证码(Message Authentication Code)与报文摘要相似,不一样的是计算的过程当中加入了密钥,只有掌握了密钥的接收者才能验证数据的完整性。
使用公钥和私钥就能够实现数字签名的功能。某个发送者使用私钥对消息进行加密,接收者使用公钥进行解密。因为私钥只有发送者知道,当接收者使用公钥解密成功以后,就能够断定消息的来源确定是特定的发送者。这就至关于发送者对消息进行了签名。数字签名由Signature服务提供,签名和验证的过程都比较直接。
1 Signature signature = Signature.getInstance("SHA1withDSA"); 2 KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("DSA"); 3 KeyPair keyPair = keyGenerator.generateKeyPair(); 4 PrivateKey privateKey = keyPair.getPrivate(); 5 signature.initSign(privateKey); 6 byte[] data = "Hello World".getBytes(); 7 signature.update(data); 8 byte[] signatureData = signature.sign(); //获得签名 9 PublicKey publicKey = keyPair.getPublic(); 10 signature.initVerify(publicKey); 11 signature.update(data); 12 boolean result = signature.verify(signatureData); //进行验证
验证数字签名使用的公钥能够经过文件或证书的方式来进行发布。
在各类数据传输方式中,网络传输目前使用较广,可是安全隐患也更多。安全套接字链接指的是对套接字链接进行加密。加密的时候能够选择对称加密算法。可是如何在发送者和接收者之间安全的共享密钥,是个很麻烦的问题。若是再用加密算法来加密密钥,则成为了一个循环问题。非对称加密算法则适合于这种状况。私钥本身保管,公钥则公开出去。发送数据的时候,用私钥加密,接收者用公开的公钥解密;接收数据的时候,则正好相反。这种作法解决了共享密钥的问题,可是另外的一个问题是如何确保接收者所获得的公钥确实来自所声明的发送者,而不是伪造的。为此,又引入了证书的概念。证书中包含了身份标识和对应的公钥。证书由用户所信任的机构签发,并用该机构的私钥来加密。在有些状况下,某个证书签发机构的真实性会须要由另一个机构的证书来证实。经过这种证实关系,会造成一个证书的链条。而链条的根则是公认的值得信任的机构。只有当证书链条上的全部证书都被信任的时候,才能信任证书中所给出的公钥。
平常开发中比较常接触的就是HTTPS,即安全的HTTP链接。大部分用Java程序访问采用HTTPS网站时出现的错误都与证书链条相关。有些网站采用的不是由正规安全机构签发的证书,或是证书已通过期。若是必须访问这样的HTTPS网站的话,能够提供本身的套接字工厂和主机名验证类来绕过去。另一种作法是经过keytool工具把证书导入到系统的信任证书库之中。
1 URL url = new URL("https://localhost:8443"); 2 SSLContext context = SSLContext.getInstance("TLS"); 3 context.init(new KeyManager[] {}, new TrustManager[] {new MyTrustManager()}, new SecureRandom());HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); 4 connection.setSSLSocketFactory(context.getSocketFactory()); 5 connection.setHostnameVerifier(new MyHostnameVerifier());
这里的MyTrustManager实现了X509TrustManager接口,可是全部方法都是默认实现。而MyHostnameVerifier实现了HostnameVerifier接口,其中的verify方法老是返回true。