CAS单点登录-自定义认证登录策略(五)

在上一节中我们使用了CAS的提供的JDBC 方式的登录认证,基本上能够满足我们多种需求的认证。
但是如果CAS框架提供的方案还是不能满足我们的需要,比如我们不仅需要用户名和密码,还要验证其他信息,比如邮箱,手机号,但是邮箱,手机信息在另一个数据库,还有在一段时间内同一IP输入错误次数限制等。这里就需要我们自定义认证策略,自定义CAS的web认证流程。

自定义认证校验策略

我们知道CAS为我们提供了多种认证数据源,我们可以选择JDBC、File、JSON等多种方式,但是如果我想在自己的认证方式中可以根据提交的信息实现不同数据源选择,这种方式就需要我们去实现自定义认证。

自定义策略主要通过现实更改CAS配置,通过AuthenticationHandler在CAS中设计和注册自定义身份验证策略,拦截数据源达到目的。

主要分为下面三个步骤:

  1. 设计自己的认证处理数据的程序
  2. 注册认证拦截器到CAS的认证引擎中
  3. 更改认证配置到CAS中

首先我们还是添加需要的依赖库:

       <!-- Custom Authentication -->
        <dependency>
            <groupId>org.apereo.cas</groupId>
            <artifactId>cas-server-core-authentication-api</artifactId>
            <version>${cas.version}</version>
        </dependency>

        <!-- Custom Configuration -->
        <dependency>
            <groupId>org.apereo.cas</groupId>
            <artifactId>cas-server-core-configuration-api</artifactId>
            <version>${cas.version}</version>
        </dependency>

        <!-- SpringSecurity Core -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>5.0.8.RELEASE</version>
        </dependency>

如果我们认证的方式仅仅是传统的用户名和密码,实现AbstractUsernamePasswordAuthenticationHandler这个抽象类就可以了,官方
给的实例也是这个。

在编写自定义认证类之前,我们这里说明下为什么选择使用springSecurity提供的加解密工具类:BCryptPasswordEncoder

BCryptPasswordEncoder相关知识:
用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。
特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。
BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。
补充说明:即使不同的用户注册时输入相同的密码,存入数据库的密文密码也会不同。

官方的实例有一个坑,给出的是5.2.x版本以前的例子,5.3.x版本后的jar包更改了,而且有个地方有坑,在5.2.x版本前的可以,新的5.3.x是不行的。

注意:这里试了很多版本最后 5.3.9这个版本没问题

接着我们自定义我们自己的实现类CustomUsernamePasswordAuthentication,如下:

package com.thtf.cas.config;

import com.thtf.cas.model.User;
import com.thtf.cas.util.SpringSecurityUtil;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.MessageDescriptor;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.UsernamePasswordCredential;
import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.security.auth.login.AccountException;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;


/**
 * ========================
 * 自定义认证校验策略
 * Created with IntelliJ IDEA.
 * User:pyy
 * Date:2019/6/26
 * Time:16:05
 * Version: v1.0
 * ========================
 */
public class CustomUsernamePasswordAuthentication extends AbstractUsernamePasswordAuthenticationHandler {

    private final static Logger logger = LoggerFactory.getLogger(CustomUsernamePasswordAuthentication.class);

    private JdbcTemplate jdbcTemplate;

    public CustomUsernamePasswordAuthentication(String name, ServicesManager servicesManager,
                                                PrincipalFactory principalFactory, Integer order, JdbcTemplate jdbcTemplate) {
        super(name, servicesManager, principalFactory, order);
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
            UsernamePasswordCredential credential, String originalPassword)
            throws GeneralSecurityException, PreventedException {
        String username = credential.getUsername();
        String password = credential.getPassword();
        logger.info("认证用户 username = {}", username);

        String sql = "select id, username, password, expired, disable, email FROM sys_user where username = ?";
        User info = (User) jdbcTemplate.queryForObject(sql, new Object[]{username}, new BeanPropertyRowMapper(User.class));
        if (info == null) {
            logger.info("用户不存在!");
            throw new AccountException("用户不存在!");
        }

        if (!SpringSecurityUtil.checkpassword(password, info.getPassword())) {
            logger.info("密码错误!");
            throw new FailedLoginException("密码错误!");
        } else {
            logger.info("校验成功");
            //可自定义返回给客户端的多个属性信息
            HashMap<String, Object> returnInfo = new HashMap<>();
            returnInfo.put("id", info.getId());
            returnInfo.put("username", info.getUsername());
            returnInfo.put("expired", info.getExpired());
            returnInfo.put("disable", info.getDisable());
            returnInfo.put("email", info.getEmail());
            final List<MessageDescriptor> list = new ArrayList<>();
            return createHandlerResult(credential, this.principalFactory.createPrincipal(username, returnInfo), list);
        }
    }
}

这里给出的与官方实例不同在两个地方:

  • 返回的为 AuthenticationHandlerExecutionResult而不是HandlerResult,其实源码是一样的,在新版本重新命名了而已。
  • createHandlerResult传入的warings不能为null,不然程序运行后提交信息始终无法认证成功!!!

代码主要通过拦截传入的Credential,获取用户名和密码,然后再自定义返回给客户端的用户信息。这里便可以通过代码方式自定义返回给客户端多个不同属性信息。

注入配置信息

继承AuthenticationEventExecutionPlanConfigurer

package com.thtf.cas.config;

import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.principal.DefaultPrincipalFactory;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

@Configuration("CustomAuthenticationConfiguration")
@EnableConfigurationProperties({CasConfigurationProperties.class, DataBaseProperties.class})
public class CustomAuthenticationConfiguration implements AuthenticationEventExecutionPlanConfigurer {
    
    @Autowired
    private DataBaseProperties databaseProperties;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private CasConfigurationProperties casProperties;

    @Autowired
    @Qualifier("servicesManager")
    private ServicesManager servicesManager;

    @Bean 
    public AuthenticationHandler myAuthenticationHandler() { 
        return new CustomUsernamePasswordAuthentication(CustomUsernamePasswordAuthentication.class.getName(), servicesManager, new DefaultPrincipalFactory(), 1, jdbcTemplate); 
    }
    
    @Override
    public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) {
        plan.registerAuthenticationHandler(myAuthenticationHandler());
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate(){
        // JDBC模板依赖于连接池来获得数据的连接,所以必须先要构造连接池 
        DriverManagerDataSource dataSource = new DriverManagerDataSource(); 
        dataSource.setDriverClassName(databaseProperties.getDriverClass()); 
        dataSource.setUrl(databaseProperties.getUrl()); 
        dataSource.setUsername(databaseProperties.getUser()); 
        dataSource.setPassword(databaseProperties.getPassword()); 
        // 创建JDBC模板 
        JdbcTemplate jdbcTemplate = new JdbcTemplate(); 
        jdbcTemplate.setDataSource(dataSource); 
        return jdbcTemplate;
    }
}

这里涉及到JdbcTemplate数据源配置如下:
数据源配置:

########## 用户认证JDBC数据源配置 ############
sso.jdbc.user=root
sso.jdbc.password=123456
sso.jdbc.driverClass=com.mysql.jdbc.Driver
sso.jdbc.url=jdbc:mysql://localhost:3306/sso?characterEncoding=utf8

DataBaseProperties:

package com.thtf.cas.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.io.Serializable;


@Component
@ConfigurationProperties(prefix = "sso.jdbc")
@Data
public class DataBaseProperties implements Serializable {

    private String user;
    
    private String password;
    
    private String driverClass;
    
    private String url;

}

最后我们我们在src/main/resources目录下新建META-INF目录,同时在下面新建spring.factories文件,将配置指定为我们自己新建的信息。

启动应用,输入用户名和密码,查看控制台我们打印的信息,可以发现我们从登陆页面提交的数据以及从数据库中查询到的数据,匹配信息,登录认证成功!!

从而现实了我们自定义用户名和密码的校验,同时我们还可以选择不同的数据源方式。


补充

可能还有读者提出疑问,我提交的信息不止用户名和密码,那该如何自定义认证?
这里就要我们继承AbstractPreAndPostProcessingAuthenticationHandler这个借口,其实上面的AbstractUsernamePasswordAuthenticationHandler就是继承实现的这个类,它只是用于简单的用户名和密码的校验。我们可以查看源码,如下:

自定义实现:参看https://blog.csdn.net/yelllowcong/article/details/79236360

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • 【环境说明】:本文演示过程在同一个机器上的(也可以在三台实体机器或者三个的虚拟机上),环境如下: windows7...
    黄海佳阅读 8,715评论 2 15
  • 主要介绍CAS SSO的认证流程。有关这方面的内容再网上也有很多资料,写这篇总结目的一来是自己在理解这块内容的时候...
    spilledyear阅读 9,757评论 1 17
  • 【环境说明】: 本文演示过程在同一个机器上的(也可以在三台实体机器或者三个的虚拟机上),环境如下: windows...
    yljava阅读 9,108评论 3 8
  • 转载自:http://blog.csdn.net/turkeyzhou/article/details/55097...
    大诗兄_zl阅读 2,154评论 0 3
  • 本章节的内容为JDBC认证,查找数据库进行验证,其中包括: 密码加密策略(无密码,简单加密,加盐处理) 认证策略(...
    匆匆岁月阅读 1,430评论 0 2