认证

authentication

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

1. 概述

2. 认证策略

在整个认证链中,会有很多个认证。

英文名称名称说明
All全部认证通过后才可以例如必须在登录后,然后输入支付密码才通过。
Any其中一个通过后,就可以最常用的登录方法,密码登录、短信登录、二维码登录
Global通过发放的验证票进行认证与 JWT 的 Token 模式类似
Groovy自定义 Groovy 脚本来认证
Not Prevented没有出现 PreventedException
Required只有指定的身份认证通过后才成功
Rest访问一个 Rest 接口,进行认证
Source Selection根据凭证源来选择认证策略例如 Spring 中的 Token 类型
Unique Principal防止多次登录开启后会增加系统负担

3. 默认认证

可以修改application.yml来改变默认的登录用户名与密码

cas:
authn:
accept:
users: user::aaa

这种认证方法是不被推荐的

4. 数据库认证

CAS 默认提供 4 中方法

  • 密码匹配
  • 用户名与密码
  • 用数据库的用户名和密码进行认证
  • 私有盐辅助认证

4.1 准备数据

# 可以登录msyql中创建一个数据库cas
cd /home/fan/01-java/wukong-framework/wukong-mall/ref/docker/basic
# 登录到mysql
docker-compose exec mysql mysql -uroot -prootmysql
# 登录到容器
docker-compose exec mysql /bin/bash

参考了 keycloak 创建的表

DROP TABLE IF EXISTS USER_ENTITY;
create table USER_ENTITY
(
USER_ID bigint unsigned auto_increment comment '用户ID' ,
EMAIL varchar(255) comment '邮箱' ,
EMAIL_VERIFIED boolean default false not null comment '邮箱是否认证通过' ,
ENABLED boolean default false not null comment '是否可用',
FIRST_NAME varchar(255) comment '姓氏:现在没有用',
LAST_NAME varchar(255) comment '名称:现在没有用',
REALM_ID varchar(255) comment '领域编号:现在没有用',
USERNAME varchar(255) comment '用户名',
mobile_phone varchar(30) comment '手机号',
EMAIL_VERIFIED_VERIFIED boolean default false not null comment '手机号是否认证通过',
gmt_create datetime default CURRENT_TIMESTAMP comment '记录创建时间',
gmt_modified datetime default CURRENT_TIMESTAMP comment '记录修改时间',
PRIMARY KEY (USER_ID),
-- 今后启用了realm_id后,再加唯一索引
UNIQUE KEY `uq_USER_ENTITY_USERNAME` (USERNAME,REALM_ID),
UNIQUE KEY `uq_USER_ENTITY_EMAIL` (EMAIL),
UNIQUE KEY `uq_USER_ENTITY_mobile_phone` (mobile_phone)
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8 COMMENT='用户表';
-- +++++++++++++++++++++++++++++++++++++++++++++++++++
DROP TABLE IF EXISTS CREDENTIAL;
create table CREDENTIAL
(
CREDENTIAL_ID bigint unsigned auto_increment comment '凭证ID' ,
USER_ID bigint unsigned comment '用户ID' ,
TYPE varchar(255) comment '类型:password,' ,
USER_LABEL varchar(255) comment '用户标签' ,
SECRET_DATA varchar(512) comment '密码,这点与keycloak不一样' ,
PRIORITY int comment '优先级' ,
gmt_create datetime default CURRENT_TIMESTAMP comment '记录创建时间',
gmt_modified datetime default CURRENT_TIMESTAMP comment '记录修改时间',
PRIMARY KEY (CREDENTIAL_ID)
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8 COMMENT='密码表';
create index IDX_USER_CREDENTIAL
on CREDENTIAL (USER_ID);

添加模拟数据

delete from USER_ENTITY where USER_ID=10000;
delete from CREDENTIAL where USER_ID=10000;
INSERT INTO USER_ENTITY (USER_ID,USERNAME, ENABLED) VALUES (10000, 'user', 1);
INSERT INTO CREDENTIAL (CREDENTIAL_ID, USER_ID, TYPE, SECRET_DATA) VALUES (10000, 10000, 'password', md5('aaa'));
INSERT INTO USER_ENTITY (USER_ID,USERNAME, ENABLED) VALUES (10001, 'xiaoyu', 1);
INSERT INTO CREDENTIAL (CREDENTIAL_ID, USER_ID, TYPE, SECRET_DATA) VALUES (10001, 10001, 'password', md5('aaa'));

4.2 配置 Gradle

主要是为了配置 Mysql 的驱动。具体做法如下

  • 在 gradle 目录下建立一个文件:my.gradle
  • 在根目录下的build.gradle文件中的适当位置添加
    • apply from: rootProject.file("gradle/my.gradle")

下面是my.gradle文件的内容

dependencies {
implementation "org.apereo.cas:cas-server-support-jdbc"
testImplementation group: 'junit', name: 'junit', version: '4.12'
}

网上有网友说要添加下面依赖是不对的。

// 这两个依赖不用添加,网上的例子有问题。当然加了也不会出现错误。
implementation "org.apereo.cas:cas-server-support-jdbc-drivers"
implementation "mysql:mysql-connector-java"

4.3 配置 application.yml

主要掉原先的内容

cas:
authn:
# 屏蔽默认的不能访问
accept:
enabled: false
jdbc:
query:
- driverClass: com.mysql.cj.jdbc.Driver
user: root
password: rootmysql
url: jdbc:mysql://localhost:33061/cas?useSSL=false&serverTimezone=Asia/Shanghai
fieldPassword: SECRET_DATA
passwordEncoder:
characterEncoding: UTF-8
encodingAlgorithm: 'MD5'
type: DEFAULT
sql: select b.SECRET_DATA from USER_ENTITY a, CREDENTIAL b where a.USER_ID=b.USER_ID and b.TYPE='password' and a.ENABLED=1 and a.USERNAME=?

4.4 测试效果

  • 第一步:执行./gradlew debug
  • 第二步:在 idea 建立一个remote,并点击 debug
  • 第三步:打开浏览器:https://localhost:8443/cas/login
    • 输入 user 密码 aaa
    • 或者 xiaoyu 密码 aaa

4.5 配置加密方法

官方文档中有说明,但是将的不细致,其中:typeencodingAlgorithm 的关系不明确。

如果了解加密的各种算法,就理解。这里必须指定 type,不同 type 下有不同的算法。

如果想了解详细的内容,可以看PasswordEncoderUtils源代码。

① 明文

passwordEncoder:
#encodingAlgorithm: 'MD5'
type: NONE

然后在数据库中输入明文的密码,就可以做实验了。

② PBKDF2

这个是 Spring 中的一个加密类型,但是不是 Spring 默认的,这个加密类型有三种算法,如果不指定,就出现错误。

passwordEncoder:
encodingAlgorithm: PBKDF2WithHmacSHA256
type: NONE

然后在数据库中输入密码ac35781caf7bba3a9373267c7e3cd69164209097d5bbb34577301b7f81cdb4a0be4dd633245c001b

然后在登录窗口使用123456进行登录。

③ BCRYPT

这个可以 Sping 默认的,这种算法的加密比较单一,所以可以不用设置encodingAlgorithm

passwordEncoder:
#encodingAlgorithm: 'MD5'
type: BCRYPT

然后在数据库中输入密码$2a$10$XJJqb2pPHD6EmJ6.pUuG5eYts8Kiyjrgh7NgZgMxnkXOb6sKpmaRC

然后在登录窗口使用aaa进行登录。

④ 自定义算法

Spring 是可以通过DelegatingPasswordEncoder来实现多种密码模式的。但是这个函数没有不带参数的构造函数,所以不能在 CAS 的配置文件中设置,那么可以自定义一个 Java 类

passwordEncoder:
#encodingAlgorithm: 'MD5'
type: org.apereo.cas.wukong.crypto.password.MyEncoder

然后在数据库中输入下面不同的密码:

  • {bcrypt}$2a$10$XJJqb2pPHD6EmJ6.pUuG5eYts8Kiyjrgh7NgZgMxnkXOb6sKpmaRC
  • {noop}aaa

然后在登录窗口使用aaa进行登录。作为一个专业的加密工具,可以推荐使用这个方法。下面给出具体的代码

/**
* 可以支持多种加密模式,从Spring中改造过来
*/
public class MyEncoder implements PasswordEncoder {
private PasswordEncoder passwordEncoder;
public MyEncoder(){
passwordEncoder= PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
public String encode(CharSequence rawPassword) {
return passwordEncoder.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword,encodedPassword);
}
@Override
public boolean upgradeEncoding(String prefixEncodedPassword) {
return passwordEncoder.upgradeEncoding(prefixEncodedPassword);
}
}

⑤ groovy

一个新的做法,还没用过,可以尝试一下。

如何执行 groovy 代码呢,查一下源代码

if (type.endsWith(".groovy")) {
LOGGER.trace("Creating Groovy-based password encoder at [{}]", type);
val resource = applicationContext.getResource(type);
return new GroovyPasswordEncoder(resource, applicationContext);
}

这里定义了一个 groovy 文件,发现 idea 很好用,可以在任意地方设置断点。

package org.apereo.cas.wukong.crypto.password
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
byte[] run(final Object... args) {
String rawPassword = args[0]
def generatedSalt = args[1]
def logger = args[2]
def casApplicationContext = args[3]
logger.debug("Encoding password...")
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
String pbk1 = pbkdf2PasswordEncoder.encode(rawPassword);
return pbk1.getBytes();
}
Boolean matches(final Object... args) {
def rawPassword = args[0]
def encodedPassword = args[1]
def logger = args[2]
def casApplicationContext = args[3]
logger.debug(rawPassword);
logger.debug("Does match or not ?");
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
boolean ren =pbkdf2PasswordEncoder.matches(rawPassword,encodedPassword)
return ren
}

yml 文件中这么来配置

passwordEncoder:
#encodingAlgorithm: 'MD5'
type: file:///home/fan/01-java/cas/cas-overelay/src/main/java/org/apereo/cas/wukong/crypto/password/Script.groovy

这里使用PBKDF2的密码算法,可以在上面代码中,找到一个加密的代码,然后实验一下就可以了。

但是总感觉这样不安全。

当然路径也可以放在 resource 文件下,例如

classpath:/resources/script/Script.groovy

5. 黑白名单

6.自定义身份验证策略

总体任务可以分为以下几类:

  1. 设计身份验证程序。
  2. 向 CAS 身份验证引擎注册该程序。
  3. 让 CAS 识别认证配置。

这里可以参考一下:QueryDatabaseAuthenticationHandler

https://blog.csdn.net/anumbrella/category_7765386.html

6.1 添加引用

implementation "org.apereo.cas:cas-server-core-authentication-api"

6.2 定义身份验证程序

这里继承了一个类AbstractUsernamePasswordAuthenticationHandler,当然也可以继承其他不用实现类。

这个类有两个重要方法

  • 构造函数:可以添加一些自定义的处理类,例如 UserDetailService
  • AuthenticationHandlerExecutionResult
public class MyAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler{
private UserDetailService userDetailService;
protected MyAuthenticationHandler(String name
, ServicesManager servicesManager
, PrincipalFactory principalFactory
, Integer order
, UserDetailService userDetailService
) {
super(name, servicesManager, principalFactory, order);
this.userDetailService=userDetailService;
}
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
UsernamePasswordCredential credential
, String originalPassword) throws GeneralSecurityException, PreventedException {
String username=credential.getUsername();
String password=credential.getPassword();
LOGGER.debug(username+":"+password);
LOGGER.debug(originalPassword);
if(userDetailService.loadByUsername(username)){
//可自定义返回给客户端的多个属性信息
HashMap<String, List<Object>> returnInfo = new HashMap<>();
List<Object> theseElements = Lists.newArrayList("alpha", "beta", "gamma");
returnInfo.put("theseElements", theseElements);
final List<MessageDescriptor> list = new ArrayList<>();
return createHandlerResult(credential,
this.principalFactory.createPrincipal(username, returnInfo), list);
}
if(username.equals("user")){
throw new PreventedException("a throws");
}
throw new FailedLoginException(username + " my customer exception");
}
}

后面演示了如果抛出异常:PreventedExceptionFailedLoginException

6.3 注册验证程序

定义一个类,用来注册认证程序

@Configuration("MyAuthenticationEventExecutionPlanConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class MyAuthenticationEventExecutionPlanConfiguration
implements AuthenticationEventExecutionPlanConfigurer {
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
@Bean
public AuthenticationHandler myAuthenticationHandler() {
var handler = new MyAuthenticationHandler(
"MyAuthenticationEventExecutionPlanConfiguration"
,servicesManager
,new DefaultPrincipalFactory()
,1
,new UserDetailService()
);
return handler;
}
@Override
public void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) throws Exception {
plan.registerAuthenticationHandler(myAuthenticationHandler());
}
}

6.4 添加 spring.factories

可以添加多个

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.apereo.cas.config.CasOverlayOverrideConfiguration,org.apereo.cas.wukong.authentication.MyAuthenticationEventExecutionPlanConfiguration

6.5 验证

https://localhost:8443/cas

输入用户名xiaoyu,密码随便输入。

6.6 多个认证系统

系统中默认有两个认证:

  • 一个是在 yml 中配置的,实际调用的是 QueryDatabaseAuthenticationHandler
  • 另外一个是自定义的 MyAuthenticationHandler。

系统中只要有一个认证通过,就可以通过。

6.7 思考

  • 如何自定义 credential
  • AbstractPreAndPostProcessingAuthenticationHandler 的作用

17. 优化

在调试的过程中发现没有输出 Sql 语句,并且启动项目时,会有一些警告。

WARN [org.apereo.cas.config.CasCoreServicesConfiguration] - <Runtime memory is used as the persistence storage for retrieving and persisting service definitions. Changes that are made to service definitions during runtime WILL be LOST when the CAS server is restarted. Ideally for production, you should choose a storage option (JSON, JDBC, MongoDb, etc) to track service definitions.>
WARN [org.apereo.cas.config.CasCoreTicketsConfiguration] - <Runtime memory is used as the persistence storage for retrieving and managing tickets. Tickets that are issued during runtime will be LOST when the web server is restarted. This MAY impact SSO functionality.>
WARN [org.apereo.cas.util.cipher.BaseStringCipherExecutor] - <Secret key for encryption is not defined for [Ticket-granting Cookie]; CAS will attempt to auto-generate the encryption key>
WARN [org.apereo.cas.util.cipher.BaseStringCipherExecutor] - <Generated encryption key [Pnc9yJ_UTLrzYg7cdU2zIcSWGtPtZObSIQiLjzunswE] of size [256] for [Ticket-granting Cookie]. The generated key MUST be added to CAS settings under setting [cas.tgc.crypto.encryption.key].>
WARN [org.apereo.cas.util.cipher.BaseStringCipherExecutor] - <Secret key for signing is not defined for [Ticket-granting Cookie]. CAS will attempt to auto-generate the signing key>
WARN [org.apereo.cas.util.cipher.BaseStringCipherExecutor] - <Generated signing key [1Zf8v3M_oMRXc-vtoaZaSjzeRscYS1XiQClrJk37wPoi9hqQEJCcnY-N45TW2dCJln9AW_Eb9yKes7grxOW5jQ] of size [512] for [Ticket-granting Cookie]. The generated key MUST be added to CAS settings under setting [cas.tgc.crypto.signing.key].>
WARN [org.apereo.cas.util.cipher.BaseBinaryCipherExecutor] - <Secret key for signing is not defined under [cas.webflow.crypto.signing.key]. CAS will attempt to auto-generate the signing key>
WARN [org.apereo.cas.util.cipher.BaseBinaryCipherExecutor] - <Generated signing key [eW6CTqtoP03byXomSgZ6HbV3-50H2ObjQfdZ9AwtamryQPo4A8iYzyH2J5yCJmEvhCh-vLQErVGEMxiPKjZI4Q] of size [512]. The generated key MUST be added to CAS settings under setting [cas.webflow.crypto.signing.key].>
WARN [org.apereo.cas.util.cipher.BaseBinaryCipherExecutor] - <Secret key for encryption is not defined under [cas.webflow.crypto.encryption.key]. CAS will attempt to auto-generate the encryption key>
WARN [org.apereo.cas.util.cipher.BaseBinaryCipherExecutor] - <Generated encryption key [62ZPauktxRKUog0vP76QtQ] of size [16]. The generated key MUST be added to CAS settings under setting [cas.webflow.crypto.encryption.key].>