1、使用场景
用户名和密码登录是一种非常常规的登录方式,但是随着手机的普及,越来越多的人采用手机号+验证码的登录方式,还有一些网站更是提供了邮箱、扫码登录等,面对一个个新增的登录方式,如何在不改变代码的情况下完成对登录方式的新增,shiro提供了多realm认证机制,我们可以利用shiro轻松的完成对一个项目的登录方式的新增、修改等。
2、多realm认证处理方式
2.1 两种认证方式
shiro给我们提供了两种认证方式,可以根据图片来理解,如下图,方式1可以理解为只要满足一个realm的条件验证即可完成登录,但是方式2则需要同时满足realm3和realm4两个的条件才可以通过登录验证,绝大部分场景是只需要方式1的,方式2用的不多,并且使用方式2会导致一个异常的问题,后续会贴出修复的方法代码。 我们只需要告诉shiro使用哪一个或者多个realm即可。
2.2 使用多realm时的认证策略
shiro在多个realm同时生效时提供了三种策略:
- AtLeastOneSuccessFulAtrategy 至少一个成功的策略-默认使用
- AllSuccessFulStrategy 全部成功策略
- FirstSuccessFulStrategy 第一个成功策略
可以通过setAuthenticationStrategy方法设置策略
3、处理思路
之前的几章已经讲过shiro可拓展性非常好,那我们如果需要多个realm的话,只需要重写shiro自带的realm选择器,并且把自定义的选择器注入到SecurityManager中,应该就可以完成多realm的处理。
4、代码部分
4.1新建realm
4.1.1 自定义realm抽象类
因为每个realm都需要重写清除缓存的方法,因此新建了ParentRealm抽象类继承AuthorizingRealm
public abstract class ParentRealm extends AuthorizingRealm {
/**
* 重写方法,清除当前用户的的 授权缓存
*
* @param principals
*/
public void clearCachedAuthorizationInfo() {
super.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
/**
* 重写方法,清除当前用户的 认证缓存
*
* @param principals
*/
public void clearCachedAuthenticationInfo() {
super.clearCachedAuthenticationInfo(SecurityUtils.getSubject().getPrincipals());
}
/**
* 清除某个用户认证和授权缓存
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
/**
* 自定义方法:清除所有 授权缓存
*/
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
/**
* 自定义方法:清除所有 认证缓存
*/
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
/**
* 自定义方法:清除所有的 认证缓存 和 授权缓存
*/
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
4.1.2新增其他的realm
1.RealmEmail
public class RealmEmail extends ParentRealm {
@Autowired
@Lazy
UserService userService;
/**
* 权限设置
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
System.out.println("进入RealmEmail权限设置方法!");
String username = (String) principals.getPrimaryPrincipal();
// 从数据库或换村中获取用户角色信息
User user = userService.findByEmail(username);
// 获取用户角色
Set<String> roles = user.getRole();
// 获取用户权限
Set<String> permissions = user.getPermission();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 设置权限
simpleAuthorizationInfo.setStringPermissions(permissions);
// 设置角色
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 身份验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("进入RealmEmail登录验证方法!");
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();// 用户输入用户名
User user = userService.findByEmail(username);// 根据用户输入用户名查询该用户
if (user == null) {
throw new UnknownAccountException();// 用户不存在
}
if("2".equals(user.getState())) {
throw new LockedAccountException();
}
String password = user.getPassword();// 数据库获取的密码
// 主要的(用户名,也可以是用户对象(最好不放对象)),资格证书(数据库获取的密码),区域名称(当前realm名称)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
// 加盐,对比的时候会使用该参数对用户输入的密码按照密码比较器指定规则加盐,加密,再去对比数据库密文
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return simpleAuthenticationInfo;
}
}
2.RealmPhone
public class RealmPhone extends ParentRealm {
@Autowired
@Lazy
UserService userService;
/**
* 权限设置
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
System.out.println("进入RealmPhone权限设置方法!");
String username = (String) principals.getPrimaryPrincipal();
// 从数据库或换村中获取用户角色信息
User user = userService.findByPhone(username);// 根据用户输入用户名查询该用户
// 获取用户角色
Set<String> roles = user.getRole();
// 获取用户权限
Set<String> permissions = user.getPermission();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 设置权限
simpleAuthorizationInfo.setStringPermissions(permissions);
// 设置角色
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 身份验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
System.out.println("进入RealmPhone登录验证方法!");
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();// 用户输入用户名
User user = userService.findByPhone(username);// 根据用户输入用户名查询该用户.
if (user == null) {
throw new UnknownAccountException();// 用户不存在
}
if("2".equals(user.getState())) {
throw new LockedAccountException();
}
String password = user.getPassword();// 数据库获取的密码
// 主要的(用户名,也可以是用户对象(最好不放对象)),资格证书(数据库获取的密码),区域名称(当前realm名称)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
// 加盐,对比的时候会使用该参数对用户输入的密码按照密码比较器指定规则加盐,加密,再去对比数据库密文
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return simpleAuthenticationInfo;
}
}
3.RealmUsername
public class RealmUsername extends ParentRealm {
@Autowired
@Lazy
UserService userService;
/**
* 权限设置
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
System.out.println("进入RealmUsername权限设置方法!");
String username = (String) principals.getPrimaryPrincipal();
// 从数据库或换村中获取用户角色信息
User user = userService.findByUsername(username);// 根据用户输入用户名查询该用户
// 获取用户角色
Set<String> roles = user.getRole();
// 获取用户权限
Set<String> permissions = user.getPermission();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 设置权限
simpleAuthorizationInfo.setStringPermissions(permissions);
// 设置角色
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 身份验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
System.out.println("进入RealmUsername自定义登录验证方法!");
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();// 用户输入用户名
User user = userService.findByUsername(username);// 根据用户输入用户名查询该用户
if (user == null) {
throw new UnknownAccountException();// 用户不存在
}
if ("2".equals(user.getState())) {
throw new LockedAccountException();
}
String password = user.getPassword();// 数据库获取的密码
// 主要的(用户名,也可以是用户对象(最好不放对象)),资格证书(数据库获取的密码),区域名称(当前realm名称)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
// 加盐,对比的时候会使用该参数对用户输入的密码按照密码比较器指定规则加盐,加密,再去对比数据库密文
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return simpleAuthenticationInfo;
}
}
4.2重写ModularRealmAuthenticator,自定义realm的使用
import java.util.ArrayList;
import java.util.Collection;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 自定义身份认证realm控制器
*
* @ClassName: MyModularRealmAuthenticator
* @Description 用于告诉shiro使用哪个realm处理
* @version
* @author JH
* @date 2019年12月31日 下午4:19:13
*/
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class);
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 强制转换回自定义的CustomizedToken
MyUsernamePasswordToken userToken = (MyUsernamePasswordToken) authenticationToken;
// 登录类型
String loginType = userToken.getLoginType();
// 所有Realm
Collection<Realm> realms = getRealms();
// 登录类型对应的所有Realm
Collection<Realm> typeRealms = new ArrayList<>();
for (Realm realm : realms) {
if (realm.getName().contains(loginType)) {
typeRealms.add(realm);
}
}
// 判断是单Realm还是多Realm
if (typeRealms.size() == 1) {
//单realm处理
return doSingleRealmAuthentication(((ArrayList<Realm>) typeRealms).get(0), userToken);
} else {
//多realm处理,需满足全部realm认证
return doMultiRealmAuthentication(typeRealms, userToken);
}
}
/**
* 重写doMultiRealmAuthentication,修复多realm联合认证只出现AuthenticationException异常,而未处理其他异常
*/
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
//处理只提示AuthenticationException异常问题
if(t instanceof AuthenticationException) {
log.debug("realmName:"+realm.getName(), t);
throw (AuthenticationException)t;
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
4.3重写UsernamePasswordToken
新增loginType字段
public class MyUsernamePasswordToken extends UsernamePasswordToken {
private static final long serialVersionUID = 1L;
//登录类型
private String loginType;
public MyUsernamePasswordToken(String username, final String password, String loginType) {
super(username, password);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
4.4 登录controller把loginType加入
@RequestMapping("/login")
public ReturnMap login(String username, Boolean rememberMe, String password, String loginType) {
Subject subject = SecurityUtils.getSubject();
MyUsernamePasswordToken token = new MyUsernamePasswordToken(username, password, loginType);
if (rememberMe != null) {
token.setRememberMe(rememberMe);
}
try {
// 登录
subject.login(token);
} catch (UnknownAccountException uae) {
// 用户名未知...
return new ReturnMap().fail().message("用户不存在!");
} catch (IncorrectCredentialsException ice) {
// 凭据不正确,例如密码不正确 ...
return new ReturnMap().fail().message("密码不正确!");
} catch (LockedAccountException lae) {
// 用户被锁定,例如管理员把某个用户禁用...
return new ReturnMap().fail().message("用户被锁定!");
} catch (ExcessiveAttemptsException eae) {
// 尝试认证次数多余系统指定次数 ...
return new ReturnMap().fail().message("尝试认证次数过多,请稍后重试!");
} catch (AuthenticationException ae) {
// 其他未指定异常
return new ReturnMap().fail().message("未知异常!");
}
return new ReturnMap().success().data("登录成功!");
}
4.5 模拟数据库数据处理
@Service
public class UserServiceImpl implements UserService {
@Override
public User findByUsername(String username) {
User user = new User();
if ("username".equals(username)) {
Set<String> roleList = new HashSet<>();
Set<String> permissionsList = new HashSet<>();
roleList.add("admin");
permissionsList.add("admin");
user.setUsername("username");
user.setPassword("b6d3d2a23b4d5e313d3f2efe3cda2614");
user.setRole(roleList);
user.setPermission(permissionsList);
return user;
} else {
return null;
}
}
@Override
public User findByEmail(String email) {
User user = new User();
if ("email".equals(email)) {
Set<String> roleList = new HashSet<>();// 角色
Set<String> permissionsList = new HashSet<>();// 权限
roleList.add("consumer");
permissionsList.add("consumer");
user.setUsername("email");
user.setPassword("5fb06af6320cb2f9f090c4f9e1337ffb");
return user;
} else {
return null;
}
}
@Override
public User findByPhone(String phone) {
User user = new User();
Set<String> roleList = new HashSet<>();// 角色
Set<String> permissionsList = new HashSet<>();// 权限
if ("phone".equals(phone)) {
user.setUsername("guest");
user.setPassword("94585d3850aa9fe1156d272ce3447a07");
roleList.add("guest");
permissionsList.add("guest");
return user;
} else {
return null;
}
}
}
4.6把自定义的realm和MyModularRealmAuthenticator 注入SecurityManager
/**
* 自定义身份认证 realmEmail;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 MyShiroRealm, 否则会影响 MyShiroRealm类 中其他类的依赖注入
*/
@Bean
public ParentRealm realmEmail() {
RealmEmail myShiroRealm = new RealmEmail();
// 设置密码比较器
myShiroRealm.setCredentialsMatcher(CredentialsMatcher());
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
myShiroRealm.setAuthenticationCachingEnabled(true);
// 启用授权缓存,即缓存AuthorizationInfo信息,默认false,一旦配置了缓存管理器,授权缓存默认开启
myShiroRealm.setAuthorizationCachingEnabled(true);
return myShiroRealm;
}
/**
* 自定义身份认证 realmPhone;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 MyShiroRealm, 否则会影响 MyShiroRealm类 中其他类的依赖注入
*/
@Bean
public ParentRealm realmPhone() {
RealmPhone realmPhone = new RealmPhone();
// 设置密码比较器
realmPhone.setCredentialsMatcher(CredentialsMatcher());
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
realmPhone.setAuthenticationCachingEnabled(true);
// 启用授权缓存,即缓存AuthorizationInfo信息,默认false,一旦配置了缓存管理器,授权缓存默认开启
realmPhone.setAuthorizationCachingEnabled(true);
return realmPhone;
}
/**
* 自定义身份认证 realmUsername;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 MyShiroRealm, 否则会影响 MyShiroRealm类 中其他类的依赖注入
*/
@Bean
public ParentRealm realmUsername() {
RealmUsername realmUsername = new RealmUsername();
// 设置密码比较器
realmUsername.setCredentialsMatcher(CredentialsMatcher());
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
realmUsername.setAuthenticationCachingEnabled(true);
// 启用授权缓存,即缓存AuthorizationInfo信息,默认false,一旦配置了缓存管理器,授权缓存默认开启
realmUsername.setAuthorizationCachingEnabled(true);
return realmUsername;
}
/**
* 自定义身份认证realm控制器
* @return
*/
@Bean
public ModularRealmAuthenticator myModularRealmAuthenticator(){
//自定义身份认证realm控制器
MyModularRealmAuthenticator modularRealmAuthenticator = new MyModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setAuthenticator(myModularRealmAuthenticator());//配置自定义realm过滤器
Set<Realm> realms = new HashSet<>();
realms.add(realmEmail());
realms.add(realmPhone());
realms.add(realmUsername());
securityManager.setRealms(realms);;//配置自定义权限认证器
securityManager.setRememberMeManager(rememberMeManager());//配置记住我管理器
securityManager.setCacheManager(cacheManager());//配置缓存管理器
return securityManager;
}
5.测试
1.使用用户名密码登录方式,输入手机号的账号提示,后台进入用户名密码验证
2. 使用手机号码登录方式,提示登录成功
6.注意事项
6.1 realm分配方式
自定义realm分配器上面我们使用了类名包含LoginType的方式来判断使用哪个realm,因此需要规定前台logintype字段的值,才能正常使用。
6.2 多realm工作时抛出自定义异常获取不到的处理
在使用多realm同时生效时,使用AtLeastOneSuccessfulStrategy或者FirstSuccessFulStrategy策略,会导致无法抛出自定义异常而抛出AuthenticationException异常的问题,需要重写ModularRealmAuthenticator的doMultiRealmAuthentication方法,把catch捕获的异常抛出即可!
doMultiRealmAuthentication部分代码
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {//这里把所有异常捕获了但是并未抛出,导致继续运行
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
修改后
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
//处理只提示AuthenticationException异常问题
if(t instanceof AuthenticationException) {
log.debug("realmName:"+realm.getName(), t);
throw ((AuthenticationException)t);
}
7、源码
- 需要源码可以点击这里获取!