четверг, июля 15, 2010

Apache Cassandra и Spring Security UserDetailsService

Если Вы в Spring MVC приложении хотите использовать Apache Cassandra как персистентное хранилище информации о пользователях и использовать ее при разграничении доступа, может быть полезной представленная далее реализация UserDetailsService. В качестве клиента Cassandra используется Hector.
Допустим у нас есть следующие декларации в конфиге Cassandra:
<Keyspace Name="Keyspace1">
<ColumnFamily Name="Configuration" CompareWith="UTF8Type"/> 
<ColumnFamily Name="Users" CompareWith="UTF8Type"/>
...
</Keyspace>
Мы объявили две ColumnFamily. В Users будем хранить записи пользователей, в Configuration общие настройки приложения, в частности в одной предопределенной записи, скажем с именем "GroupsAuthority", в столбцах будет определение групп и прав.

ColumnFamily:Users
|--------------------------------------------------------------------------
|UserName\Property| enabled | group      |           password (MD5)       |
|--------------------------------------------------------------------------
|user1            |  true   | user,admin |24c9e15e52afc47c225b757e7bee1f9d|
|userN            |  false  | editor     |362edb266f062db3ddbd1098b8affcbd|
|--------------------------------------------------------------------------

ColumnFamily:Configuration
|-------------------------------------------------------------------
|               |   user   | editor   |           admin            |
|-------------------------------------------------------------------
|GroupsAuthority| canView  | canEdit  | canView, canEdit, canBackup|
|-------------------------------------------------------------------

Далее необходимая конфигурация в Spring
<authentication-manager>
    <authentication-provider user-service-ref="userServiceBean">
        <password-encoder hash="md5"/>                                 
    </authentication-provider>
</authentication-manager>

<beans:bean id="userServiceBean" class="com.applusion.cassadraspring.CassandraHectorUserDetailsServiceImpl">
 <beans:property name="cassandraClientPool" ref="cassandraClientPool"/>
 <beans:property name="keyspace" value="Keyspace1"/>
 <beans:property name="usersCF" value="Users"/>
 <beans:property name="passwordColumn" value="password"/>
 <beans:property name="enabledColumn" value="enabled"/>
 <beans:property name="groupColumn" value="group"/>
 <beans:property name="configurationCF" value="Configuration"/>
 <beans:property name="groupsAuthorityKey" value="GroupsAuthority"/>
</beans:bean>

Не забудем совершенно типичную конфигурацию в Spring с использованием Cassandra и Hector:

    
<bean id="cassandraClientMonitor" class="me.prettyprint.cassandra.service.CassandraClientMonitor"/>

<bean id="jmxMonitor" class="me.prettyprint.cassandra.service.JmxMonitor" factory-method="getInstance"/>

<bean id="cassandraClientPoolFactory" class="me.prettyprint.cassandra.service.CassandraClientPoolFactory" factory-method="getInstance"/>

<bean id="cassandraClientPool" factory-bean="cassandraClientPoolFactory" factory-method="createNew" >
        <constructor-arg><ref bean="cassandraHostConfigurator"/></constructor-arg>
</bean>

<bean id="cassandraHostConfigurator" class="me.prettyprint.cassandra.service.CassandraHostConfigurator">
<constructor-arg value="localhost:9160"/>
</bean>

Сам класс:
/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.applusion.cassadraspring;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import me.prettyprint.cassandra.dao.SpringCommand;
import me.prettyprint.cassandra.service.CassandraClient;
import me.prettyprint.cassandra.service.CassandraClientPool;
import me.prettyprint.cassandra.service.Keyspace;

import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.NotFoundException;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * UserDetailsService implementation that reads user details from a Apache Cassandra.
 * 
 * @author Oleg Smith
 */

public class CassandraHectorUserDetailsServiceImpl implements UserDetailsService {
 private CassandraClientPool cassandraClientPool;
 private String keyspace = "Keyspace1";
 private String usersCF = "Standard2";
 private String passwordColumn = "password";
 private String enabledColumn = "enabled";
 private String groupColumn = "group";
 private String configurationCF="Configuration";
 private String groupsAuthorityKey="GroupsAuthority";

 
 private ConsistencyLevel consistencyLevel = CassandraClient.DEFAULT_CONSISTENCY_LEVEL;

 protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
 
 public CassandraHectorUserDetailsServiceImpl() {
   } 
 
 public CassandraHectorUserDetailsServiceImpl(CassandraClientPool cassandraClientPool) {
     this.cassandraClientPool = cassandraClientPool;
   }
 
 @Override
 public UserDetails loadUserByUsername(String username)
   throws UsernameNotFoundException, DataAccessException {
  List<Column> cl, cl2;
  
  try {
   cl = getUserProperties(username);
   if (cl.size()==0) {
     throw new UsernameNotFoundException(
                      messages.getMessage("CassandraHectorUserDetailsServiceImpl.notFound", new Object[]{username}, "Username {0} not found"), username);   
   }
   
   cl2 = getGroupsAuthority();
   if (cl2.size()==0) {
     throw new UsernameNotFoundException(
                      messages.getMessage("CassandraHectorUserDetailsServiceImpl.notFound", new Object[]{username}, "Not found any GroupsAuthority in base"));   
   }
      
   Map<String,String> userProperties=new HashMap<String,String>();
   for (Column cli:cl) {
    userProperties.put(new String(cli.name),new String(cli.value));
   }
   String[] userGroups=userProperties.get(groupColumn).split(",");
   String password=userProperties.get(passwordColumn);
   boolean enabled=userProperties.get(enabledColumn).trim().equals("true") ? true: false;
   
   
   Map<String,String> groupsAuthority=new HashMap<String,String>();
   for (Column cli:cl2) {
    groupsAuthority.put(new String(cli.name),new String(cli.value));
   }

   List<String> authorities=new ArrayList<String>(); 
   
   for (String userGroup:userGroups) {
    String[] gauthorities=groupsAuthority.get(userGroup).split(","); 
    for (String authority:gauthorities) {
     authorities.add(authority); 
    }    
   }
      
   Collection<GrantedAuthority> dbAuths = new ArrayList<GrantedAuthority>();
   
   for (String auth:authorities) {
    if (!auth.equals("")) dbAuths.add(new GrantedAuthorityImpl(auth.trim()));
   }
   
   return new User(username, password, enabled, true,true, true, dbAuths);
   

   
  } catch (Exception e) {
   e.printStackTrace();
  }

  return null;
 }
 
 
   public List<Column> getUserProperties(final String key) throws Exception {
      return execute(new SpringCommand<List<Column>>(cassandraClientPool){
        public List<Column> execute(final Keyspace ks) throws Exception {
          try {
           ColumnParent clp = new ColumnParent(usersCF);           
              SliceRange sr = new SliceRange(new byte[0], new byte[0], false, 150);
              SlicePredicate sp = new SlicePredicate();
              sp.setSlice_range(sr);
              List<Column> cols = ks.getSlice(key, clp, sp);           
           return cols;
          } catch (NotFoundException e) {
            return null;
          }
        }
      });
    } 
 
   public List<Column> getGroupsAuthority() throws Exception {
      return execute(new SpringCommand<List<Column>>(cassandraClientPool){
        public List<Column> execute(final Keyspace ks) throws Exception {
          try {
           ColumnParent clp = new ColumnParent(configurationCF);           
              SliceRange sr = new SliceRange(new byte[0], new byte[0], false, 150);
              SlicePredicate sp = new SlicePredicate();
              sp.setSlice_range(sr);
              List<Column> cols = ks.getSlice(groupsAuthorityKey, clp, sp);           
           return cols;
          } catch (NotFoundException e) {
            return null;
          }
        }
      });
    }  
   
   
  protected <T> T execute(SpringCommand<T> command) throws Exception {
      CassandraClient c = cassandraClientPool.borrowClient();
      Keyspace ks = c.getKeyspace(keyspace, consistencyLevel);
      try {
        return command.execute(ks);
      } finally {
        cassandraClientPool.releaseClient(ks.getClient());        
      }
  }

 public CassandraClientPool getCassandraClientPool() {
  return cassandraClientPool;
 }

 public void setCassandraClientPool(CassandraClientPool cassandraClientPool) {
  this.cassandraClientPool = cassandraClientPool;
 }

 public String getKeyspace() {
  return keyspace;
 }

 public void setKeyspace(String keyspace) {
  this.keyspace = keyspace;
 }

 public String getUsersCF() {
  return usersCF;
 }

 public void setUsersCF(String usersCF) {
  this.usersCF = usersCF;
 }

 public String getPasswordColumn() {
  return passwordColumn;
 }

 public void setPasswordColumn(String passwordColumn) {
  this.passwordColumn = passwordColumn;
 }

 public String getEnabledColumn() {
  return enabledColumn;
 }

 public void setEnabledColumn(String enabledColumn) {
  this.enabledColumn = enabledColumn;
 }

 public String getGroupColumn() {
  return groupColumn;
 }

 public void setGroupColumn(String groupColumn) {
  this.groupColumn = groupColumn;
 }

 public String getConfigurationCF() {
  return configurationCF;
 }

 public void setConfigurationCF(String configurationCF) {
  this.configurationCF = configurationCF;
 }

 public String getGroupsAuthorityKey() {
  return groupsAuthorityKey;
 }

 public void setGroupsAuthorityKey(String groupsAuthorityKey) {
  this.groupsAuthorityKey = groupsAuthorityKey;
 }

 public ConsistencyLevel getConsistencyLevel() {
  return consistencyLevel;
 }

 public void setConsistencyLevel(ConsistencyLevel consistencyLevel) {
  this.consistencyLevel = consistencyLevel;
 }
      
}

Вроде все. Теперь в контроллере можно аннотациями ограничивать права. Например так PreAuthorize("hasRole('canBackup')")

Комментариев нет:

Мой список блогов