如何使用数据库在 Java 代码中对用户进行身份验证

Posted

技术标签:

【中文标题】如何使用数据库在 Java 代码中对用户进行身份验证【英文标题】:How to authenticate user in Java code with database 【发布时间】:2016-08-07 23:52:00 【问题描述】:

我想将 Apache Shiro 与数据库身份验证一起使用。但我无法更改数据库设计。我想使用我的自定义 SQL 命令和 Java 逻辑来验证用户身份。这可能吗?我在 shiro.ini 中试过这个配置:

saltedJdbcRealm=com.crm.web.authentication.JdbcRealm

以及自定义 Java 类:

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;
    

但是当我运行代码时,我得到:

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.

我是否缺少 shiro.ini 中的一些配置

【问题讨论】:

类似问题***.com/questions/22353445/… 我没有看到任何对我的案子有用的东西。 我不知道 Shiro,但我首先要检查的是类路径,您是否将自定义类包含到 Shiro 类路径中? 是的。类路径工作正常。 把你的 shiro.ini 放在你的问题上。看起来您需要更多配置。我建议你扩展 shiro.apache.org/static/1.2.4/apidocs/org/apache/shiro/realm/… 而不是 AuthorizingRealm。 【参考方案1】:

这就是我们在 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> 

您可以将其包含在应用程序配置文件(web.xml)中

【讨论】:

【参考方案2】:

所有 shiro 需要将会话标记为已验证的是 AuthenticationInfo 对象。它的构建方式取决于您。 该领域应与安全管理器相关联。

【讨论】:

【参考方案3】:

我想给你两个建议。希望对你有帮助。

建议 - 1:

配置文件没有为 Realm 完全配置。 您应该为 AuthorizingRealm 编写一个类,然后该类将被配置到配置文件中。

如果你使用spring,那么配置如下:

<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>

shiro.ini配置文件中添加验证器

authenticator = com.realm.MyRealm

资源链接:

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

建议 - 2:

您需要首先确保supports() 实际已到达并执行。

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

如果您有多个领域,其中一个引发错误,则不会处理其他领域。 因此,如果您需要解决抛出的异常,您可以为 authz 执行 this 和为 authc 执行 this 之类的操作。

资源链接:

    http://shiro-user.582556.n2.nabble.com/Still-having-an-issue-with-multiple-realms-td7579698.html

【讨论】:

以上是关于如何使用数据库在 Java 代码中对用户进行身份验证的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Android 中对 Uber SDK 中的用户进行身份验证?

如何在 blazor webassembly 项目中对服务器端控制器中的用户进行身份验证?

如何在 wordpress 中对用户进行身份验证?

如何在没有登录的角度应用程序中对 Web Api 进行身份验证

如何在 .NET Core 中对 Spotify 进行经过身份验证的调用

在 SPA 中对访客/匿名用户进行身份验证