2
0
Ben Alex 21 жил өмнө
parent
commit
d5c14142d1

+ 40 - 5
core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java

@@ -22,11 +22,18 @@ import net.sf.acegisecurity.BadCredentialsException;
 import net.sf.acegisecurity.DisabledException;
 import net.sf.acegisecurity.providers.AuthenticationProvider;
 import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent;
+import net.sf.acegisecurity.providers.dao.event.AuthenticationFailurePasswordEvent;
+import net.sf.acegisecurity.providers.dao.event.AuthenticationSuccessEvent;
 import net.sf.acegisecurity.providers.encoding.PasswordEncoder;
 import net.sf.acegisecurity.providers.encoding.PlaintextPasswordEncoder;
 
+import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.InitializingBean;
 
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
 import org.springframework.dao.DataAccessException;
 
 import java.util.Date;
@@ -56,14 +63,23 @@ import java.util.Date;
  * <code>UsernamePasswordAuthenticationToken</code>. This avoids complications
  * if the user changes their password during the session.
  * </p>
+ * 
+ * <P>
+ * If an application context is detected (which is automatically the case when
+ * the bean is started within a Spring container), application events will be
+ * published to the context. See {@link
+ * net.sf.acegisecurity.providers.dao.event.AuthenticationEvent} for further
+ * information.
+ * </p>
  *
  * @author Ben Alex
  * @version $Id$
  */
 public class DaoAuthenticationProvider implements AuthenticationProvider,
-    InitializingBean {
+    InitializingBean, ApplicationContextAware {
     //~ Instance fields ========================================================
 
+    private ApplicationContext ctx;
     private AuthenticationDao authenticationDao;
     private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();
     private SaltSource saltSource;
@@ -72,6 +88,11 @@ public class DaoAuthenticationProvider implements AuthenticationProvider,
 
     //~ Methods ================================================================
 
+    public void setApplicationContext(ApplicationContext applicationContext)
+        throws BeansException {
+        this.ctx = applicationContext;
+    }
+
     public void setAuthenticationDao(AuthenticationDao authenticationDao) {
         this.authenticationDao = authenticationDao;
     }
@@ -175,6 +196,15 @@ public class DaoAuthenticationProvider implements AuthenticationProvider,
             throw new AuthenticationServiceException(repositoryProblem
                 .getMessage(), repositoryProblem);
         }
+        
+        if (!user.isEnabled()) {
+            if (this.ctx != null) {
+                ctx.publishEvent(new AuthenticationFailureDisabledEvent(
+                        authentication, user));
+            }
+
+            throw new DisabledException("User is disabled");
+        }
 
         if (!(authentication instanceof DaoAuthenticationToken)) {
             // Must validate credentials, as this is not simply a token refresh
@@ -186,17 +216,22 @@ public class DaoAuthenticationProvider implements AuthenticationProvider,
 
             if (!passwordEncoder.isPasswordValid(user.getPassword(),
                     authentication.getCredentials().toString(), salt)) {
+                if (this.ctx != null) {
+                    ctx.publishEvent(new AuthenticationFailurePasswordEvent(
+                            authentication, user));
+                }
+
                 throw new BadCredentialsException("Bad credentials presented");
             }
         }
 
-        if (!user.isEnabled()) {
-            throw new DisabledException("User is disabled");
-        }
-
         Date expiry = new Date(new Date().getTime()
                 + this.getRefreshTokenInterval());
 
+        if (this.ctx != null) {
+            ctx.publishEvent(new AuthenticationSuccessEvent(authentication, user));
+        }
+
         return new DaoAuthenticationToken(this.getKey(), expiry,
             user.getUsername(), user.getPassword(), user.getAuthorities());
     }

+ 83 - 0
core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationEvent.java

@@ -0,0 +1,83 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.dao.event;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.providers.dao.User;
+
+import org.springframework.context.ApplicationEvent;
+
+
+/**
+ * Represents a <code>net.sf.acegisecurity.provider.dao</code> application
+ * event.
+ * 
+ * <P>
+ * Subclasses exist for different types of authentication events. All
+ * authentication events relate to a particular {@link User} and are caused by
+ * a particular {@link Authentication} object. This is intended to permit
+ * logging of successful and unsuccessful login attempts, and facilitate the
+ * locking of accounts.
+ * </p>
+ * 
+ * <P>
+ * The <code>ApplicationEvent</code>'s <code>source</code> will be the
+ * <code>Authentication</code> object.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public abstract class AuthenticationEvent extends ApplicationEvent {
+    //~ Instance fields ========================================================
+
+    private User user;
+
+    //~ Constructors ===========================================================
+
+    public AuthenticationEvent(Authentication authentication, User user) {
+        super(authentication);
+
+        // No need to check authentication isn't null, as done by super
+        if (user == null) {
+            throw new IllegalArgumentException("User is required");
+        }
+
+        this.user = user;
+    }
+
+    //~ Methods ================================================================
+
+    /**
+     * Getters for the <code>Authentication</code> request that caused the
+     * event. Also available from <code>super.getSource()</code>.
+     *
+     * @return the authentication request
+     */
+    public Authentication getAuthentication() {
+        return (Authentication) super.getSource();
+    }
+
+    /**
+     * Getter for the <code>User</code> related to the
+     * <code>Authentication</code> attempt.
+     *
+     * @return the user
+     */
+    public User getUser() {
+        return user;
+    }
+}

+ 36 - 0
core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureDisabledEvent.java

@@ -0,0 +1,36 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.dao.event;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.providers.dao.User;
+
+
+/**
+ * Application event which indicates authentication failure due to the user's
+ * account being locked.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AuthenticationFailureDisabledEvent extends AuthenticationEvent {
+    //~ Constructors ===========================================================
+
+    public AuthenticationFailureDisabledEvent(Authentication authentication,
+        User user) {
+        super(authentication, user);
+    }
+}

+ 36 - 0
core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailurePasswordEvent.java

@@ -0,0 +1,36 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.dao.event;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.providers.dao.User;
+
+
+/**
+ * Application event which indicates authentication failure due to invalid
+ * password.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AuthenticationFailurePasswordEvent extends AuthenticationEvent {
+    //~ Constructors ===========================================================
+
+    public AuthenticationFailurePasswordEvent(Authentication authentication,
+        User user) {
+        super(authentication, user);
+    }
+}

+ 34 - 0
core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationSuccessEvent.java

@@ -0,0 +1,34 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.dao.event;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.providers.dao.User;
+
+
+/**
+ * Application event which indicates successful authentication.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AuthenticationSuccessEvent extends AuthenticationEvent {
+    //~ Constructors ===========================================================
+
+    public AuthenticationSuccessEvent(Authentication authentication, User user) {
+        super(authentication, user);
+    }
+}

+ 75 - 0
core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java

@@ -0,0 +1,75 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.dao.event;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+
+
+/**
+ * Outputs authentication-related application events to Commons Logging.
+ * 
+ * <P>
+ * All authentication failures are logged at the warning level, whilst
+ * authentication successes are logged at the information level.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class LoggerListener implements ApplicationListener {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(LoggerListener.class);
+
+    //~ Methods ================================================================
+
+    public void onApplicationEvent(ApplicationEvent event) {
+        if (event instanceof AuthenticationFailurePasswordEvent) {
+            AuthenticationFailurePasswordEvent authEvent = (AuthenticationFailurePasswordEvent) event;
+
+            if (logger.isWarnEnabled()) {
+                logger.warn("Authentication failed due to incorrect password for user: "
+                    + authEvent.getUser().getUsername() + "; details: "
+                    + authEvent.getAuthentication().getDetails());
+            }
+        }
+
+        if (event instanceof AuthenticationFailureDisabledEvent) {
+            AuthenticationFailureDisabledEvent authEvent = (AuthenticationFailureDisabledEvent) event;
+
+            if (logger.isWarnEnabled()) {
+                logger.warn(
+                    "Authentication failed due to account being disabled for user: "
+                    + authEvent.getUser().getUsername() + "; details: "
+                    + authEvent.getAuthentication().getDetails());
+            }
+        }
+
+        if (event instanceof AuthenticationSuccessEvent) {
+            AuthenticationSuccessEvent authEvent = (AuthenticationSuccessEvent) event;
+
+            if (logger.isInfoEnabled()) {
+                logger.info("Authentication success for user: "
+                    + authEvent.getUser().getUsername() + "; details: "
+                    + authEvent.getAuthentication().getDetails());
+            }
+        }
+    }
+}

+ 9 - 0
core/src/main/java/org/acegisecurity/providers/dao/event/package.html

@@ -0,0 +1,9 @@
+<html>
+<body>
+Enables events to be published to the Spring application context.
+
+<P>The <code>DaoAuthenticationProvider</code> automatically publishes
+events to the application context. These events are received by all
+registered Spring <code>ApplicationListener</code>s.</P>
+</body>
+</html>

+ 105 - 0
core/src/test/java/org/acegisecurity/providers/dao/event/AuthenticationEventTests.java

@@ -0,0 +1,105 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.dao.event;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.providers.dao.User;
+
+
+/**
+ * Tests {@link AuthenticationEvent} and its subclasses.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AuthenticationEventTests extends TestCase {
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AuthenticationEventTests.class);
+    }
+
+    public void testDisabledEvent() {
+        Authentication auth = getAuthentication();
+        User user = getUser();
+        AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(auth,
+                user);
+        assertEquals(auth, event.getAuthentication());
+        assertEquals(user, event.getUser());
+    }
+
+    public void testPasswordEvent() {
+        Authentication auth = getAuthentication();
+        User user = getUser();
+        AuthenticationFailurePasswordEvent event = new AuthenticationFailurePasswordEvent(auth,
+                user);
+        assertEquals(auth, event.getAuthentication());
+        assertEquals(user, event.getUser());
+    }
+
+    public void testRejectsNullAuthentication() {
+        try {
+            AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(null,
+                    getUser());
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testRejectsNullUser() {
+        try {
+            AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(getAuthentication(),
+                    null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testSuccessEvent() {
+        Authentication auth = getAuthentication();
+        User user = getUser();
+        AuthenticationSuccessEvent event = new AuthenticationSuccessEvent(auth,
+                user);
+        assertEquals(auth, event.getAuthentication());
+        assertEquals(user, event.getUser());
+    }
+
+    private Authentication getAuthentication() {
+        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("Principal",
+                "Credentials");
+        authentication.setDetails("127.0.0.1");
+
+        return authentication;
+    }
+
+    private User getUser() {
+        User user = new User("foo", "bar", true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_FOOBAR")});
+
+        return user;
+    }
+}

+ 82 - 0
core/src/test/java/org/acegisecurity/providers/dao/event/LoggerListenerTests.java

@@ -0,0 +1,82 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.dao.event;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.providers.dao.User;
+
+
+/**
+ * Tests {@link LoggerListener}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class LoggerListenerTests extends TestCase {
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(LoggerListenerTests.class);
+    }
+
+    public void testLogsDisabledEvents() {
+        AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(getAuthentication(),
+                getUser());
+        LoggerListener listener = new LoggerListener();
+        listener.onApplicationEvent(event);
+        assertTrue(true);
+    }
+
+    public void testLogsPasswordEvents() {
+        AuthenticationFailurePasswordEvent event = new AuthenticationFailurePasswordEvent(getAuthentication(),
+                getUser());
+        LoggerListener listener = new LoggerListener();
+        listener.onApplicationEvent(event);
+        assertTrue(true);
+    }
+
+    public void testLogsSuccessEvents() {
+        AuthenticationSuccessEvent event = new AuthenticationSuccessEvent(getAuthentication(),
+                getUser());
+        LoggerListener listener = new LoggerListener();
+        listener.onApplicationEvent(event);
+        assertTrue(true);
+    }
+
+    private Authentication getAuthentication() {
+        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("Principal",
+                "Credentials");
+        authentication.setDetails("127.0.0.1");
+
+        return authentication;
+    }
+
+    private User getUser() {
+        User user = new User("foo", "bar", true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_FOOBAR")});
+
+        return user;
+    }
+}

+ 61 - 2
docs/reference/src/index.xml

@@ -7,7 +7,7 @@
 
     <subtitle>Reference Documentation</subtitle>
 
-    <releaseinfo>0.5</releaseinfo>
+    <releaseinfo>0.51</releaseinfo>
 
     <authorgroup>
       <author>
@@ -76,7 +76,9 @@
           <listitem>
             <para>An <literal>Authentication</literal> object which holds the
             principal, credentials and the authorities granted to the
-            principal.</para>
+            principal. The object can also store additional information
+            associated with an authentication request, such as the source
+            TCP/IP address.</para>
           </listitem>
 
           <listitem>
@@ -952,6 +954,63 @@
         desired.</para>
       </sect2>
 
+      <sect2 id="security-authentication-provider-in-memory">
+        <title>Event Publishing</title>
+
+        <para>The <literal>DaoAuthenticationProvider</literal> automatically
+        obtains the <literal>ApplicationContext</literal> it is running in at
+        startup time. This allows the provider to publish events through the
+        standard Spring event framework. Three types of event messages are
+        published:</para>
+
+        <itemizedlist spacing="compact">
+          <listitem>
+            <para><literal>AuthenticationSuccessEvent</literal> is published
+            when an authentication request is successful.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>AuthenticationFailureDisabledEvent</literal> is
+            published when an authentication request is unsuccessful because
+            the returned <literal>User</literal> is disabled. This is normally
+            the case when an account is locked.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>AuthenticationFailurePasswordEvent</literal> is
+            published when an authentication request is unsuccessful because
+            the presented password did not match that in the
+            <literal>User</literal>.</para>
+          </listitem>
+        </itemizedlist>
+
+        <para>Each event contains two objects: the
+        <literal>Authentication</literal> object that represented the
+        authentication request, and the <literal>User</literal> object that
+        was found in response to the authentication request. The
+        <literal>Authentication</literal> interface provides a
+        <literal>getDetails()</literal> method which often includes
+        information that event consumers may find useful (eg the TCP/IP
+        address that the authentication request originated from).</para>
+
+        <para>As per standard Spring event handling, you can receive these
+        events by adding a bean to the application context which implements
+        the <literal>ApplicationListener</literal> interface. Included with
+        Acegi Security is a <literal>LoggerListener</literal> class which
+        receives these events and publishes their details to Commons Logging.
+        Refer to the JavaDocs for <literal>LoggerListener</literal> for
+        details on the logging priorities used for different message
+        types.</para>
+
+        <para>This event publishing system enables you to implement account
+        locking and record authentication event history. This might be of
+        interest to application users, who can be advised of the times and
+        source IP address of all unsuccessful password attempts (and account
+        lockouts) since their last successful login. Such capabilities are
+        simple to implement and greatly improve the security of your
+        application.</para>
+      </sect2>
+
       <sect2 id="security-authentication-provider-in-memory">
         <title>In-Memory Authentication</title>
 

+ 3 - 0
samples/contacts/etc/filter/applicationContext.xml

@@ -46,6 +46,9 @@
 		<property name="key"><value>my_password</value></property>
 	</bean>
 
+	<!-- Automatically receives AuthenticationEvent messages from DaoAuthenticationProvider -->
+	<bean id="loggerListener" class="net.sf.acegisecurity.providers.dao.event.LoggerListener"/>
+
 	<bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
 		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
 		<property name="authenticationEntryPoint"><ref bean="basicProcessingFilterEntryPoint"/></property>