在Shiro中,Realm扮演着和DAO类似的中间人角色,通过Realm获取存储在各种地方的用户凭证及权限,但具体怎么获取则交给用户去实现,实现了解耦。我们可以自己实现Realm,来控制如何获取这些数据。不过Shiro已经贴心的实现了一些通用的Realm,如下图所示:
通常我们会将用户密码,用户权限存在关系型数据库里面,因此JdbcRealm应该是最常用的。JdbcRealm已经将数据库的读取封装好了,分别是doGetAuthenticationInfo方法和doGetAuthorizationInfo方法。我们以获取认证信息的doGetAuthenticationInfo方法为例:
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);
}
// Rethrow any SQL errors as an authentication exception
throw new AuthenticationException(message, e);
} finally {
JdbcUtils.closeConnection(conn);
}
return info;
}
获取到前端登录传递过来的用户名,然后调用password = getPasswordForUser(conn, username)[0];方法从数据库获取密码:
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;
}
可以看到都是基础的jdbc代码,那么获取用户名密码的SQL从哪里来的呢?当然这个需要自己提供,不过Shiro给了一个默认的SQL语句:
/**
* The default query used to retrieve account data for the user.
*/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
很明显不同的应用表结构是不一样的,Shiro提供这样一个SQL,似乎有点画蛇添足,会有多少人会用默认的SQL呢?根据我自己的表结构,我提供的SQL如下:
<!-- 配置shrio的验证 -->
<!--<bean id="shiroRealm" class="com.gameloft9.demo.security.ShiroRealm">-->
<!--<property name="userServiceImpl" ref="sysUserServiceImpl"/>-->
<!--</bean>-->
<bean id="shiroRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
<property name="dataSource" ref="dataSource"/>
<property name="authenticationQuery" value="select password from USER_TEST WHERE login_name = ?"/>
<property name="userRolesQuery" value="select r.role_name from SYS_ROLE_TEST r where r.id in( select t.role_id from SYS_USER_ROLE_TEST t where t.user_id = (select id from user_test where login_name = ?)) and r.is_deleted = '0'"/>
</bean>
<!-- 配置shrio的验证管理 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="shiroRealm"/>
</list>
</property>
</bean>
综上所述,我们只需要提供一个DataSource和获取用户认证数据的SQL就可以了,其他的事情就交给Shiro吧!
如果你不想被条条框框束缚,那么可以参考我之前写的Spring mvc集成Shrio,自定义一个Realm,自己实现用户数据的获取逻辑。下面将自定义的Realm贴出来以供参考:
package com.gameloft9.demo.security;
import com.gameloft9.demo.service.api.system.SysUserService;
import com.gameloft9.demo.dataaccess.model.system.UserTest;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.List;
/**
* 认证授权。
* @author gameloft9
*/
@Slf4j
@Data
public class ShiroRealm extends AuthorizingRealm {
/**
* 通过setter注入,这里没有通过@Autowired注入
* */
private SysUserService userServiceImpl;
/**
* 获取授权信息方法,返回用户角色信息
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
UserTest user = (UserTest) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (user != null) {//获取用户角色信息
List<String> roles = userServiceImpl.getRoleNames(user.getId());
info.addRoles(roles);
} else {
SecurityUtils.getSubject().logout();
}
return info;
}
/**
* 重写回调认证方法,subject.login()调用后回调此方法,获取认证信息。
* 如果是与第三方用户系统集成,可在此处进行身份认证,成功后可构造一个同登录token一致的认证信息。
* 或者干脆跳过shiro的认证,自己实现认证逻辑,成功后将用户信息放入session、cookie.
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authcToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
UserTest user = userServiceImpl.getByLoginName(token.getUsername());
if (user == null) {//用户不存在
throw new UnknownAccountException();
}
//构造一个用户认证信息并返回,后面会通过这个和token的pwd进行对比。
return new SimpleAuthenticationInfo(user,user.getPassword(),user.getRealName());
}
}