前面粗略的介绍了对于返回更多信息的简单配置,但是有些时候业务的需求 不是简单的查询语句就能解决,比如说我查询用户信息需要结合登录凭证中的多个字段
例如:我们有个项目的登录用户确定是根据访问域名和登录名同时确定的,这样就不得不进行拓展。
primaryPrincipalResolver:解析Credential(用户凭证,简单点就是用户登录信息)处理成用户登录成功以后的返回信息
在deployerConfigContext.xml文件中,我们可以找到id为primaryPrincipalResolver的bean
<bean id="primaryPrincipalResolver"
class="org.jasig.cas.authentication.principal.PersonDirectoryPrincipalResolver" >
<property name="attributeRepository" ref="attributeRepository" />
</bean>
同样要理清楚其中的逻辑还是得看PersonDirectoryPrincipalResolver这个类是怎么处理的,其中关键代码:
public final Principal resolve(final Credential credential) {
logger.debug("Attempting to resolve a principal...");
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 IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId);
final Map<String, List<Object>> attributes;
if (personAttributes == null) {
attributes = null;
} else {
attributes = personAttributes.getAttributes();
}
if (attributes == null & !this.returnNullIfNoAttributes) {
return new SimplePrincipal(principalId);
}
if (attributes == null) {
return null;
}
final Map<String, Object> convertedAttributes = new HashMap<String, Object>();
for (final String key : attributes.keySet()) {
final List<Object> values = attributes.get(key);
if (key.equalsIgnoreCase(this.principalAttributeName)) {
if (values.isEmpty()) {
logger.debug("{} is empty, using {} for principal", this.principalAttributeName, principalId);
} else {
principalId = values.get(0).toString();
logger.debug(
"Found principal attribute value {}; removing {} from attribute map.",
principalId,
this.principalAttributeName);
}
} else {
convertedAttributes.put(key, values.size() == 1 ? values.get(0) : values);
}
}
return new SimplePrincipal(principalId, convertedAttributes);
}
其中principalId是通过credential得到
String principalId = extractPrincipalId(credential);
代码如下:
protected String extractPrincipalId(final Credential credential) {
return credential.getId();
}
可以看到其实就是拿用户凭证的Id 至于这个getId()返回的是什么呢,我们看下UsernamePasswordCredential类
public String getId() {
return this.username;
}
发现这个其实就是返回的登录名,继续看resolve方法的代码会发现这行代码
final IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId);
可以看到通过attributeRepository的getPerson方法来获取需要返回的用户属性 而参数就是我们的principalId
从上面可以看出attributeRepository作为primaryPrincipalResolver的一个属性注入进来,而attributeRepository的配置呢,前面文章已经有了相关配置,jdbc的方式
实现类 SingleRowJdbcPersonAttributeDao 其实中字面意思也能看出 是singleRow 即单列的 ,同样该类的代码:
public class SingleRowJdbcPersonAttributeDao extends AbstractJdbcPersonAttributeDao<Map<String, Object>> {
private static final ParameterizedRowMapper<Map<String, Object>> MAPPER = new ColumnMapParameterizedRowMapper(true);
public SingleRowJdbcPersonAttributeDao(DataSource ds, String sql) {
super(ds, sql);
}
@Override
protected ParameterizedRowMapper<Map<String, Object>> getRowMapper() {
return MAPPER;
}
@Override
protected List<IPersonAttributes> parseAttributeMapFromResults(List<Map<String, Object>> queryResults, String queryUserName) {
final List<IPersonAttributes> peopleAttributes = new ArrayList<IPersonAttributes>(queryResults.size());
for (final Map<String, Object> queryResult : queryResults) {
final Map<String, List<Object>> multivaluedQueryResult = MultivaluedPersonAttributeUtils.toMultivaluedMap(queryResult);
final IPersonAttributes person;
if (queryUserName != null) {
person = new CaseInsensitiveNamedPersonImpl(queryUserName, multivaluedQueryResult);
}
else {
//Create the IPersonAttributes doing a best-guess at a userName attribute
final String userNameAttribute = this.getConfiguredUserNameAttribute();
person = new CaseInsensitiveAttributeNamedPersonImpl(userNameAttribute, multivaluedQueryResult);
}
peopleAttributes.add(person);
}
return peopleAttributes;
}
}
继承了AbstractJdbcPersonAttributeDao的类,继续跟,构造函数代码
public AbstractJdbcPersonAttributeDao(DataSource ds, String queryTemplate) {
Validate.notNull(ds, "DataSource can not be null");
Validate.notNull(queryTemplate, "queryTemplate can not be null");
this.simpleJdbcTemplate = new SimpleJdbcTemplate(ds);
this.queryTemplate = queryTemplate;
}
联想之前在在deployerConfigContext.xml文件中对attributeRepository的配置
<bean id= "attributeRepository" class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao" >
<constructor-arg index= "0" ref ="dataSource"/>
<constructor-arg index= "1" value ="SELECT * FROM T_BASE_USER WHERE {0} AND USER_STATUS IN (1,2)" />
<property name= "queryAttributeMapping" >
<map>
<entry key= "username" value ="USER_LOGIN_NAME" />
</map>
</property>
<property name="resultAttributeMapping" >
<map>
<entry key="USER_ACTUAL_NAME" value ="userName"/>
<entry key="SCHOOL_CODE" value="schoolCode" />
<entry key="USER_ID" value="userId" />
<entry key="USER_TYPE" value="userType"/>
</map>
</property>
</bean >
constructor-arg 构造函数的方式初始化属性 正好ds 对应我们传入的数据源(dataSource)queryTemplate 对应我们设置的sql语句
接着看往下看
上面也看到了其实是调用了getPerson方法 ,对于该方法的实现是在 AbstractDefaultAttributePersonAttributeDao 类中(AbstractJdbcPersonAttributeDao的祖父类)
代码如下:
public IPersonAttributes getPerson(String uid) {
Validate.notNull(uid, "uid may not be null.");
//Generate the seed map for the uid
final Map<String, List<Object>> seed = this.toSeedMap(uid);
//Run the query using the seed
final Set<IPersonAttributes> people = this.getPeopleWithMultivaluedAttributes(seed);
//Ensure a single result is returned
IPersonAttributes person = (IPersonAttributes)DataAccessUtils.singleResult(people);
if (person == null) {
return null;
}
//Force set the name of the returned IPersonAttributes if it isn't provided in the return object
if (person.getName() == null) {
person = new NamedPersonImpl(uid, person.getAttributes());
}
return person;
}
其中this.toSeedMap(uid)其实就是对我们传入的username做一个转换 转换为map
其中关键代码
final Set<IPersonAttributes> people = this.getPeopleWithMultivaluedAttributes(seed);
其中getPeopleWithMultivaluedAttributes的是实现在其子类 AbstractQueryPersonAttributeDao中,具体代码:
public final Set<IPersonAttributes> getPeopleWithMultivaluedAttributes(Map<String, List<Object>> query) {
Validate.notNull(query, "query may not be null.");
final QB queryBuilder = this.generateQuery(query);
if (queryBuilder == null && (this.queryAttributeMapping != null || this.useAllQueryAttributes == true)) {
this.logger.debug("No queryBuilder was generated for query " + query + ", null will be returned");
return null;
}
//Get the username from the query, if specified
final IUsernameAttributeProvider usernameAttributeProvider = this.getUsernameAttributeProvider();
final String username = usernameAttributeProvider.getUsernameFromQuery(query);
//Execute the query in the subclass
final List<IPersonAttributes> unmappedPeople = this.getPeopleForQuery(queryBuilder, username);
if (unmappedPeople == null) {
return null;
}
//Map the attributes of the found people according to resultAttributeMapping if it is set
final Set<IPersonAttributes> mappedPeople = new LinkedHashSet<IPersonAttributes>();
for (final IPersonAttributes unmappedPerson : unmappedPeople) {
final IPersonAttributes mappedPerson = this.mapPersonAttributes(unmappedPerson);
mappedPeople.add(mappedPerson);
}
return Collections.unmodifiableSet(mappedPeople);
}
第二行: final QB queryBuilder = this.generateQuery(query); 字面意思就已经很清晰了 生成查询语句
protected final QB generateQuery(Map<String, List<Object>> query) {
QB queryBuilder = null;
if (this.queryAttributeMapping != null) {
for (final Map.Entry<String, Set<String>> queryAttrEntry : this.queryAttributeMapping.entrySet()) {
final String queryAttr = queryAttrEntry.getKey();
final List<Object> queryValues = query.get(queryAttr);
if (queryValues != null ) {
final Set<String> dataAttributes = queryAttrEntry.getValue();
if (dataAttributes == null) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Adding attribute '" + queryAttr + "' with value '" + queryValues + "' to query builder '" + queryBuilder + "'");
}
queryBuilder = this.appendAttributeToQuery(queryBuilder, null, queryValues);
}
else {
for (final String dataAttribute : dataAttributes) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Adding attribute '" + dataAttribute + "' with value '" + queryValues + "' to query builder '" + queryBuilder + "'");
}
queryBuilder = this.appendAttributeToQuery(queryBuilder, dataAttribute, queryValues);
}
}
}
else if (this.requireAllQueryAttributes) {
this.logger.debug("Query " + query + " does not contain all nessesary attributes as specified by queryAttributeMapping " + this.queryAttributeMapping + ", null will be returned for the queryBuilder");
return null;
}
}
}
else if (this.useAllQueryAttributes) {
for (final Map.Entry<String, List<Object>> queryAttrEntry : query.entrySet()) {
final String queryKey = queryAttrEntry.getKey();
final List<Object> queryValues = queryAttrEntry.getValue();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Adding attribute '" + queryKey + "' with value '" + queryValues + "' to query builder '" + queryBuilder + "'");
}
queryBuilder = this.appendAttributeToQuery(queryBuilder, queryKey, queryValues);
}
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Generated query builder '" + queryBuilder + "' from query Map " + query + ".");
}
return queryBuilder;
}
大致意思就是如何配置了queryAttributeMapping字段 就按配置的来处理,没有就使用默认的,重点在
queryBuilder = this.appendAttributeToQuery(queryBuilder, dataAttribute, queryValues);
appendAttributeToQuery方法的实现 同样在其子类 也就是AbstractJdbcPersonAttributeDao类中
protected PartialWhereClause appendAttributeToQuery(PartialWhereClause queryBuilder, String dataAttribute, List<Object> queryValues) {
for (final Object queryValue : queryValues) {
final String queryString = queryValue != null ? queryValue.toString() : null;
if (StringUtils.isNotBlank(queryString)) {
if (queryBuilder == null) {
queryBuilder = new PartialWhereClause();
}
else if (queryBuilder.sql.length() > 0) {
queryBuilder.sql.append(" ").append(this.queryType.toString()).append(" ");
}
//Convert to SQL wildcard
final Matcher queryValueMatcher = IPersonAttributeDao.WILDCARD_PATTERN.matcher(queryString);
final String formattedQueryValue = queryValueMatcher.replaceAll("%");
queryBuilder.arguments.add(formattedQueryValue);
if (dataAttribute != null) {
queryBuilder.sql.append(dataAttribute);
if (formattedQueryValue.equals(queryString)) {
queryBuilder.sql.append(" = ");
}
else {
queryBuilder.sql.append(" LIKE ");
}
}
queryBuilder.sql.append("?");
}
}
return queryBuilder;
}
其中
final Matcher queryValueMatcher = IPersonAttributeDao.WILDCARD_PATTERN.matcher(queryString);
根据queryString 生成匹配器 而queryString 是对 queryValues的循环 queryValues呢 当然得看generateQuery方法咯
final String queryAttr = queryAttrEntry.getKey();
final List<Object> queryValues = query.get(queryAttr);
是不是
很明了了 ,其实就是取之前生成的seed 对应的key的值,但是我们发现 之前生成的是个单key的map 也就是说 这里 我们只可能获取到一个key的value值
其实这几个方法都是为了拼装我们需要的查询sql,对于queryAttributeMapping是一个Map<String,Set<String>>类型的集合
首先是循环这个map 根据我们的配置
<property name= "queryAttributeMapping" >
<map>
<entry key= "username" value ="USER_LOGIN_NAME" />
</map>
</property>
拿到queryAttr 应该就为username了,接着在query中获取key为username的值 结合上面说的 这里配置多个queryAttributeMapping其实起作用的也就key为username的
虽然query为Map<String,List<Object>> 类型 通过它得到的value为List<Object> 由于set的无序的我们无法确定 生成的sql会按照我们配置的循序 ,怎么说呢
在我理解是 我们只能配置一个查询key为username value可以配置多个,对应query中list同样可以多个 但是没法确定顺序 可以set中第一个字段对应list中的第一个字段
也可能set中的第一个字段对应list中的第二个字段 这种不确定性 导致我们没法通过这种方式来配置复杂的查询sql
以上纯属个人理解,如有不对还望指出,不想误导他人。
说了这么多,其实就是为了证明以这种方式有时候没法实现我们需要的业务、。。。。。。。。。。。。。。
不过通过上面的分析,起码我们知道为什么sql需要以{0} 这种方式作为占位符,以及queryAttributeMapping中 key的值 为什么只能是username
接下来才是进入正题,怎样实现这种复杂的登录查询呢
其实很简单,直接替换掉这种拼装sql的方式
当然入口还是从用户凭证开始,自定义用户凭证,之前文章已经说过了,这里我们只简单对getId方法重写就行了
public String getId(){
return getUsername()+","+getRequestUrl();
}
比如我这个,就是由用户名和请求域名来进行处理
接着,自定义我们的primaryPrincipalResolver 也很简单
public class CustomPrincipalResolver implements PrincipalResolver {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
private boolean returnNullIfNoAttributes = false;
@NotNull
private IPersonAttributeDao attributeRepository = new StubPersonAttributeDao(new HashMap<String, List<Object>>());
private String principalAttributeName;
@Override
public boolean supports(final Credential credential) {
return true;
}
@Override
public final Principal resolve(final Credential credential) {
logger.debug("Attempting to resolve a principal...");
String tempPrincipal = extractPrincipalId(credential);
String principalId = tempPrincipal.split(",")[0];
// 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 IPersonAttributes personAttributes = this.attributeRepository.getPerson(tempPrincipal);
final Map<String, List<Object>> attributes;
if (personAttributes == null) {
attributes = null;
} else {
attributes = personAttributes.getAttributes();
}
if (attributes == null & !this.returnNullIfNoAttributes) {
return new SimplePrincipal(principalId);
}
if (attributes == null) {
return null;
}
final Map<String, Object> convertedAttributes = new HashMap<String, Object>();
for (final String key : attributes.keySet()) {
final List<Object> values = attributes.get(key);
if (key.equalsIgnoreCase(this.principalAttributeName)) {
if (values.isEmpty()) {
logger.debug("如果为空,设置默认值", this.principalAttributeName, principalId);
} else {
principalId = values.get(0).toString();
logger.debug(
"找到需要的属性,移除",
principalId,
this.principalAttributeName);
}
} else {
convertedAttributes.put(key, values.size() == 1 ? values.get(0) : values);
}
}
return new SimplePrincipal(principalId, convertedAttributes);
}
public final void setAttributeRepository(final IPersonAttributeDao attributeRepository) {
this.attributeRepository = attributeRepository;
}
public void setReturnNullIfNoAttributes(final boolean returnNullIfNoAttributes) {
this.returnNullIfNoAttributes = returnNullIfNoAttributes;
}
public void setPrincipalAttributeName(final String attribute) {
this.principalAttributeName = attribute;
}
protected String extractPrincipalId(final Credential credential) {
return credential.getId();
}
}
唯一改动就是对 credential.getId()的返回值进行解析
然后getPerson方法中传入我们需要的组合值
当然对于getPerson方法也得重写啦
自定义JdbcPersonAttributeDao类 CustomSingleRowJdbcPersonAttributeDao继承AbstractJdbcPersonAttributeDao类
代码参考如下:
public class CustomSingleRowJdbcPersonAttributeDao extends
AbstractJdbcPersonAttributeDao<Map<String, Object>> {
private static final Logger logger = LoggerFactory.getLogger(CustomSingleRowJdbcPersonAttributeDao.class);
private static final ParameterizedRowMapper<Map<String, Object>> MAPPER = new ColumnMapParameterizedRowMapper(
true);
private final SimpleJdbcTemplate simpleJdbcTemplate;
private final String queryTemplate;
public CustomSingleRowJdbcPersonAttributeDao(DataSource ds,
String queryTemplate) {
super(ds, queryTemplate);
this.simpleJdbcTemplate = new SimpleJdbcTemplate(ds);
this.queryTemplate = queryTemplate;
// TODO Auto-generated constructor stub
}
@Override
protected List<IPersonAttributes> parseAttributeMapFromResults(
List<Map<String, Object>> queryResults, String queryUserName) {
// TODO Auto-generated method stub
final List<IPersonAttributes> peopleAttributes = new ArrayList<IPersonAttributes>(
queryResults.size());
for (final Map<String, Object> queryResult : queryResults) {
final Map<String, List<Object>> multivaluedQueryResult = MultivaluedPersonAttributeUtils
.toMultivaluedMap(queryResult);
final IPersonAttributes person;
if (queryUserName != null) {
person = new CaseInsensitiveNamedPersonImpl(queryUserName,
multivaluedQueryResult);
} else {
// Create the IPersonAttributes doing a best-guess at a userName
// attribute
final String userNameAttribute = this
.getConfiguredUserNameAttribute();
person = new CaseInsensitiveAttributeNamedPersonImpl(
userNameAttribute, multivaluedQueryResult);
}
peopleAttributes.add(person);
}
return peopleAttributes;
}
@Override
protected ParameterizedRowMapper<Map<String, Object>> getRowMapper() {
// TODO Auto-generated method stub
return MAPPER;
}
@Override
public IPersonAttributes getPerson(String uid) {
Validate.notNull(uid, "uid may not be null.");
// Run the query using the seed
final Set<IPersonAttributes> people = this
.getPeopleWithMultivaluedAttributesNew(uid);
// Ensure a single result is returned
IPersonAttributes person = (IPersonAttributes) DataAccessUtils
.singleResult(people);
if (person == null) {
return null;
}
// Force set the name of the returned IPersonAttributes if it isn't
// provided in the return object
if (person.getName() == null) {
person = new NamedPersonImpl(uid, person.getAttributes());
}
return person;
}
public final Set<IPersonAttributes> getPeopleWithMultivaluedAttributesNew(
String query) {
Validate.notNull(query, "query may not be null.");
// Get the username from the query, if specified
final String username = query.split(",")[0];
// Execute the query in the subclass
final List<IPersonAttributes> unmappedPeople = this
.getPeopleForQueryNew(query, username);
if (unmappedPeople == null) {
return null;
}
// Map the attributes of the found people according to
// resultAttributeMapping if it is set
final Set<IPersonAttributes> mappedPeople = new LinkedHashSet<IPersonAttributes>();
for (final IPersonAttributes unmappedPerson : unmappedPeople) {
final IPersonAttributes mappedPerson = this
.mapPersonAttributes(unmappedPerson);
mappedPeople.add(mappedPerson);
}
return Collections.unmodifiableSet(mappedPeople);
}
protected List<IPersonAttributes> getPeopleForQueryNew(String query,
String queryUserName) {
// Execute the query
final ParameterizedRowMapper rowMapper = this.getRowMapper();
List results = this.simpleJdbcTemplate.query(this.queryTemplate,
rowMapper, query.split(","));
//List results = new ArrayList();
/*byte[] resultByteArr = this.getRedisManager().get(SerializeUtils.serialize(query));
if(resultByteArr!=null){
results = (List) SerializeUtils.deserialize(resultByteArr);
logger.debug("user is select from redis");
}else{
results = this.simpleJdbcTemplate.query(this.queryTemplate,
rowMapper, query.split(","));
this.getRedisManager().set(SerializeUtils.serialize(query), SerializeUtils.serialize(results),18000000);
logger.debug("user is select from DB");
} */
return this.parseAttributeMapFromResults(results, queryUserName);
}
}
很简单,就是对几个方法的重写,首先是重写getPerson方法
final Set<IPersonAttributes> people = this
.getPeopleWithMultivaluedAttributesNew(uid);
getPeopleWithMultivaluedAttributesNew方法为自定义方法 可以看到 和源码的差距就是去掉了generateQuery一步
自定义了getPeopleForQuery方法
而我们sql中需要的参数值 都包含在query中 也就是credential.getId()的返回值
到此对登录成功以后查询用户信息的复杂改造完结。