banbanbai 发表于 2017-2-9 10:46:20

tomcat下摘要认证(数据库配置用户角色)+java代码模拟请求

1. 如果你不明白摘要认证,可以看看这个网站:【http://zh.wikipedia.org/wiki/HTTP摘要认证】
 
2. 这篇文章解决了什么呢?①基于tomcat的摘要认证配置 ②用户名与角色存储到数据库中 ③java代码模拟客户端请求服务端;好了,让我们一起来理解摘要认证原理吧。
 
3. 怎么给自己的资源(被请求的文件)加上摘要认证呢?且按照下面一步步配置:


[*]准备好环境,我的环境是:linux虚拟机+tomcat7.0+mysql+jdk1.7
[*]需要在mysql数据库中插入这两张表,并且需要在里面加入几条数据,我想这肯定难不倒你们的,贴出我的表结构吧。
[*]用户表:


  CREATE TABLE `TNt_tomcat_users` (
         `TNc_user_name` varchar(20) NOT NULL,
         `TNc_user_pass` varchar(64) NOT NULL,
         PRIMARY KEY (`TNc_user_name`)
       ) ENGINE=MyISAM DEFAULT CHARSET=utf8;


[*]角色表:
  CREATE TABLE `TNt_tomcat_roles` (
         `TNc_user_name` varchar(20) NOT NULL,
         `TNc_role_name` varchar(20) NOT NULL,
         PRIMARY KEY (`TNc_user_name`,`TNc_role_name`)
       ) ENGINE=MyISAM DEFAULT CHARSET=utf8;


[*]用户数据:
  insert into TNt_tomcat_users (TNc_user_name, TNc_user_pass) values ( 'zhang3', '123456');
  insert into TNt_tomcat_users (TNc_user_name, TNc_user_pass) values ( 'li4', '123456');


[*]角色数据:
  insert into TNt_tomcat_roles (TNc_user_name, TNc_role_name) values ( 'zhang3', 'ADMIN');
  insert into TNt_tomcat_roles (TNc_user_name, TNc_role_name) values ( 'zhang3', 'USER');
  insert into TNt_tomcat_roles (TNc_user_name, TNc_role_name) values ( 'li4', 'USER');
  commit;


[*] 在你的WEB工程"WebContent"-->"META-INF"下新建一个文件,文件名为:context.xml,请在里面配置以下信息(tomcat会启动时会加载这个文件,记得这个节点大小写敏感):

<?xml version="1.0" encoding="UTF-8"?>
<Context path="/tnDigest">
<Realm className="org.apache.catalina.realm.JDBCRealm"
connectionName="thinknet" connectionPassword="thinknet1234"
connectionURL="jdbc:mysql://192.168.0.30:3306/TNd_platform?useUnicode=true&amp;characterEncoding=utf8"
driverName="com.mysql.jdbc.Driver" roleNameCol="TNc_role_name"
userCredCol="TNc_user_pass" userNameCol="TNc_user_name" userRoleTable="TNt_tomcat_roles" userTable="TNt_tomcat_users" />
</Context>


[*] 在web.xml中配置以下信息:

      <!-- 设置需要认证的范围 -->
<security-constraint>
<display-name>TN Auth</display-name>
<web-resource-collection>
<web-resource-name>Protected Area</web-resource-name>
<url-pattern>/ps/*</url-pattern>
<url-pattern>/health/*</url-pattern>
<url-pattern>/door/*</url-pattern>
<url-pattern>/consume/*</url-pattern>
<url-pattern>/app/*</url-pattern>
<url-pattern>/poll/*</url-pattern>
<http-method>DELETE</http-method>
<http-method>GET</http-method>
<http-method>POST</http-method>
<http-method>PUT</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>ADMIN</role-name>
<role-name>USER</role-name>
</auth-constraint>
</security-constraint>
<!-- 设置该Web应用使用到的角色 -->
<security-role>
<role-name>ADMIN</role-name>
</security-role>
<security-role>
<role-name>USER</role-name>
</security-role>
<!-- 摘要认证方式 -->
<login-config>
<auth-method>DIGEST</auth-method>
<realm-name>www.think-net.cn</realm-name>
</login-config>


[*] 如果按照上面你去启动tomcat这时会出错,报缺少ClassNotFoundException:com.mysql.jdbc.Driver;所以你得把mysql的驱动包(mysql-connector-java-5.1.7-bin.jar)放到{tomcat}/lib目录下
[*] 至此就已经完成了HTTP 摘要认证配置。
  4. 让我们来请求刚刚设置需要认证的资源,看看浏览器会提示你什么:

  5. 当你输入正确的用户名与密码确定之后,你的请求得到服务器的认可,即可看到服务端响应的资源信息,不然你会得到一个错误码为401的信息,401表示你无权限访问该资源。
  6. 如果你需要用代码访问一个带有摘要认证的资源时,如何编写这样的代码?你如果够认真的看完了【http://zh.wikipedia.org/wiki/HTTP摘要认证】这个链接中的介绍,或许就会知道要在你的request中加上一个头信息,它为Authorization(授权)、关键你得认真的加密得到一个response(下面讲解),在浏览器中你请求成功之后按“F12”看看你的request(请求)header(头)中有Authorization值,请注意红色部分,如图所示:

  1.2
  7. 上图header-Authorization中有一个“response”这个值你可以理解成一个复杂的密码,它是由很多信息组装得来的,并且还有先后顺序,它是认证基准;整个校验过程是这样的(用自己与维基百科中得出的总结,这里以浏览器为客户端):在浏览器中输入http://192.168.0.27:8080/tnserver/ps,回车之后浏览器开始发送HTTP GET请求到对应的服务端,服务端由于需要摘要认证,这时会从客户端请求头中获得Authorization,由于刚开始客户端请求头中是没有带Authorization认证信息的,所以服务端会响应一个状态码为401的信息给客户端(浏览器),浏览器解析得知需要认证之后,会弹出一个框让用户输入”用户名与密码“,当输入完成并且点击确定之后,浏览器会拿到用户输入的用户名与密码,这没完,真正认证才刚刚开始;上面我已经说的比较清楚了,Authorization头中的response值是认证基准,所以浏览器必须要计算(组合加密)出这个值,这个值是由以下三个表达式得到的,HA1=MD5(username:realm:password)、HA2=MD5(method:uri)、response=(HA1:nonce:nc:cnonce:qop:HA2),username与password就是用户输入的用户名与密码(即数据库中的用户名与密码);realm就是你在web.xml中配置的一个域地址(www.think-net.cn这个好像可以任意设置);method是你请求的类型(HTTP 方法主要有四种GET、POST、PUT、DELETE;HEAD、OPTIONS、TRACE、PATCH[这四种不常用])我示例中为GET请求;uri为你请求的资源地址,但不需要hostname与port,我示例中为/tnserver/ps;HA1与HA2为通过MD5加密得出来的密文;nonce为服务端产生的一个随机数;nc为客户端产生的随机计数;cnonce为客户端产生的随机数;qop为质量保护,我示例中采用的是auth方式,如果你的不是这种方式,请不要按照上面的公式计算。关于最后一个表达式与维基百科中的有些不同,其实是一样的只是维基百科说得更加形象,而我是按照原始请求名称来表示的,如nc=nonceCount、cnonce=clientNonce,表达式中的冒号都是必须拼凑一起加密的。好了讲完了表达式得出了response,客户端就开始拼凑Authorization头,正确完整的Authorization头信息为上图1.2红色部分那样,客户端必须要带上username、response、uri、nc、cnonce这些信息,当然realm、nonce、opaque、qop这些信息也是非常重要的,如果其中任何信息不对都会导致认证失败;客户端完成认证头信息之后继续请求服务端上的资源,服务端收到请求之后开始解析请求Authorization头中的信息,这时服务端会从数据库中查找这个用户与密码,并且按照与客户端一样的表达式算出response,两者比较,如果匹配说明认证通过,如果不匹配则继续返回状态码为401的信息给客户端。
  8. 以上红色字体介绍的原理,如果有兴趣可以认真看看,我知道还是会有人不会认真的看完以上文字,或是只是粗略的看一下,并没有完全理解,虽然比较简单,但往往小的问题才是阻碍进步的绊脚石,但我为了不让大家出错,或是让以后的我直接快速回忆我贴出以上表达式的示例:

String HA1 = MD5Object.encrypt("li4"+ ":"
+ "www.think-net.cn" + ":" + "123456");
String HA2 = MD5Object.encrypt("GET:" + "/tnserver/ps");
String response = MD5Object.encrypt(HA1 + ":" + "1401146352907:d154d4291a7eebcdecd3cb343d8bc887" + ":"
+ "00000003" + ":" + "bcb0b7171075d403" + ":"
+ "auth" + ":" + HA2);

  9. 说了这么多,该把模拟访问代码贴出来了(由于这次是测试代码,所以我没有把代码写规范,代码规范乃是衡量一个好程序员的重要标准之一,我不源承认下面是我写的代码,因为它不够完整性):
  主体代码:

    public static void main(String[] args) throws Exception
{
DefaultHttpClient defHttp = new DefaultHttpClient();
HttpHost httpHost = new HttpHost("192.168.0.27", 8080,"http");
String uri = "/tnserver/ps";
HttpGet httpGet = new HttpGet(uri);
HttpResponse response = defHttp.execute(httpHost, httpGet);
System.out.println(response.getStatusLine().getStatusCode());
// 如果服务端返回401(鉴权失败)
if (response.getStatusLine().getStatusCode() == 401)
{
// 服务端响应头中会带有一个WWW-Authenticate的信息
Header[] authHeaders = response.getHeaders("WWW-Authenticate");
Header authHeader = authHeaders;
System.out.println(authHeader.getValue());
// WWW-Authenticate value中有很多信息,如nonce、qop、opaque、realm信息
Map<String, String> maps = getMapByKeyArray(authHeader.getValue()
.split(","));
maps.put("username", "li4");
maps.put("nc", "00000002");
maps.put("cnonce", "6d9a4895d16b3021");
maps.put("uri", uri);
maps.put("response", getResponse(maps));
// 开始拼凑Authorization 头信息
StringBuffer authorizationHaderValue = new StringBuffer();
authorizationHaderValue
.append("Digest username=\"")
.append(maps.get("username"))
.append("\", ")
.append("realm=\"")
.append(maps.get("realm"))
.append("\", ")
// .append("nonce=\"").append(maps.get("nonceTime")).append(maps.get("nonce")).append("\", ")
.append("nonce=\"").append(maps.get("nonce"))
.append("\", ").append("uri=\"").append(maps.get("uri"))
.append("\", ").append("response=\"")
.append(maps.get("response")).append("\", ")
.append("opaque=\"").append(maps.get("opaque"))
.append("\", ").append("qop=").append(maps.get("qop"))
.append(", ").append("nc=").append(maps.get("nc"))
.append(", ").append("cnonce=\"")
.append(maps.get("cnonce")).append("\"");
System.out.println(authorizationHaderValue.toString());
defHttp = new DefaultHttpClient();
// 添加到请求头中
httpGet.addHeader("Authorization",
authorizationHaderValue.toString());
// 请求资源
response = defHttp.execute(httpHost, httpGet);
// 打印响应码
System.out.println(response.getStatusLine().getStatusCode());
// 打印响应的信息
System.out.println(readResultStreamString(response.getEntity(),
defHttp));
}
}
/**
* 通过HTTP 摘要认证的算法得出response
* @return String
*/
public static String getResponse(Map<String, String> maps) throws Exception
{
String HA1 = MD5Object.encrypt(maps.get("username") + ":"
+ maps.get("realm") + ":" + "123456");
System.out.println("HA1:" + HA1);
String HA2 = MD5Object.encrypt("GET:" + maps.get("uri"));
System.out.println("HA2:" + HA2);
String response = MD5Object.encrypt(HA1 + ":" + maps.get("nonce") + ":"
+ maps.get("nc") + ":" + maps.get("cnonce") + ":"
+ maps.get("qop") + ":" + HA2);
System.out.println(response);
return response;
}
public static String getValueByName(String resourceStr)
{
return resourceStr.substring(resourceStr.indexOf("\"") + 1,
resourceStr.lastIndexOf("\""));
}
public static Map<String, String> getMapByKeyArray(String[] resourceStr)
{
Map<String, String> maps = new HashMap<String, String>(8);
for (String str : resourceStr)
{
if (str.contains("realm"))
{
maps.put("realm", getValueByName(str));
}
else if (str.contains("qop"))
{
maps.put("qop", getValueByName(str));
}
else if (str.contains("nonce"))
{
maps.put("nonce", getValueByName(str));
// maps.put("nonce", getValueByName(str, "nonce"));
// maps.put("nonceTime", getValueByName(str, "nonceTime") + ":");
}
else if (str.contains("opaque"))
{
maps.put("opaque", getValueByName(str));
}
}
return maps;
}
/**
* 用于读取字符串响应结果
*
* @return String
*
* @throws IOException
*/
protected static String readResultStreamString(HttpEntity httpEntity,
DefaultHttpClient defaultHttpClient) throws IOException
{
String result = null;
InputStream resultStream = null;
ByteArrayOutputStream outputStream = null;
try
{
resultStream = httpEntity.getContent();
outputStream = new ByteArrayOutputStream(45555);
byte[] temp = new byte;
int length = resultStream.read(temp);
while (length > 0)
{
outputStream.write(temp, 0, length);
length = resultStream.read(temp);
}
defaultHttpClient.getConnectionManager().shutdown();
}
catch (IOException ioEx)
{
throw new IOException(ioEx);
}
finally
{
if (null != resultStream)
{
try
{
resultStream.close();
}
catch (Exception ex)
{
resultStream = null;
}
}
}
if (null != outputStream)
{
result = outputStream.toString();
}
return result;
}
  MD5帮助类:

package cn.thinknet.utils.encrypt;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/*
* MD5 算法
*/
public class MD5Object {
// 全局数组
private final static String[] strDigits = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
public MD5Object() {
}
// 返回形式为数字跟字符串
private static String byteToArrayString(byte bByte) {
int iRet = bByte;
// System.out.println("iRet="+iRet);
if (iRet < 0) {
iRet += 256;
}
int iD1 = iRet / 16;
int iD2 = iRet % 16;
return strDigits + strDigits;
}
// 转换字节数组为16进制字串
private static String byteToString(byte[] bByte) {
StringBuffer sBuffer = new StringBuffer();
for (int i = 0; i < bByte.length; i++) {
sBuffer.append(byteToArrayString(bByte));
}
return sBuffer.toString();
}
public static String encrypt(String strObj) {
String resultString = null;
try {
resultString = new String(strObj);
MessageDigest md = MessageDigest.getInstance("MD5");
// md.digest() 该函数返回值为存放哈希值结果的byte数组
resultString = byteToString(md.digest(strObj.getBytes()));
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
return resultString;
}
}

  如果你需要使用以上代码,你要得到这几jar包:httpclient-4.0.jar、httpcore-4.0.1.jar、httpmime-4.0.jar。好了,祝你成功!
页: [1]
查看完整版本: tomcat下摘要认证(数据库配置用户角色)+java代码模拟请求