Skip to content

Latest commit

 

History

History
1537 lines (1317 loc) · 59.3 KB

自定义验证码.md

File metadata and controls

1537 lines (1317 loc) · 59.3 KB

自定义验证码

在 cas 登录页添加验证码,防止暴力破解

CAS - Design Authentication Strategies (apereo.github.io)

apereo/cas at 5.3.x (github.com)

CAS单点登录(五)——Service配置及管理_Anumbrella-CSDN博客

CAS单点登录(六)——自定义登录界面和表单信息_Anumbrella-CSDN博客

CAS单点登录(七)——自定义验证码以及自定义错误信息_Anumbrella-CSDN博客

验证码生成和显示

验证码工具类

com.chunshu.cas.utils.CaptchaCodeUtils

package com.chunshu.cas.utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Random;

/**
 * @author anumbrella
 */
public class CaptchaCodeUtils {

    //宽度
    private static final int CAPTCHA_WIDTH = 100;
    //高度
    private static final int CAPTCHA_HEIGHT = 35;
    //数字的长度
    private static final int NUMBER_CNT = 6;
    //图片类型
    private static final String IMAGE_TYPE = "JPEG";

    private Random r = new Random();
    //  字体
    //  private String[] fontNames = { "宋体", "华文楷体", "黑体", "华文新魏", "华文隶书", "微软雅黑", "楷体_GB2312" };
    private String[] fontNames = {"宋体", "黑体", "微软雅黑"};

    // 可选字符
    private String codes = "23456789abcdefghjkmnopqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ";

    // 背景色,白色
    private Color bgColor = new Color(255, 255, 255);

    // 验证码上的文本
    private String text;

    private static CaptchaCodeUtils utils = null;

    /**
     * 实例化对象
     *
     * @return
     */
    public static CaptchaCodeUtils getInstance() {
        if (utils == null) {
            synchronized (CaptchaCodeUtils.class) {
                if (utils == null) {
                    utils = new CaptchaCodeUtils();
                }
            }
        }
        return utils;
    }

    /**
     * 创建验证码
     *
     * @param path 路径地址
     * @return
     * @throws Exception
     */
    public String getCode(String path) throws Exception {
        BufferedImage bi = utils.getImage();
        output(bi, new FileOutputStream(path));
        return this.text;
    }

    /**
     * 生成图片对象,并返回
     *
     * @return
     * @throws Exception
     */
    public CaptchaCode getCode() throws Exception {
        BufferedImage img = utils.getImage();

        //返回验证码对象
        CaptchaCode code = new CaptchaCode();
        code.setText(this.text);
        code.setData(this.copyImage2Byte(img));
        return code;
    }

    /**
     * 将图片转化为 二进制数据
     *
     * @param img
     * @return
     * @throws Exception
     */
    public byte[] copyImage2Byte(BufferedImage img) throws Exception {
        //字节码输出流
        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        //写数据到输出流中
        ImageIO.write(img, IMAGE_TYPE, bout);

        //返回数据
        return bout.toByteArray();
    }

    /**
     * 将二进制数据转化为文件
     *
     * @param data
     * @param file
     * @throws Exception
     */
    public boolean copyByte2File(byte[] data, String file) throws Exception {
        ByteArrayInputStream in = new ByteArrayInputStream(data);
        FileOutputStream out = new FileOutputStream(file);
        try {
            byte[] buff = new byte[1024];
            int len = 0;
            while ((len = in.read(buff)) > -1) {
                out.write(buff, 0, len);
            }
            out.flush();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            out.close();
            in.close();
        }
    }

    /**
     * 生成随机的颜色
     *
     * @return
     */
    private Color randomColor() {
        int red = r.nextInt(150);
        int green = r.nextInt(150);
        int blue = r.nextInt(150);
        return new Color(red, green, blue);
    }

    /**
     * 生成随机的字体
     *
     * @return
     */
    private Font randomFont() {
        int index = r.nextInt(fontNames.length);
        String fontName = fontNames[index];// 生成随机的字体名称
        int style = r.nextInt(4);// 生成随机的样式, 0(无样式), 1(粗体), 2(斜体), 3(粗体+斜体)
        int size = r.nextInt(5) + 24; // 生成随机字号, 24 ~ 28
        return new Font(fontName, style, size);
    }

    /**
     * 画干扰线
     *
     * @param image
     */
    private void drawLine(BufferedImage image) {
        int num = 5;// 一共画5条
        Graphics2D g2 = (Graphics2D) image.getGraphics();
        for (int i = 0; i < num; i++) {// 生成两个点的坐标,即4个值
            int x1 = r.nextInt(CAPTCHA_WIDTH);
            int y1 = r.nextInt(CAPTCHA_HEIGHT);
            int x2 = r.nextInt(CAPTCHA_WIDTH);
            int y2 = r.nextInt(CAPTCHA_HEIGHT);
            g2.setStroke(new BasicStroke(1.5F));
            g2.setColor(randomColor()); // 随机生成干扰线颜色
            g2.drawLine(x1, y1, x2, y2);// 画线
        }
    }


    /**
     * 随机生成一个字符
     *
     * @return
     */
    private char randomChar() {
        int index = r.nextInt(codes.length());
        return codes.charAt(index);
    }

    /**
     * 创建BufferedImage
     *
     * @return
     */
    private BufferedImage createImage() {
        BufferedImage image = new BufferedImage(CAPTCHA_WIDTH, CAPTCHA_HEIGHT, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = (Graphics2D) image.getGraphics();
        g2.setColor(this.bgColor);
        g2.fillRect(0, 0, CAPTCHA_WIDTH, CAPTCHA_HEIGHT);
        return image;
    }

    /**
     * 获取验证码
     *
     * @return
     */
    public BufferedImage getImage() {
        BufferedImage image = createImage();// 创建图片缓冲区
        Graphics2D g2 = (Graphics2D) image.getGraphics();// 得到绘制环境
        StringBuilder sb = new StringBuilder();// 用来装载生成的验证码文本
        // 向图片中画4个字符
        for (int i = 0; i < NUMBER_CNT; i++) {// 循环四次,每次生成一个字符
            String s = randomChar() + "";// 随机生成一个字母
            sb.append(s); // 把字母添加到sb中
            float x = i * 1.0F * CAPTCHA_WIDTH / NUMBER_CNT; // 设置当前字符的x轴坐标
            g2.setFont(randomFont()); // 设置随机字体
            g2.setColor(randomColor()); // 设置随机颜色
            g2.drawString(s, x, CAPTCHA_HEIGHT - 5); // 画图
        }
        this.text = sb.toString(); // 把生成的字符串赋给了this.text
        drawLine(image); // 添加干扰线
        return image;
    }

    /**
     * @return 返回验证码图片上的文本
     */
    public String getText() {
        return text;
    }

    // 保存图片到指定的输出流
    public static void output(BufferedImage image, OutputStream out) throws IOException {
        ImageIO.write(image, IMAGE_TYPE, out);
    }

    /**
     * 图片验证码对象
     */
    public static class CaptchaCode {
        //验证码文字信息
        private String text;
        //验证码二进制数据
        private byte[] data;

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public byte[] getData() {
            return data;
        }

        public void setData(byte[] data) {
            this.data = data;
        }
    }
}

验证码接口

com.chunshu.cas.controller.CaptchaController

package com.chunshu.cas.controller;

import com.chunshu.cas.utils.CaptchaCodeUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * @author anumbrella
 */
@Controller
public class CaptchaController {

    /**
     * 工具类生成captcha验证码路径
     *
     * @param request
     * @param response
     * @throws Exception
     */
    @GetMapping(value = "/captcha", produces = "image/png")
    public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
        OutputStream out = null;
        try {
            //设置response头信息
            //禁止缓存
            response.setHeader("Cache-Control", "no-cache");
            response.setContentType("image/png");
            //存储验证码到session
            CaptchaCodeUtils.CaptchaCode code = CaptchaCodeUtils.getInstance().getCode();

            //获取验证码code
            String codeTxt = code.getText();
            request.getSession().setAttribute("captcha_code", codeTxt);
            //写文件到客户端
            out = response.getOutputStream();
            byte[] imgs = code.getData();
            out.write(imgs, 0, imgs.length);
            out.flush();
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }

    /**
     * 用于前端ajax校验
     */
    @RequestMapping(value = "/chkCode", method = RequestMethod.POST)
    public void checkCode(String code, HttpServletRequest req, HttpServletResponse resp) {

        //获取session中的验证码
        String storeCode = (String) req.getSession().getAttribute("captcha_code");
        code = code.trim();
        //返回值
        Map<String, Object> map = new HashMap<String, Object>();
        //验证是否对,不管大小写
        if (!StringUtils.isEmpty(storeCode) && code.equalsIgnoreCase(storeCode)) {
            map.put("error", false);
            map.put("msg", "验证成功");
        } else if (StringUtils.isEmpty(code)) {
            map.put("error", true);
            map.put("msg", "验证码不能为空");
        } else {
            map.put("error", true);
            map.put("msg", "验证码错误");
        }
        this.writeJSON(resp, map);
    }

    /**
     * 在SpringMvc中获取到Session
     *
     * @return
     */
    public void writeJSON(HttpServletResponse response, Object object) {
        try {
            //设定编码
            response.setCharacterEncoding("UTF-8");
            //表示是json类型的数据
            response.setContentType("application/json");
            //获取PrintWriter 往浏览器端写数据
            PrintWriter writer = response.getWriter();

            ObjectMapper mapper = new ObjectMapper(); //转换器
            //获取到转化后的JSON 数据
            String json = mapper.writeValueAsString(object);
            //写数据到浏览器
            writer.write(json);
            //刷新,表示全部写完,把缓存数据都刷出去
            writer.flush();
            //关闭writer
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

控制器 bean 注入 spring

src/main/resources/META-INF/spring.factories里注入控制器 bean 到 spring boot

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.apereo.cas.config.CasEmbeddedContainerTomcatConfiguration,\
  org.apereo.cas.config.CasEmbeddedContainerTomcatFiltersConfiguration,\
  com.chunshu.cas.controller.CaptchaController

类似 spring 扫描包

也可以使用@Bean注入控制器 bean,创建自定义控制器配置类CustomControllerConfigurer,在该类中注入控制器

com.chunshu.cas.config.CustomControllerConfigurer

package com.chunshu.cas.config;

import com.chunshu.cas.controller.CaptchaController;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author anumbrella
 */
@Configuration("captchaConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomControllerConfigurer {

    /**
     * 验证码配置,注入bean到spring中
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(name = "captchaController")
    public CaptchaController captchaController() {
        return new CaptchaController();
    }
}

src/main/resources/META-INF/spring.factories里将控制器注入CustomControllerConfigurer

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.apereo.cas.config.CasEmbeddedContainerTomcatConfiguration,\
  org.apereo.cas.config.CasEmbeddedContainerTomcatFiltersConfiguration,\
  com.chunshu.cas.config.CustomControllerConfigurer

重启 cas,在/captcha接口可查看验证码图片

自定义用户名密码凭证

cas 使用UsernamePasswordCredential作为凭证,将表单用户名密码传递到 handler 进行认证。扩展UsernamePasswordCredential,加入验证码字段

com.chunshu.cas.entity.CaptchaUsernamePasswordCredential

package com.chunshu.cas.entity;

import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apereo.cas.authentication.UsernamePasswordCredential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.constraints.Size;

/**
 * @author Anumbrella
 */
public class CaptchaUsernamePasswordCredential extends UsernamePasswordCredential {

    private static final Logger LOGGER = LoggerFactory.getLogger(CaptchaUsernamePasswordCredential.class);

    private static final long serialVersionUID = -4166149641561667276L;

    // 验证码字段
    @Size(min = 4, max = 8, message = "require.captcha")
    private String captcha;

    public String getCaptcha() {
        return captcha;
    }

    public void setCaptcha(String captcha) {
        this.captcha = captcha;
    }

    public CaptchaUsernamePasswordCredential() {
    }

    public CaptchaUsernamePasswordCredential(final String captcha) {
        this.captcha = captcha;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof CaptchaUsernamePasswordCredential)) {
            return false;
        } else {
            CaptchaUsernamePasswordCredential other = (CaptchaUsernamePasswordCredential) o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof CaptchaUsernamePasswordCredential;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .appendSuper(super.hashCode())
                .append(this.captcha)
                .toHashCode();
    }
}

自定义 web 流程

自定义的 web 流程 webflow 可以在 cas 认证过程中使用自定义的用户名密码凭证 credential,即自定义的认证字段可以从表单提交到自定义的用户名密码凭证credential中,然后可以在自定义的认证处理器AuthenticationHandler中获取到该字段用于认证。

This is the most traditional yet most powerful method of dynamically altering the webflow internals. You will be asked to write components that auto-configure the webflow and inject themselves into the running CAS application context only to be executed at runtime.

这是动态更改 webflow 内部结构的最传统但最强大的方法。您将被要求编写自动配置 webflow 的组件,并将它们自己注入到正在运行的 CAS 应用程序上下文中,以便在运行时执行。

CAS - Web Flow Extensions (apereo.github.io)

修改 webflow

com.chunshu.cas.config.CaptchaCasWebflowConfigurer

package com.chunshu.cas.config;

import com.chunshu.cas.entity.CaptchaUsernamePasswordCredential;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.CasWebflowConstants;
import org.apereo.cas.web.flow.configurer.AbstractCasWebflowConfigurer;
import org.springframework.context.ApplicationContext;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.ViewState;
import org.springframework.webflow.engine.builder.BinderConfiguration;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;

/**
 * @author anumbrella
 */
public class CaptchaCasWebflowConfigurer extends AbstractCasWebflowConfigurer {


    public CaptchaCasWebflowConfigurer(FlowBuilderServices flowBuilderServices,
                                       FlowDefinitionRegistry flowDefinitionRegistry,
                                       ApplicationContext applicationContext,
                                       CasConfigurationProperties casProperties) {
        super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties);
    }

    @Override
    protected void doInitialize() {
        final Flow flow = super.getLoginFlow();
        bindCredential(flow);
    }

    /**
     * 绑定自定义的Credential信息
     *
     * @param flow
     */
    protected void bindCredential(Flow flow) {
        // 重写绑定自定义credential
        createFlowVariable(flow, CasWebflowConstants.VAR_ID_CREDENTIAL, CaptchaUsernamePasswordCredential.class);

        // 登录页绑定新参数
        final ViewState state = (ViewState) flow.getState(CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM);
        final BinderConfiguration cfg = getViewStateBinderConfiguration(state);
        // 由于用户名以及密码已经绑定,所以只需对新加系统参数绑定即可
        // 字段名,转换器,是否必须字段
        cfg.addBinding(new BinderConfiguration.Binding("captcha", null, true));
    }
}

验证码非空校验提示参看自定义异常提示

注册自定义的 web 流程

com.chunshu.cas.config.CaptchaCasWebflowExecutionPlanConfigurer

package com.chunshu.cas.config;

import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.CasWebflowConfigurer;
import org.apereo.cas.web.flow.CasWebflowExecutionPlan;
import org.apereo.cas.web.flow.CasWebflowExecutionPlanConfigurer;
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.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;

/**
 * @author anumbrella
 */
@Configuration("CaptchaCasWebflowExecutionPlanConfigurer")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CaptchaCasWebflowExecutionPlanConfigurer implements CasWebflowExecutionPlanConfigurer {

    @Autowired
    private CasConfigurationProperties casProperties;

    @Autowired
    @Qualifier("loginFlowRegistry")
    private FlowDefinitionRegistry loginFlowDefinitionRegistry;

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private FlowBuilderServices flowBuilderServices;

    @Bean
    public CasWebflowConfigurer customWebflowConfigurer() {
        // 实例化自定义的表单配置类
        final CaptchaCasWebflowConfigurer c = new CaptchaCasWebflowConfigurer(flowBuilderServices, loginFlowDefinitionRegistry,
                applicationContext, casProperties);
        // 初始化
        c.initialize();
        // 返回对象
        return c;
    }

    @Override
    public void configureWebflowExecutionPlan(final CasWebflowExecutionPlan plan) {
        plan.registerWebflowConfigurer(customWebflowConfigurer());
    }
}

src/main/resources/META-INF/spring.factories里将控制器注入 spring boot

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.apereo.cas.config.CasEmbeddedContainerTomcatConfiguration,\
  org.apereo.cas.config.CasEmbeddedContainerTomcatFiltersConfiguration,\
  com.chunshu.cas.config.CaptchaCasWebflowExecutionPlanConfigurer,\
  com.chunshu.cas.config.CustomControllerConfigurer

CAS - Web Flow Extensions (apereo.github.io)

在表单添加验证码字段

<!-- 验证码信息 -->
<section class="form-group" style="margin-bottom: 2rem;">
    <label for="password">验证码:</label>
    <div class="pwdInput">
        <input type="text" id="code" class="form-control" th:field="*{captcha}" style="width:50%; position: relative; float: left;"/>
        <img id="captcha_img" th:src="@{/captcha}" onclick="changeCode()" style="width:40%; height:38px; position: relative; float: left; margin-left: 10px; cursor:pointer;"/>
        <div onclick="changeCode()" style="position:absolute;top:44px;right:0px; font-size:10px;font-weight:200;cursor:pointer;color:#428bca;">
            <i>答案不对?点击更换图片</i>
        </div>
    </div>
</section>
function changeCode(){
    var node = document.getElementById("captcha_img");
    //修改验证码
    if (node){
        node.src = node.src+'?id='+uuid();
    }
}

function uuid(){
    //获取系统当前的时间
    var d = new Date().getTime();
    //替换uuid里面的x和y
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        //取余 16进制
        var r = (d + Math.random()*16)%16 | 0;
        //向下去整
        d = Math.floor(d/16);
        //toString 表示编程16进制的数据
        return (c=='x' ? r : (r&0x3|0x8)).toString(16);
    });
    return uuid;
};

自定义认证

The overall tasks may be categorized as such:

  1. Design the authentication handler.

  2. Register the authentication handler with the CAS authentication engine.

  3. Let CAS to recognize the authentication configuration.

自定义认证主要包含几步:

  1. 设计认证处理器
  2. 使用 cas 认证引擎注册这个认证处理器
  3. 让 cas 识别到认证配置

CAS - Design Authentication Strategies (apereo.github.io)

使用用户名密码凭证为UsernamePasswordCredential的示例

package com.example.cas;

public class MyAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler {
 ...
 protected HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential,
                                                              final String originalPassword) {
     if (everythingLooksGood()) {
         return createHandlerResult(credential,
                 this.principalFactory.createPrincipal(username), null);
     }
     throw new FailedLoginException("Sorry, you are a failure!");
 }
 ...
}
  • Authentication handlers have the ability to produce a fully resolved principal along with attributes. If you have the ability to retrieve attributes from the same place as the original user/principal account store, the final object that is resolved here must then be able to carry all those attributes and claims inside it at construction time.Principal

    认证处理器可以生成用户凭证(包含表单提交的信息),将其与原始的用户身份信息(如用户名、密码)匹配,用所有这些信息构造HandlerResult返回,完成用户认证

  • The last parameter, , is effectively a collection of warnings that is eventually worked into the authentication chain and conditionally shown to the user. Examples of such warnings include password status nearing an expiration date, etc.null

    构造HandlerResult的最后一个参数 ,实际上是警告的集合,最终会进入身份验证链并有条件地显示给用户。此类警告的示例包括接近到期日期的密码状态等

  • Authentication handlers also have the ability to block authentication by throwing a number of specific exceptions. A more common exception to throw back is to note authentication failure. Other specific exceptions may be thrown to indicate abnormalities with the account status itself, such as .FailedLoginException``AccountDisabledException

    认证处理器也可以抛出很多特定异常阻止认证,即登录失败;也可以抛出特定异常指示用户账户的状态异常,如FailedLoginExceptionAccountDisabledException

  • Various other components such as s, s and such may also be injected into our handler if need be, though these are skipped for now in this post for simplicity.PrincipalNameTransformer``PasswordEncoder

    很多其他的组件也可以注入认证处理器,如PrincipalNameTransformerPasswordEncoder

CAS - Design Authentication Strategies (apereo.github.io)

认证处理器生成的用户凭证包含了所有表单提交的信息,并根据认证逻辑判断登录是否成功,因此自定义的认证处理器可以对登录认证进行极大程度的定制化。

所有的认证处理器扩展自源码core/cas-server-core-authentication-api/org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler

package org.apereo.cas.authentication.handler.support;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.authentication.AbstractAuthenticationHandler;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.BasicCredentialMetaData;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.DefaultAuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.MessageDescriptor;
import org.apereo.cas.authentication.PrePostAuthenticationHandler;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;

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

/**
 * Abstract authentication handler that allows deployers to utilize the bundled
 * AuthenticationHandlers while providing a mechanism to perform tasks before
 * and after authentication.
 *
 * @author Scott Battaglia
 * @author Marvin S. Addison
 * @since 3.1
 */
@Slf4j
public abstract class AbstractPreAndPostProcessingAuthenticationHandler extends AbstractAuthenticationHandler implements PrePostAuthenticationHandler {

    public AbstractPreAndPostProcessingAuthenticationHandler(final String name, final ServicesManager servicesManager, final PrincipalFactory principalFactory,
                                                             final Integer order) {
        super(name, servicesManager, principalFactory, order);
    }

    @Override
    public AuthenticationHandlerExecutionResult authenticate(final Credential credential) throws GeneralSecurityException, PreventedException {
        if (!preAuthenticate(credential)) {
            throw new FailedLoginException();
        }
        return postAuthenticate(credential, doAuthentication(credential));
    }

    /**
     * Performs the details of authentication and returns an authentication handler result on success.
     *
     * @param credential Credential to authenticate.
     * @return Authentication handler result on success.
     * @throws GeneralSecurityException On authentication failure that is thrown out to the caller of
     *                                  {@link #authenticate(Credential)}.
     * @throws PreventedException       On the indeterminate case when authentication is prevented.
     */
    protected abstract AuthenticationHandlerExecutionResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException;

    /**
     * Helper method to construct a handler result
     * on successful authentication events.
     *
     * @param credential the credential on which the authentication was successfully performed.
     *                   Note that this credential instance may be different from what was originally provided
     *                   as transformation of the username may have occurred, if one is in fact defined.
     * @param principal  the resolved principal
     * @param warnings   the warnings
     * @return the constructed handler result
     */
    protected AuthenticationHandlerExecutionResult createHandlerResult(@NonNull final Credential credential,
                                                                       @NonNull final Principal principal,
                                                                       final List<MessageDescriptor> warnings) {
        return new DefaultAuthenticationHandlerExecutionResult(this, new BasicCredentialMetaData(credential), principal, warnings);
    }

    /**
     * Helper method to construct a handler result
     * on successful authentication events.
     *
     * @param credential the credential on which the authentication was successfully performed.
     *                   Note that this credential instance may be different from what was originally provided
     *                   as transformation of the username may have occurred, if one is in fact defined.
     * @param principal  the resolved principal
     * @return the constructed handler result
     */
    protected AuthenticationHandlerExecutionResult createHandlerResult(@NonNull final Credential credential,
                                                                       @NonNull final Principal principal) {
        return new DefaultAuthenticationHandlerExecutionResult(this, new BasicCredentialMetaData(credential),
            principal, new ArrayList<>(0));
    }
}

可以看到主要包含两个方法,doAuthentication(Credential credential)负责根据用户凭据进行认证,认证成功调用createHandlerResult构造HandlerResult对象返回,认证失败抛出异常。

另外可以发现同包下的AbstractUsernamePasswordAuthenticationHandlerdoAuthentication方法主要实现了对用户名、密码的非空检查和passwordEncoder编码等,最后调用了抽象方法authenticateUsernamePasswordInternal,由子类完成具体的校验逻辑。

值得注意的是,authenticateUsernamePasswordInternal中传入的UsernamePasswordCredential是用用户名和密码重新构造的:

final UsernamePasswordCredential userPass = new UsernamePasswordCredential(originalUserPass.getUsername(), originalUserPass.getPassword());

即官方的子类认证处理器只关注用户名和密码,而自定义的认证处理器在处理自定义的用户凭证(自定义的认证字段),应该重写 doAuthentication 方法进行处理

Once the handler is designed, it needs to be registered with CAS and put into the authentication engine. This is done via the magic of classes that are picked up automatically at runtime, per your approval, whose job is to understand how to dynamically modify the application context.@Configuration

自定义认证处理器需要通过@Configuration注册到 cas 认证引擎

package com.example.cas;

@Configuration("MyAuthenticationEventExecutionPlanConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class MyAuthenticationEventExecutionPlanConfiguration
                    implements AuthenticationEventExecutionPlanConfigurer {
    @Autowired
    private CasConfigurationProperties casProperties;

    @Bean
    public AuthenticationHandler myAuthenticationHandler() {
        final MyAuthenticationHandler handler = new MyAuthenticationHandler();
        /*
            Configure the handler by invoking various setter methods.
            Note that you also have full access to the collection of resolved CAS settings.
            Note that each authentication handler may optionally qualify for an 'order`
            as well as a unique name.
        */
        return h;
    }

    @Override
    public void configureAuthenticationExecutionPlan(final AuthenticationEventExecutionPlan plan) {
        if (feelingGoodOnAMondayMorning()) {
            plan.registerAuthenticationHandler(myAuthenticationHandler());
        }
    }
}

创建认证处理器

本例自定义验证码功能仅是在查询数据库认证的基础上加上验证码校验,因此只需扩展api/cas-server-support-jdbc-authentication/org.apereo.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler,在doAuthentication方法中根据 session 中的验证码对用户提交的验证码进行验证,其余验证调用super.doAuthentication(credential)由父类处理即可。

com.chunshu.cas.authentication.CaptchaQueryDatabaseAuthenticationHandler

package com.chunshu.cas.authentication;

import com.chunshu.cas.entity.CaptchaUsernamePasswordCredential;
import com.chunshu.cas.exception.CaptchaErrorException;
import org.apache.commons.lang.StringUtils;
import org.apereo.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler;
import org.apereo.cas.authentication.*;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.sql.DataSource;
import java.security.GeneralSecurityException;
import java.util.Map;

/**
 * 验证码校验,由QueryDatabaseAuthenticationHandler处理数据库用户认证
 */
public class CaptchaQueryDatabaseAuthenticationHandler extends QueryDatabaseAuthenticationHandler {

    public CaptchaQueryDatabaseAuthenticationHandler(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order, DataSource dataSource, String sql, String fieldPassword, String fieldExpired, String fieldDisabled, Map<String, Object> attributes) {
        super(name, servicesManager, principalFactory, order, dataSource, sql, fieldPassword, fieldExpired, fieldDisabled, attributes);
    }

    @Override
    protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException {
        final CaptchaUsernamePasswordCredential customCredential = (CaptchaUsernamePasswordCredential) credential;

        // 用户提交的验证码
        String captcha = customCredential.getCaptcha();
        if (StringUtils.isBlank(captcha)) {
            throw new CaptchaErrorException("captcha is null.");
        }

        // 取出session中的验证码
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String sessionCaptcha = attributes.getRequest().getSession().getAttribute("captcha_code").toString();

        // 验证码校验
        if(!captcha.equalsIgnoreCase(sessionCaptcha)){
            throw new CaptchaErrorException("Sorry, capcha not correct !");
        }

        return super.doAuthentication(credential);
    }

    @Override
    public boolean supports(Credential credential) {
        return credential instanceof CaptchaUsernamePasswordCredential;
    }
}

com.chunshu.cas.exception.CaptchaErrorException参看自定义异常提示

注册认证处理器

AuthenticationHandlerbean 中初始化认证处理器时,将构造方法中order参数设为1,作为最优先的认证处理器

package com.chunshu.cas.config;

import com.chunshu.cas.authentication.CaptchaQueryDatabaseAuthenticationHandler;
import com.google.common.collect.Multimap;
import org.apache.commons.lang.StringUtils;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.CoreAuthenticationUtils;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalFactoryUtils;
import org.apereo.cas.authentication.principal.PrincipalNameTransformerUtils;
import org.apereo.cas.authentication.support.password.PasswordEncoderUtils;
import org.apereo.cas.authentication.support.password.PasswordPolicyConfiguration;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.support.JpaBeans;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apereo.cas.util.CollectionUtils;

/**
 * 注册验证码+数据库认证处理器
 */
@Configuration("CaptchaAuthenticationEventExecutionPlanConfigurer")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CaptchaAuthenticationEventExecutionPlanConfigurer implements AuthenticationEventExecutionPlanConfigurer {

    @Autowired
    private ChunshuQueryJdbcAuthenticationProperties queryProperties;

    @Autowired(required = false)
    @Qualifier("queryPasswordPolicyConfiguration")
    private PasswordPolicyConfiguration queryPasswordPolicyConfiguration;

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

    @Autowired(required = false)
    @Qualifier("queryAndEncodePasswordPolicyConfiguration")
    private PasswordPolicyConfiguration queryAndEncodePasswordPolicyConfiguration;

    @Bean
    public AuthenticationHandler myAuthenticationHandler() {
        final Multimap<String, Object> attributes = CoreAuthenticationUtils.transformPrincipalAttributesListIntoMultiMap(queryProperties.getPrincipalAttributeList());

        // 验证码+数据库认证处理器,顺序为1,最优先执行
        CaptchaQueryDatabaseAuthenticationHandler handler = new CaptchaQueryDatabaseAuthenticationHandler(queryProperties.getName(), servicesManager,
                jdbcPrincipalFactory(), 1,
                JpaBeans.newDataSource(queryProperties), queryProperties.getSql(), queryProperties.getFieldPassword(),
                queryProperties.getFieldExpired(), queryProperties.getFieldDisabled(), CollectionUtils.wrap(attributes));

        handler.setPasswordEncoder(PasswordEncoderUtils.newPasswordEncoder(queryProperties.getPasswordEncoder()));
        handler.setPrincipalNameTransformer(PrincipalNameTransformerUtils.newPrincipalNameTransformer(queryProperties.getPrincipalTransformation()));

        if (queryPasswordPolicyConfiguration != null) {
            handler.setPasswordPolicyConfiguration(queryPasswordPolicyConfiguration);
        }

        handler.setPrincipalNameTransformer(PrincipalNameTransformerUtils.newPrincipalNameTransformer(queryProperties.getPrincipalTransformation()));

        if (StringUtils.isNotBlank(queryProperties.getCredentialCriteria())) {
            handler.setCredentialSelectionPredicate(CoreAuthenticationUtils.newCredentialSelectionPredicate(queryProperties.getCredentialCriteria()));
        }
        return handler;
    }

    @ConditionalOnMissingBean(name = "jdbcPrincipalFactory")
    @Bean
    @RefreshScope
    public PrincipalFactory jdbcPrincipalFactory() {
        return PrincipalFactoryUtils.newPrincipalFactory();
    }

    @Override
    public void configureAuthenticationExecutionPlan(final AuthenticationEventExecutionPlan plan) {
        plan.registerAuthenticationHandler(myAuthenticationHandler());
    }
}

从源码support/cas-server-support-jdbc/org.apereo.cas.adaptors.jdbc.config.CasJdbcAuthenticationConfiguration中可以看到几种数据库认证处理器的注册、初始化方法

@ConditionalOnMissingBean(name = "jdbcAuthenticationHandlers")
@Bean
@RefreshScope
public Collection<AuthenticationHandler> jdbcAuthenticationHandlers() {
    final Collection<AuthenticationHandler> handlers = new HashSet<>();
    final JdbcAuthenticationProperties jdbc = casProperties.getAuthn().getJdbc();

    jdbc.getQuery().forEach(b -> handlers.add(queryDatabaseAuthenticationHandler(b)));
    return handlers;
}

private AuthenticationHandler queryDatabaseAuthenticationHandler(final QueryJdbcAuthenticationProperties b) {
    final Multimap<String, Object> attributes = CoreAuthenticationUtils.transformPrincipalAttributesListIntoMultiMap(b.getPrincipalAttributeList());
    LOGGER.debug("Created and mapped principal attributes [{}] for [{}]...", attributes, b.getUrl());

    final QueryDatabaseAuthenticationHandler h = new QueryDatabaseAuthenticationHandler(b.getName(), servicesManager,
                                                                                        jdbcPrincipalFactory(), b.getOrder(),
                                                                                        JpaBeans.newDataSource(b), b.getSql(), b.getFieldPassword(),
                                                                                        b.getFieldExpired(), b.getFieldDisabled(), CollectionUtils.wrap(attributes));

    h.setPasswordEncoder(PasswordEncoderUtils.newPasswordEncoder(b.getPasswordEncoder()));
    h.setPrincipalNameTransformer(PrincipalNameTransformerUtils.newPrincipalNameTransformer(b.getPrincipalTransformation()));

    if (queryPasswordPolicyConfiguration != null) {
        h.setPasswordPolicyConfiguration(queryPasswordPolicyConfiguration);
    }

    h.setPrincipalNameTransformer(PrincipalNameTransformerUtils.newPrincipalNameTransformer(b.getPrincipalTransformation()));

    if (StringUtils.isNotBlank(b.getCredentialCriteria())) {
        h.setCredentialSelectionPredicate(CoreAuthenticationUtils.newCredentialSelectionPredicate(b.getCredentialCriteria()));
    }

    LOGGER.debug("Created authentication handler [{}] to handle database url at [{}]", h.getName(), b.getUrl());
    return h;
}

@ConditionalOnMissingBean(name = "jdbcAuthenticationEventExecutionPlanConfigurer")
@Bean
public AuthenticationEventExecutionPlanConfigurer jdbcAuthenticationEventExecutionPlanConfigurer() {
    return plan -> jdbcAuthenticationHandlers().forEach(h -> plan.registerAuthenticationHandlerWithPrincipalResolver(h, personDirectoryPrincipalResolver));
}

从源码support/cas-server-support-cassandra-authentication/org.apereo.cas.config.CassandraAuthenticationConfiguration可以看到更单纯的认证处理器的注册方法,包括密码编码器passwordEncoder的设置

package org.apereo.cas.config;

import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.CassandraAuthenticationHandler;
import org.apereo.cas.authentication.CassandraRepository;
import org.apereo.cas.authentication.DefaultCassandraRepository;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalFactoryUtils;
import org.apereo.cas.authentication.principal.PrincipalNameTransformerUtils;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import org.apereo.cas.authentication.support.password.PasswordEncoderUtils;
import org.apereo.cas.cassandra.CassandraSessionFactory;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.model.support.cassandra.authentication.CassandraAuthenticationProperties;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * This is {@link CassandraAuthenticationConfiguration}.
 *
 * @author Misagh Moayyed
 * @author Dmitriy Kopylenko
 * @since 5.2.0
 */
@Configuration("cassandraAuthenticationConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
@Slf4j
public class CassandraAuthenticationConfiguration {

    @Autowired
    @Qualifier("cassandraSessionFactory")
    private CassandraSessionFactory cassandraSessionFactory;

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

    @Autowired
    @Qualifier("personDirectoryPrincipalResolver")
    private PrincipalResolver personDirectoryPrincipalResolver;

    @Autowired
    private CasConfigurationProperties casProperties;

    @Bean
    public PrincipalFactory cassandraPrincipalFactory() {
        return PrincipalFactoryUtils.newPrincipalFactory();
    }

    @Bean
    @RefreshScope
    public CassandraRepository cassandraRepository() {
        final CassandraAuthenticationProperties cassandra = casProperties.getAuthn().getCassandra();
        return new DefaultCassandraRepository(cassandra, cassandraSessionFactory);
    }
    
    @Bean
    public AuthenticationHandler cassandraAuthenticationHandler() {
        final CassandraAuthenticationProperties cassandra = casProperties.getAuthn().getCassandra();
        final CassandraAuthenticationHandler handler = new CassandraAuthenticationHandler(cassandra.getName(), servicesManager,
                cassandraPrincipalFactory(),
                cassandra.getOrder(), cassandra, cassandraRepository());
        handler.setPrincipalNameTransformer(PrincipalNameTransformerUtils.newPrincipalNameTransformer(cassandra.getPrincipalTransformation()));
        handler.setPasswordEncoder(PasswordEncoderUtils.newPasswordEncoder(cassandra.getPasswordEncoder()));
        return handler;
    }

    @ConditionalOnMissingBean(name = "cassandraAuthenticationEventExecutionPlanConfigurer")
    @Bean
    public AuthenticationEventExecutionPlanConfigurer cassandraAuthenticationEventExecutionPlanConfigurer() {
        return plan -> plan.registerAuthenticationHandlerWithPrincipalResolver(cassandraAuthenticationHandler(), personDirectoryPrincipalResolver);
    }
}
自定义查询数据库参数

从源码可以看到QueryDatabaseAuthenticationHandler的构造方法参数基本都来自QueryJdbcAuthenticationProperties,即配置文件cas.authn.jdbc.query[x],因此可以参照它在AuthenticationHandlerbean 中初始化认证处理器。

但我们不可直接使用cas.authn.jdbc.query[x]的参数,因为进行此配置 cas 认为开启默认的查询数据库认证,即相当于开启了两个认证处理器,一个是我们自定义的验证码查询数据库认证CaptchaQueryDatabaseAuthenticationHandler,一个是 cas 的查询数据库认证处理器;而 cas 的认证机制是根据认证处理器的 order 逐个进行认证,只要有其中一个认证成功,即视为登录成功;本例中当验证码校验失败,便会使用 cas 的查询数据库认证处理器进行认证,事实上验证码已经不对认证起作用,因此每一个认证处理器都必须做到逻辑完整严密。

可以参照QueryJdbcAuthenticationProperties创建自定义的查询数据库配置对象,读取自定义的配置

复制源码api/cas-server-core-api-configuration-model/org.apereo.cas.configuration.model.support.jdbc.QueryJdbcAuthenticationProperties,创建 com.chunshu.cas.config.ChunshuQueryJdbcAuthenticationProperties,加上@ConfigurationProperties(prefix = "com.chunshu.cas.query")注解读取自定义的com.chunshu.cas.query配置

package com.chunshu.cas.config;

import org.apereo.cas.configuration.model.core.authentication.PasswordEncoderProperties;
import org.apereo.cas.configuration.model.core.authentication.PrincipalTransformationProperties;
import org.apereo.cas.configuration.model.support.jpa.AbstractJpaProperties;
import org.apereo.cas.configuration.support.RequiredProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

import java.util.ArrayList;
import java.util.List;

/**
 * This is {@link ChunshuQueryJdbcAuthenticationProperties}.
 * 数据库配置信息
 *
 * @author Misagh Moayyed
 * @since 5.2.0
 */
@ConfigurationProperties(prefix = "com.chunshu.cas")
public class ChunshuQueryJdbcAuthenticationProperties extends AbstractJpaProperties {

    private static final long serialVersionUID = 7806132208223986680L;

    /**
     * SQL query to execute. Example: {@code SELECT * FROM table WHERE name=?}.
     */
    @RequiredProperty
    private String sql;

    /**
     * A number of authentication handlers are allowed to determine whether they can operate on the provided credential
     * and as such lend themselves to be tried and tested during the authentication handler selection phase.
     * The credential criteria may be one of the following options:<ul>
     * <li>1) A regular expression pattern that is tested against the credential identifier.</li>
     * <li>2) A fully qualified class name of your own design that implements {@code Predicate<Credential>}.</li>
     * <li>3) Path to an external Groovy script that implements the same interface.</li>
     * </ul>
     */
    private String credentialCriteria;

    /**
     * Password field/column name to retrieve.
     */
    @RequiredProperty
    private String fieldPassword;

    /**
     * Boolean field that should indicate whether the account is expired.
     */
    private String fieldExpired;

    /**
     * Boolean field that should indicate whether the account is disabled.
     */
    private String fieldDisabled;

    /**
     * List of column names to fetch as user attributes.
     */
    private List<String> principalAttributeList = new ArrayList<>();

    /**
     * Principal transformation settings for this authentication.
     */
    @NestedConfigurationProperty
    private PrincipalTransformationProperties principalTransformation = new PrincipalTransformationProperties();

    /**
     * Password encoding strategies for this authentication.
     */
    @NestedConfigurationProperty
    private PasswordEncoderProperties passwordEncoder = new PasswordEncoderProperties();

    /**
     * Name of the authentication handler.
     */
    private String name;

    /**
     * Order of the authentication handler in the chain.
     */
    private int order = Integer.MAX_VALUE;

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

    public String getCredentialCriteria() {
        return credentialCriteria;
    }

    public void setCredentialCriteria(String credentialCriteria) {
        this.credentialCriteria = credentialCriteria;
    }

    public String getFieldPassword() {
        return fieldPassword;
    }

    public void setFieldPassword(String fieldPassword) {
        this.fieldPassword = fieldPassword;
    }

    public String getFieldExpired() {
        return fieldExpired;
    }

    public void setFieldExpired(String fieldExpired) {
        this.fieldExpired = fieldExpired;
    }

    public String getFieldDisabled() {
        return fieldDisabled;
    }

    public void setFieldDisabled(String fieldDisabled) {
        this.fieldDisabled = fieldDisabled;
    }

    public List<String> getPrincipalAttributeList() {
        return principalAttributeList;
    }

    public void setPrincipalAttributeList(List<String> principalAttributeList) {
        this.principalAttributeList = principalAttributeList;
    }

    public PrincipalTransformationProperties getPrincipalTransformation() {
        return principalTransformation;
    }

    public void setPrincipalTransformation(PrincipalTransformationProperties principalTransformation) {
        this.principalTransformation = principalTransformation;
    }

    public PasswordEncoderProperties getPasswordEncoder() {
        return passwordEncoder;
    }

    public void setPasswordEncoder(PasswordEncoderProperties passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }
}

可以不使用lombok.Getter/lombok.Setter,手动创建get/set方法

application.properties中添加自定义配置com.chunshu.cas.query,并注释cas.authn.jdbc.query[x]配置以关闭 cas 的查询数据库认证处理器

#cas.authn.jdbc.query[0].sql=select * from t_user where phone=?
#cas.authn.jdbc.query[0].fieldPassword=password
#cas.authn.jdbc.query[0].url=jdbc:mysql://192.168.3.88:3306/sjzt_eflow?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
#cas.authn.jdbc.query[0].user=root
#cas.authn.jdbc.query[0].password=password
#cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
#cas.authn.jdbc.query[0].passwordEncoder.type=EFlowPasswordEncoder
#cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8

com.chunshu.cas.query.sql=select * from t_user where phone=?
com.chunshu.cas.query.fieldPassword=password
com.chunshu.cas.query.url=jdbc:mysql://192.168.3.88:3306/sjzt_eflow?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
com.chunshu.cas.query.user=root
com.chunshu.cas.query.password=password
com.chunshu.cas.query.driverClass=com.mysql.jdbc.Driver
com.chunshu.cas.query.passwordEncoder.type=com.chunshu.cas.passwordencoder.EFlowPasswordEncoder
com.chunshu.cas.query.passwordEncoder.characterEncoding=UTF-8

src/main/resources/META-INF/spring.factories里将认证处理器注册配置和配置对象注入 spring boot

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.apereo.cas.config.CasEmbeddedContainerTomcatConfiguration,\
  org.apereo.cas.config.CasEmbeddedContainerTomcatFiltersConfiguration,\
  com.chunshu.cas.config.CaptchaAuthenticationEventExecutionPlanConfigurer,\
  com.chunshu.cas.config.ChunshuQueryJdbcAuthenticationProperties

自定义异常提示

创建验证码校验错误异常 com.chunshu.cas.exception.CaptchaErrorException

package com.chunshu.cas.exception;

import org.apereo.cas.authentication.AuthenticationException;

/**
 * @author Anumbrella
 */
public class CaptchaErrorException extends AuthenticationException {

    public CaptchaErrorException(){
        super();
    }

    public CaptchaErrorException(String msg) {
        super(msg);
    }
}

application.properties中添加自定义的异常类型

cas.authn.exceptions.exceptions=com.chunshu.cas.exception.CaptchaErrorException

拷贝target/cas/war/WEB-INF/classes/messages_zh_CN.propertiessrc/main/resources下,添加异常提示

# 用户名或密码不正确
authenticationFailure.AccountNotFoundException=\u7528\u6237\u540d\u6216\u5bc6\u7801\u4e0d\u6b63\u786e
authenticationFailure.FailedLoginException=\u7528\u6237\u540d\u6216\u5bc6\u7801\u4e0d\u6b63\u786e
authenticationFailure.UNKNOWN=\u7528\u6237\u540d\u6216\u5bc6\u7801\u4e0d\u6b63\u786e
# 验证码不正确
authenticationFailure.CaptchaErrorException=\u9a8c\u8bc1\u7801\u4e0d\u6b63\u786e

由于webflow的视图绑定配置了验证码必填,所以验证码的非空校验在webflow完成

# 请输入xxx
username.required=\u8bf7\u8f93\u5165\u7528\u6237\u540d\u0020
password.required=\u8bf7\u8f93\u5165\u5bc6\u7801\u0020
captcha.required=\u8bf7\u8f93\u5165\u9a8c\u8bc1\u7801\u0020

完整配置

得益于Overlay特性,cas 可以很方便的扩展源码类、借助spring.factories@configuration/@Bean/@ConfigurationProperties/@Controller注解增加配置和控制器、使用源码的工具类等,只需在 pom.xml 引入相关的 cas 包即可。

pom.xml

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-jdbc</artifactId>
    <version>${cas.version}</version>
</dependency>

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-jdbc-drivers</artifactId>
    <version>${cas.version}</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.5.3</version>
</dependency>

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-core-webflow</artifactId>
    <version>${cas.version}</version>
</dependency>

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-core-webflow-api</artifactId>
    <version>${cas.version}</version>
</dependency>

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-core-util-api</artifactId>
    <version>${cas.version}</version>
</dependency>

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

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-jdbc-authentication</artifactId>
    <version>${cas.version}</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.apereo.cas.config.CasEmbeddedContainerTomcatConfiguration,\
  org.apereo.cas.config.CasEmbeddedContainerTomcatFiltersConfiguration,\
  com.chunshu.cas.config.CaptchaAuthenticationEventExecutionPlanConfigurer,\
  com.chunshu.cas.config.CaptchaCasWebflowExecutionPlanConfigurer,\
  com.chunshu.cas.config.CustomControllerConfigurer,\
  com.chunshu.cas.config.ChunshuQueryJdbcAuthenticationProperties