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只需要编写ShiroFilterFactoryBeanDefaultWebSecurityManager单个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。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值