View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.acegisecurity.userdetails.jdbc;
17  
18  import org.acegisecurity.GrantedAuthority;
19  import org.acegisecurity.GrantedAuthorityImpl;
20  
21  import org.acegisecurity.userdetails.User;
22  import org.acegisecurity.userdetails.UserDetails;
23  import org.acegisecurity.userdetails.UserDetailsService;
24  import org.acegisecurity.userdetails.UsernameNotFoundException;
25  
26  import org.springframework.context.ApplicationContextException;
27  
28  import org.springframework.dao.DataAccessException;
29  
30  import org.springframework.jdbc.core.SqlParameter;
31  import org.springframework.jdbc.core.support.JdbcDaoSupport;
32  import org.springframework.jdbc.object.MappingSqlQuery;
33  
34  import java.sql.ResultSet;
35  import java.sql.SQLException;
36  import java.sql.Types;
37  
38  import java.util.List;
39  
40  import javax.sql.DataSource;
41  
42  
43  /**
44   * <p>Retrieves user details (username, password, enabled flag, and authorities) from a JDBC location.</p>
45   *  <p>A default database structure is assumed, (see {@link #DEF_USERS_BY_USERNAME_QUERY} and {@link
46   * #DEF_AUTHORITIES_BY_USERNAME_QUERY}, which most users of this class will need to override, if using an existing
47   * scheme. This may be done by setting the default query strings used. If this does not provide enough flexibility,
48   * another strategy would be to subclass this class and override the {@link MappingSqlQuery} instances used, via the
49   * {@link #initMappingSqlQueries()} extension point.</p>
50   *  <p>In order to minimise backward compatibility issues, this DAO does not recognise the expiration of user
51   * accounts or the expiration of user credentials. However, it does recognise and honour the user enabled/disabled
52   * column.</p>
53   *
54   * @author Ben Alex
55   * @author colin sampaleanu
56   * @version $Id: JdbcDaoImpl.java 1784 2007-02-24 21:00:24Z luke_t $
57   */
58  public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService {
59      //~ Static fields/initializers =====================================================================================
60  
61      public static final String DEF_USERS_BY_USERNAME_QUERY =
62              "SELECT username,password,enabled FROM users WHERE username = ?";
63      public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
64              "SELECT username,authority FROM authorities WHERE username = ?";
65  
66      //~ Instance fields ================================================================================================
67  
68      protected MappingSqlQuery authoritiesByUsernameMapping;
69      protected MappingSqlQuery usersByUsernameMapping;
70      private String authoritiesByUsernameQuery;
71      private String rolePrefix = "";
72      private String usersByUsernameQuery;
73      private boolean usernameBasedPrimaryKey = true;
74  
75      //~ Constructors ===================================================================================================
76  
77      public JdbcDaoImpl() {
78          usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;
79          authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;
80      }
81  
82      //~ Methods ========================================================================================================
83  
84      /**
85       * Allows subclasses to add their own granted authorities to the list to be returned in the
86       * <code>User</code>.
87       *
88       * @param username the username, for use by finder methods
89       * @param authorities the current granted authorities, as populated from the <code>authoritiesByUsername</code>
90       *        mapping
91       */
92      protected void addCustomAuthorities(String username, List authorities) {}
93  
94      public String getAuthoritiesByUsernameQuery() {
95          return authoritiesByUsernameQuery;
96      }
97  
98      public String getRolePrefix() {
99          return rolePrefix;
100     }
101 
102     public String getUsersByUsernameQuery() {
103         return usersByUsernameQuery;
104     }
105 
106     protected void initDao() throws ApplicationContextException {
107         initMappingSqlQueries();
108     }
109 
110     /**
111      * Extension point to allow other MappingSqlQuery objects to be substituted in a subclass
112      */
113     protected void initMappingSqlQueries() {
114         this.usersByUsernameMapping = new UsersByUsernameMapping(getDataSource());
115         this.authoritiesByUsernameMapping = new AuthoritiesByUsernameMapping(getDataSource());
116     }
117 
118     public boolean isUsernameBasedPrimaryKey() {
119         return usernameBasedPrimaryKey;
120     }
121 
122     public UserDetails loadUserByUsername(String username)
123         throws UsernameNotFoundException, DataAccessException {
124         List users = usersByUsernameMapping.execute(username);
125 
126         if (users.size() == 0) {
127             throw new UsernameNotFoundException("User not found");
128         }
129 
130         UserDetails user = (UserDetails) users.get(0); // contains no GrantedAuthority[]
131 
132         List dbAuths = authoritiesByUsernameMapping.execute(user.getUsername());
133 
134         addCustomAuthorities(user.getUsername(), dbAuths);
135 
136         if (dbAuths.size() == 0) {
137             throw new UsernameNotFoundException("User has no GrantedAuthority");
138         }
139 
140         GrantedAuthority[] arrayAuths = (GrantedAuthority[]) dbAuths.toArray(new GrantedAuthority[dbAuths.size()]);
141 
142         String returnUsername = user.getUsername();
143 
144         if (!usernameBasedPrimaryKey) {
145             returnUsername = username;
146         }
147 
148         return new User(returnUsername, user.getPassword(), user.isEnabled(), true, true, true, arrayAuths);
149     }
150 
151     /**
152      * Allows the default query string used to retrieve authorities based on username to be overriden, if
153      * default table or column names need to be changed. The default query is {@link
154      * #DEF_AUTHORITIES_BY_USERNAME_QUERY}; when modifying this query, ensure that all returned columns are mapped
155      * back to the same column names as in the default query.
156      *
157      * @param queryString The query string to set
158      */
159     public void setAuthoritiesByUsernameQuery(String queryString) {
160         authoritiesByUsernameQuery = queryString;
161     }
162 
163     /**
164      * Allows a default role prefix to be specified. If this is set to a non-empty value, then it is
165      * automatically prepended to any roles read in from the db. This may for example be used to add the
166      * <code>ROLE_</code> prefix expected to exist in role names (by default) by some other Acegi Security framework
167      * classes, in the case that the prefix is not already present in the db.
168      *
169      * @param rolePrefix the new prefix
170      */
171     public void setRolePrefix(String rolePrefix) {
172         this.rolePrefix = rolePrefix;
173     }
174 
175     /**
176      * If <code>true</code> (the default), indicates the {@link #getUsersByUsernameQuery()} returns a username
177      * in response to a query. If <code>false</code>, indicates that a primary key is used instead. If set to
178      * <code>true</code>, the class will use the database-derived username in the returned <code>UserDetails</code>.
179      * If <code>false</code>, the class will use the {@link #loadUserByUsername(String)} derived username in the
180      * returned <code>UserDetails</code>.
181      *
182      * @param usernameBasedPrimaryKey <code>true</code> if the mapping queries return the username <code>String</code>,
183      *        or <code>false</code> if the mapping returns a database primary key.
184      */
185     public void setUsernameBasedPrimaryKey(boolean usernameBasedPrimaryKey) {
186         this.usernameBasedPrimaryKey = usernameBasedPrimaryKey;
187     }
188 
189     /**
190      * Allows the default query string used to retrieve users based on username to be overriden, if default
191      * table or column names need to be changed. The default query is {@link #DEF_USERS_BY_USERNAME_QUERY}; when
192      * modifying this query, ensure that all returned columns are mapped back to the same column names as in the
193      * default query. If the 'enabled' column does not exist in the source db, a permanent true value for this column
194      * may be returned by using a query similar to <br><pre>
195      * "SELECT username,password,'true' as enabled FROM users WHERE username = ?"</pre>
196      *
197      * @param usersByUsernameQueryString The query string to set
198      */
199     public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
200         this.usersByUsernameQuery = usersByUsernameQueryString;
201     }
202 
203     //~ Inner Classes ==================================================================================================
204 
205     /**
206      * Query object to look up a user's authorities.
207      */
208     protected class AuthoritiesByUsernameMapping extends MappingSqlQuery {
209         protected AuthoritiesByUsernameMapping(DataSource ds) {
210             super(ds, authoritiesByUsernameQuery);
211             declareParameter(new SqlParameter(Types.VARCHAR));
212             compile();
213         }
214 
215         protected Object mapRow(ResultSet rs, int rownum)
216             throws SQLException {
217             String roleName = rolePrefix + rs.getString(2);
218             GrantedAuthorityImpl authority = new GrantedAuthorityImpl(roleName);
219 
220             return authority;
221         }
222     }
223 
224     /**
225      * Query object to look up a user.
226      */
227     protected class UsersByUsernameMapping extends MappingSqlQuery {
228         protected UsersByUsernameMapping(DataSource ds) {
229             super(ds, usersByUsernameQuery);
230             declareParameter(new SqlParameter(Types.VARCHAR));
231             compile();
232         }
233 
234         protected Object mapRow(ResultSet rs, int rownum)
235             throws SQLException {
236             String username = rs.getString(1);
237             String password = rs.getString(2);
238             boolean enabled = rs.getBoolean(3);
239             UserDetails user = new User(username, password, enabled, true, true, true,
240                     new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")});
241 
242             return user;
243         }
244     }
245 }