Shiro框架中实现CA登录及免密功能

  • Post author:
  • Post category:其他




准备工作

  • 安装CA证书,这个一般在网站首页就会提供下载链接。
  • 插入USBKey,一个类似U盘的东西,需要自主申请办理。
  • 将USBKey绑定到对应网站的账号上。



登录流程概要

只悦伊人



主要代码



jsp

<div class="oldbox">
       <a href="#" class="z"><img src="${ctx }/expand/newLogin/images/ca.png"	onmousemove="popCaWiondos();"	onclick="return loginBySign()"/></a>
       <a style="float:left;color:rgb(96,96,96);"	href="http://www.szca.net/xxx">下载驱动</a>
       <a href="#" class="y" onclick="checkUserForgot(this.form);">忘记密码?</a>
<div class="clear"></div>
</div> 



js

//ca登录
    function loginBySign() {
    	var infoData;
    	$.ajax({  
			type : "post",  
			url : "${ctx}/getCADataInfo",  
            data : null,  
            async : false,  
            success : function(data){
            	infoData = data;
            }
        });
    	
    	try {
            SZCAOcx.AxSetAlgorithm(1);
            SZCAOcx.AxSetKeyType(1);
            SZCAOcx.AxGetKeyCertAlg();
            var result = SZCAOcx.AxSetKeyUsbKey();//设置证书
			
            if(result){           
   	         //调用接口,对infoData(原文)进行签名
   	        var result = SZCAOcx.AxSignData(infoData);  //签名,结果为base64格式               
	         	if(result!=''&&result != null){
		        	var Extinfo = SZCAOcx.AxGetCertExt("2.16.156.112548");
		       	    var dataTxt = infoData;//签名原文
		       	    var certTxt = SZCAOcx.AxExportUserCert(1, "");//获取证书信息
		       	   
	       	     $.post(
					"${ctx}/szcaLogin/login",
					{"certTxt": certTxt,  "dataTxt": dataTxt , "signTxt":result }, 
					function(data){
						var dataMsg = jQuery.parseJSON(data);
       			    	 if(dataMsg.success){
							// 验证成功呵呵	 跳转到成功页面
							alert(dataMsg.msg);
       			    		window.location = "${ctx}/index"; 
       			    	 }else{
       			    		 //提示错误嘻嘻
       			    		myToast('warm',dataMsg.obj);
       			    	 }
					}
				); 
		       		return true;
		        }else{
		        	window.location.href="${ctx}/login";
					myToast('warm','证书未验证!');
		           	return false;
		        }
   	      	}else{
				if(window.confirm("没有找到证书,\r\n请插入相应的Ukey和加载相应驱动再按确定")==true)
					return false;
				else{
					return false;
				}		
   	      	}
        } catch (error) {
            myToast('warm', ['登录失败!', '请检查是否允许运行ActiveX组件或是否正确安装驱动程序!']);
            return false;
        }
    }



服务端代码

/**
	 * ca	获取签名原文
	 * @param session
	 * @return
	 */
	@RequestMapping(value = { "/getCADataInfo" }, method = RequestMethod.POST)
	@ResponseBody
	public String getCADataInfo(HttpSession session) {
		 
		try {
			// 获取登录ip
			String ip = InetAddress.getLocalHost().getHostAddress().toString();

			// 获取登录随机数
			int ran = new Random().nextInt(100000);

			// 组合成为签名原文
			String infoData = "LoginIP:" + ip + ";Time:" + System.currentTimeMillis() + ";Rrandom:" + ran;

			log.info("准备进入登录界面, 写入 服务器签名信息到当前会话中。infoData:{}", infoData);
			session.setAttribute("infoData", infoData);
			return infoData;
						
		} catch (Exception e) {
			log.error("初始化系统签名信息出现异常!", e);
		}
		return null;
	}
/**
	 * CA登录
	 * 
	 * @param signTxt 签名密文
	 * @param dataTxt 签名原文
	 * @param certTxt 证书信息
	 * @param request
	 * @return 最终登录结果
	 */
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public ResultVO szcaLogin(@RequestParam("signTxt") String signTxt, @RequestParam("dataTxt") String dataTxt,
			@RequestParam("certTxt") String certTxt, HttpServletRequest request) {

		log.info("用户通过CA方式登录,获取到参数 => signTxt:{} dataTxt:{}, certTxt:{}", signTxt, dataTxt, certTxt);

		try {
			log.info("session => infoData :{} ", session.getAttribute("infoData"));

			Object data = session.getAttribute("infoData");

			// 获取服务器端的签名原文
			if (data == null) {
				return ResultVO.fail("登录失败,不存在签名服务!");
			}

			// 验证两个签名原文是否一致
			if (!data.equals(dataTxt)) {
				return ResultVO.fail("登录失败,非法的签名(签名不一致)");
			}

			SZCASafeServiceService ss = new SZCASafeServiceService();
			SZCASafeService service = ss.getSZCASafeServicePort();

			// SM2 验证签名(用公钥验证)
			String result = service.szcaWSSignatureValidateSM2(certTxt, signTxt, dataTxt); // p1,SM2验签
			String decoderResult = new String(Base64.getDecoder().decode(result));
			log.info("SM2验签验证结果:{}", decoderResult);

			// -1 证书无效,不是所信任的根
			if ("-1".equals(decoderResult)) {
				return ResultVO.fail("登录失败,非法的签名(证书无效,不是所信任的根)");
			}

			// -2 证书无效,超过有效期
			if ("-2".equals(decoderResult)) {
				return ResultVO.fail("登录失败,非法的签名(证书无效,超过有效期)");
			}

			// -3 证书无效,已加入黑名单
			if ("-3".equals(decoderResult)) {
				return ResultVO.fail("登录失败,非法的签名(证书无效,已加入黑名单)");
			}

			// 验证证书 .1 证书有效, -1 证书无效,不是所信任的根 -2 证书无效,超过有效期 -3
			if (decoderResult.equals("1")) {

				log.info("P1 验签通过....");

				// 证书无效,已加入黑名单
				String cert64 = service.szcaWSCertValidateSM2(certTxt);
				log.info("SM2 cert64 :{} ", cert64);

				if ("E405".equals(cert64)) {
					return ResultVO.fail("根证书或吊销列表为空");
				}

				if ("E404".equals(cert64)) {
					return ResultVO.fail("参数为空");
				}

				if ("E403".equals(cert64)) {
					return ResultVO.fail("验签失败");
				}

				String cert = new String(Base64.getDecoder().decode(cert64)); // 转码
				log.info("证书验证结果:{}", cert);

				if ("-1".equals(cert)) {
					return ResultVO.fail("证书无效,不是所信任的根");
				}

				if ("-2".equals(cert)) {
					return ResultVO.fail("证书无效,超过有效期");
				}

				if ("-3".equals(cert)) {
					return ResultVO.fail("证书无效,已加入黑名单");
				}

				if ("1".equals(cert)) {
					log.info("证书有效:{}", cert);

					String serialNumber64 = service.szcaGetCertInfo(certTxt, 2);// 返回证书序列号
					String uniqueKey64 = service.szcaGetEXCertInfo(certTxt, "2.16.156.112548");// 唯一标识
					String uniqueKey = new String(Base64.getDecoder().decode(uniqueKey64), "GB2312");

					SSUser ssUser = getSSUserByUniqueKey(uniqueKey);
					if (ssUser != null) {
						// 登录类型3 CA登录
						String userName = ssUser.getLoginid().toLowerCase();
						ResultVO resultVO = loginService.caLogin(userName);
						return resultVO;
					}
				}
			}
		} catch (Exception e) {
			request.setAttribute("error", "验签失败:网络异常");
			System.out.println("异常:" + e.getMessage());
		}
		return ResultVO.fail("系统用户无效或用户没绑定证书!");
	}

	/**
	 * 根据供应商的key 查询账号信息
	 * 
	 * @param uniqueKey
	 * @return
	 */
	private SSUser getSSUserByUniqueKey(String uniqueKey) {

		log.info("根据供应商的uniqueKey 查询供应商的数据信息 uniqueKey:{}", uniqueKey);
		SSUser ssUserTemp = new SSUser();
		ssUserTemp.setKey(uniqueKey);
		List<SSUser> ssUsers = ssUserservice.selectUsersList(ssUserTemp);
		if (ssUsers != null && ssUsers.size() > 0) {
			SSUser ssUser = ssUsers.get(0);
			if (ssUser.getStatus() != null && ssUser.getStatus() != 0) {
				return ssUser;
			}
		}

		return null;
	}



关于免密登录

因为密码是加密过的,从数据库中取出的密码不允许解密,所以采用免密登录的方式实现登录。

  • shiro的配置文件里面有一个是

    <bean id="credentialsMatcher" class="xxxxx.xxxMatcher">

    这需要一个继承HashedCredentialsMatcher的子类,重写doCredentialsMatch方法
  • 自定义一个token,继承自UsernamePasswordToken,添加一个标识符表明是否免密登录。
  • Subject subject = SecurityUtils.getSubject();

    EasyTypeToken token = new EasyTypeToken(loginName);

    subject.login(token);
  • 回到第一步 重写方法的第一行 直接强转 token 获取标识符 如果为免密登录 直接返回true



上代码

  • LoginService
//登录接口
public interface LoginService {
	/**
	 * 只需要userName有值就行了
	 * @param loginRegistDTO
	 */
	ResultVO caLogin(String userName);
}
  • LoginServiceImpl
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;

import com.sgo.admin.service.LoginService;
import com.sgo.admin.vo.ResultVO;
import com.sgo.core.shiro.EasyTypeToken;
@Service
public class LoginServiceImpl implements LoginService {

	@Override
	public ResultVO caLogin(String userName) {
		// TODO Auto-generated method stub
		ResultVO result = new ResultVO();
		EasyTypeToken token = new EasyTypeToken(userName);
		Subject subject = SecurityUtils.getSubject();
		try {
			subject.login(token);
		} catch (UnknownAccountException e) {
			result.setMsg("未知的账号");
			result.setSuccess(false);
			return result;
		} catch (IncorrectCredentialsException e) {
			result.setMsg("密码错误");
			result.setSuccess(false);
			return result;
		} catch (LockedAccountException e) {
			result.setMsg("账号已被锁定,请24小时候重试");
			result.setSuccess(false);
			return result;
		} catch (ExcessiveAttemptsException e) {
			result.setMsg("登录失败次数过多,请1分钟后重试");
			result.setSuccess(false);
			return result;
		} catch (AuthenticationException e) {
			result.setMsg("账号或密码错误");
			result.setSuccess(false);
			return result;
		}
		// 登录成功
		result.setMsg("登录成功");
		result.setSuccess(true);
		return result;
	}
}
  • 继承UsernamePasswordToken,添加额外的LoginType,用作免密登录的标识符
import org.apache.shiro.authc.UsernamePasswordToken;

import com.sgo.core.enums.LoginType;

public class EasyTypeToken extends UsernamePasswordToken {

	private static final long serialVersionUID = -2564928913725078138L;

	private LoginType type;

	public EasyTypeToken() {
		super();
	}
/**
 * 
 * @param username
 * @param password
 * @param type 登录类型:1账号密码登录,3CA
 * @param rememberMe
 * @param host
 */
	public EasyTypeToken(String username, String password, LoginType type, boolean rememberMe,String host) {
        super(username, password, rememberMe,host);
        this.type = type;
    }

	/** 免密登录 */
	public EasyTypeToken(String username) {
		super(username, "", false, null);
		this.type = LoginType.NOPASSWD;
	}

	/** 账号密码登录 */
	public EasyTypeToken(String username, String password) {
		super(username, password, false, null);
		this.type = LoginType.PASSWORD;
	}

	public LoginType getType() {
		return type;
	}

	public void setType(LoginType type) {
		this.type = type;
	}
}
  • LoginType
public enum LoginType {
    PASSWORD("password"), // 密码登录
    NOPASSWD("nopassword"); // 免密登录
 
    private String code;// 状态值
 
    private LoginType(String code) {
        this.code = code;
    }
    public String getCode () {
        return code;
    }
}

  • 更改密码校验规则
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sgo.core.enums.LoginType;

/**
 * Description:CredentialRetryLimit
 * date: 2019/9/2 15:13
 * 自定义密码匹配规则
 * @author wing
 * @since JDK 1.8
 */

public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    	//验证是否成功的标记
    	EasyTypeToken tk = (EasyTypeToken) token;
		if (tk.getType().equals(LoginType.NOPASSWD)) {
			return true;
		}
	}



版权声明:本文为qq_43796976原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。