如何进行用户身份认证
在第四章节中已经分析了如何输入请求地址跳转到登录页面的流程,下面就要进行登录页面输入用户信息进行用户认证的具体流程。
1. 行为状态realSubmit
*login-webflow.xml:*
<action-state id="realSubmit">
<evaluate
expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
<transition on="warn" to="warn"/>
<!--
To enable AUP workflows, replace the 'success' transition with the following:
<transition on="success" to="acceptableUsagePolicyCheck" />
-->
<transition on="success" to="sendTicketGrantingTicket"/>
<transition on="successWithWarnings" to="showMessages"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="initializeLogin"/>
</action-state>
前端Form表单提交了用户信息后,就到realSubmit行为状态中,在此行为中先执行表达式authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)。
2. 用户身份认证authenticationViaFormAction.submit()
用户身份认证AuthenticationViaFormAction此类在cas-server-webapp-actions模块下的org.jasig.cas.web.flow包下。用于认证用户表单和获取TGT为用户认证信息。
*AuthenticationViaFormAction.java*
@Component("authenticationViaFormAction")
public class AuthenticationViaFormAction {
public final Event submit(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
//第一次进行认证时,条件为false,不进行st的创建。
if (isRequestAskingForServiceTicket(context)) {
return grantServiceTicket(context, credential);
}
//创建TGT
return createTicketGrantingTicket(context, credential, messageContext);
}
protected boolean isRequestAskingForServiceTicket(final RequestContext context) {
//由于是第一次登录,因此TGT为空
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
//直接访问CAS单点登录服务端,没有service参数
final Service service = WebUtils.getService(context);
return (StringUtils.isNotBlank(context.getRequestParameters().get(CasProtocolConstants.PARAMETER_RENEW))
&& ticketGrantingTicketId != null
&& service != null);
}
…………
在本例中由于是第一次登录认证,而且没有service,因此执行createTicketGrantingTicket。执行成功后转向《transition on=“success” to=“sendTicketGrantingTicket”/》。
3. AuthenticationViaFormAction中createTicketGrantingTicket()方法
*AuthenticationViaFormAction.java*
protected Event createTicketGrantingTicket(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
try {
final Service service = WebUtils.getService(context);
final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder( this.authenticationSystemSupport.getPrincipalElectionStrategy());
//从form表单提交信息封装认证信息
final AuthenticationTransaction transaction =
AuthenticationTransaction.wrap(credential);
//进行登录的用户登录名和密码的校验,通过下面方法进行this.authenticationSystemSupport.getAuthenticationTransactionManager().handle(transaction, builder);
final AuthenticationContext authenticationContext = builder.build(service);
//认证成功后进行TGT的创建工作
final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationContext);
WebUtils.putTicketGrantingTicketInScopes(context, tgt);
WebUtils.putWarnCookieIfRequestParameterPresent(this.warnCookieGenerator, context);
putPublicWorkstationToFlowIfRequestParameterPresent(context);
//如果只是进行服务端代码分析,此处不进行警告
if (addWarningMessagesToMessageContextIfNeeded(tgt, messageContext)) {
return newEvent(SUCCESS_WITH_WARNINGS);
}
return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_SUCCESS);
} catch (final AuthenticationException e) {
logger.debug(e.getMessage(), e);
return newEvent(AUTHENTICATION_FAILURE, e);
} catch (final Exception e) {
logger.debug(e.getMessage(), e);
return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e);
}
}
上面代码分析中在生成tgt之前会对用户请求的信息进行认证和具体的校验,我们将在下面的进行具体的用户名和密码的校验代码分析。
由于根据service地址(http://localhost:8088/cas-client/ )创建认证上下文authenticationContext,但是在此用例中我们只登录CAS单点登录服务端,因此service参数为空。然后将它作为参数,调用创建TGT的接口。执行完成后返回成功事件(AbstractCasWebflowConfigurer.TRANSITION_ID_SUCCESS)。final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationContext)具体的代码分析将会在下文中具体展开。
4. PolicyBasedAuthenticationManager中authenticate ()方法
在上面的解析中没有细化说明是如何进行用户的身份认证的,下面我们在此展开相关的用户密码的认证过程。PolicyBasedAuthenticationManage此类在cas-server-core-authentication模块下的org.jasig.cas.authentication包中。
this.authenticationSystemSupport.getAuthenticationTransactionManager()此处获取到的为默认的DefaultAuthenticationTransactionManager。通过默认的AuthenticationTransactionManager中的handle方法进行处理认证过程
*DefaultAuthenticationTransactionManager.java*
@Autowired
@Qualifier("authenticationManager")
private AuthenticationManager authenticationManager;
@Override
public AuthenticationTransactionManager handle(final AuthenticationTransaction authenticationTransaction,
final AuthenticationContextBuilder authenticationContext)
throws AuthenticationException {
if (!authenticationTransaction.getCredentials().isEmpty()) {
//认证登录用户信息
final Authentication authentication = this.authenticationManager.authenticate(authenticationTransaction);
LOGGER.debug("Successful authentication; Collecting authentication result [{}]", authentication);
authenticationContext.collect(authentication);
}
LOGGER.debug("Transaction ignored since there are no credentials to authenticate");
return this;
}
在此类中,通过authenticationManager中的方法进行登录信息认证,下面我们看在这个类中是如何进行认证的。先确认authenticationManager这个类所注入的是哪个具体的实现类,通过代码分析可知在认证过程中使用的是PolicyBasedAuthenticationManager这个类来进行具体的认证。
*PolicyBasedAuthenticationManager.java*
@Component("authenticationManager")
public class PolicyBasedAuthenticationManager implements AuthenticationManager {
/** Map of authentication handlers to resolvers to be used when handler does not resolve a principal. */
@Resource(name="authenticationHandlersResolvers")
private Map<AuthenticationHandler, PrincipalResolver> handlerResolverMap;
public Authentication authenticate(final AuthenticationTransaction transaction) throws AuthenticationException {
//进行用户信息认证,并返回认证信息
final AuthenticationBuilder builder = authenticateInternal(transaction.getCredentials());
//获取返回的认证信息
final Authentication authentication = builder.build();
final Principal principal = authentication.getPrincipal();
if (principal instanceof NullPrincipal) {
throw new UnresolvedPrincipalException(authentication);
}
addAuthenticationMethodAttribute(builder, authentication);
logger.info("Authenticated {} with credentials {}.", principal, transaction.getCredentials());
logger.debug("Attribute map for {}: {}", principal.getId(), principal.getAttributes());
populateAuthenticationMetadataAttributes(builder, transaction.getCredentials());
return builder.build();
}
protected AuthenticationBuilder authenticateInternal(final Collection<Credential> credentials)
throws AuthenticationException {
final AuthenticationBuilder builder = new DefaultAuthenticationBuilder(NullPrincipal.getInstance());
for (final Credential c : credentials) {
builder.addCredential(new BasicCredentialMetaData(c));
}
boolean found;
for (final Credential credential : credentials) {
found = false;
//通过handlerResolverMap中获取到对应的认证处理handler和认证对象解决类
for (final Map.Entry<AuthenticationHandler, PrincipalResolver> entry : this.handlerResolverMap.entrySet()) {
final AuthenticationHandler handler = entry.getKey();
//判断认证处理handler是否支持这个认证对象
if (handler.supports(credential)) {
found = true;
try {
//认证用户信息并获取到用户相关的扩展属性信息
authenticateAndResolvePrincipal(builder, credential, entry.getValue(), handler);
//判断认证结果是否满足认证策略,满足就直接返回
if (this.authenticationPolicy.isSatisfiedBy(builder.build())) {
return builder;
}
} catch (final GeneralSecurityException e) {
logger.info("{} failed authenticating {}", handler.getName(), credential);
logger.debug("{} exception details: {}", handler.getName(), e.getMessage());
builder.addFailure(handler.getName(), e.getClass());
} catch (final PreventedException e) {
logger.error("{}: {} (Details: {})", handler.getName(), e.getMessage(), e.getCause().getMessage());
builder.addFailure(handler.getName(), e.getClass());
}
}
}
if (!found) {
logger.warn(
"Cannot find authentication handler that supports [{}] of type [{}], which suggests a configuration problem.",
credential, credential.getClass().getSimpleName());
}
}
//判断认证结果和是否满足认证策略
evaluateProducedAuthenticationContext(builder);
return builder;
}
…………
private void authenticateAndResolvePrincipal(final AuthenticationBuilder builder, final Credential credential,
final PrincipalResolver resolver, final AuthenticationHandler handler)
throws GeneralSecurityException, PreventedException {
Principal principal;
//通过handler进行用户登录信息认证
final HandlerResult result = handler.authenticate(credential);
//认证成功后把结果放到AuthenticationBuilder中
builder.addSuccess(handler.getName(), result);
logger.info("{} successfully authenticated {}", handler.getName(), credential);
if (resolver == null) {
principal = result.getPrincipal();
logger.debug(
"No resolver configured for {}. Falling back to handler principal {}",
handler.getName(),
principal);
} else {
//如果resolver不为空,通过resolver获取认证用户的相关扩展属性信息
principal = resolvePrincipal(handler.getName(), resolver, credential);
if (principal == null) {
logger.warn("Principal resolution handled by {} produced a null principal. "
+ "This is likely due to misconfiguration or missing attributes; CAS will attempt to use the principal "
+ "produced by the authentication handler, if any.", resolver.getClass().getSimpleName());
principal = result.getPrincipal();
}
}
// Must avoid null principal since AuthenticationBuilder/ImmutableAuthentication
// require principal to be non-null
if (principal != null) {
builder.setPrincipal(principal);
}
logger.debug("Final principal resolved for this authentication event is {}", principal);
}
}
authenticateInternal()方法是具体的进行用户信息认证的过程,在此方法中通过循环handlerResolverMap中对应的处理类进行用户信息的认证。最终进行认证和获取用户扩展属性是在authenticateAndResolvePrincipal()方法中实现的,那么这个方法中是如何进行认证和获取扩展属性的。具体的handler如何处理登录请求信息,我们将会在下一章节进行说明。
5. deployerConfigContext.xml配置文件解析
在PolicyBasedAuthenticationManager类中可以看到如下的代码。从注释中可以看到这个map是用于认证用户信息。
/** Map of authentication handlers to resolvers to be used when handler does not resolve a principal. */
@Resource(name="authenticationHandlersResolvers")
private Map<AuthenticationHandler, PrincipalResolver> handlerResolverMap;
上面的资源我们需要在配置文件deployerConfigContext.xml中找到对应处理的handler。
*deployerConfigContext.xml:*
<util:map id=" authenticationHandlersResolvers ">
<entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
<entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
</util:map>
…………
<alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />
从上面的配置中我们可以看到authenticationHandlersResolvers这个配置了两个对应的handler和resolver。在本例中我们是通过前端浏览器输入用户名和密码来进行用户认证的,所以我们将重点分析primaryAuthenticationHandler这个处理器因为它是进行用户信息认证的核心所在。同时此handler对应的resolver将对用户的扩展属性进行维护,并返回给单点登录的客户端(后续我们将在单点登录客户端的代码中进行具体分析)。
6. AcceptUsersAuthenticationHandler中的认证用户authenticateUsernamePasswordInternal ()方法
AcceptUsersAuthenticationHandler为默认实现类,通过配置文件获取配置的用户信息,进行用户的校验。此类在cas-server-core-authentication模块下的org.jasig.cas.authentication包中。
*AcceptUsersAuthenticationHandler.java*
@Component("acceptUsersAuthenticationHandler")
public class AcceptUsersAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler {
/** The default separator in the file. */
private static final String DEFAULT_SEPARATOR = "::";
private static final Pattern USERS_PASSWORDS_SPLITTER_PATTERN = Pattern.compile(DEFAULT_SEPARATOR);
/** The list of users we will accept. */
private Map<String, String> users;
@Value("${accept.authn.users:}")
private String acceptedUsers;
//通过Spring的配置文件获取到配置的用户信息,默认的例子中直接读取配置文件中的用户名和密码
@PostConstruct
public void init() {
if (StringUtils.isNotBlank(this.acceptedUsers) && this.users == null) {
final Set<String> usersPasswords = org.springframework.util.StringUtils.commaDelimitedListToSet(this.acceptedUsers);
final Map<String, String> parsedUsers = new HashMap<>();
for (final String usersPassword : usersPasswords) {
final String[] splitArray = USERS_PASSWORDS_SPLITTER_PATTERN.split(usersPassword);
parsedUsers.put(splitArray[0], splitArray[1]);
}
setUsers(parsedUsers);
}
}
@Override
protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)
throws GeneralSecurityException, PreventedException {
if (users == null || users.isEmpty()) {
throw new FailedLoginException("No user can be accepted because none is defined");
}
//获取前端提交的用户名
final String username = credential.getUsername();
//根据前端提交的用户名获取到配置文件中初始化用户对象中是否有对应的密码
final String cachedPassword = this.users.get(username);
if (cachedPassword == null) {
logger.debug("{} was not found in the map.", username);
throw new AccountNotFoundException(username + " not found in backing map.");
}
//判断前端输入密码和通过配置文件配置的密码是否一致,进行密码的校验
//默认情况下,直接采用明文密码比较,可进行扩展
final String encodedPassword = this.getPasswordEncoder().encode(credential.getPassword());
if (!cachedPassword.equals(encodedPassword)) {
throw new FailedLoginException();
}
//认证成功返回结果,仍然会把表单提交的对象进行封装进行返回
return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
}
…………
}
在用户认证的handler中,最终返回的带有用户登录名的认证信息,如果想要返回更多的用户属性就要通过resolver来进行相关的处理。
7. PersonDirectoryPrincipalResolver中的获取用户扩展属性信息resolve()方法
在解析deployerConfigContext.xml中我们已经知道默认使用的是PersonDirectoryPrincipalResolve这个类来转换用户的扩展属性,此类在cas-server-core-authentication模块下的org.jasig.cas.authentication.principal包下。
*PersonDirectoryPrincipalResolve.java*
@Component("personDirectoryPrincipalResolver")
public class PersonDirectoryPrincipalResolver implements PrincipalResolver {
/**
* Repository of principal attributes to be retrieved.
*/
@NotNull
protected IPersonAttributeDao attributeRepository = new StubPersonAttributeDao(new HashMap<String, List<Object>>());
/**
* Factory to create the principal type.
**/
@NotNull
protected PrincipalFactory principalFactory = new DefaultPrincipalFactory();
/**
* return null if no attributes are found.
*/
@Value("${cas.principal.resolver.persondir.return.null:false}")
protected boolean returnNullIfNoAttributes;
@Override
public Principal resolve(final Credential credential) {
logger.debug("Attempting to resolve a principal...");
//默认情况下获取到的为登录用户名
final String principalId = extractPrincipalId(credential);
if (principalId == null) {
logger.debug("Got null for extracted principal ID; returning null.");
return null;
}
logger.debug("Creating SimplePrincipal for [{}]", principalId);
//根据登录用户名和认证信息获取到扩展属性
final Map<String, List<Object>> attributes = retrievePersonAttributes(principalId, credential);
if (attributes == null || attributes.isEmpty()) {
logger.debug("Principal id [{}] did not specify any attributes", principalId);
if (!this.returnNullIfNoAttributes) {
logger.debug("Returning the principal with id [{}] without any attributes", principalId);
return this.principalFactory.createPrincipal(principalId);
}
logger.debug("[{}] is configured to return null if no attributes are found for [{}]",
this.getClass().getName(), principalId);
return null;
}
logger.debug("Retrieved [{}] attribute(s) from the repository", attributes.size());
//转换用户扩展属性到认证结果中
final Pair<String, Map<String, Object>> pair = convertPersonAttributesToPrincipal(principalId, attributes);
return this.principalFactory.createPrincipal(pair.getFirst(), pair.getSecond());
}
…………
protected Map<String, List<Object>> retrievePersonAttributes(final String principalId, final Credential credential) {
//获取用户扩展属性
final IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId);
final Map<String, List<Object>> attributes;
if (personAttributes == null) {
attributes = null;
} else {
attributes = personAttributes.getAttributes();
}
return attributes;
}
}
其实最终我们获取的用户扩展属性是通过attributeRepository进行展开,具体的代码在此不展开,可以自行查看一下就可明白。
8. CentralAuthenticationServiceImpl中的createTicketGrantingTicket()方法
在上文中我们还留了一个是如何创建TGT的内容没有说明,下面我们就展开来进行具体的解读。
真正创建TGT并发布的类是CentralAuthenticationServiceImpl。此类在cas-server-core模块下的org.jasig.cas包中。
*CentralAuthenticationServiceImpl.java*
public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationContext context)
throws AuthenticationException, AbstractTicketException {
final Authentication authentication = context.getAuthentication();
//获取到tgt创建工厂类
final TicketGrantingTicketFactory factory = this.ticketFactory.get(TicketGrantingTicket.class);
//创建TGT,使用默认的DefaultTicketGrantingTicketFactory进行TGT的创建
final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication);
//创建的TGT放入应用缓存
this.ticketRegistry.addTicket(ticketGrantingTicket);
//产生一个事件通知
doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket));
return ticketGrantingTicket;
}
执行成功后转向《transition on=“success” to=“sendTicketGrantingTicket”/》。
创建TGT是由DefaultTicketGrantingTicketFactory最终进行执行。此类在cas-server-core-tickets模块下的org.jasig.cas.ticket包中。
DefaultTicketGrantingTicketFactory.java
@Component("defaultTicketGrantingTicketFactory")
public class DefaultTicketGrantingTicketFactory implements TicketGrantingTicketFactory {
protected final transient Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* UniqueTicketIdGenerator to generate ids for {@link TicketGrantingTicket}s
* created.
*/
@NotNull
@Resource(name="ticketGrantingTicketUniqueIdGenerator")
protected UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;
/** Expiration policy for ticket granting tickets. */
@NotNull
@Resource(name="grantingTicketExpirationPolicy")
protected ExpirationPolicy ticketGrantingTicketExpirationPolicy;
@Override
public <T extends TicketGrantingTicket> T create(final Authentication authentication) {
//最终还是有ticketGrantingTicketUniqueTicketIdGenerator进行实现,默认的实现类为DefaultUniqueTicketIdGenerator。同时入参中还有TGT的失效策略
final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl( this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX),
authentication, ticketGrantingTicketExpirationPolicy);
return (T) ticketGrantingTicket;
}
DefaultUniqueTicketIdGenerator.java
//默认实现创建TGT
@Override
public final String getNewTicketId(final String prefix) {
final String number = this.numericGenerator.getNextNumberAsString();
final StringBuilder buffer = new StringBuilder(prefix.length() + 2
+ (StringUtils.isNotBlank(this.suffix) ? this.suffix.length() : 0) + this.randomStringGenerator.getMaxLength()
+ number.length());
buffer.append(prefix);
buffer.append('-');
buffer.append(number);
buffer.append('-');
buffer.append(this.randomStringGenerator.getNewString());
if (this.suffix != null) {
buffer.append(this.suffix);
}
return buffer.toString();
}
至此,用户登录认证到产生TGT全部完成,后续章节就要发送TGT和进行跳转到登录成功页面的解析。