Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to authenticate user in Java code with database

Tags:

java

shiro

I would like to use Apache Shiro with database authentication. But I can't make database design changes. I would like to use my custom SQL command and Java logic to authenticate user. Is this possible? I tried this configuration in shiro.ini:

saltedJdbcRealm=com.crm.web.authentication.JdbcRealm

And custom Java class:

public class JdbcRealm extends AuthorizingRealm
{
    @Resource(name = "jdbc/DefaultDB")
    private DataSource dataSource;

    protected static final String DEFAULT_AUTHENTICATION_QUERY = "select passwd from user where username = ?";

    protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select passwd, passwd_salt from user where username = ?";

    protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";

    protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
    private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class);

    public enum SaltStyle
    {
        NO_SALT, CRYPT, COLUMN, EXTERNAL
    };
    protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
    protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY;
    protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
    protected boolean permissionsLookupEnabled = false;

    protected SaltStyle saltStyle = SaltStyle.NO_SALT;

    public void setDataSource(DataSource dataSource)
    {
        this.dataSource = dataSource;
    }

    public void setAuthenticationQuery(String authenticationQuery)
    {
        this.authenticationQuery = authenticationQuery;
    }

    public void setUserRolesQuery(String userRolesQuery)
    {
        this.userRolesQuery = userRolesQuery;
    }

    public void setPermissionsQuery(String permissionsQuery)
    {
        this.permissionsQuery = permissionsQuery;
    }

    public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled)
    {
        this.permissionsLookupEnabled = permissionsLookupEnabled;
    }

    public void setSaltStyle(SaltStyle saltStyle)
    {
        this.saltStyle = saltStyle;
        if (saltStyle == SaltStyle.COLUMN && authenticationQuery.equals(DEFAULT_AUTHENTICATION_QUERY))
        {
            authenticationQuery = DEFAULT_SALTED_AUTHENTICATION_QUERY;
        }
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
    {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        // Null username is invalid
        if (username == null)
        {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }
        Connection conn = null;
        SimpleAuthenticationInfo info = null;
        try
        {
            conn = dataSource.getConnection();
            String password = null;
            String salt = null;
            switch (saltStyle)
            {
                case NO_SALT:
                    password = getPasswordForUser(conn, username)[0];
                    break;
                case CRYPT:
                    // TODO: separate password and hash from getPasswordForUser[0]
                    throw new ConfigurationException("Not implemented yet");
                //break;
                case COLUMN:
                    String[] queryResults = getPasswordForUser(conn, username);
                    password = queryResults[0];
                    salt = queryResults[1];
                    break;
                case EXTERNAL:
                    password = getPasswordForUser(conn, username)[0];
                    salt = getSaltForUser(username);
            }
            if (password == null)
            {
                throw new UnknownAccountException("No account found for user [" + username + "]");
            }
            info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());

            if (salt != null)
            {
                info.setCredentialsSalt(ByteSource.Util.bytes(salt));
            }
        }
        catch (SQLException e)
        {
            final String message = "There was a SQL error while authenticating user [" + username + "]";
            if (log.isErrorEnabled())
            {
                log.error(message, e);
            }
            throw new AuthenticationException(message, e);
        }
        finally
        {
            JdbcUtils.closeConnection(conn);
        }
        return info;
    }

    private String[] getPasswordForUser(Connection conn, String username) throws SQLException
    {
        String[] result;
        boolean returningSeparatedSalt = false;
        switch (saltStyle)
        {
            case NO_SALT:
            case CRYPT:
            case EXTERNAL:
                result = new String[1];
                break;
            default:
                result = new String[2];
                returningSeparatedSalt = true;
        }

        PreparedStatement ps = null;
        ResultSet rs = null;
        try
        {
            ps = conn.prepareStatement(authenticationQuery);
            ps.setString(1, username);
            // Execute query
            rs = ps.executeQuery();
            // Loop over results - although we are only expecting one result, since usernames should be unique
            boolean foundResult = false;
            while (rs.next())
            {
                // Check to ensure only one row is processed
                if (foundResult)
                {
                    throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
                }
                result[0] = rs.getString(1);
                if (returningSeparatedSalt)
                {
                    result[1] = rs.getString(2);
                }
                foundResult = true;
            }
        }
        finally
        {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(ps);
        }
        return result;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
    {
        //null usernames are invalid
        if (principals == null)
        {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        String username = (String) getAvailablePrincipal(principals);
        Connection conn = null;
        Set<String> roleNames = null;
        Set<String> permissions = null;
        try
        {
            conn = dataSource.getConnection();
            // Retrieve roles and permissions from database
            roleNames = getRoleNamesForUser(conn, username);
            if (permissionsLookupEnabled)
            {
                permissions = getPermissions(conn, username, roleNames);
            }
        }
        catch (SQLException e)
        {
            final String message = "There was a SQL error while authorizing user [" + username + "]";
            if (log.isErrorEnabled())
            {
                log.error(message, e);
            }
            // Rethrow any SQL errors as an authorization exception
            throw new AuthorizationException(message, e);
        }
        finally
        {
            JdbcUtils.closeConnection(conn);
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
        info.setStringPermissions(permissions);
        return info;
    }

    protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException
    {
        PreparedStatement ps = null;
        ResultSet rs = null;
        Set<String> roleNames = new LinkedHashSet<String>();
        try
        {
            ps = conn.prepareStatement(userRolesQuery);
            ps.setString(1, username);
            // Execute query
            rs = ps.executeQuery();
            // Loop over results and add each returned role to a set
            while (rs.next())
            {
                String roleName = rs.getString(1);
                // Add the role to the list of names if it isn't null
                if (roleName != null)
                {
                    roleNames.add(roleName);
                }
                else
                {
                    if (log.isWarnEnabled())
                    {
                        log.warn("Null role name found while retrieving role names for user [" + username + "]");
                    }
                }
            }
        }
        finally
        {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(ps);
        }
        return roleNames;
    }

    protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException
    {
        PreparedStatement ps = null;
        Set<String> permissions = new LinkedHashSet<>();
        try
        {
            ps = conn.prepareStatement(permissionsQuery);
            for (String roleName : roleNames)
            {
                ps.setString(1, roleName);
                ResultSet rs = null;
                try
                {
                    // Execute query
                    rs = ps.executeQuery();
                    // Loop over results and add each returned role to a set
                    while (rs.next())
                    {
                        String permissionString = rs.getString(1);
                        // Add the permission to the set of permissions
                        permissions.add(permissionString);
                    }
                }
                finally
                {
                    JdbcUtils.closeResultSet(rs);
                }
            }
        }
        finally
        {
            JdbcUtils.closeStatement(ps);
        }
        return permissions;
    }

    protected String getSaltForUser(String username)
    {
        return username;
    }
}

But when I run the code I get:

org.apache.shiro.authc.AuthenticationException: Authentication token of type [class org.apache.shiro.authc.UsernamePasswordToken] could not be authenticated by any configured realms.  Please ensure that at least one realm can authenticate these tokens.

Am I missing some configuration in shiro.ini

like image 571
Peter Penzov Avatar asked Apr 15 '16 21:04

Peter Penzov


3 Answers

This is how we do it in XML(shiro.xml) :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:mvc="http://www.springframework.org/schema/mvc"
   xmlns:util="http://www.springframework.org/schema/util"
   xsi:schemaLocation="
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
    <bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
        <property name="redirectUrl" value="YOUR_LOGIN_URL" />
    </bean>
     <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="YOUR_SUCCESS_URL"/>
    <property name="unauthorizedUrl" value="YOUR_ACCESS_DENIED_URL"/>

    <property name="filters">
      <util:map>
        <entry key="logout" value-ref="logout"/>
       </util:map>
    </property>
    <property name="filterChainDefinitions">
        <value>
        /** = authc <!--SPECIFY_OTHERS_FILTERS_CHAINS-->
        </value>
    </property>

</bean>

<bean id="builtInCacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!-- Single realm app.  If you have multiple realms, use the 'realms' property instead. -->
    <property name="realm" ref="myRealm"/>
    <property name="cacheManager" ref="builtInCacheManager"/>
    <!-- By default the servlet container sessions will be used.  Uncomment this line-->
         <!-- to use shiro's native sessions (see the JavaDoc for more): -->
    <!-- <property name="sessionMode" value="native"/> -->
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- Define the Shiro Realm implementation you want to use to connect to your back-end -->
<!-- security datasource: -->
<bean id="myRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
    <property name="credentialsMatcher" ref="hashMatcher"/> 
     <property name="authenticationQuery" value="select password from user_login where user_id = ?"/>
     <property name="userRolesQuery" value="YOUR_ROLE_QUERY"/>

     <property name="permissionsQuery" value="YOUR_PERMISSION_QUERY" />
     <property name="permissionsLookupEnabled" value="true"></property>
     <property name="dataSource" ref="YOUR_DATA_SOURCE_NAME"/> <!-- i.e. being used for the DB connection -->
</bean>

<!-- Hash Matcher Bean responsible for matching credentials of logging user -->
<bean id="hashMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--        Algorithm name -->
    <property name="hashAlgorithmName" value="SHA-512"/> 

<!--        No. of Hash Iterations. Note: must match with iterations used to save password in database. -->
    <property name="hashIterations" value="10000"/>  
<!--        true if Stored Credentials(i.e. password and salt) are in Hexadecimal form. False denotes BASE64 encoding.-->
    <property name="storedCredentialsHexEncoded" value="true"/>
</bean>

<!-- Enable Shiro Annotations for Spring-configured beans.  Only run after -->
<!-- the lifecycleBeanProcessor has run: -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
</bean>
</beans> 

You can include it in the application config file (web.xml)

like image 119
Pankaj Verma Avatar answered Oct 17 '22 15:10

Pankaj Verma


All shiro needs to mark session as authenticated is AuthenticationInfo object. How it's built is up to you. The realm should be tied to the security manager.

like image 40
Amit Yatagiri Avatar answered Oct 17 '22 15:10

Amit Yatagiri


I want to give you 2 suggestions. Hope it will help you.

Suggestion - 1:

Configuration file is not fully configured for Realm. You should write a class for AuthorizingRealm, then the class will be configured to configuration file.

If you use the spring, then the configuration will look like below:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="localRealm" />
</bean>

<bean id="localRealm" class="com.xxxx.xxxxx.infra.LocalSecurityRealm">
    <constructor-arg index="0" ref="securityApplication" />
</bean>

Add authenticator in shiro.ini configuration file

authenticator = com.realm.MyRealm

Resource Link:

  1. http://www.oschina.net/question/617087_72790#answers

Suggestion - 2:

You need to first make sure that supports() are actually reached and executed.

  @Override
  public boolean supports(AuthenticationToken authenticationToken) {
    return (authenticationToken instanceof UsernamePasswordToken)
  }

If you have multiple realms and one throws an error, the others will NOT be processed. So if you need to work around thrown Exceptions you can do something like this for authz and this for authc.

Resource Link:

  1. http://shiro-user.582556.n2.nabble.com/Still-having-an-issue-with-multiple-realms-td7579698.html
like image 40
SkyWalker Avatar answered Oct 17 '22 15:10

SkyWalker