I am using log4j2 in my web project. I was trying to put logs directly to kafka by extending abstractAppender. As per documentation my understanding is that i can specify patternlayout for a custom appender and with that being set, my logger will send log events to kafka with formatted string but that is not happening. log4j2.xml looks like
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info" packages="com.abc.webservice.log.appender">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L- %X{sessionId}--%X{guid}- %m #]%n</pattern>
</PatternLayout>
</Console>
<Kafka name="kafka" topic="test">
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L- %X{sessionId}--%X{guid}- %m #]%n</pattern>
</PatternLayout>
<Property name="metadata.broker.list">127.0.0.1:9092</Property>
<Property name="serializer.class">kafka.serializer.StringEncoder</Property>
</Kafka>
</Appenders>
<Loggers>
<AsyncLogger name="async">
<AppenderRef ref="kafka" />
<AppenderRef ref="console" />
</AsyncLogger>
<Root level="info">
<AppenderRef ref="console" />
<AppenderRef ref="kafka" />
</Root>
<Logger name="com.abc" level="debug">
<!-- <appender-ref ref="console" level="debug"/>-->
<!--<appender-ref ref="kafka" level="debug"/>-->
<!--<appender-ref ref="console" level="error"/>-->
<appender-ref ref="kafka" level="error"/>
</Logger>
<Logger name="org.hibernate.SQL" >
<appender-ref ref="kafka" level="info" />
<appender-ref ref="console" level="info"/>
</Logger>
<Logger name="org.hibernate.type">
<appender-ref ref="console" level="info"/>
<appender-ref ref="kafka" level="info"/>
</Logger>
<Root level="info">
<AppenderRef ref="kafka"/>
<AppenderRef ref="console"/>
</Root>
</Loggers>
</Configuration>
If i use console appender then log comes in proper format but when i use custom appender, log is received without format. How can i send logs to kafka with specified paatternlayout.
Please find my appender implementation
import java.io.Serializable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.appender.AppenderLoggingException;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.util.Booleans;
import org.apache.logging.log4j.message.Message;
#Plugin(name = "Kafka", category = "Core", elementType = "appender", printObject = true)
public final class KafkaAppender extends AbstractAppender {
private final Lock lock = new ReentrantLock();
private KafkaManager manager;
protected KafkaAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions, KafkaManager manager) {
super(name, filter, layout, ignoreExceptions);
System.err.println("hello world hello");
this.manager = manager;
}
#PluginFactory
public static KafkaAppender createAppender(#PluginAttribute("name") final String name, #PluginElement("Filter") final Filter filter,
#PluginAttribute("ignoreExceptions") final String ignore, #PluginAttribute("topic") final String topic,
#PluginElement("Properties") final Property[] properties, #PluginElement("layout") final Layout layout) {
boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
KafkaManager kafkaManager = KafkaManager.getKafkaManager(name, topic, properties);
if (kafkaManager == null) {
return null;
}
// Layout patternLayout = PatternLayout.createLayout("%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L- %X{sessionId}--%X{guid}- %m #]%n",
// null, null, null, true, false, null, null);
// System.err.println(patternLayout.toString());
return new KafkaAppender(name, filter, layout, ignoreExceptions, kafkaManager);
}
#Override
public final void start() {
if (this.getManager() == null) {
LOGGER.error("No KafkaManager set for the appender named [{}].", this.getName());
}
super.start();
if (this.getManager() != null) {
this.getManager().startup();
}
}
#Override
public final void stop() {
super.stop();
if (this.getManager() != null) {
this.getManager().release();
}
}
public final KafkaManager getManager() {
return this.manager;
}
public void append(LogEvent event) {
this.lock.lock();
try {
String s = event.getMessage().getFormattedMessage();
Message logEvent1 = event.getMessage();
String sp = logEvent1.getFormattedMessage();
this.getManager().send(event.getMessage().getFormattedMessage());
} catch (final Exception e) {
LOGGER.error("Unable to write to kafka [{}] for appender [{}].", this.getManager().getName(), this.getName(), e);
throw new AppenderLoggingException("Unable to write to kafka in appender: " + e.getMessage(), e);
} finally {
this.lock.unlock();
}
}
#Override
public Layout<? extends Serializable> getLayout() {
Layout patternLayout = PatternLayout.createLayout("%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L- %X{sessionId}--%X{guid}- %m #]%n",
null, null, null, true, false, null, null);
return patternLayout;
}
}
In class KafkaAppender, your append method should call getLayout().toByteArray(event) to format the event.
I noticed that the sample code overrides getLayout. I would not recommend this. The AbstractAppender implementation of getLayout returns the configured layout, which allows you to control the layout in configuration without code changes.
#Override
public void append(LogEvent event) {
this.lock.lock();
try {
// let the Layout format the data in the LogEvent object
final byte[] bytes = getLayout().toByteArray(event);
// then pass the byte[] array with the formatted event to the manager
// (I assume that your manager provides this method)
manager.write(bytes, 0, bytes.length);
} catch (Exception e) {
LOGGER.error("Unable to write to kafka [{}] for appender [{}].",
this.getManager().getName(), this.getName(), e);
if (!ignoreExceptions()) {
throw new AppenderLoggingException(
"Unable to write to kafka in appender: " + e.getMessage(), e);
}
} finally {
this.lock.unlock();
}
}
// I would recommend not to override getLayout.
// The AbstractAppender implementation of getLayout returns the configured
// layout, which allows you to control the layout in configuration
// without code changes.
// #Override
// public Layout<? extends Serializable> getLayout() {...
Related
I have a problem when dragging an image from the camera and making it wallpaper to ImageView.
I used the following code and it works on versions less than 5 .
But versions 5 or higher code does not work permanently and the camera does not work at all
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.camera_capimage" android:grantUriPermissions="true" android:installLocation="auto">
<uses-sdk android:minSdkVersion="17" android:targetSdkVersion="28" />
<application android:allowBackup="true" android:icon="#mipmap/ic_launcher" android:label="#string/app_name" android:roundIcon="#mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="#style/AppTheme"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" />
</manifest>
MainActivity
public class MainActivity : AppCompatActivity
{
public static Bitmap bitmap;
public static string fileName;
public ImageView _imageView;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.activity_main);
Button button = FindViewById<Button>(Resource.Id.button1);
_imageView = FindViewById<ImageView>(Resource.Id.imageView1);
button.Click += BtnCamera_Click;
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
Bitmap bitmap = (Bitmap)data.Extras.Get("data");
_imageView.SetImageBitmap(bitmap);
}
private void BtnCamera_Click(object sender, System.EventArgs e)
{
Intent intent = new Intent(MediaStore.ActionImageCapture);
StartActivityForResult(intent, 0);
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
error message
Java.Lang.SecurityException: 'Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE cmp=com.android.camera/.Camera } from ProcessRecord{273b163 23970:com.companyname.camera_capimage/u0a98} (pid=23970, uid=10098) with revoked permission android.permission.CAMERA'
I have a tomcat server with different applications and each one has it's own log4j2 appender which logs to it's own file, each java class logs it's exceptions properly, and for the JSF exceptions such as unclosed tags, wrong class/method/property name I made a defaultexceptionhandler which logs the FacesExceptions, but for some reason I can't find it doesn't log at all.
Here are the specs of the enviroment
Ubuntu 18.04 and 16.04 (tried it on both just in case)
tomcat 8.0.47
oracle jdk 1.8.0_181
myfaces 2.3.1
log4j2 2.11.0
So the thing is that even while debugging the handle() while forcing exceptions by leaving tags open and so on, it does reach the breakpoints on the logger.error() call, but it just doesn't append to the file.
This is the declaration of the handler in the faces-config.xml of the project (I spared all the navigation cases and other junk, all of which works so far, I'll edit it all in if needed)
<faces-config version="2.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<factory>
<exception-handler-factory>
gescoweb.tools.DefaultExceptionHandlerFactory
</exception-handler-factory>
</factory>
</faces-config>
This is the DefaultExceptionHandler.java
package gescoweb.tools;
import java.util.Iterator;
import javax.faces.FacesException;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerWrapper;
import javax.faces.context.FacesContext;
import javax.faces.event.ExceptionQueuedEvent;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
/**
*
*/
public class DefaultExceptionHandler extends ExceptionHandlerWrapper {
private ExceptionHandler wrapped;
public static final Logger LOGGER = LogManager.getLogger("gescoweb");
public DefaultExceptionHandler(ExceptionHandler wrapped) {
this.wrapped = wrapped;
}
#Override
public ExceptionHandler getWrapped() {
return this.wrapped;
}
/** Recibe la lista de excepciones y las procesa segĂșn su tipo.
* #throws FacesException
*/
#Override
public void handle() throws FacesException {
for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) {
Throwable t = i.next().getContext().getException();
if (t != null) {
LOGGER.error("Error inesperado.", new Exception(t));
} else {
LOGGER.error("", new Exception(t));
}
}
getWrapped().handle();
}
/**
* Tratamiento especial de otras excepciones.
* #param facesContext
* #param t
* #return
*/
protected String handleUnexpected(FacesContext facesContext, final Throwable t) {
if (t instanceof IllegalStateException) {
return "key.exception.IllegalStateException";
} else {
super.handle();
return "";
}
}
}
And this is the handler factory
package gescoweb.tools;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerFactory;
/**
*
*/
public class DefaultExceptionHandlerFactory extends ExceptionHandlerFactory {
private ExceptionHandlerFactory parent;
public DefaultExceptionHandlerFactory(ExceptionHandlerFactory parent) {
this.parent = parent;
}
/**
* Crea los handlers modificados.
*
* #return
*/
#Override
public ExceptionHandler getExceptionHandler() {
ExceptionHandler eh = parent.getExceptionHandler();
eh = new DefaultExceptionHandler(eh);
return eh;
}
}
And finally this is the log4j2.xml config file
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="log-path">${sys:catalina.base}/logs</Property>
<Property name="layout"> [%-5level] %d{yyyy-MM-dd HH:mm:ss} %c{1} - %msg%ex%n </Property>
</Properties>
<Appenders>
<!-- console appender -->
<Console name="console-log" target="SYSTEM_OUT">
<PatternLayout pattern="${layout}"/>
</Console>
<!-- logger gescoweb -->
<RollingFile name="gescoweb-log" fileName="${log-path}/gescoweb.log"
filePattern="${log-path}/gescoweb/$${date:yyyy-MM}/gescoweb-%d{dd}.log.gz">
<!-- log pattern -->
<PatternLayout pattern="${layout}"/>
<!-- set file size policy -->
<CronTriggeringPolicy schedule="0 0 12 */7 * ?"/>
</RollingFile>
<!-- logger catalina -->
<RollingFile name="catalina-log"
fileName="${log-path}/catalina.out"
filePattern="${log-path}/catalina/$${date:yyyy-MM}/catalina-%d{dd}.log.gz">
<PatternLayout pattern="${layout}"/>
<CronTriggeringPolicy schedule="0 0 12 */7 * ?"/>
</RollingFile>
<!-- logger localhost -->
<RollingFile name="localhost-log"
fileName="${log-path}/localhost.log"
filePattern="${log-path}/localhost/$${date:yyyy-MM}/localhost-%d{dd}.log.gz">
<PatternLayout pattern="${layout}"/>
<CronTriggeringPolicy schedule="0 0 12 */7 * ?"/>
</RollingFile>
<!-- logger manager -->
<RollingFile name="manager-log"
fileName="${log-path}/manager.log"
filePattern="${log-path}/manager/$${date:yyyy-MM}/manager-%d{dd}.log.gz">
<PatternLayout pattern="${layout}"/>
<CronTriggeringPolicy schedule="0 0 12 */7 * ?"/>
</RollingFile>
<!-- logger host-manager -->
<RollingFile name="host-manager-log"
fileName="${log-path}/host-manager.log"
filePattern="${log-path}/host-manager/$${date:yyyy-MM}/host-manager-%d{dd}.log.gz">
<PatternLayout pattern="${layout}"/>
<CronTriggeringPolicy schedule="0 0 12 */7 * ?"/>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="gescoweb" level="debug">
<AppenderRef ref="gescoweb-log"/>
</Logger>
<Logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost]"
level="info"
additivity="false">
<AppenderRef ref="localhost-log"/>
</Logger>
<Logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager]"
level="info"
additivity="false">
<AppenderRef ref="manager-log"/>
</Logger>
<Logger name="org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager]"
level="info"
additivity="false">
<AppenderRef ref="host-manager-log"/>
</Logger>
<Root level="info">
<AppenderRef ref="console-log"/>
<AppenderRef ref="catalina-log"/>
</Root>
</Loggers>
</Configuration>
Nevermind it, I imported the wrong log4j libraries in the DefaultExceptionHandler, changed the imports to
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
And everything works now...
My requirements are to add logging to every request and response and errors to a file. I have tried this,
public class LogRequestAndResponseHandler : DelegatingHandler
{
private static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// log request body
string requestBody = await request.Content.ReadAsStringAsync();
Log.Info(requestBody);
// let other handlers process the request
var result = await base.SendAsync(request, cancellationToken);
if (result.Content != null)
{
// once response body is ready, log it
var responseBody = await result.Content.ReadAsStringAsync();
Log.Info(responseBody);
}
return result;
}
}
Added this it to WebApiConfig of MVC5 as following,
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
log4net.Config.XmlConfigurator.Configure();
config.MessageHandlers.Add(new LogRequestAndResponseHandler());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Web config has this section,
<log4net>
<!-- file appender -->
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="C:/logs/my_log_file.log"/>
<appendToFile value="true"/>
<rollingStyle value="Date"/>
<maxSizeRollBackups value="30"/>
<datePattern value=".yyyy-MM-dd"/>
<staticLogFileName value="true"/>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline"/>
</layout>
</appender>
<root>
<level value="DEBUG"/>
<appender-ref ref="RollingFileAppender"/>
</root>
</log4net>
Nothing is being added to file and there are no errors, is there anything I am doing wrong, it creates the file but no logging ever ?
I use Spring Security to authenticate a user against an Active Directory server. A CustomUserContext is also injected into the ldapAuthenticationProvider bean to provide access to additional LDAP attributes. Everything works quite well. I have no problem pulling whatever I want from the authenticated user.
The issue I have is that I want to retrieve some attributes, most specifically the email address, from the Active Directory server on a user other than the user that is logged in. Is it possible to achieve this by leveraging what I already have, or is my only option to use a totally separate method to access LDAP attributes from a different user?
[edit]
Configuration follows
security-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
<property name="url" value="ldap://xxxx.xxxx.xxx:389" />
<property name="base" value="dc=corp,dc=global,dc=xxxxx,dc=com" />
<property name="userDn" value="CN=lna.authquery,OU=LDAPGroups,OU=NorthAmerica,DC=corp,DC=global,DC=xxxxx,DC=com" />
<property name="password" value="xxxxxxx" />
<property name="pooled" value="true" />
<!-- AD Specific Setting for avoiding the partial exception error -->
<property name="referral" value="follow" />
</bean>
<bean id="ldapAuthenticationProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider" >
<constructor-arg>
<bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
<constructor-arg ref="contextSource" />
<property name="userSearch">
<bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0" value="" />
<constructor-arg index="1" value="(sAMAccountName={0})" />
<constructor-arg index="2" ref="contextSource" />
</bean>
</property>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
<constructor-arg ref="contextSource" />
<constructor-arg value="" />
<property name="groupSearchFilter" value="(member={0})" />
<property name="searchSubtree" value="true" />
<!-- Settings below convert the adds the prefix ROLE_ to roles returned from AD -->
</bean>
</constructor-arg>
<property name="userDetailsContextMapper">
<bean class="net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper" />
</property>
</bean>
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
<constructor-arg>
<list>
<ref local="ldapAuthenticationProvider" />
</list>
</constructor-arg>
</bean>
<sec:http pattern="/css/**" security="none"/>
<sec:http pattern="/images/**" security="none"/>
<sec:http auto-config="true" authentication-manager-ref="authenticationManager" >
<sec:intercept-url pattern="/login.jsp*" requires-channel="https" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/**" requires-channel="https" access="IS_AUTHENTICATED_FULLY"/>
<sec:form-login login-page='/login.jsp'
default-target-url="/home.html"
authentication-failure-url="/login.jsp" />
</sec:http>
CustomeUserDetails.java
package net.xxxx.xxxx.utilities;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
public class CustomUserDetails extends User {
private static final long serialVersionUID = 1416132138315457558L;
// extra instance variables
final String fullname;
final String email;
final String title;
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities, String fullname,
String email, String title) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired,
accountNonLocked, authorities);
this.fullname = fullname;
this.email = email;
this.title = title;
}
public String getFullname() {
return this.fullname;
}
public String getEmail() {
return this.email;
}
public String getTitle() {
return this.title;
}
}
CustomUserDetailsContextMapper.java
package net.xxxx.xxxxx.utilities;
import java.util.Collection;
public class CustomUserDetailsContextMapper implements UserDetailsContextMapper {
public UserDetails mapUserFromContext(DirContextOperations ctx,
String username, Collection<? extends GrantedAuthority> authorities) {
String fullname = "";
String email = "";
String title = "";
Attributes attributes = ctx.getAttributes();
try {
fullname = (String) attributes.get("displayName").get();
email = (String) attributes.get("mail").get();
title = (String) attributes.get("title").get();
} catch (NamingException e) {
e.printStackTrace();
}
CustomUserDetails details = new CustomUserDetails(username, "", true, true, true, true, authorities, fullname, email, title);
return details;
}
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
}
}
I finally did end up figuring out how to do this. I'm answering this in case it helps someone else who needs to do this. I'd be surprised if I'm the only one.
First I had to move my security-config.xml file out of the WEB-INF structure and put it under the spring resources directory. The contextSource bean I was able to reuse. However I could not reuse the CustomUserDetailsContextMapper.java nor the CustomUserDetails.java class as they were too specific to Spring security and not to just retrieving LDAP data from an unauthenticated user.
I ended up writing a separate class for the LDAP access that had the common contextSource autowired in. That class is below.
LdapDao.java
package net.xxxxx.xxx.dao;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import javax.naming.directory.Attributes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.stereotype.Component;
#Component
public class LdapDao {
LdapTemplate template;
#Autowired
public LdapDao(LdapContextSource contextSource) {
template = new LdapTemplate(contextSource);
}
#SuppressWarnings("unchecked")
public Map<String, String> getUserAttributes(String username) {
Map<String, String> results = new HashMap<String, String>();
String objectClass = "samAccountName=" + username;
LinkedList<Map<String, String>> list = (LinkedList<Map<String, String>>) template.search("", objectClass, new UserAttributesMapper());
if (!list.isEmpty()) {
// Should only return one item
results = list.get(0);
}
return results;
}
private class UserAttributesMapper implements AttributesMapper {
#Override
public Map<String, String> mapFromAttributes(Attributes attributes) throws javax.naming.NamingException {
Map<String, String> map = new HashMap<String, String>();
String fullname = (String) attributes.get("displayName").get();
String email = (String) attributes.get("mail").get();
String title = (String) attributes.get("title").get();
map.put("fullname", fullname);
map.put("email", email);
map.put("title", title);
return map;
}
}
}
#Bill what you've done is great, though there is actually an easier way. Instead of resorting to the LdapTemplate, just use the beans you've already registered for DefaultLdapAuthoritiesPopulator and FilterBasedLdapUserSearch. This way you can get the same UserDetails object which also has the authorities populated and reuses your existing code for your net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper.
Here's what you need to do:
Split out the beens you need to inject as named beans and use ref attributes for the properties and constructor-args (DefaultLdapAuthoritiesPopulator, FilterBasedLdapUserSearch, net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper).
In your LdapDao inject references to:
FilterBasedLdapUserSearch - userSearch
DefaultLdapAuthoritiesPopulator - authPop
net.xxxx.xxxxx.utilities.CustomUserDetailsContextMapper - userMapper
Add the following method to your LdapDao:
.
public UserDetails getUserDetails(final String username) {
try {
DirContextOperations ctx = userSearch.searchForUser(username);
return userMapper.mapUserFromContext(ctx, username,
authPop.getGrantedAuthorities(ctx, username));
} catch (UsernameNotFoundException ex) {
return null;
}
}
Now you can just call getUserDetails(String) to get the same object you do when retrieving the currently logged in context, and can use the same code etc.
The default behavior of the Channel Processors is to do a sendRedirect (which is redirect temporary with 302 code). I need to change this behavior so that a permanent (301) redirect is done instead of 302 redirect. I tried to do the following:
Create a custom ChannelProcessingFilter by extending the ChannelProcessingFilter:
public class MyChannelProcessingFilter extends ChannelProcessingFilter{
//No implementation, I needed this to just make sure that a custom filter is created and I can configure it as a custom filter in the xml file.
}
Create a custom EntryPoint by extending the AbstractRetryEntryPoint
public class RetryWithHttpsEntryPoint extends org.springframework.security.web.access.channel.AbstractRetryEntryPoint {
private PortResolver portResolver = new PortResolverImpl();
private final String scheme ="https://";
/** The standard port for the scheme (80 for http, 443 for https) */
private final int standardPort = 443;
public RetryWithHttpsEntryPoint() {
super("https://", 443);
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse res) throws IOException, ServletException {
String queryString = request.getQueryString();
String redirectUrl = request.getRequestURI() + ((queryString == null) ? "" : ("?" + queryString));
Integer currentPort = new Integer(portResolver.getServerPort(request));
Integer redirectPort = getMappedPort(currentPort);
if (redirectPort != null) {
boolean includePort = redirectPort.intValue() != standardPort;
redirectUrl = scheme + request.getServerName() + ((includePort) ? (":" + redirectPort) : "") + redirectUrl;
}
if (logger.isDebugEnabled()) {
logger.debug("Redirecting to: " + redirectUrl);
}
res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
res.setHeader("Location", redirectUrl);
res.setHeader("Connection", "close");
}
protected Integer getMappedPort(Integer mapFromPort) {
return getPortMapper().lookupHttpsPort(mapFromPort);
}
}
Configure the same in the applicationContext-security.xml file. I am putting the complete xml file for your reference (removing the parts that are not needed. If you require the other parts do let me know)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<security:http auto-config="false"
entry-point-ref="authenticationProcessingFilterEntryPoint"
access-decision-manager-ref="accessDecisionManager" >
<security:intercept-url pattern="/activ8/protectedCheckEligibility.html**" access="user" requires-channel="https"/>
<security:intercept-url pattern="/siteMap.html" access="ROLE_ANONYMOUS,user,admin" requires-channel="http"/>
<security:intercept-url pattern="/privacyPolicy.html" access="ROLE_ANONYMOUS,user,admin" requires-channel="http"/>
<!-- other urls configured over here -->
<security:intercept-url pattern="/*.jsp" access="ROLE_ANONYMOUS,admin,user" requires-channel="https"/>
<security:intercept-url pattern="/**/*.html**" access="ROLE_ANONYMOUS,user,admin" requires-channel="https"/>
<security:intercept-url pattern="/fb_activities.html**" access="parent" />
<security:remember-me key="appfuseRocks" />
<security:custom-filter position="SWITCH_USER_FILTER" ref="careSwitchUserProcessingFilter"/>
<security:custom-filter position="FORM_LOGIN_FILTER" ref="myCustomAuthenticationProcessingFilter"/>
<!-- configured the custom channel filter over here -->
<security:custom-filter position="CHANNEL_FILTER" ref="myChannelProcessingFilter"/>
</security:http>
<bean id="myChannelProcessingFilter" class="com.my.webapp.filter.myChannelProcessingFilter">
<property name="channelDecisionManager" ref="channelDecisionManager" />
<property name="securityMetadataSource">
<security:filter-security-metadata-source path-type="ant">
<security:intercept-url pattern="/**" access="REQUIRES_INSECURE_CHANNEL" />
</security:filter-security-metadata-source>
</property>
</bean>
<bean id="channelDecisionManager" class="org.springframework.security.web.access.channel.ChannelDecisionManagerImpl">
<property name="channelProcessors">
<list>
<ref bean="secureChannelProcessor"/>
</list>
</property>
</bean>
<bean id="secureChannelProcessor" class="org.springframework.security.web.access.channel.SecureChannelProcessor">
<property name="entryPoint" ref="secureChannelEntryPoint"/>
<!-- <property name="portMapper" ref="portMapper" /> -->
<property name="secureKeyword" value="REQUIRES_SECURE_CHANNEL"/>
</bean>
<bean id="secureChannelEntryPoint" class="com.my.webapp.filter.RetryWithHttpsEntryPoint"/>
<!-- lot of other configuratons... removed -->
</beans>
I am getting following errors when I try to run my tomcat:
ERROR 2011-12-26 21:13:21,569 [ina].[localhost].[/]]: Exception sending context initialized event to listener instance of class com.kajeet.webapp.listener.StartupListener
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Filter beans '' and 'Root bean: class [org.springframework.security.web.access.channel.ChannelProcessingFilter]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null' have the same 'order' value. When using custom filters, please make sure the positions do not conflict with default filters. Alternatively you can disable the default filters by removing the corresponding child elements from and avoiding the use of .
Offending resource: ServletContext resource [/WEB-INF/applicationContext-security.xml]
at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68)
at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85)
at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:72)
at org.springframework.security.config.http.HttpSecurityBeanDefinitionParser.checkFilterChainOrder(HttpSecurityBeanDefinitionParser.java:196)
at org.springframework.security.config.http.HttpSecurityBeanDefinitionParser.parse(HttpSecurityBeanDefinitionParser.java:132)
at org.springframework.security.config.SecurityNamespaceHandler.parse(SecurityNamespaceHandler.java:86)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1335)
at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1325)
at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:135)
at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:93)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334)
at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:143)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:178)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:149)
at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:124)
at org.springframework.web.context.support.XmlWebApplicationContext.loadBeanDefinitions(XmlWebApplicationContext.java:93)
at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:130)
at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:467)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:397)
at org.springframework.web.context.ContextLoader.createWebApplicationContext(ContextLoader.java:276)
at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:197)
at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:47)
at com.kajeet.webapp.listener.StartupListener.contextInitialized(StartupListener.java:51)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:3764)
at org.apache.catalina.core.StandardContext.start(StandardContext.java:4216)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:760)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:740)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:544)
at org.apache.catalina.startup.HostConfig.deployDirectory(HostConfig.java:920)
at org.apache.catalina.startup.HostConfig.deployDirectories(HostConfig.java:883)
at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:492)
at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1138)
at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java:311)
at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:120)
at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1022)
at org.apache.catalina.core.StandardHost.start(StandardHost.java:736)
at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1014)
at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:443)
at org.apache.catalina.core.StandardService.start(StandardService.java:448)
at org.apache.catalina.core.StandardServer.start(StandardServer.java:700)
at org.apache.catalina.startup.Catalina.start(Catalina.java:552)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:295)
at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:433)
I have also overridden other filters and it does not complain about those. This application was running perfectly fine before. We had this additional requirement and hence I added the new filter and ran into such errors.
Second approach that I tried is just configuring the default ChannelProcessingFilter in the XML, since in Spring 3.0 the filters are automatically called, I was under impression that I can configure them in XML file and spring will automatically load them, but it didn't:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<security:http auto-config="false"
entry-point-ref="authenticationProcessingFilterEntryPoint"
access-decision-manager-ref="accessDecisionManager" >
<security:intercept-url pattern="/activ8/protectedCheckEligibility.html**" access="user" requires-channel="https"/>
<security:intercept-url pattern="/siteMap.html" access="ROLE_ANONYMOUS,user,admin" requires-channel="http"/>
<security:intercept-url pattern="/privacyPolicy.html" access="ROLE_ANONYMOUS,user,admin" requires-channel="http"/>
<!-- other urls configured over here -->
<security:intercept-url pattern="/*.jsp" access="ROLE_ANONYMOUS,admin,user" requires-channel="https"/>
<security:intercept-url pattern="/**/*.html**" access="ROLE_ANONYMOUS,user,admin" requires-channel="https"/>
<security:intercept-url pattern="/fb_activities.html**" access="parent" />
<security:remember-me key="appfuseRocks" />
<security:custom-filter position="SWITCH_USER_FILTER" ref="careSwitchUserProcessingFilter"/>
<security:custom-filter position="FORM_LOGIN_FILTER" ref="myCustomAuthenticationProcessingFilter"/>
</security:http>
<bean id="channelDecisionManager" class="org.springframework.security.securechannel.ChannelDecisionManagerImpl">
<property name="channelProcessors">
<list>
<ref bean="secureChannelProcessor"/>
<ref bean="insecureChannelProcessor"/>
</list>
</property>
</bean>
<bean id="secureChannelProcessor" class="org.springframework.security.web.access.channel.SecureChannelProcessor"/>
<bean id="insecureChannelProcessor" class="org.springframework.security.web.access.channel.InsecureChannelProcessor"/>
<!-- lot of other configuratons... removed -->
</beans>
Any help will be definitely appreciated. I am not a Spring pro, but I have done some work on it, a pointer or two may definitely help me to resolve this. Thank you in advance
Solution:
The issue is that we cannot have both the security:http as well as the myChannelProcessingFilter (the one I had overridden) to deal with the access argument of the security:intercept-url, hence I removed the http tag and added the access thing in the myChannelProcessingFilter, where I wanted it to process. The XML that resolved it is follows
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<!--
The http element responsible for creating a FilterChainProxy and the filter beans which it uses.
Common problems like incorrect filter ordering are no longer an issue as the filter positions are predefined.
-->
<security:http auto-config="false"
entry-point-ref="authenticationProcessingFilterEntryPoint"
access-decision-manager-ref="accessDecisionManager" >
<security:custom-filter position="CHANNEL_FILTER" ref="channelProcessingFilter"/>
<security:intercept-url pattern="/*.html*" access="ROLE_ANONYMOUS,admin,user" />
<security:intercept-url pattern="/*.jsp" access="ROLE_ANONYMOUS,admin,user" />
<security:intercept-url pattern="/**/*.html**" access="ROLE_ANONYMOUS,user,admin" />
</security:http>
<bean id="channelProcessingFilter" class="org.springframework.security.web.access.channel.ChannelProcessingFilter">
<property name="channelDecisionManager" ref="channelDecisionManager"/>
<property name="securityMetadataSource">
<security:filter-security-metadata-source path-type="ant">
<security:intercept-url pattern="/*.jsp**" access="REQUIRES_SECURE_CHANNEL" />
<security:intercept-url pattern="/**/*.html**" access="REQUIRES_SECURE_CHANNEL" />
</security:filter-security-metadata-source>
</property>
</bean>
<bean id="channelDecisionManager" class="org.springframework.security.web.access.channel.ChannelDecisionManagerImpl">
<property name="channelProcessors">
<list>
<ref bean="secureProcessor"/>
<ref bean="insecureProcessor"/>
</list>
</property>
</bean>
<bean id="secureProcessor" class="org.springframework.security.web.access.channel.SecureChannelProcessor" >
<property name="entryPoint" ref="retryWithHttps"/>
</bean>
<bean id="insecureProcessor" class="org.springframework.security.web.access.channel.InsecureChannelProcessor">
<property name="entryPoint" ref="retryWithHttp"/>
</bean>
<bean id="retryWithHttps" class="com.my.webapp.filter.RetryWithHttpsEntryPoint" />
<bean id="retryWithHttp" class="com.my.webapp.filter.RetryWithHttpEntryPoint" />
</beans>
I found another way to achieve the same thing with much less code and complexity. You can simply use a BeanPostProcessor to get the SecureChannelProcessor and InsecureChannelProcessor and then set your own entry point on them. That way, you can still use the defaults on everything else.
The BeanPostProcessor:
#Component
public class ChannelProcessorsPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
if (bean instanceof SecureChannelProcessor) ((SecureChannelProcessor)bean).setEntryPoint(new MyEntryRetryPoint("https://", 443));
else if (bean instanceof InsecureChannelProcessor) ((InsecureChannelProcessor)bean).setEntryPoint(new MyEntryRetryPoint("http://", 80));
return bean;
}
#Override
public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
return bean;
}
}
I think it's better to write a redirect strategy:
#Component
public class PermanentRedirectStrategy implements RedirectStrategy {
private boolean contextRelative;
#Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
response.setHeader("Location", response.encodeRedirectURL(calculateRedirectUrl(request.getContextPath(), url)));
}
/**
* Unfortunately DefaultRedirectStrategy.calculateRedirectUrl is private
* If this weren't the case, we could extend this class from DefaultRedirectStrategy
* to use its method directly without copying it
*/
private String calculateRedirectUrl(String contextPath, String url) {
if (!UrlUtils.isAbsoluteUrl(url)) {
if (contextRelative) {
return url;
} else {
return contextPath + url;
}
}
// Full URL, including http(s)://
if (!contextRelative) {
return url;
}
// Calculate the relative URL from the fully qualified URL, minus the last
// occurence of the scheme and base context
url = url.substring(url.lastIndexOf("://") + 3); // strip off scheme
url = url.substring(url.indexOf(contextPath) + contextPath.length());
if (url.length() > 1 && url.charAt(0) == '/') {
url = url.substring(1);
}
return url;
}
}
and then setting it to the existing entry point:
#Component
public class ChannelProcessorsPostProcessor implements BeanPostProcessor {
#Autowired
private RedirectStrategy permanentRedirectStrategy;
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
ChannelEntryPoint entryPoint = null;
if (bean instanceof SecureChannelProcessor) {
entryPoint = ((SecureChannelProcessor) bean).getEntryPoint();
} else if (bean instanceof InsecureChannelProcessor) {
entryPoint = ((InsecureChannelProcessor) bean).getEntryPoint();
}
if (entryPoint != null && AbstractRetryEntryPoint.class.isAssignableFrom(entryPoint.getClass())) {
((AbstractRetryEntryPoint) entryPoint).setRedirectStrategy(permanentRedirectStrategy);
}
return bean;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}