shiro多realm异常解决
1、情况描述
我是需要实现两个realm进行登录认证,分别是passwordRealm(用户名+密码登录)、codeRealm(手机号码+验证码登录)。验证码登录的验证码是使用阿里云短信服务, 缓存是使用redis,设置10分钟过期。
不会短信验证码的可以看我的另一篇文章:短信验证码教程
报错类型是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.
这个是我参考网上文章和自己摸索得知的,如有错误,还请指出,谢谢!
2、自定义token(用户名+密码_使用shiro自带token)
1、TelCodeToken(参考shiro自带UserNamePasswordToken)
温馨提示: TelCodeToken我只需要一个参数,就是手机号,只需shiro帮我验证手机号。
package com.panyk.hotel_system.config.shiroSecurity.myToken;
import org.apache.shiro.authc.HostAuthenticationToken;
import org.apache.shiro.authc.RememberMeAuthenticationToken;
public class TelCodeToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
private String tel;
private Boolean rememberMe = false;
private String host;
public TelCodeToken() {
this.rememberMe = false;
}
public TelCodeToken(final String tel) {
this(tel, false, null);
}
public TelCodeToken(final String tel, boolean rememberMe) {
this(tel, rememberMe, null);
}
public TelCodeToken(final String tel, final boolean rememberMe, final String host) {
this.tel = tel;
this.rememberMe = rememberMe;
this.host = host;
}
public String getTel() {
return tel;
}
public void setTel(String tel) {
this.tel = tel;
}
public Object getPrincipal() {
return getTel();
}
public Object getCredentials() {
return getTel();
}
@Override
public String getHost() {
return host;
}
@Override
public boolean isRememberMe() {
return rememberMe;
}
}
3、自定义Realm
1、ParentRealm(清理缓存,可有可无)
public abstract class ParentRealm extends AuthorizingRealm {
public void clearCachedAuthorizationInfo() {
super.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
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();
}
}
2、PasswordRealm
温馨提示: 我是PasswordRealm extends ParentRealm
, 你们不清理缓存可以直接extends AuthorizingRealm
public class PasswordRealm extends ParentRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String name = token.getUsername();
User user = userService.getOne(new QueryWrapper<User>().eq("user_name", name).last("LIMIT 1"));
if (null == user)
throw new UnknownAccountException();
String password = user.getPassword();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, this.getName());
info.setCredentialsSalt(ByteSource.Util.bytes(name));
return info;
}
@Override
public boolean supports(AuthenticationToken var1){
return var1 instanceof UsernamePasswordToken;
}
}
3、CodeRealm
温馨提示: 我是PasswordRealm extends ParentRealm
, 你们不清理缓存可以直接extends AuthorizingRealm
public class CodeRealm extends ParentRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
TelCodeToken token = (TelCodeToken) authenticationToken;
String tel = (String) token.getPrincipal();
User user = userService.getOne(new QueryWrapper<User>().eq("tel", tel).last("LIMIT 1"));
if (null == user)
throw new UnknownAccountException();
return new SimpleAuthenticationInfo(user, tel, this.getName());
}
@Override
public boolean supports(AuthenticationToken var1){
return var1 instanceof TelCodeToken;
}
}
4、重写realm选择器
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;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Component
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(ModularRealmAuthenticator.class);
/**
* 重写doMultiRealmAuthentication,抛出异常,便于自定义ExceptionHandler捕获
*/
@SneakyThrows
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token){
AuthenticationStrategy strategy = this.getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
AuthenticationException exception = null;
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;
try{
info = realm.getAuthenticationInfo(token);
}catch (AuthenticationException e){
exception = e;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, e);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, exception);
}else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
if(exception != null){
throw exception;
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
5、编写shiro配置类
import com.panyk.hotel_system.config.shiroSecurity.myRealm.CodeRealm;
import com.panyk.hotel_system.config.shiroSecurity.myRealm.PasswordRealm;
import org.apache.shiro.authc.AbstractAuthenticator;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.*;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(defaultWebSecurityManager);
HashMap<String, String> map = new LinkedHashMap<>();
// anon:无需认证、authc认证、user记住我、role对应角色、perms资源
map.put("/test.html", "authc");
factoryBean.setFilterChainDefinitionMap(map);
factoryBean.setLoginUrl("/loginOrRegi");
return factoryBean;
}
@Bean(name = "getDefaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(PasswordRealm passwordRealm, CodeRealm codeRealm, AbstractAuthenticator abstractAuthenticator){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> realms = new ArrayList<>();
realms.add(passwordRealm);
realms.add(codeRealm);
securityManager.setRealms(realms);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setAuthenticator(abstractAuthenticator);//解决多realm的异常问题重点在此
return securityManager;
}
@Bean(name = "abstractAuthenticator")
public AbstractAuthenticator abstractAuthenticator(PasswordRealm passwordRealm, CodeRealm codeRealm){
ModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
List<Realm> realms = new ArrayList<>();
realms.add(passwordRealm);
realms.add(codeRealm);
authenticator.setRealms(realms);
return authenticator;
}
@Bean(name = "passwordRealm")
public PasswordRealm passwordRealm(){
return new PasswordRealm();
}
@Bean(name = "codeRealm")
public CodeRealm codeRealm(){
return new CodeRealm();
}
}
温馨提示:配置类核心是getDefaultWebSecurityManager
, 如果是单realm只需要编写ShiroFilterFactoryBean
、DefaultWebSecurityManager
、单个realm
; 多realm的话需要多配置abstractAuthenticator
记得要注入到DefaultWebSecurityManager
,用于多realm选择策略。
认证策略类型:
1.AtLeastOneSuccessfulStrategy 有一个realm成功认证就可以。
2.FirstSuccessfulStrategy 第一个realm成功认证就可以,只看第一个,所以realm的顺序也有关系。
3.AllSuccessfulStrategy 全部realm认证通过才可以。
6、controller(登录部分)
温馨提示: R
是我自己编写的工具类,用于响应前端。StringUtils
也是自己编写的工具类,用于验证字符串是否为null或者空串、只包含空格。
@Autowired
private UserService userService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@ResponseBody
@RequestMapping("/usr/login")
public R login(String name, String tel, String pwd, String code, String type){
if (!StringUtils.isEmpty(type)){
if ("pwd".equals(type)){
if (StringUtils.isEmpty(name) || StringUtils.isEmpty(pwd)) {
return R.fail(207, "输入值为空");
} else{
UsernamePasswordToken token = new UsernamePasswordToken(name, pwd);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return R.ok(200, "登录成功");
}catch (UnknownAccountException e){
return R.fail(214, "用户名错误或不存在");
}catch (IncorrectCredentialsException e){
return R.fail(212, "密码错误");
}
}
}else{
if (StringUtils.isEmpty(tel) || StringUtils.isEmpty(code)) {
return R.fail(207, "输入值为空");
} else{
TelCodeToken token = new TelCodeToken(tel);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
String existCode = redisTemplate.opsForValue().get(tel);
if (StringUtils.isEmpty(existCode) || !existCode.equals(code))
return R.fail(209, "验证码错误或已失效");
return R.ok(200, "登录成功");
}catch (UnknownAccountException e){
return R.fail(211, "手机号码错误或不存在");
}
}
}
}else{
return R.fail(207, "输入值为空");
}
}
7、小问题
1、一般shiro自带的异常类型是足够用的,当然也可以自定义异常、异常处理器。
2、单realm的话,可以在realm中return null, 在controller中捕获shiro自带异常即可, 很智能很方便。
3、多realm的话,需要在realm中抛出异常,也要在controller中捕获异常,不然报错,could not be authenticated by any configured realms. Please ensure that at least one realm can authenticate these tokens
, 这是在说你的token没有任何realm处理,而realm选择策略是至少一个realm认证通过。(我踩的坑)
4、我的controller有许多数据校验,虽然前端可以校验,但为了严谨还是在后端也进行校验。
5、验证码我在controller判断,当然也可以放在realm里,但是我除了登录,还有注册、修改密码等业务用到验证码判断,为了方便将redisTemplate写在controller。