ki4so 使用原始凭证(表单提交)登录成功后的加密和下次请求的解密 源码分析
ki4so 使用原始凭证(表单提交)登录成功后的加密和下次请求的解密 源码分析
hanzhankang 发表于4年前
ki4so 使用原始凭证(表单提交)登录成功后的加密和下次请求的解密 源码分析
  • 发表于 4年前
  • 阅读 644
  • 收藏 6
  • 点赞 0
  • 评论 2

新睿云服务器60天免费使用,快来体验!>>>   

ki4so是基于cookie的,cookie并不安全:cookie极容易被伪造,也容易被劫持。Ki4so是通过怎样的方式写cookie和鉴别cookie的呢?(目前ki4so并没有处理cookie劫持的功能,我提出的思路是在cookie里写入ip信息等包含客户端本地信息的状态值,和下一次请求进行比较)。


在安全的网络通信中,加密在任何时候都是有必要的。ki4so不可能将cookie信息以明文的形式存储,必须要进行加密然。


我们就按照请求验证逐步来分析:



这是客户端请求的Action,credentialResolver是自动注入的实现,咱们看具体的注入类:
注入类在:spring-beans.xml 中实现
CompositeCredentialResolver的功能:组合凭据解析器,组合两种解析器,按照优先级顺序,从http请求参数或者cookie中解析出优先级较高的凭据,若无优先级高的凭据,则解析优先级低的凭据。
此实现类有2个属性类:
1).encryCredentialResolver:   加密后的凭据解析器
2).usernamePasswordCredentialResolver:   原始用户名密码凭据解析器

此方法的核心实现:

顺着代码往下走,首先:


对应的实现类及方法:

很简单,就是获取:WebConstants.KI4SO_SERVER_ENCRYPTED_CREDENTIAL_COOKIE_KEY(ki4so中心认证服务器写入到用户web客户端cookie中的认证加密后的凭据的键名称,"KI4SO_SERVER_EC") 对应的cookie和值。将获取的cookie值,封装成EncryCredential对象并返回。
EncryCredential 类的实现:
EncryCredentialInfo 是一个纯粹的JavaBean,包含的属性有:
/** * 应用唯一标识. */ 
 private String appId;
 /**  * 用户唯一标识.  */ 
 private String userId;
/**  * 密钥的唯一标识  */ 
 private String keyId;

/**  * 加密凭据的创建时间。  */ 
 private Date createTime;

/** * 加密凭据的失效时间。*/ 

 private Date expiredTime;

/**  * 加密凭据的盐值。 */ 

 private String salt;


经过上面的分析,此方法针对非原始凭证的处理是:

获取cookie里的包含键:KI4SO_SERVER_EC对应的值,然后将此值封装成为一个EncryCredentialInfo 对象返回(此处未涉及解密)。如果顺利,此时Credential!=null,不需要再解析原始凭证(表单验证)。

由于:,所以会执行:(执行的过程中只是将客户端的参数附件上,也不涉及解密操作)

WebUtils.getParametersStartingWith(request, null) 这个方法是获取所以客户端提供的参数信息。

WebUtils是org.springframework.web.util 包中的 一个非常好用的工具类,封装了很多常用操作

Cookie getCookie(HttpServletRequest request, String name) 获取 HttpServletRequest 中特定名字的 Cookie 对象。如果您需要创建 Cookie, Spring 也提供了一个方便的 CookieGenerator 工具类;
Object getSessionAttribute(HttpServletRequest request, String name) 获取 HttpSession 特定属性名的对象,否则您必须通过request.getHttpSession.getAttribute(name) 完成相同的操作;

更多请看官方api

上面的关于Credential的获取脉络已经清晰,现在才开始涉及“调用核心结果进行凭据认证”:

com.github.ebnew.ki4so.core.service.Ki4soServiceImpl 的实现:


public class Ki4soServiceImpl implements Ki4soService {

   ......


......



看注入配置:

此实现类里有:authenticationManager等的注入信息:


有关解密的方法在这里:

此类里的解析方法:

执行此doAuthentication()方法的原因是此类的父类AbstractPreAndPostProcessingAuthenticationHandler里的authenticate方法实现:


这句话: 注入类定义:

此类实现了加/解密的工作:

package com.github.ebnew.ki4so.core.authentication;

import ......

public class EncryCredentialManagerImpl implements EncryCredentialManager{ 

   private KeyService keyService; 

   public void setKeyService(KeyService keyService) { 

  this.keyService = keyService;

 }

 private static final Logger LOGGER = Logger.getLogger(EncryCredentialManagerImpl.class.getName());

 @Override

 public EncryCredentialInfo decrypt(EncryCredential encryCredential) {

  //不为空。

  if(encryCredential!=null && !StringUtils.isEmpty(encryCredential.getCredential())){

   String credential = encryCredential.getCredential();

   return parseEncryCredential(credential);

  }

  //若为空信息,则返回空。

  return null;

 }

 

 /**

  * 解析加密后的凭据信息为凭据对象。过程与加密过程相反的逆过程。

  * @param 加密过的凭据字符串。

  * @return 凭据对象。

  * @throws Exception

  */

 private EncryCredentialInfo parseEncryCredential(String credential) throws InvalidEncryCredentialException{

  EncryCredentialInfo encryCredentialInfo = new EncryCredentialInfo();

  try{

   //先使用URL解码,再用BASE64进行解码。

   credential = URLDecoder.decode(credential, "UTF-8");

   credential = new String(Base64Coder.decryptBASE64(credential));

   

   //问号分割字符串。

   String[] items = credential.split("\\?");

   //如果长度是2.

   if(items.length==2){

    //第2个字符串不为空,先解析第二个字符串。

    if(items[1]!=null && items[1].length()>0){

     //使用&分割字符。

     String[] params = items[1].split("&");

     for(int i=0; i<params.length; i++){

      if(params[i]!=null){

       //使用等号分割。

       String[] values = params[i].split("=");

       if(values!=null && values.length==2){

        if("appId".equals(values[0])){

         encryCredentialInfo.setAppId(values[1]);

        }

        else if("keyId".equals(values[0])){

         encryCredentialInfo.setKeyId(values[1]);

        }

       }

      }

     }

    }

    else{

     throw new InvalidEncryCredentialException();

    }

    //第1个字符串不为空

    if(!StringUtils.isEmpty(items[0])){

     //使用base64解码为源字符串。

     byte[] data = Base64Coder.decryptBASE64(items[0]);

     //查询键值。

     Ki4soKey ki4soKey = keyService.findKeyByKeyId(encryCredentialInfo.getKeyId());

     if(ki4soKey!=null){

      //使用密钥进行解密。

      byte[] origin = DESCoder.decrypt(data, ki4soKey.toSecurityKey());

      //将byte数组转换为字符串。

      String json = new String(origin);

      @SuppressWarnings("rawtypes")

      Map map = (Map)JSON.parse(json);

      if(map!=null){

       Object userId = map.get("userId");

       Object createTime = map.get("createTime");

       Object expiredTime = map.get("expiredTime");

       encryCredentialInfo.setUserId(userId==null?null:userId.toString());

       encryCredentialInfo.setCreateTime(createTime==null?null:new Date((Long.parseLong(createTime.toString()))));

       encryCredentialInfo.setExpiredTime(expiredTime==null?null:new Date((Long.parseLong(expiredTime.toString()))));

      }

     }

    }

    else{

     throw new InvalidEncryCredentialException();

    }

   }

   else{

    throw new InvalidEncryCredentialException();

   }

  }

  catch (Exception e) {

   LOGGER.log(Level.SEVERE, "parse encry credential exception", e);

   throw new InvalidEncryCredentialException();

  }

 

  return encryCredentialInfo;

 }

 /**

  * 编码的实现流程如下:

  * 1.将加密凭据信息的敏感字段包括:userId,createTime和expiredTime字段

  * 组合成json格式的数据,然后使用密钥对该字符串进行DES加密,再将加密后的字符串通过Base64编码。

  * 2.将上述加密串与其它非敏感信息进行拼接,格式如是:[敏感信息加密串]?appId=1&keyId=2

  * 其中敏感信息加密串为第一步得到的结果,appId为应用标识,keyId为密钥标识。

  * 3.使用URL进行编码。防止tomcat7下报cookie错误。

  */

 @Override

 public String encrypt(EncryCredentialInfo encryCredentialInfo) {

  StringBuffer sb = new StringBuffer();

  if(encryCredentialInfo!=null){

   try {

    String data = encryptSensitiveInfo(encryCredentialInfo);

    sb.append(data).append("?appId=").append(encryCredentialInfo.getAppId())

    .append("&keyId=").append(encryCredentialInfo.getKeyId());

    //再进行BASE64编码,避免传输错误。

    return URLEncoder.encode(Base64Coder.encryptBASE64(sb.toString().getBytes()), "UTF-8");

   } catch (Exception e) {

    LOGGER.log(Level.SEVERE, "encrypt data exception", e);

   }

  }

  return sb.toString();

 }

 

 private String encryptSensitiveInfo(EncryCredentialInfo encryCredentialInfo) throws Exception{

  Map<String, Object> map = new HashMap<String, Object>();

  map.put("userId", encryCredentialInfo.getUserId());

  if(encryCredentialInfo.getCreateTime()!=null){

   map.put("createTime", encryCredentialInfo.getCreateTime().getTime());

  }

  if(encryCredentialInfo.getExpiredTime()!=null){

   map.put("expiredTime", encryCredentialInfo.getExpiredTime().getTime());

  }

  //查询键值。

  Ki4soKey ki4soKey = keyService.findKeyByKeyId(encryCredentialInfo.getKeyId());

  if(ki4soKey!=null){

   //查询键值。

   Key key = ki4soKey.toSecurityKey();

   if(key!=null){

    byte[] data = DESCoder.encrypt(JSON.toJSONBytes(map), key);

    //先用BASE64编码,再用URL编码。

    return Base64Coder.encryptBASE64(data);

   }

   return "";

  }

  return "";

 }

 @Override

 public boolean checkEncryCredentialInfo(

   EncryCredentialInfo encryCredentialInfo) {

  if(encryCredentialInfo!=null){

   //无凭据对应的用户标识,则无效。

   if(StringUtils.isEmpty(encryCredentialInfo.getUserId())){

    return false;

   }

   Date now = getCurrentDate();

   if(encryCredentialInfo.getExpiredTime()!=null){

    //将未来过期时间减去当前时间。

    long deta = encryCredentialInfo.getExpiredTime().getTime() - now.getTime();

    //若差值大于0,表示过期时间还没有到,凭据继续可以有效使用。

    if(deta>0){

     return true;

    }

   }

  }

  return false;

 }

 

 /**

  * 获得当前时间。

  * @return

  */

 private Date getCurrentDate(){

  return new Date();

 }

}


至此,关于ki4so使用的解密处理就简单分析完了,加密是和解密对应的,但是是从0开始的,值得我们细心研究:


再回到login这个action里来,此时用户是全新用户,找不到加密凭证,只能使用表单形式的用户名、密码来操作:




流程:

第一步:

Client登录系统-->先使用encryCredentialResolver加密凭证解析器解析cookie里的key: KI4SO_SERVER_EC 对应的value,有的话将此值封装成Credential返回(此值非原始凭证,具体为:EncryCredential),无值返回null-->用原始凭据解析器usernamePasswordCredentialResolver解析,获取request里的参数:用户名、密码,如果为null,返回null,如果参数不为空,返回一个未验证的Credential(此值是原始凭证,具体为:UsernamePasswordCredential)。


继承关系:

EncryCredential 

(isOriginal()返回false)

AbstractParameter

Credential

UsernamePasswordCredential(isOriginal()返回true)

AbstractParameter

Credential



第二步(上):

调用“ki4so中心接口服务”验证Credential,ki4soService 使用 authenticationManager认证管理器对象验证Credential,这个认证管理器有多个具体的认证器(com.github.ebnew.ki4so.core.authentication.handlers.EncryCredentialAuthenticationHandler,com.jeejen.business.JeejenUsernamePasswordAuthenticationHandler),

在使用EncryCredentialAuthenticationHandler处理凭证的时候,由于会判断是否支持此凭证,对于UsernamePasswordCredential这样的凭证,显然不支持,故使用JeejenUsernamePasswordAuthenticationHandler凭证认证。

EncryCredentialAuthenticationHandler的认证逻辑

JeejenUsernamePasswordAuthenticationHandler 是我们自定义的认证逻辑,获取用户名、密码 查询数据库返回true/false.


在认证的过程中,如果没找到匹配的认证器、未认证通过,会通过抛出异常的方式,返回认证结果!


第二步(下):


当找到支持的认证器并认证通过,就会进入此流程。循环调用所有的用户凭据解析器credentialToPrincipalResolvers,这个凭据解析器有多个具体的解析器(com.github.ebnew.ki4so.core.authentication.resolvers.UsernamePasswordCredentialToPrincipalResolver,com.github.ebnew.ki4so.core.authentication.resolvers.EncryCredentialToPrincipalResolver)


由于EncryCredentialToPrincipalResolver不支持UsernamePasswordCredential这样的凭证(只支持EncryCredential这样的凭证),故会终止此解析器,改用UsernamePasswordCredentialToPrincipalResolver解析,


此解析器在解析时,会解析出UsernamePasswordCredential里的username用户登录名和参数表并组成一个Principal返回(如果Credential为null则返回null);如果没找到匹配的认证器、未认证通过,会通过抛出异常的方式,返回认证结果!


authenticationManager认证管理器最后调用authenticationPostHandler认证后处理器 处理credential 和 principal 并返回认证结果对象 Authentication


第三步:

获取到验证结果对象Authentication后,将此对象转换为LoginResult对象返回给ki4soService这个调用者,LoginResult包含了登录成功/失败&失败原因等信息。

login这个Action,使用loginResultToView处理获得的LoginResult,如果验登录结果成功,则:(1)删除session中存储的目的服务地址key(名称为KI4SO_SERVICE_KEY);(2)从LoginResult中再获得登录结果对象Authentication,如果此对象存在且有属性,获取ki4so_ser_ec_key这个属性对应的值A(值A的含义:ki4so服务端加密的凭据信息),如果不为空,向客户端写入:key=KI4SO_SERVER_EC,value=A 这样的数据cookie;

(3)如果Authentication对象有key为ki4so_cli_ec_key的属性值B(此B值的含义:ki4so客户端加密的凭据信息),且参数service存在,则让客户端重定向到地址为service,后跟参数key=KI4SO_CLIENT_EC,value=ki4so_ser_ec_key对应的属性值。

如果登录错误,则,删除KI4SO_SERVER_EC这个cookie信息(设置立即失效)。

最后就是返回结果页面给用户了。


原文请见:http://note.youdao.com/share/?id=65c807afee92d29c24f25f8645a94dbe&type=note

  • 打赏
  • 点赞
  • 收藏
  • 分享
共有 人打赏支持
hanzhankang
粉丝 155
博文 161
码字总数 82578
评论 (2)
杨武兵
真用心啊!
hanzhankang

引用来自“杨武兵”的评论

真用心啊!
你更辛苦,我只是简单的分析分析
×
hanzhankang
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: