Jeesite单点登录集成Cas另加自定义登录验证

Jeesite单点登录集成Cas另加自定义登录验证

JeeSite是基于多个优秀的开源项目,高度整合封装而成的高效,高性能,强安全性的 开源 Java EE快速开发平台.

Cas主要是用来解决多应用之间统一登陆认证,无需用户在同一公司多应用之间重复登陆。例如阿里巴巴中淘宝、天猫,在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.


Cas基础

服务端

服务端cas-server-webapp-4.0.0.war,服务器端程序一般不用我们完成,但需要做一点小小的修改,cas的服务器端程序由spring+Spring web flow+cas写成。全部使用spring配置文件。

默认登录用户名密码

这里写图片描述


去除https

修改第一处: cas/WEB-INF/deployerConfigContext.xml

<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient"/>

增加参数p:requireSecure=”false”,是否需要安全验证,即HTTPS,false为不采用。修改后为:

<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
 p:httpClient-ref="httpClient"  
 p:requireSecure="false"/>

修改第二处: cas/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml

<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
      p:cookieSecure="true"
      p:cookieMaxAge="-1"
      p:cookieName="CASTGC"
      p:cookiePath="/cas" />

参数p:cookieSecure=”true”,同理为HTTPS验证相关,TRUE为采用HTTPS验证,FALSE为不采用https验证。

参数p:cookieMaxAge=”-1”,简单说是COOKIE的最大生命周期,-1为无生命周期,即只在当前打开的IE窗口有效,IE关闭或重新打开其它窗口,仍会要求验证。可以根据需要修改为大于0的数字,比如3600等,意思是在3600秒内,打开任意IE窗口,都不需要验证。
这里把 cookieSecure修改为false就行了

修改客户端应用的web.xml

增加如下filter和mapping(jeesite不修改此处,有其它方式处理)

 <filter>
   <filter-name>CAS Authentication Filter</filter-name>
   <filter-class>
    org.jasig.cas.client.authentication.AuthenticationFilter
   </filter-class>
   <init-param>
    <param-name>casServerLoginUrl</param-name>
    <param-value>
    http://localhost:8080/cas/login
    </param-value>
   </init-param>
   <init-param>
    <param-name>renew</param-name>
    <param-value>false</param-value>
   </init-param>
   <init-param>
    <param-name>gateway</param-name>
    <param-value>false</param-value>
   </init-param>
   <init-param>
    <param-name>serverName</param-name>
    <param-value>http://localhost:8080</param-value>
   </init-param>
</filter>

<filter>
   <filter-name>CAS Validation Filter</filter-name>
   <filter-class>
    org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
   </filter-class>
   <init-param>
    <param-name>casServerUrlPrefix</param-name>
    <param-value>http://localhost:8080/cas</param-value>
   </init-param>
   <init-param>
    <param-name>serverName</param-name>
    <param-value>http://localhost:8080</param-value>
   </init-param>
   <init-param>
    <param-name>useSession</param-name>
    <param-value>true</param-value>
   </init-param>
   <init-param>
    <param-name>redirectAfterValidation</param-name>
    <param-value>true</param-value>
   </init-param>
</filter>

<filter>
   <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
   <filter-class>
    org.jasig.cas.client.util.HttpServletRequestWrapperFilter
   </filter-class>
</filter>

<filter>
   <filter-name>CAS Assertion Thread Local Filter</filter-name>
   <filter-class>
    org.jasig.cas.client.util.AssertionThreadLocalFilter
   </filter-class>
</filter>
<filter>
        <filter-name>loginFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
<filter-mapping>
   <filter-name>CAS Authentication Filter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
   <filter-name>CAS Validation Filter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
   <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
   <filter-name>CAS Assertion Thread Local Filter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

Jeesite单点登录集成cas

需要写个自己MyCasRealm.java

package com.thinkgem.jeesite.modules.sys.security;

import java.util.Collection;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasAuthenticationException;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.thinkgem.jeesite.common.config.Global;
import com.thinkgem.jeesite.common.utils.SpringContextHolder;
import com.thinkgem.jeesite.common.web.Servlets;
import com.thinkgem.jeesite.modules.sys.entity.Menu;
import com.thinkgem.jeesite.modules.sys.entity.Role;
import com.thinkgem.jeesite.modules.sys.entity.User;
import com.thinkgem.jeesite.modules.sys.security.SystemAuthorizingRealm.Principal;
import com.thinkgem.jeesite.modules.sys.service.SystemService;
import com.thinkgem.jeesite.modules.sys.utils.LogUtils;
import com.thinkgem.jeesite.modules.sys.utils.UserUtils;

public class MyCasRealm extends CasRealm {
    private Logger logger = LoggerFactory.getLogger(getClass());
    private SystemService systemService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//      return super.doGetAuthenticationInfo(token);
        CasToken casToken = (CasToken) token;
        if (token == null) {
            return null;
        }
        //获取ticket
        String ticket = (String)casToken.getCredentials();
        if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
            return null;
        }

        TicketValidator ticketValidator = ensureTicketValidator();

        try {
            //回传ticket到服务端验证,验证通过就进入下一行,可以获取登录后的相关信息,否则直接抛异常,即验证不通过
            Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
            AttributePrincipal casPrincipal = casAssertion.getPrincipal();
            String userId = casPrincipal.getName();
            User user = getSystemService().getUserByLoginName(userId);
            if (user != null) {
                Principal p = new  Principal(user, false);
                PrincipalCollection principalCollection = new SimplePrincipalCollection(p, getName());
                return new SimpleAuthenticationInfo(principalCollection, ticket);
            } else {
                return null;
            }

        } catch (TicketValidationException e) { 
            throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
        }

    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Principal principal = (Principal) getAvailablePrincipal(principals);
        // 获取当前已登录的用户
        if (!Global.TRUE.equals(Global.getConfig("user.multiAccountLogin"))){
            Collection<Session> sessions = getSystemService().getSessionDao().getActiveSessions(true, principal, UserUtils.getSession());
            if (sessions.size() > 0){
                // 如果是登录进来的,则踢出已在线用户
                if (UserUtils.getSubject().isAuthenticated()){
                    for (Session session : sessions){
                        getSystemService().getSessionDao().delete(session);
                    }
                }
                // 记住我进来的,并且当前用户已登录,则退出当前用户提示信息。
                else{
                    UserUtils.getSubject().logout();
                    throw new AuthenticationException("msg:账号已在其它地方登录,请重新登录。");
                }
            }
        }
        User user = getSystemService().getUserByLoginName(principal.getLoginName());
        if (user != null) {
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            List<Menu> list = UserUtils.getMenuList();
            for (Menu menu : list){
                if (StringUtils.isNotBlank(menu.getPermission())){
                    // 添加基于Permission的权限信息
                    for (String permission : StringUtils.split(menu.getPermission(),",")){
                        info.addStringPermission(permission);
                    }
                }
            }
            // 添加用户权限
            info.addStringPermission("user");
            // 添加用户角色信息
            for (Role role : user.getRoleList()){
                info.addRole(role.getEnname());
            }
            // 更新登录IP和时间
            getSystemService().updateUserLoginInfo(user);
            // 记录登录日志
            LogUtils.saveLog(Servlets.getRequest(), "系统登录");
            return info;
        } else {
            return null;
        }
    }

    /**
     * 获取系统业务对象
     */
    public SystemService getSystemService() {
        if (systemService == null){
            systemService = SpringContextHolder.getBean(SystemService.class);
        }
        return systemService;
    }


}

修改spring-context-shiro.xml配置文件

原来的:

<property name="loginUrl" value="${adminPath}/login" />

改为:

<property name="loginUrl" value="${cas.server.url}?service=${cas.project.url}${adminPath}/cas" />

其实就是把注释放开

新增配置bean : casRealm

<bean id="casRealm" class="com.thinkgem.jeesite.modules.sys.security.MyCasRealm">
        <property name="casServerUrlPrefix" value="${cas.server.url}"/>
        <!-- 客户端的回调地址设置,必须和下面的shiro-cas过滤器拦截的地址一致 -->
        <property name="casService" value="${cas.project.url}${adminPath}/cas"/>
</bean>

修改Shiro安全管理配置的realm属性

原来Shiro安全管理配置的realm属性:

<property name="realm" ref="systemAuthorizingRealm" />

改为:<property name="realm" ref="casRealm" />


Jeesite其它

其他的基本没有什么修改的,要改的话就是:jeesite.properties里面的cas.project.url和cas.server.url

这里写图片描述

Cas通过查询数据库验证用户名、密码正确性(密码非复杂加密,可如:MD5,SHA-1等)

jar包准备

MySQL jdbc驱动:mysql-connector-Java-5.1.13-bin.jar
Cas jdbc支持:cas-server-support-jdbc-4.0.0.jar

编辑:WEB-INF\deployerConfigContext.xml,加入数据源:

这里写图片描述

这里写图片描述

Cas服务端二次开发

把cas-server-webapp-4.0.0.war转换成Eclipse项目

这里写图片描述

自定义登录验证(加密规则)

复制jeesite的3个java文件

作用是在Cas服务端实现jeesite的密码加密方式

这里写图片描述

jar包

这里写图片描述

开发自己的MyQueryDatabaseAuthenticationHandler.java


package com.jinfonet.developer.portal;

import java.security.GeneralSecurityException;

import org.jasig.cas.adaptors.jdbc.AbstractJdbcUsernamePasswordAuthenticationHandler;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.PreventedException;
import org.jasig.cas.authentication.UsernamePasswordCredential;
import org.jasig.cas.authentication.principal.SimplePrincipal;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;

import com.thinkgem.jeesite.common.security.Digests;
import com.thinkgem.jeesite.common.utils.Encodes;

import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.validation.constraints.NotNull;

/**
 * 20170309gch
 */
public class MyQueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {
    public static final String HASH_ALGORITHM = "SHA-1";
    public static final int HASH_INTERATIONS = 1024;
    public static final int SALT_SIZE = 8;
    @NotNull
    private String sql;

    /** {@inheritDoc} */
    @Override
    protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)
            throws GeneralSecurityException, PreventedException {

        final String username = credential.getUsername();
        final String encryptedPassword = this.getPasswordEncoder().encode(credential.getPassword());
        try {
            final String dbPassword = getJdbcTemplate().queryForObject(this.sql, String.class, username);

//          String plain = Encodes.unescapeHtml(credential.getPassword());
//          byte[] salt = Digests.generateSalt(SALT_SIZE);
//          byte[] hashPassword = Digests.sha1(plain.getBytes(), salt, HASH_INTERATIONS);
//          String s=Encodes.encodeHex(salt)+Encodes.encodeHex(hashPassword);
//          boolean ss= dbPassword.equals(s);

            String plainPassword="123456";
            plainPassword=credential.getPassword();
            String password="9ec51dc31c730f4b6a719842d1c5a6d2034e653f4fbdafa45ed60104";
            password=dbPassword;
            String plain1 = Encodes.unescapeHtml(plainPassword);
            byte[] salt1 = Encodes.decodeHex(password.substring(0,16));
            byte[] hashPassword1 = Digests.sha1(plain1.getBytes(), salt1, HASH_INTERATIONS);
            boolean ss1= password.equals(Encodes.encodeHex(salt1)+Encodes.encodeHex(hashPassword1));
            if (!ss1) {
//            if (!dbPassword.equals(encryptedPassword)) {
                throw new FailedLoginException("Password does not match value on record.");
            }
        } catch (final IncorrectResultSizeDataAccessException e) {
            if (e.getActualSize() == 0) {
                throw new AccountNotFoundException(username + " not found with SQL query");
            } else {
                throw new FailedLoginException("Multiple records found for " + username);
            }
        } catch (final DataAccessException e) {
            throw new PreventedException("SQL exception while executing query for " + username, e);
        }
        return createHandlerResult(credential, new SimplePrincipal(username), null);
    }

    /**
     * @param sql The sql to set.
     */
    public void setSql(final String sql) {
        this.sql = sql;
    }
}

修改deployerConfigContext.xml文件

这里写图片描述

至此完成Jeesite单点登录集成Cas和自定义登录验证


jeesite加密研究

这里写图片描述

如果直接给密码散列,黑客可以通过查散列值字典(例如MD5密码破解网站),得到某用户的密码。加上salt后就会难上很多,即便是你获得了其中的salt和最终密文,破解也是相当麻烦的。


盐(Salt),在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
安全因素
通常情况下,当字段经过散列处理(如MD5),会生成一段散列值,而散列后的值一般是无法通过特定算法得到原始字段的。但是某些情况,比如一个大型的彩虹表,通过在表中搜索该MD5值,很有可能在极短的时间内找到该散列值对应的真实字段内容。

加盐后的散列值,可以极大的降低由于用户数据被盗而带来的密码泄漏风险,即使通过彩虹表寻找到了散列后的数值所对应的原始内容,但是由于经过了加盐,插入的字符串扰乱了真正的密码,使得获得真实密码的概率大大降低。

实现原理
加盐的实现过程通常是在需要散列的字段的特定位置增加特定的字符,打乱原始的字符串,使其生成的散列结果产生变化。比如,用户使用了一个密码:

x7faqgjw
经过MD5散列后,可以得出结果:

455e0e5c2bc109deae749e7ce0cdd397
但是由于用户密码位数不足,短密码的散列结果很容易被彩虹表破解,因此,在用户的密码末尾添加特定字符串(粗体下划线为加盐的字段):

x7faqgjwabcdefghijklmnopqrstuvwxyz

因此,加盐后的密码位数更长了,散列的结果也发生了变化:

4a1690d5eb6c126ef68606dda68c2f79
以上就是加盐过程的简单描述,在实际使用过程中,还需要通过特定位数插入、倒序或多种方法对原始密码进行固定的加盐处理,使得散列的结果更加不容易被破解或轻易得到原始密码,比如(绿色字体为加盐字符串):

这里写图片描述

其它方式实现单点登录

使用Cookie解决单点登录 技术点:
1、设置Cookie的路径为setPath(“/”) .即Tomcat的目录下都有效
2、设置Cookie的域setDomain(“.gch.com”);即bbs.gch.com,或是mail.gch.com有效。即跨域。
3、设置Cookie的时间。即使用户不选择在几天内自动登录,也应该保存Cookie以保存在当前浏览器没有关闭的情况下有效。
4、使用Filter自动登录。

网上查询到的,未测试,个人觉得可行。

其他
`package com.jinfonet.developer.portal;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.validation.constraints.NotNull;

import org.apache.log4j.chainsaw.Main;
//import org.inspektr.common.ioc.annotation.NotNull;
import org.jasig.cas.authentication.handler.PasswordEncoder;
import org.springframework.util.StringUtils;

import com.thinkgem.jeesite.common.security.Digests;
import com.thinkgem.jeesite.common.utils.Encodes;

import sun.misc.BASE64Encoder;

/**
*
*/
public class JeeSitePasswordEncoder implements PasswordEncoder {

public static final String HASH_ALGORITHM = "SHA-1";
public static final int HASH_INTERATIONS = 1024;
public static final int SALT_SIZE = 8;

private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
        'e', 'f' };
@NotNull
private final String encodingAlgorithm;
private String characterEncoding;

public JeeSitePasswordEncoder(final String encodingAlgorithm) {
    this.encodingAlgorithm = encodingAlgorithm;
}

public String encode(final String password) {
    if (password == null) {
        return null;
    }

    try {
        MessageDigest messageDigest = MessageDigest
            .getInstance(this.encodingAlgorithm);

        if (StringUtils.hasText(this.characterEncoding)) {
            messageDigest.update(password.getBytes(this.characterEncoding));
        } else {
            messageDigest.update(password.getBytes());
        }


        final byte[] digest = messageDigest.digest();

        return getFormattedText(digest);
    } catch (final NoSuchAlgorithmException e) {
        throw new SecurityException(e);
    } catch (final UnsupportedEncodingException e) {
        throw new RuntimeException(e);
    }
}

 /**
 * Takes the raw bytes from the digest and formats them correct.
 *
 * @param bytes the raw bytes from the digest.
 * @return the formatted bytes.
 */
private String getFormattedText(final byte[] bytes) {
    final StringBuilder buf = new StringBuilder(bytes.length * 2);

    for (int j = 0; j < bytes.length; j++) {
        buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
        buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
    }
    return buf.toString();
}

public final void setCharacterEncoding(final String characterEncoding) {
    this.characterEncoding = characterEncoding;
}

/**
 * 生成安全的密码,生成随机的16位salt并经过1024次 sha-1 hash
 */
public static String entryptPassword(String plainPassword) {
    System.out.println("原密码:"+plainPassword);
    String plain = Encodes.unescapeHtml(plainPassword);
    byte[] salt = Digests.generateSalt(SALT_SIZE);
    byte[] hashPassword = Digests.sha1(plain.getBytes(), salt, HASH_INTERATIONS);
    String mm=Encodes.encodeHex(salt)+Encodes.encodeHex(hashPassword);
    System.out.println("加密后:"+mm);
    return mm;
}

/*
public static void main(String[] args) {
String s=”1”;
System.out.println(s);
System.out.println(entryptPassword(s));
}*/

}`

posted @ 2017-03-09 10:33  llhl  阅读(632)  评论(0编辑  收藏  举报