1.向SharePoint发起认证请求
如图所示SharePoint Server需要我们按照其指定的方式提供Credentials。在这里其实就是需要对用户名和密码按照图中给定的NTLM认证方式进行完整的认证流程。
2.NTLM安全认证机制
NTLM-New Technology Lan Manager是微软弄出来的一套Windows NT早期的安全认证机制。
其主要的认证步骤如图所示:
Step1:Client端输入username,password,client会计算password的hash值然后将其缓存在本地
Step2:Client将username以明文的形式发送给DC(Distribution Center)
Step3:DC会生成一个16字节的随机数,即challenge(挑战码),再传回给client
Step4:当client收到challenge以后,会先复制一份出来,然后和缓存中的密码hash再一同混合hash一次,混合后的值称为response,之后client再将challenge,response及username一并都传给server
Step5:Server端在收到client传过来的这三个值以后会把它们都转发给DC
Step6:当DC接到过来的这三个值的以后,会根据username到域控的账号数据库(ntds.dit)里面找到该username对应的hash,然后把这个hash拿出来和传过来的challenge值再混合hash
Step7:将(6)中混合后的hash值跟传来的response进行比较,相同则认证成功,反之,则失败,当然,如果是本地登录,所有验证肯定也全部都直接在本地进行了
Note:NTLM是一种基于挑战(challenge)/响应(response)消息交互模式的认证过程,其认证过程存在很大安全问题。目前基本不怎么使用,至于SharePoint和SharePoint OnPremise仍然采用该认证方式,也许是历史遗留问题导致的。
3.Android端实现NTLM认证
Android6.0之前的版本我们可以使用Apache提供的HttpClient,其已经帮我们实现了NTLM认证方式。Google在6.0以后抛弃掉了HttpClient,经过一番调研找到HttpClient中实现NTLM的子模块[jcifs-1.3.19.jar],然后顺藤摸瓜就有了下文。
由于项目中网络框架使用的是OKHttp【https://github.com/square/okhttp/wiki/Recipes】,Okhttp官方给出的文档中有一段关于处理认证的描述:
[Handling authentication]
OkHttp can automatically retry unauthenticated requests. When a response is 401 Not Authorized
, an Authenticator
is asked to supply credentials. Implementations should build a new request that includes the missing credentials. If no credentials are available, return null to skip the retry.
Use Response.challenges()
to get the schemes and realms of any authentication challenges. When fulfilling a Basic
challenge, use Credentials.basic(username, password)
to encode the request header.
private final OkHttpClient client;
public Authenticate() {
client = new OkHttpClient.Builder()
.authenticator(new Authenticator() {
@Override public Request authenticate(Route route, Response response) throws IOException {
if (response.request().header("Authorization") != null) {
return null; // Give up, we've already attempted to authenticate.
}
System.out.println("Authenticating for response: " + response);
System.out.println("Challenges: " + response.challenges());
String credential = Credentials.basic("jesse", "password1");
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
})
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Authenticator接口说明.png
当Autheticator回调接口的时候,以Basic认证为例,这时需要Base64(username+":"+password)所生成的credentials添加在Authorization请求头里面即addHeader("Authorization",credentials)以响应DC发起的challenge。
4.NTLMAuthenticator具体实现,使用
@Override
public OkHttpClient createClient() {
return new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.authenticator(new NTLMAuthenticator(mUsername, mPassword))
.build();
}
class NTLMAuthenticator implements Authenticator {
private static final int TYPE_1_FLAGS =
NtlmFlags.NTLMSSP_NEGOTIATE_56 |
NtlmFlags.NTLMSSP_NEGOTIATE_128 |
NtlmFlags.NTLMSSP_NEGOTIATE_NTLM2 |
NtlmFlags.NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
NtlmFlags.NTLMSSP_REQUEST_TARGET;
private String mDomain;
private String mUsername;
private String mPassword;
private String mWorkstation;
NTLMAuthenticator(String username, String password) {
this("", username, password, "");
}
NTLMAuthenticator(String domain, String login, String password, String workstation) {
mUsername = login;
mPassword = password;
mDomain = domain;
mWorkstation = workstation;
}
@Override
public Request authenticate(Route route, Response response) throws IOException {
List<String> authHeaders = response.headers("WWW-Authenticate");
if (authHeaders != null) {
boolean negociate = false;
boolean ntlm = false;
String ntlmValue = null;
for (String authHeader : authHeaders) {
if (authHeader.equalsIgnoreCase("Negotiate")) {
negociate = true;
}
if (authHeader.equalsIgnoreCase("NTLM")) {
ntlm = true;
}
if (authHeader.startsWith("NTLM ")) {
ntlmValue = authHeader.substring(5);
}
}
if (negociate && ntlm) {
String type1Msg = generateType1Msg(mDomain, mWorkstation);
String header = "NTLM " + type1Msg;
return response.request().newBuilder().header("Authorization", header).build();
} else if (ntlmValue != null) {
String type3Msg = generateType3Msg(mUsername, mPassword, mDomain, mWorkstation, ntlmValue);
String ntlmHeader = "NTLM " + type3Msg;
return response.request().newBuilder().header("Authorization", ntlmHeader).build();
}
}
if (responseCount(response) <= 3) {
String credential = Credentials.basic(mUsername, mPassword);
return response.request().newBuilder().header("Authorization", credential).build();
}
return null;
}
private String generateType1Msg(@NonNull String domain, @NonNull String workstation) {
final Type1Message type1Message = new Type1Message(TYPE_1_FLAGS, domain, workstation);
byte[] source = type1Message.toByteArray();
return Base64.encode(source);
}
private String generateType3Msg(final String login, final String password, final String domain, final String workstation, final String challenge) {
Type2Message type2Message;
try {
byte[] decoded = Base64.decode(challenge);
type2Message = new Type2Message(decoded);
} catch (final IOException exception) {
exception.printStackTrace();
return null;
}
final int type2Flags = type2Message.getFlags();
final int type3Flags = type2Flags
& (0xffffffff ^ (NtlmFlags.NTLMSSP_TARGET_TYPE_DOMAIN | NtlmFlags.NTLMSSP_TARGET_TYPE_SERVER));
final Type3Message type3Message = new Type3Message(type2Message, password, domain,
login, workstation, type3Flags);
return Base64.encode(type3Message.toByteArray());
}
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
}
@Override
public String getResources(OkHttpClient client, String url) throws Exception {
// path amend
if (url.contains(" ")) {
url = url.replaceAll(" ", "%20");
}
Request request = new Request.Builder()
.url(url)
.addHeader("accept", "application/json;odata=verbose")
.addHeader("ContentType", "application/json;odata=verbose;charset=utf-8")
.get()
.build();
try {
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new Exception(response.message());
}
String results = response.body().string();
Log.d(TAG, "getResources: " + results);
return results;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
jcifs-1.3.19.jar的下载地址:https://jcifs.samba.org/
okhttp:https://github.com/square/okhttp/wiki/Recipes
部分参考链接:https://klionsec.github.io/2016/08/10/ntlm-kerberos/
http://davenport.sourceforge.net/ntlm.html
网友评论