001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.shiro.realm.jdbc; 020 021import org.apache.shiro.authc.AccountException; 022import org.apache.shiro.authc.AuthenticationException; 023import org.apache.shiro.authc.AuthenticationInfo; 024import org.apache.shiro.authc.AuthenticationToken; 025import org.apache.shiro.authc.SimpleAuthenticationInfo; 026import org.apache.shiro.authc.UnknownAccountException; 027import org.apache.shiro.authc.UsernamePasswordToken; 028import org.apache.shiro.authz.AuthorizationException; 029import org.apache.shiro.authz.AuthorizationInfo; 030import org.apache.shiro.authz.SimpleAuthorizationInfo; 031import org.apache.shiro.codec.Base64; 032import org.apache.shiro.config.ConfigurationException; 033import org.apache.shiro.realm.AuthorizingRealm; 034import org.apache.shiro.subject.PrincipalCollection; 035import org.apache.shiro.util.ByteSource; 036import org.apache.shiro.util.JdbcUtils; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040import javax.sql.DataSource; 041import java.sql.Connection; 042import java.sql.PreparedStatement; 043import java.sql.ResultSet; 044import java.sql.SQLException; 045import java.util.Collection; 046import java.util.LinkedHashSet; 047import java.util.Set; 048 049 050/** 051 * Realm that allows authentication and authorization via JDBC calls. The default queries suggest a potential schema 052 * for retrieving the user's password for authentication, and querying for a user's roles and permissions. The 053 * default queries can be overridden by setting the query properties of the realm. 054 * <p/> 055 * If the default implementation 056 * of authentication and authorization cannot handle your schema, this class can be subclassed and the 057 * appropriate methods overridden. (usually {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)}, 058 * {@link #getRoleNamesForUser(java.sql.Connection,String)}, and/or {@link #getPermissions(java.sql.Connection,String,java.util.Collection)} 059 * <p/> 060 * This realm supports caching by extending from {@link org.apache.shiro.realm.AuthorizingRealm}. 061 * 062 * @since 0.2 063 */ 064public class JdbcRealm extends AuthorizingRealm { 065 066 //TODO - complete JavaDoc 067 068 /*-------------------------------------------- 069 | C O N S T A N T S | 070 ============================================*/ 071 /** 072 * The default query used to retrieve account data for the user. 073 */ 074 protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?"; 075 076 /** 077 * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN. 078 */ 079 protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?"; 080 081 /** 082 * The default query used to retrieve the roles that apply to a user. 083 */ 084 protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?"; 085 086 /** 087 * The default query used to retrieve permissions that apply to a particular role. 088 */ 089 protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?"; 090 091 private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class); 092 093 /** 094 * Password hash salt configuration. <ul> 095 * <li>NO_SALT - password hashes are not salted.</li> 096 * <li>CRYPT - password hashes are stored in unix crypt format.</li> 097 * <li>COLUMN - salt is in a separate column in the database.</li> 098 * <li>EXTERNAL - salt is not stored in the database. {@link #getSaltForUser(String)} will be called 099 * 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 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 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}