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.lang.codec.Base64; 032import org.apache.shiro.config.ConfigurationException; 033import org.apache.shiro.realm.AuthorizingRealm; 034import org.apache.shiro.subject.PrincipalCollection; 035import org.apache.shiro.lang.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 * Realm that allows authentication and authorization via JDBC calls. The default queries suggest a potential schema 051 * for retrieving the user's password for authentication, and querying for a user's roles and permissions. The 052 * default queries can be overridden by setting the query properties of the realm. 053 * <p/> 054 * If the default implementation 055 * of authentication and authorization cannot handle your schema, this class can be subclassed and the 056 * appropriate methods overridden. (usually {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)}, 057 * {@link #getRoleNamesForUser(java.sql.Connection, String)}, 058 * 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 /*-------------------------------------------- 067 | C O N S T A N T S | 068 ============================================*/ 069 /** 070 * The default query used to retrieve account data for the user. 071 */ 072 protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?"; 073 074 /** 075 * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN. 076 */ 077 protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY 078 = "select password, password_salt from users where username = ?"; 079 080 /** 081 * The default query used to retrieve the roles that apply to a user. 082 */ 083 protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?"; 084 085 /** 086 * The default query used to retrieve permissions that apply to a particular role. 087 */ 088 protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?"; 089 090 private static final Logger LOGGER = LoggerFactory.getLogger(JdbcRealm.class); 091 092 /** 093 * Password hash salt configuration. <ul> 094 * <li>NO_SALT - password hashes are not salted.</li> 095 * <li>CRYPT - password hashes are stored in unix crypt format.</li> 096 * <li>COLUMN - salt is in a separate column in the database.</li> 097 * <li>EXTERNAL - salt is not stored in the database. {@link #getSaltForUser(String)} will be called 098 * to get the salt</li></ul> 099 */ 100 public enum SaltStyle { NO_SALT, CRYPT, COLUMN, EXTERNAL } 101 102 /*-------------------------------------------- 103 | I N S T A N C E V A R I A B L E S | 104 ============================================*/ 105 protected DataSource dataSource; 106 107 protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY; 108 109 protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY; 110 111 protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY; 112 113 protected boolean permissionsLookupEnabled; 114 115 protected SaltStyle saltStyle = SaltStyle.NO_SALT; 116 117 protected boolean saltIsBase64Encoded = true; 118 119 /*-------------------------------------------- 120 | C O N S T R U C T O R S | 121 ============================================*/ 122 123 /*-------------------------------------------- 124 | A C C E S S O R S / M O D I F I E R S | 125 ============================================*/ 126 127 /** 128 * Sets the datasource that should be used to retrieve connections used by this realm. 129 * 130 * @param dataSource the SQL data source. 131 */ 132 public void setDataSource(DataSource dataSource) { 133 this.dataSource = dataSource; 134 } 135 136 /** 137 * Overrides the default query used to retrieve a user's password during authentication. When using the default 138 * implementation, this query must take the user's username as a single parameter and return a single result 139 * with the user's password as the first column. If you require a solution that does not match this query 140 * structure, you can override {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} or 141 * just {@link #getPasswordForUser(java.sql.Connection, String)} 142 * 143 * @param authenticationQuery the query to use for authentication. 144 * @see #DEFAULT_AUTHENTICATION_QUERY 145 */ 146 public void setAuthenticationQuery(String authenticationQuery) { 147 this.authenticationQuery = authenticationQuery; 148 } 149 150 /** 151 * Overrides the default query used to retrieve a user's roles during authorization. When using the default 152 * implementation, this query must take the user's username as a single parameter and return a row 153 * per role with a single column containing the role name. If you require a solution that does not match this query 154 * structure, you can override {@link #doGetAuthorizationInfo(PrincipalCollection)} or just 155 * {@link #getRoleNamesForUser(java.sql.Connection, String)} 156 * 157 * @param userRolesQuery the query to use for retrieving a user's roles. 158 * @see #DEFAULT_USER_ROLES_QUERY 159 */ 160 public void setUserRolesQuery(String userRolesQuery) { 161 this.userRolesQuery = userRolesQuery; 162 } 163 164 /** 165 * Overrides the default query used to retrieve a user's permissions during authorization. When using the default 166 * implementation, this query must take a role name as the single parameter and return a row 167 * per permission with a single column, containing the permission. 168 * If you require a solution that does not match this query 169 * structure, you can override {@link #doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)} or just 170 * {@link #getPermissions(java.sql.Connection, String, java.util.Collection)}</p> 171 * <p/> 172 * <b>Permissions are only retrieved if you set {@link #permissionsLookupEnabled} to true. Otherwise, 173 * this query is ignored.</b> 174 * 175 * @param permissionsQuery the query to use for retrieving permissions for a role. 176 * @see #DEFAULT_PERMISSIONS_QUERY 177 * @see #setPermissionsLookupEnabled(boolean) 178 */ 179 public void setPermissionsQuery(String permissionsQuery) { 180 this.permissionsQuery = permissionsQuery; 181 } 182 183 /** 184 * Enables lookup of permissions during authorization. The default is "false" - meaning that only roles 185 * are associated with a user. Set this to true in order to lookup roles <b>and</b> permissions. 186 * 187 * @param permissionsLookupEnabled true if permissions should be looked up during authorization, or false if only 188 * roles should be looked up. 189 */ 190 public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) { 191 this.permissionsLookupEnabled = permissionsLookupEnabled; 192 } 193 194 /** 195 * Sets the salt style. See {@link #saltStyle}. 196 * 197 * @param saltStyle new SaltStyle to set. 198 */ 199 public void setSaltStyle(SaltStyle saltStyle) { 200 this.saltStyle = saltStyle; 201 if (saltStyle == SaltStyle.COLUMN && authenticationQuery.equals(DEFAULT_AUTHENTICATION_QUERY)) { 202 authenticationQuery = DEFAULT_SALTED_AUTHENTICATION_QUERY; 203 } 204 } 205 206 /** 207 * Makes it possible to switch off base64 encoding of password salt. 208 * The default value is true, i.e. expect the salt from a string 209 * value in a database to be base64 encoded. 210 * 211 * @param saltIsBase64Encoded the saltIsBase64Encoded to set 212 */ 213 public void setSaltIsBase64Encoded(boolean saltIsBase64Encoded) { 214 this.saltIsBase64Encoded = saltIsBase64Encoded; 215 } 216 217 /*-------------------------------------------- 218 | M E T H O D S | 219 ============================================*/ 220 221 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 222 223 UsernamePasswordToken upToken = (UsernamePasswordToken) token; 224 String username = upToken.getUsername(); 225 226 // Null username is invalid 227 if (username == null) { 228 throw new AccountException("Null usernames are not allowed by this realm."); 229 } 230 231 Connection conn = null; 232 SimpleAuthenticationInfo info = null; 233 try { 234 conn = dataSource.getConnection(); 235 236 String password = null; 237 String salt = null; 238 switch (saltStyle) { 239 case NO_SALT: 240 password = getPasswordForUser(conn, username)[0]; 241 break; 242 case CRYPT: 243 // TODO: separate password and hash from getPasswordForUser[0] 244 throw new ConfigurationException("Not implemented yet"); 245 //break; 246 case COLUMN: 247 String[] queryResults = getPasswordForUser(conn, username); 248 password = queryResults[0]; 249 salt = queryResults[1]; 250 break; 251 case EXTERNAL: 252 password = getPasswordForUser(conn, username)[0]; 253 salt = getSaltForUser(username); 254 break; 255 default: 256 } 257 258 if (password == null) { 259 throw new UnknownAccountException("No account found for user [" + username + "]"); 260 } 261 262 info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName()); 263 264 if (salt != null) { 265 if (saltStyle == SaltStyle.COLUMN && saltIsBase64Encoded) { 266 info.setCredentialsSalt(ByteSource.Util.bytes(Base64.decode(salt))); 267 } else { 268 info.setCredentialsSalt(ByteSource.Util.bytes(salt)); 269 } 270 } 271 272 } catch (SQLException e) { 273 final String message = "There was a SQL error while authenticating user [" + username + "]"; 274 if (LOGGER.isErrorEnabled()) { 275 LOGGER.error(message, e); 276 } 277 278 // Rethrow any SQL errors as an authentication exception 279 throw new AuthenticationException(message, e); 280 } finally { 281 JdbcUtils.closeConnection(conn); 282 } 283 284 return info; 285 } 286 287 private String[] getPasswordForUser(Connection conn, String username) throws SQLException { 288 289 String[] result; 290 boolean returningSeparatedSalt = false; 291 switch (saltStyle) { 292 case NO_SALT: 293 case CRYPT: 294 case EXTERNAL: 295 result = new String[1]; 296 break; 297 default: 298 result = new String[2]; 299 returningSeparatedSalt = true; 300 } 301 302 PreparedStatement ps = null; 303 ResultSet rs = null; 304 try { 305 ps = conn.prepareStatement(authenticationQuery); 306 ps.setString(1, username); 307 308 // Execute query 309 rs = ps.executeQuery(); 310 311 // Loop over results - although we are only expecting one result, since usernames should be unique 312 boolean foundResult = false; 313 while (rs.next()) { 314 315 // Check to ensure only one row is processed 316 if (foundResult) { 317 throw new AuthenticationException("More than one user row found for user [" 318 + username + "]. Usernames must be unique."); 319 } 320 321 result[0] = rs.getString(1); 322 if (returningSeparatedSalt) { 323 result[1] = rs.getString(2); 324 } 325 326 foundResult = true; 327 } 328 } finally { 329 JdbcUtils.closeResultSet(rs); 330 JdbcUtils.closeStatement(ps); 331 } 332 333 return result; 334 } 335 336 /** 337 * This implementation of the interface expects the principals collection to return a String username keyed off of 338 * this realm's {@link #getName() name} 339 * 340 * @see #getAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection) 341 */ 342 @Override 343 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 344 345 //null usernames are invalid 346 if (principals == null) { 347 throw new AuthorizationException("PrincipalCollection method argument cannot be null."); 348 } 349 350 String username = (String) getAvailablePrincipal(principals); 351 352 Connection conn = null; 353 Set<String> roleNames = null; 354 Set<String> permissions = null; 355 try { 356 conn = dataSource.getConnection(); 357 358 // Retrieve roles and permissions from database 359 roleNames = getRoleNamesForUser(conn, username); 360 if (permissionsLookupEnabled) { 361 permissions = getPermissions(conn, username, roleNames); 362 } 363 364 } catch (SQLException e) { 365 final String message = "There was a SQL error while authorizing user [" + username + "]"; 366 if (LOGGER.isErrorEnabled()) { 367 LOGGER.error(message, e); 368 } 369 370 // Rethrow any SQL errors as an authorization exception 371 throw new AuthorizationException(message, e); 372 } finally { 373 JdbcUtils.closeConnection(conn); 374 } 375 376 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames); 377 info.setStringPermissions(permissions); 378 return info; 379 380 } 381 382 protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException { 383 PreparedStatement ps = null; 384 ResultSet rs = null; 385 Set<String> roleNames = new LinkedHashSet<String>(); 386 try { 387 ps = conn.prepareStatement(userRolesQuery); 388 ps.setString(1, username); 389 390 // Execute query 391 rs = ps.executeQuery(); 392 393 // Loop over results and add each returned role to a set 394 while (rs.next()) { 395 396 String roleName = rs.getString(1); 397 398 // Add the role to the list of names if it isn't null 399 if (roleName != null) { 400 roleNames.add(roleName); 401 } else { 402 if (LOGGER.isWarnEnabled()) { 403 LOGGER.warn("Null role name found while retrieving role names for user [" + username + "]"); 404 } 405 } 406 } 407 } finally { 408 JdbcUtils.closeResultSet(rs); 409 JdbcUtils.closeStatement(ps); 410 } 411 return roleNames; 412 } 413 414 protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException { 415 PreparedStatement ps = null; 416 Set<String> permissions = new LinkedHashSet<String>(); 417 try { 418 ps = conn.prepareStatement(permissionsQuery); 419 for (String roleName : roleNames) { 420 421 ps.setString(1, roleName); 422 423 ResultSet rs = null; 424 425 try { 426 // Execute query 427 rs = ps.executeQuery(); 428 429 // Loop over results and add each returned role to a set 430 while (rs.next()) { 431 432 String permissionString = rs.getString(1); 433 434 // Add the permission to the set of permissions 435 permissions.add(permissionString); 436 } 437 } finally { 438 JdbcUtils.closeResultSet(rs); 439 } 440 441 } 442 } finally { 443 JdbcUtils.closeStatement(ps); 444 } 445 446 return permissions; 447 } 448 449 protected String getSaltForUser(String username) { 450 return username; 451 } 452 453}