CAS 实现站内单点登录及实现第三方 OAuth、OpenId 登录(二)

原创
2014/03/06 13:22
阅读数 1.9K

一、登录表单

<form:form id="loginForm" method="post" commandName="${commandName}" htmlEscape="true">
    <form:errors path="*" element="em" cssClass="" />
    <c:if test="${not empty sessionScope.openIdLocalId}">
        用户名<input id="username" name="username" type="text" value="${sessionScope.openIdLocalId}" />
    </c:if>
    <c:if test="${empty sessionScope.openIdLocalId}">
        <!-- 这里您不要直接使用 input,而使用 springframework 的 form:input 能够在登录出错时,还能保证显示之前登录时输入的用户名 -->
        <form:input id="username" path="username" htmlEscape="true" />
    </c:if>
    密码:<input id="password" name="password" type="password" />
    验证码:<input id="validateCode" name="validateCode" type="text" /><img src="/captcha.jpg" />
    <input id="rememberMe" name="rememberMe" type="checkbox" value="true" /><label for="rememberMe">记住用户名</label>
    <input id="login-Btn" type="button" value="登录" />
    <input name="lt" type="hidden" value="${loginTicket}" />
    <input name="execution" type="hidden" value="${flowExecutionKey}" />
    <input name="_eventId" type="hidden" value="submit" />
</form:form>
<!-- 需要 jquery.cookie 和 jquery.md5 插件 -->
<script type="text/javascript">
    $(document).ready(function(){
        var loginUsername = $.cookie("loginUsername");
        if(loginUsername){
            $("#username").val(loginUsername);
            $("#rememberMe").attr("checked", true);
        }
        $("#login-Btn").click(function(){
            /* 为了密码传输安全,表单提交之前,进行 md5 加密 */
            $("#password").val($.md5($("#password").val()));
            if($("#rememberMe").is(":checked") == true){
                $.cookie("loginUsername", $("#username").val(), {expires: 365});
            }else{
                $.cookie("loginUsername", null, {expires: -1});
            }
            $("#loginForm").submit();
        });
    });
</script>

二、验证码

        此例子中的验证实现是使用的 Google 的 Kaptcha

  1. Google Kaptcha 配置(在 WEB-INF/spring-configuration/applicationContext.xml 添加)

    <bean id="captchaConfig" class="com.google.code.kaptcha.util.Config">
        ... ...
    </bean>
    <bean id="captchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha" p:config-ref="captchaConfig" />
  2. Servlet 配置

    1)修改 WEB-INF/cas-servlet.xml 中的

    <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
            p:centralAuthenticationService-ref="centralAuthenticationService"
            p:warnCookieGenerator-ref="warnCookieGenerator"/>

        为 : AuthenticationCaptchaViaFormAction.java(用户验证 Action)

    <bean id="authenticationViaFormAction" class="com.buession.cas.web.flow.AuthenticationCaptchaViaFormAction"
            p:captchaConfig-ref="captchaConfig"
            p:centralAuthenticationService-ref="centralAuthenticationService"
            p:warnCookieGenerator-ref="warnCookieGenerator" />

    2)在 WEB-INF/cas-servlet.xml 添加
    CaptchaController.java(验证码控制器)
    ValidateCaptchaController.java(验证码异步验证控制器)

    <bean id="handlerMappingC" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/captcha.jpg">captchaController</prop><!-- 验证码 Controller -->
                <prop key="/validateCaptcha">validateCaptchaController</prop><!-- 验证码异步验证 Controller,不是必须 -->
                ... ...
            </props>
        </property>
    </bean>
    <bean id="captchaController" class="com.buession.cas.web.controller.CaptchaController"
        p:config-ref="captchaConfig" />
    <bean id="validateCaptchaController" class="com.buession.cas.web.controller.ValidateCaptchaController" 
        p:config-ref="captchaConfig" />

    3)记得在 web.xml 添加 Servlet 和 URL 之间映射

三、用户认证

  1. 数据库配置(在添加 WEB-INF/spring-configuration/applicationContext.xml 添加)

    <bean id="masterJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
        p:dataSource-ref="masterDataSource" />
    <bean id="slaveJdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
        p:dataSource-ref="slaveDataSource" />
    
    <bean id="masterDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        ... ...
        destroy-method="close" />
    <bean id="slaveDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        ... ...
        destroy-method="close" />

    为什么要配置 master 数据源和 slave 数据源?
    用户登录时,查询用户信息是只读,我们就从从库中查询,主库是可以用于向数据库中写入用户登录日志,或者密码错误次数(当然这里可以根据实际需求调整,比如用户密码输入错误次数,也是可以记录到 Memcache、Redis 等缓存服务器中的)

  2. 修改用户凭证 credentials(WEB-INF/login-webflow.xml)

    修改

    <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />

    为:RememberMeUsernamePasswordCaptchaCredentials.java(带有 ”RememberMe“ 和 ”验证码“的用户凭证)

    <var name="credentials" class="com.buession.cas.authentication.principal.RememberMeUsernamePasswordCaptchaCredentials" />
  3. 修改 AuthenticationViaFormAction(WEB-INF/login-webflow.xml)

    修改

    <view-state id="viewLoginForm" view="casLoginView" model="credentials">
        <binder>
            <binding property="username" />
            <binding property="password" />
        </binder>
        ... ...
    </view-state>

    为(binding 的 property 属性值一定要与 Credentials 的属性名和登录表单控件的 name 属性的值保持一致)

    <view-state id="viewLoginForm" view="casLoginView" model="credentials">
        <binder>
            <binding property="username" />
            <binding property="password" />
            <binding property="validateCode" />
        </binder>
        ... ...
    </view-state>
    1. 添加数据库 authentication handlers(在 WEB-INF/deployerConfigContext.xml 中添加)

      <bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl">
          ... ...
          <property name="authenticationHandlers">
              <list>
                  <bean class="com.domain.authentication.handler.DatabaseAuthenticationHandler">
                      <property name="jdbcTemplate" ref="slaveJdbcTemplate" />
                      <!--(1)--><property name="sql" value="SELECT `password`, `salt`, `algo` FROM `member` WHERE `username` = ? LIMIT 1" />
                      <!--(2)--><property name="sql" value="SELECT `password`, `salt`, `algo` FROM `member` WHERE `username` = ? OR `email` = ? OR `mobile` = ? LIMIT 1" />
                      <!--(3)--><property name="sql" value="SELECT `password`, `salt`, `algo` FROM `member` WHERE `username` = ? OR `email` = ? OR `mobile` = ?" />
                      <!-- (1)适用于单一条件登录;(2)适用于多条件,且这些条件中不会出现相同值的情况;(3)适用于多条件,但这些条件中可能会存在相同值的情况,例如:存在 username 为:13800138000,和 mobile 也为:13800138000 的不同两条用户数据 -->
                      <property name="passwordEncoder" ref="passwordEncoder" />
                  </bean>
              </list>
          </property>
      </bean>
      class DatabaseAuthenticationHandler extends com.buession.cas.authentication.handler.support.DatabaseQueryAuthenticationHandler {
      
          @Override
          protected boolean authenticateUsernamePasswordInternal(UsernamePasswordCredentials credentials)
                  throws AuthenticationException {
              PasswordEncoder passwordEncoder = (PasswordEncoder) getPasswordEncoder();
              String username = getPrincipalNameTransformer().transform(credentials.getUsername());
              String password = credentials.getPassword();
      
              (1)、(2)
              try {
                  final Map<String, Object> r = jdbcTemplate.queryForMap(sql, username, username,
                          username);
                  if (valid(username, password, r, passwordEncoder) == true) {
                       /**
                         * 对旧用户数据更换加密方式,且重新生成密码
                       */
                      if (Mcrypt.MD5.equals(r.get("algo")) == true) {
                          modifyEncoder((String) r.get("username"), credentials.getPassword(),
                                  (String) r.get("salt"));
                      }
                      return ture;
                  }
              } catch (IncorrectResultSizeDataAccessException e) {
              }
      
              (3)
              try {
                  List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, username, username,
                          username);
      
                  if (result != null && result.size() > 0) {
                      for (Map<String, Object> r : result) {
                          if (valid(username, password, r, passwordEncoder) == true) {
                              /**
                               * 对旧用户数据更换加密方式,且重新生成密码
                               */
                              if (Mcrypt.MD5.equals(r.get("algo")) == true) {
                                  modifyEncoder((String) r.get("username"), credentials.getPassword(),
                                          (String) r.get("salt"));
                              }
                              return true;
                          }
                      }
                  }
              } catch (IncorrectResultSizeDataAccessException e) {
              }
      
              return false;
          }
      
          private boolean valid(String username, String password, final Map<String, Object> object,
                  PasswordEncoder passwordEncoder) {
              String salt = (String) object.get("salt");
              String algo = (String) object.get("algo");
      
              passwordEncoder.setAlgo(algo);
      
              /**
               * 如果加密方式为 "MD5",则为老的加密方式
               * 旧系统中,密码为明文传输,在后端 MD5 加密的,
               * 现在密码在前端进行了 MD5 加密传输,所以无需双重加密,而直接 encode(password+salt)
               */
              if (Mcrypt.MD5.equals(algo) == true) {
                  password += salt;
              } else {
                  passwordEncoder.setSalt(salt);
              }
      
              final String encodedPassword = passwordEncoder.encode(password);
              return encodedPassword != null && encodedPassword.equalsIgnoreCase((String) object.get("password"));
          }
      
          private void modifyEncoder(final String username, final String password, final String salt) {
              PasswordEncoder passwordEncoder = (PasswordEncoder) getPasswordEncoder();
      
              passwordEncoder.setAlgo(Mcrypt.SHA512);
              passwordEncoder.setSalt(salt);
      
              String sql = "UPDATE `member` SET `algo` = ?, `password` = ? WHERE `username` = ?";
              jdbcTemplate.update(sql, Mcrypt.SHA512, passwordEncoder.encode(password), username);
          }
      }
  4. 密码加密配置(在添加 WEB-INF/deployerConfigContext.xml 添加)

    <bean id="passwordEncoder" class="com.buession.cas.authentication.handler.PasswordEncoder"
        p:characterEncoding="UTF-8" />

    PasswordEncoder.java

  5. 添加验证码验证消息(WEB-INF/classes/messages_xxx.properties)

    required.captcha=验证码不能为空
    INVALID_CAPTCHA=验证码错误
展开阅读全文
打赏
0
4 收藏
分享
加载中

引用来自“tuer”的评论

好的,我先研究官网,然后再来看博主的博文,多谢!

OK 了

2014/04/18 15:32
回复
举报
好的,我先研究官网,然后再来看博主的博文,多谢!
2014/04/14 16:39
回复
举报

引用来自“tuer”的评论

你好,想请教些问题,麻烦博主允许查看你 的关于CAS实现第三方登陆的博文吧!学习一下,很是感激!

如果你只是看第三方登录的话 https://wiki.jasig.org/display/CASUM/Configuration+for+the+OAuth+client+support+in+CAS+server+version+%3E%3D+3.5.1 可以看这个,我的博文里面重构了部分官方提供的 API 和 将第三方用户绑定到站内

2014/04/14 15:31
回复
举报

引用来自“tuer”的评论

你好,想请教些问题,麻烦博主允许查看你 的关于CAS实现第三方登陆的博文吧!学习一下,很是感激!

@tuer 还在整理完善之中,所以半吊子的状况下就没有公开出来,等这两天完善后就公开

2014/04/14 15:28
回复
举报
你好,想请教些问题,麻烦博主允许查看你 的关于CAS实现第三方登陆的博文吧!学习一下,很是感激!
2014/04/14 15:26
回复
举报
更多评论
打赏
5 评论
4 收藏
0
分享
返回顶部
顶部