I'm setting up CAS 5.3 to do Bind authentication agains an Oracle database. But I don't know how to setup a OracleDataSource while using WAR Overlay method. Any guidance would be appreciated, thanks.
Just setting up Driver and URL doesn't appear to work. It seems the HikariDataSource is used by default, and it doesn't implement the required getConnection(String username, String password).
#SneakyThrows
public static DataSource newDataSource(final AbstractJpaProperties jpaProperties) {
final String dataSourceName = jpaProperties.getDataSourceName();
final boolean proxyDataSource = jpaProperties.isDataSourceProxy();
if (StringUtils.isNotBlank(dataSourceName)) {
try {
final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
dsLookup.setResourceRef(false);
final DataSource containerDataSource = dsLookup.getDataSource(dataSourceName);
if (!proxyDataSource) {
return containerDataSource;
}
return new DataSourceProxy(containerDataSource);
} catch (final DataSourceLookupFailureException e) {
LOGGER.warn("Lookup of datasource [{}] failed due to {} falling back to configuration via JPA properties.", dataSourceName, e.getMessage());
}
}
final HikariDataSource bean = new HikariDataSource();
if (StringUtils.isNotBlank(jpaProperties.getDriverClass())) {
bean.setDriverClassName(jpaProperties.getDriverClass());
}
bean.setJdbcUrl(jpaProperties.getUrl());
bean.setUsername(jpaProperties.getUser());
bean.setPassword(jpaProperties.getPassword());
bean.setLoginTimeout((int) Beans.newDuration(jpaProperties.getPool().getMaxWait()).getSeconds());
bean.setMaximumPoolSize(jpaProperties.getPool().getMaxSize());
bean.setMinimumIdle(jpaProperties.getPool().getMinSize());
bean.setIdleTimeout((int) Beans.newDuration(jpaProperties.getIdleTimeout()).toMillis());
bean.setLeakDetectionThreshold(jpaProperties.getLeakThreshold());
bean.setInitializationFailTimeout(jpaProperties.getFailFastTimeout());
bean.setIsolateInternalQueries(jpaProperties.isIsolateInternalQueries());
bean.setConnectionTestQuery(jpaProperties.getHealthQuery());
bean.setAllowPoolSuspension(jpaProperties.getPool().isSuspension());
bean.setAutoCommit(jpaProperties.isAutocommit());
bean.setValidationTimeout(jpaProperties.getPool().getTimeoutMillis());
return bean;
}
I would require that the bean created above, would be a OracleDataSource instance.
Related
I have an application where I am trying to distribute reads & writes between two replicas. For some reason JPA is only using my read-replica, not the write replica. The write replica is the primary replica. The result is that when I use JPA to try and write data I get and 'UPDATE command denied' error because it is using the read only datasource. I have tried doing my own annotation and using the #Transactional annotation. Both annotations are called via AOP with the correct datasource but JPA will not use it.
FYI Spring JDBC works correctly via the custom annotation. This is strictly a JPA issue. Below is some code:
My AOP class:
#Aspect
#Order(20)
#Component
public class RouteDataSourceInterceptor {
#Around("#annotation(com.kenect.db.common.annotations.UseDataSource) && execution(* *(..))")
public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
try {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
UseDataSource annotation = method.getAnnotation(UseDataSource.class);
RoutingDataSource.setDataSourceName(annotation.value());
return pjp.proceed();
} finally {
RoutingDataSource.resetDataSource();
}
}
#Around("#annotation(transactional)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint, Transactional transactional) throws Throwable {
try {
if (transactional.readOnly()) {
RoutingDataSource.setDataSourceName(SQL_READ_REPLICA);
Klogger.info("Routing database call to the read replica");
} else {
RoutingDataSource.setDataSourceName(SQL_MASTER_REPLICA);
Klogger.info("Routing database call to the primary replica");
}
return proceedingJoinPoint.proceed();
} finally {
RoutingDataSource.resetDataSource();
}
}
}
My RoutingDataSource class:
public class RoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> currentDataSourceName = new ThreadLocal<>();
public static synchronized void setDataSourceName(String name) {
currentDataSourceName.set(name);
}
public static synchronized void resetDataSource() {
currentDataSourceName.remove();
}
#Override
protected Object determineCurrentLookupKey() {
return currentDataSourceName.get();
}
}
AbstractDynamicDataSourceConfig
public abstract class AbstractDynamicDataSourceConfig {
private final ConfigurableEnvironment environment;
public AbstractDynamicDataSourceConfig(ConfigurableEnvironment environment) {
this.environment = environment;
}
protected DataSource getRoutingDataSource() {
Map<String, String> props = DBConfigurationUtils.getAllPropertiesStartingWith("spring.datasource", environment);
List<String> dataSourceNames = DBConfigurationUtils.getDataSourceNames(props.keySet());
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> dataSources = new HashMap<>();
DataSource masterDataSource = null;
for (String name : dataSourceNames) {
DataSource dataSource = getDataSource("spring.datasource." + name);
dataSources.put(name, dataSource);
if (masterDataSource == null && name.toLowerCase().contains("master")) {
masterDataSource = dataSource;
}
}
if (dataSources.isEmpty()) {
throw new KenectInvalidParameterException("No datasources found.");
}
routingDataSource.setTargetDataSources(dataSources);
if (masterDataSource == null) {
masterDataSource = (DataSource) dataSources.get(dataSourceNames.get(0));
}
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
protected DataSource getDataSource(String prefix) {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(environment.getProperty(prefix + ".jdbcUrl"));
hikariConfig.setUsername(environment.getProperty(prefix + ".username"));
hikariConfig.setPassword(environment.getProperty(prefix + ".password"));
return new HikariDataSource(hikariConfig);
}
}
application.yaml
spring:
datasource:
master:
jdbcUrl: jdbc:mysql://my-main-replica
username: some-user
password: some-password
read-replica:
jdbcUrl: jdbc:mysql://my-read-replica
username: another-user
password: another-password
If I use the annotation on with JDBC template then it works as expected:
THIS WORKS:
// Uses main replica as it is not specified
public Message insertMessage(Message message) {
String sql = "INSERT INTO message(" +
" `conversationId`," +
" `body`)" +
" VALUE (" +
" :conversationId," +
" :body" +
")";
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("conversationId", message.getConversationId());
parameters.addValue("body", message.getBody());
namedJdbcTemplate.update(sql, parameters);
}
// Uses read replica
#UseDataSource(SQL_READ_REPLICA)
public List<Message> getMessage(long id) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("id", id);
String sql = "SELECT " +
" conversationId," +
" body" +
" FROM message"
" WHERE id = :id";
return namedJdbcTemplate.query(sql, parameters, new BeanPropertyRowMapper<>(Message.class));
}
If I use a JPA interface it always uses the read replica:
THIS FAILS:
#Repository
public interface MessageJpaRepository extends JpaRepository<MessageEntity, Long> {
// Should use the main-replica but always uses the read-replica
#Modifying
#Query(value =
"UPDATE clarioMessage SET" +
" body = :body" +
" WHERE id = :id" +
" AND organizationId = :organizationId",
nativeQuery = true)
#Transactional
int updateMessageBodyByIdAndOrganizationId(#Param("body") String body, #Param("id")long id, #Param("organizationId")long organizationId);
}
So I am just getting the error below when I try to use the main-replica. I have tried using the #UseDataSource annotation and AOP does actually intercept it. But, it still uses the read-replica.
java.sql.SQLSyntaxErrorException: UPDATE command denied to user 'read-replica-user'#'read replica IP' for table 'message'
What am I missing?
When you use #UseDataSource, it is working so it seems rules out any issues with implementation of aspect.
And When you #Transactional, it uses the secondary replica, regardless of your your AOP being invoked. My suspicion is by the TransactionInterceptor created by spring is invoked before your RouteDataSourceInterceptor. You can try the following:
Put a breakpoint in your aop method as well as a break point in org.springframework.transaction.interceptor.TransactionInterceptor.invoke method to see which one invokes first. You want your interceptor invoked first
If your interceptor is not invoked first, I would modify your interceptor to have high order as follows.
#Aspect
#Order(Ordered.HIGHEST_PRECEDENCE)
#Component
public class RouteDataSourceInterceptor {
I still don't understand how you are telling TransactionInterceptor to choose the DataSource you set in RouteDataSourceInterceptor. I have not used multi tenant setup but recently I came across a question which I helped to solve and I can see it is implementing AbstractDataSourceBasedMultiTenantConnectionProviderImpl. So I hope you have something similar. Not able to switch database after defining Spring AOP
I have a custom handler like this:
Public class DatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {
#Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
UsernamePasswordCredential credential, String originalPassword) throws GeneralSecurityException, PreventedException {
final String username = credential.getUsername();
logger.debug("***Username:"+username);
logger.debug("***Password:"+credential.getPassword());
return createHandlerResult(credential, new SimplePrincipal(), null);
}
#Override
public boolean supports(final Credential credential) {
return true;
}
}
To me, this should always log a user in no matter what. But I see in the logs this:
ERROR [org.apereo.cas.authentication.PolicyBasedAuthenticationManager]
- <Authentication has failed. Credentials may be incorrect or CAS cannot find authentication handler that supports
[UsernamePasswordCredential(username=sadf, source=MyJDBCAuthenticationManager)] of type [UsernamePasswordCredential].
Examine the configuration to ensure a method of authentication is defined and analyze CAS logs at DEBUG level to trace the authentication event.
which makes no sense to me as I can see in the logs that cas is calling the authenticatUsernamePasswordInternal method. Obviously this handler supports, well everything.
Why can't I log in?
I think you best use principalFactory.createPrincipal to create the principal rather than returning an new SimplePrincipal().
In your AuthenticationEventExecutionPlanConfigurer & DatabaseAuthenticationHandler, add the following:
AuthenticationEventExecutionPlanConfigurer.java
#Autowired
#Qualifier("principalFactory")
private PrincipalFactory principalFactory;
#Bean
public DatabaseAuthenticationHandler databaseAuthenticationHandler() {
return new DatabaseAuthenticationHandler(principalFactory);
}
DatabaseAuthenticationHandler
Public class DatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {
private final PrincipalFactory principalFactory;
public DatabaseAuthenticationHandler (PrincipalFactory principalFactory){
this.principalFactory = principalFactory;
}
#Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
UsernamePasswordCredential credential, String originalPassword) throws GeneralSecurityException, PreventedException {
final String username = credential.getUsername();
logger.debug("***Username:"+username);
logger.debug("***Password:"+credential.getPassword());
/////// below here's the change /////////
return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
}
#Override
public boolean supports(final Credential credential) {
return true;
}
}
See if the above works, thanks.
The root cause of this problem is that you pass a null parameter to createHandlerResult method,you can change it to new ArrayList<>. I also encountered this problem(My CAS version is 5.3.9).And I also tried the solution gaving by Ng Sek Long,but it didn't work.Then I tried to solve it by myself. I searched for the error message in CAS code and found it in PolicyBasedAuthenticationManager class.
try {
PrincipalResolver resolver = this.getPrincipalResolverLinkedToHandlerIfAny(handler, transaction);
LOGGER.debug("Attempting authentication of [{}] using [{}]", credential.getId(), handler.getName());
this.authenticateAndResolvePrincipal(builder, credential, resolver, handler);
AuthenticationCredentialsThreadLocalBinder.bindInProgress(builder.build());
Pair<Boolean, Set<Throwable>> failures = this.evaluateAuthenticationPolicies(builder.build(), transaction);
proceedWithNextHandler = !(Boolean)failures.getKey();
} catch (Exception var15) {
LOGGER.error("Authentication has failed. Credentials may be incorrect or CAS cannot find authentication handler that supports [{}] of type [{}]. Examine the configuration to ensure a method of authentication is defined and analyze CAS logs at DEBUG level to trace the authentication event.", credential, credential.getClass().getSimpleName());
this.handleAuthenticationException(var15, handler.getName(), builder);
proceedWithNextHandler = true;
}
In the above code snippet, the authenticateAndResolvePrincipal method declaired two kinds of exception.Looked at this method, I found there is a line of code which may throws that two.
AuthenticationHandlerExecutionResult result = handler.authenticate(credential);
The key code which lead to this problem is in DefaultAuthenticationHandlerExecutionResult class.
public DefaultAuthenticationHandlerExecutionResult(final AuthenticationHandler source, final CredentialMetaData metaData, final Principal p, #NonNull final List<MessageDescriptor> warnings) {
this(StringUtils.isBlank(source.getName()) ? source.getClass().getSimpleName() : source.getName(), metaData, p, warnings);
if (warnings == null) {
throw new NullPointerException("warnings is marked #NonNull but is null");
}
}
So, if you use createHandlerResult(credential, new SimplePrincipal(), null), NullPointerException will throw at runtime.It will be catched by catch (Exception var15) code bock and log the error message you see.
I'm creating a multi tenant spring boot - JPA application.
In this application, I want to connect to MySQL Databases using DB name which is sent through API request as header.
I checked many multi tenant project samples online but still can't figure out a solution.
Can anyone suggest me a way to do this?
You can use AbstractRoutingDataSource to achieve this. AbstractRoutingDataSource requires information to know which actual DataSource to route to(referred to as Context), which is provided by determineCurrentLookupKey() method. Using example from here.
Define Context like:
public enum ClientDatabase {
CLIENT_A, CLIENT_B
}
Then you need to define Context Holder which will be used in determineCurrentLookupKey()
public class ClientDatabaseContextHolder {
private static ThreadLocal<ClientDatabase> CONTEXT = new ThreadLocal<>();
public static void set(ClientDatabase clientDatabase) {
Assert.notNull(clientDatabase, "clientDatabase cannot be null");
CONTEXT.set(clientDatabase);
}
public static ClientDatabase getClientDatabase() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
Then you can extend AbstractRoutingDataSource like below:
public class ClientDataSourceRouter extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
return ClientDatabaseContextHolder.getClientDatabase();
}
}
Finally, DataSource bean configuration:
#Bean
public DataSource clientDatasource() {
Map<Object, Object> targetDataSources = new HashMap<>();
DataSource clientADatasource = clientADatasource();
DataSource clientBDatasource = clientBDatasource();
targetDataSources.put(ClientDatabase.CLIENT_A,
clientADatasource);
targetDataSources.put(ClientDatabase.CLIENT_B,
clientBDatasource);
ClientDataSourceRouter clientRoutingDatasource
= new ClientDataSourceRouter();
clientRoutingDatasource.setTargetDataSources(targetDataSources);
clientRoutingDatasource.setDefaultTargetDataSource(clientADatasource);
return clientRoutingDatasource;
}
https://github.com/wmeints/spring-multi-tenant-demo
Following this logic, I can solve it now. Some of the versions need to be upgraded and the codes as well.
Spring Boot version have changed.
org.springframework.boot
spring-boot-starter-parent
2.1.0.RELEASE
Mysql version has been removed.
And some small changed in MultitenantConfiguration.java
#Configuration
public class MultitenantConfiguration {
#Autowired
private DataSourceProperties properties;
/**
* Defines the data source for the application
* #return
*/
#Bean
#ConfigurationProperties(
prefix = "spring.datasource"
)
public DataSource dataSource() {
File[] files = Paths.get("tenants").toFile().listFiles();
Map<Object,Object> resolvedDataSources = new HashMap<>();
if(files != null) {
for (File propertyFile : files) {
Properties tenantProperties = new Properties();
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(this.getClass().getClassLoader());
try {
tenantProperties.load(new FileInputStream(propertyFile));
String tenantId = tenantProperties.getProperty("name");
dataSourceBuilder.driverClassName(properties.getDriverClassName())
.url(tenantProperties.getProperty("datasource.url"))
.username(tenantProperties.getProperty("datasource.username"))
.password(tenantProperties.getProperty("datasource.password"));
if (properties.getType() != null) {
dataSourceBuilder.type(properties.getType());
}
resolvedDataSources.put(tenantId, dataSourceBuilder.build());
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
// Create the final multi-tenant source.
// It needs a default database to connect to.
// Make sure that the default database is actually an empty tenant database.
// Don't use that for a regular tenant if you want things to be safe!
MultitenantDataSource dataSource = new MultitenantDataSource();
dataSource.setDefaultTargetDataSource(defaultDataSource());
dataSource.setTargetDataSources(resolvedDataSources);
// Call this to finalize the initialization of the data source.
dataSource.afterPropertiesSet();
return dataSource;
}
/**
* Creates the default data source for the application
* #return
*/
private DataSource defaultDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create(this.getClass().getClassLoader())
.driverClassName(properties.getDriverClassName())
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword());
if(properties.getType() != null) {
dataSourceBuilder.type(properties.getType());
}
return dataSourceBuilder.build();
}
}
This change is here due to the DataSourceBuilder has been moved to another path and its constructor has been changed.
Also changed the MySQL driver class name in application.properties like this
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
Pls explain me this like i am a retard kid. I am stuck at this point for three days. There so so much documentation for multiple use of JDBC, but when i do it one it won't work for me.
So I build project whit maven vaadin build:
.
Now I need to connect to my MySQL database. I will need to push data once per day. Just for store. I need one simple solution which will be good for my use.
This code work for me all ready but now i am trying whit Pool:
public class Povezavapool {
private static final BasicDataSource dataSource = new BasicDataSource();
static {
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/patrioti");
dataSource.setUsername("root");
dataSource.setPassword("1234567890");
}
private Povezavapool() {
//
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
But now i trying to do smt like this because I read it it is faster and better. I have no idea how to set up it.
public static JDBCConnectionPool getConnectionPool() {
SimpleJDBCConnectionPool pool = null;
try {
pool = new SimpleJDBCConnectionPool("com.mysql.jdbc.Driver",
"jdbc:mysql://localhost:3306/patrioti", "rooot",
"1234567890");
} catch (SQLException e) {
e.printStackTrace();
}
return pool;
}
I need to create a database named test in my local mysql server which i will use to setup my datasource bean. I am using the following spring configuration for setting up the datasource and jdbctemplate for my testing
#Configuration
class Config {
#Bean(initMethod = "setupDatabase")
public DataSource getDataSource() {
String url = "jdbc:mysql://localhost:3306/test";
DriverManagerDataSource dataSource = new DriverManagerDataSource(
url, settings.getUsername(), settings.getPassword());
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
return dataSource;
}
#Bean
public JdbcTemplate getJdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(((DataSourceTransactionManager)transactionManager()).getDataSource());
return jdbcTemplate;
}
#Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(getDataSource());
}
#Bean
public DataSourceInitializer dataSourceInitializer(final DataSource dataSource) {
final DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
initializer.setDatabasePopulator(databasePopulator());
return initializer;
}
private DatabasePopulator databasePopulator() {
final ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(schemaScript);
return populator;
}
#PostConstruct
public void setupDatabase() {
String url = "jdbc:mysql://localhost:3306";
try {
Connection connection = DriverManager.getConnection(url, settings.getUsername(), settings.getPassword());
Statement statement = connection.createStatement();
statement.execute("create database test");
} catch (SQLException exception) {
LOGGER.error("Could not setup database for test", exception);
throw new RuntimeException(exception);
}
}
}
I am getting the following error Caused by:
org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown database 'test'
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:80)
at org.springframework.jdbc.datasource.init.DatabasePopulatorUtils.execute(DatabasePopulatorUtils.java:46)
Can someone explain what is going wrong with this configuration?
Did you create the test database at your local mysql? You need to create the db first to connect from your application. If you want to create a database by executing sql query connect to the database as root user and create the db.
The source of the exception: your setupDatabase() method is executed actually by Spring AFTER getDataSource() cause #Bean(initMethod = "setupDatabase") forces Spring to execute initMetod AFTER a bean creation for the bean initialization. But it's not you expect here.
You need somehow define your getDataSource() is depended on setupDatabase(). E.g. with a help of #DependsOn annotation.
Also see Spring 3 bean instantiation sequence
P.S. but why you don't simple manually add setupDatabase() call into getDataSource() method?