View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.realm.jdbc;
20  
21  import org.apache.shiro.authc.AccountException;
22  import org.apache.shiro.authc.AuthenticationException;
23  import org.apache.shiro.authc.AuthenticationInfo;
24  import org.apache.shiro.authc.AuthenticationToken;
25  import org.apache.shiro.authc.SimpleAuthenticationInfo;
26  import org.apache.shiro.authc.UnknownAccountException;
27  import org.apache.shiro.authc.UsernamePasswordToken;
28  import org.apache.shiro.authz.AuthorizationException;
29  import org.apache.shiro.authz.AuthorizationInfo;
30  import org.apache.shiro.authz.SimpleAuthorizationInfo;
31  import org.apache.shiro.codec.Base64;
32  import org.apache.shiro.config.ConfigurationException;
33  import org.apache.shiro.realm.AuthorizingRealm;
34  import org.apache.shiro.subject.PrincipalCollection;
35  import org.apache.shiro.util.ByteSource;
36  import org.apache.shiro.util.JdbcUtils;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import javax.sql.DataSource;
41  import java.sql.Connection;
42  import java.sql.PreparedStatement;
43  import java.sql.ResultSet;
44  import java.sql.SQLException;
45  import java.util.Collection;
46  import java.util.LinkedHashSet;
47  import java.util.Set;
48  
49  
50  /**
51   * Realm that allows authentication and authorization via JDBC calls.  The default queries suggest a potential schema
52   * for retrieving the user's password for authentication, and querying for a user's roles and permissions.  The
53   * default queries can be overridden by setting the query properties of the realm.
54   * <p/>
55   * If the default implementation
56   * of authentication and authorization cannot handle your schema, this class can be subclassed and the
57   * appropriate methods overridden. (usually {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)},
58   * {@link #getRoleNamesForUser(java.sql.Connection,String)}, and/or {@link #getPermissions(java.sql.Connection,String,java.util.Collection)}
59   * <p/>
60   * This realm supports caching by extending from {@link org.apache.shiro.realm.AuthorizingRealm}.
61   *
62   * @since 0.2
63   */
64  public class JdbcRealm extends AuthorizingRealm {
65  
66      //TODO - complete JavaDoc
67  
68      /*--------------------------------------------
69      |             C O N S T A N T S             |
70      ============================================*/
71      /**
72       * The default query used to retrieve account data for the user.
73       */
74      protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
75      
76      /**
77       * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
78       */
79      protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
80  
81      /**
82       * The default query used to retrieve the roles that apply to a user.
83       */
84      protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
85  
86      /**
87       * The default query used to retrieve permissions that apply to a particular role.
88       */
89      protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
90  
91      private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class);
92      
93      /**
94       * Password hash salt configuration. <ul>
95       *   <li>NO_SALT - password hashes are not salted.</li>
96       *   <li>CRYPT - password hashes are stored in unix crypt format.</li>
97       *   <li>COLUMN - salt is in a separate column in the database.</li> 
98       *   <li>EXTERNAL - salt is not stored in the database. {@link #getSaltForUser(String)} will be called
99       *       to get the salt</li></ul>
100      */
101     public enum SaltStyle {NO_SALT, CRYPT, COLUMN, EXTERNAL};
102 
103     /*--------------------------------------------
104     |    I N S T A N C E   V A R I A B L E S    |
105     ============================================*/
106     protected DataSource dataSource;
107 
108     protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
109 
110     protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;
111 
112     protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
113 
114     protected boolean permissionsLookupEnabled = false;
115     
116     protected SaltStyle saltStyle = SaltStyle.NO_SALT;
117 
118     protected boolean saltIsBase64Encoded = true;
119 
120     /*--------------------------------------------
121     |         C O N S T R U C T O R S           |
122     ============================================*/
123 
124     /*--------------------------------------------
125     |  A C C E S S O R S / M O D I F I E R S    |
126     ============================================*/
127     
128     /**
129      * Sets the datasource that should be used to retrieve connections used by this realm.
130      *
131      * @param dataSource the SQL data source.
132      */
133     public void setDataSource(DataSource dataSource) {
134         this.dataSource = dataSource;
135     }
136 
137     /**
138      * Overrides the default query used to retrieve a user's password during authentication.  When using the default
139      * implementation, this query must take the user's username as a single parameter and return a single result
140      * with the user's password as the first column.  If you require a solution that does not match this query
141      * structure, you can override {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} or
142      * just {@link #getPasswordForUser(java.sql.Connection,String)}
143      *
144      * @param authenticationQuery the query to use for authentication.
145      * @see #DEFAULT_AUTHENTICATION_QUERY
146      */
147     public void setAuthenticationQuery(String authenticationQuery) {
148         this.authenticationQuery = authenticationQuery;
149     }
150 
151     /**
152      * Overrides the default query used to retrieve a user's roles during authorization.  When using the default
153      * implementation, this query must take the user's username as a single parameter and return a row
154      * per role with a single column containing the role name.  If you require a solution that does not match this query
155      * structure, you can override {@link #doGetAuthorizationInfo(PrincipalCollection)} or just
156      * {@link #getRoleNamesForUser(java.sql.Connection,String)}
157      *
158      * @param userRolesQuery the query to use for retrieving a user's roles.
159      * @see #DEFAULT_USER_ROLES_QUERY
160      */
161     public void setUserRolesQuery(String userRolesQuery) {
162         this.userRolesQuery = userRolesQuery;
163     }
164 
165     /**
166      * Overrides the default query used to retrieve a user's permissions during authorization.  When using the default
167      * implementation, this query must take a role name as the single parameter and return a row
168      * per permission with a single column, containing the permission.
169      * If you require a solution that does not match this query
170      * structure, you can override {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} or just
171      * {@link #getPermissions(java.sql.Connection,String,java.util.Collection)}</p>
172      * <p/>
173      * <b>Permissions are only retrieved if you set {@link #permissionsLookupEnabled} to true.  Otherwise,
174      * this query is ignored.</b>
175      *
176      * @param permissionsQuery the query to use for retrieving permissions for a role.
177      * @see #DEFAULT_PERMISSIONS_QUERY
178      * @see #setPermissionsLookupEnabled(boolean)
179      */
180     public void setPermissionsQuery(String permissionsQuery) {
181         this.permissionsQuery = permissionsQuery;
182     }
183 
184     /**
185      * Enables lookup of permissions during authorization.  The default is "false" - meaning that only roles
186      * are associated with a user.  Set this to true in order to lookup roles <b>and</b> permissions.
187      *
188      * @param permissionsLookupEnabled true if permissions should be looked up during authorization, or false if only
189      *                                 roles should be looked up.
190      */
191     public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) {
192         this.permissionsLookupEnabled = permissionsLookupEnabled;
193     }
194     
195     /**
196      * Sets the salt style.  See {@link #saltStyle}.
197      * 
198      * @param saltStyle new SaltStyle to set.
199      */
200     public void setSaltStyle(SaltStyle saltStyle) {
201         this.saltStyle = saltStyle;
202         if (saltStyle == SaltStyle.COLUMN && authenticationQuery.equals(DEFAULT_AUTHENTICATION_QUERY)) {
203             authenticationQuery = DEFAULT_SALTED_AUTHENTICATION_QUERY;
204         }
205     }
206 
207     /**
208      * Makes it possible to switch off base64 encoding of password salt.
209      * The default value is true, ie. expect the salt from a string
210      * value in a database to be base64 encoded.
211      *
212      * @param saltIsBase64Encoded the saltIsBase64Encoded to set
213      */
214     public void setSaltIsBase64Encoded(boolean saltIsBase64Encoded) {
215         this.saltIsBase64Encoded = saltIsBase64Encoded;
216     }
217 
218     /*--------------------------------------------
219     |               M E T H O D S               |
220     ============================================*/
221 
222     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
223 
224         UsernamePasswordTokenrg/apache/shiro/authc/UsernamePasswordToken.html#UsernamePasswordToken">UsernamePasswordToken upToken = (UsernamePasswordToken) token;
225         String username = upToken.getUsername();
226 
227         // Null username is invalid
228         if (username == null) {
229             throw new AccountException("Null usernames are not allowed by this realm.");
230         }
231 
232         Connection conn = null;
233         SimpleAuthenticationInfo info = null;
234         try {
235             conn = dataSource.getConnection();
236 
237             String password = null;
238             String salt = null;
239             switch (saltStyle) {
240             case NO_SALT:
241                 password = getPasswordForUser(conn, username)[0];
242                 break;
243             case CRYPT:
244                 // TODO: separate password and hash from getPasswordForUser[0]
245                 throw new ConfigurationException("Not implemented yet");
246                 //break;
247             case COLUMN:
248                 String[] queryResults = getPasswordForUser(conn, username);
249                 password = queryResults[0];
250                 salt = queryResults[1];
251                 break;
252             case EXTERNAL:
253                 password = getPasswordForUser(conn, username)[0];
254                 salt = getSaltForUser(username);
255             }
256 
257             if (password == null) {
258                 throw new UnknownAccountException("No account found for user [" + username + "]");
259             }
260 
261             info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
262             
263             if (salt != null) {
264             	if (saltStyle == SaltStyle.COLUMN && saltIsBase64Encoded) {
265                     info.setCredentialsSalt(ByteSource.Util.bytes(Base64.decode(salt)));
266             	} else {
267                     info.setCredentialsSalt(ByteSource.Util.bytes(salt));
268             	}
269             }
270 
271         } catch (SQLException e) {
272             final String message = "There was a SQL error while authenticating user [" + username + "]";
273             if (log.isErrorEnabled()) {
274                 log.error(message, e);
275             }
276 
277             // Rethrow any SQL errors as an authentication exception
278             throw new AuthenticationException(message, e);
279         } finally {
280             JdbcUtils.closeConnection(conn);
281         }
282 
283         return info;
284     }
285 
286     private String[] getPasswordForUser(Connection conn, String username) throws SQLException {
287 
288         String[] result;
289         boolean returningSeparatedSalt = false;
290         switch (saltStyle) {
291         case NO_SALT:
292         case CRYPT:
293         case EXTERNAL:
294             result = new String[1];
295             break;
296         default:
297             result = new String[2];
298             returningSeparatedSalt = true;
299         }
300         
301         PreparedStatement ps = null;
302         ResultSet rs = null;
303         try {
304             ps = conn.prepareStatement(authenticationQuery);
305             ps.setString(1, username);
306 
307             // Execute query
308             rs = ps.executeQuery();
309 
310             // Loop over results - although we are only expecting one result, since usernames should be unique
311             boolean foundResult = false;
312             while (rs.next()) {
313 
314                 // Check to ensure only one row is processed
315                 if (foundResult) {
316                     throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
317                 }
318 
319                 result[0] = rs.getString(1);
320                 if (returningSeparatedSalt) {
321                     result[1] = rs.getString(2);
322                 }
323 
324                 foundResult = true;
325             }
326         } finally {
327             JdbcUtils.closeResultSet(rs);
328             JdbcUtils.closeStatement(ps);
329         }
330 
331         return result;
332     }
333 
334     /**
335      * This implementation of the interface expects the principals collection to return a String username keyed off of
336      * this realm's {@link #getName() name}
337      *
338      * @see #getAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
339      */
340     @Override
341     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
342 
343         //null usernames are invalid
344         if (principals == null) {
345             throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
346         }
347 
348         String username = (String) getAvailablePrincipal(principals);
349 
350         Connection conn = null;
351         Set<String> roleNames = null;
352         Set<String> permissions = null;
353         try {
354             conn = dataSource.getConnection();
355 
356             // Retrieve roles and permissions from database
357             roleNames = getRoleNamesForUser(conn, username);
358             if (permissionsLookupEnabled) {
359                 permissions = getPermissions(conn, username, roleNames);
360             }
361 
362         } catch (SQLException e) {
363             final String message = "There was a SQL error while authorizing user [" + username + "]";
364             if (log.isErrorEnabled()) {
365                 log.error(message, e);
366             }
367 
368             // Rethrow any SQL errors as an authorization exception
369             throw new AuthorizationException(message, e);
370         } finally {
371             JdbcUtils.closeConnection(conn);
372         }
373 
374         SimpleAuthorizationInfoonInfo.html#SimpleAuthorizationInfo">SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
375         info.setStringPermissions(permissions);
376         return info;
377 
378     }
379 
380     protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
381         PreparedStatement ps = null;
382         ResultSet rs = null;
383         Set<String> roleNames = new LinkedHashSet<String>();
384         try {
385             ps = conn.prepareStatement(userRolesQuery);
386             ps.setString(1, username);
387 
388             // Execute query
389             rs = ps.executeQuery();
390 
391             // Loop over results and add each returned role to a set
392             while (rs.next()) {
393 
394                 String roleName = rs.getString(1);
395 
396                 // Add the role to the list of names if it isn't null
397                 if (roleName != null) {
398                     roleNames.add(roleName);
399                 } else {
400                     if (log.isWarnEnabled()) {
401                         log.warn("Null role name found while retrieving role names for user [" + username + "]");
402                     }
403                 }
404             }
405         } finally {
406             JdbcUtils.closeResultSet(rs);
407             JdbcUtils.closeStatement(ps);
408         }
409         return roleNames;
410     }
411 
412     protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException {
413         PreparedStatement ps = null;
414         Set<String> permissions = new LinkedHashSet<String>();
415         try {
416             ps = conn.prepareStatement(permissionsQuery);
417             for (String roleName : roleNames) {
418 
419                 ps.setString(1, roleName);
420 
421                 ResultSet rs = null;
422 
423                 try {
424                     // Execute query
425                     rs = ps.executeQuery();
426 
427                     // Loop over results and add each returned role to a set
428                     while (rs.next()) {
429 
430                         String permissionString = rs.getString(1);
431 
432                         // Add the permission to the set of permissions
433                         permissions.add(permissionString);
434                     }
435                 } finally {
436                     JdbcUtils.closeResultSet(rs);
437                 }
438 
439             }
440         } finally {
441             JdbcUtils.closeStatement(ps);
442         }
443 
444         return permissions;
445     }
446     
447     protected String getSaltForUser(String username) {
448         return username;
449     }
450 
451 }