实现shiro多方式登录系统

实现shiro多方式登录系统

致谢

首先感谢张开涛大神,写了很多有关shiro的博客,内容详实并佐以实例,使鄙人可以很快上手使用shiro,在这里给出大神的跟我学shiro的博客,方便自己跟他人浏览学习。

前言

shiro的确很强大,可以在页面使用标签,也可以在控制层以注解的方式进行访问控制,只是做过网站的都知道,我们往往需要实现多种方式的登陆,例如:手机验证码登陆,QQ,微信登陆,等等,这个时候原生的shiro就无法达到这个功能了,作为一名java开发人员,最熟悉的就是封装/继承/多态了,OK,以手机验证码登陆为例,我们自己实现吧!

正文

理论

要进行多方式登陆

1.首先要区分用户使用的登陆方式

2.然后根据登陆方式选择我们注册到shiro中的某个匹配当前登陆方式的Realm,每个Realm中都能用来获取用户的认证信息以及权限信息,并根据这些信息判断用户是否有权利登陆系统以及访问哪些功能,那么就至少需要有两个以上的Realm提供给shiro的ModularRealmAuthenticator(模块化用户认证器)

3.shiro提供了三种策略来进行多Realm下认证:AllSuccessfulStrategy(全部匹配策略),AtLeastOneSuccessfulStrategy(至少一个匹配),FirstSuccessfulStrategy(第一个匹配),默认使用AtLeastOneSuccessfulStrategy,这些策略并符合我们的需要,因此重写ModularRealmAuthenticator

下面,了解一下需要继承的类/接口,以及需要重写的方法:

UsernamePasswordToken

此类保存了用户的登陆信息,但是不足以满足多登陆方式的要求,因而需要进行一些调整,增加一个loginType属性,用来保存用户的登陆方式

FormAuthenticationFilter.createToken(ServletRequest request, ServletResponse response)

此类是用户提交表单进行登陆认证的过滤器,此方法是用来以为用户提交的信息合成一个Token(令牌),并拿着这个令牌去判断信息是否匹配

ModularRealmAuthenticator.doMultiRealmAuthentication(Collection realms, AuthenticationToken token)

此类是模块化凭证匹配器此方法是用来在注入的realms中按照指定的策略,逐个获取保存在系统中认证信息并整合在一起,返回这个整合后的信息

AuthenticatingRealm.supports(AuthenticationToken token)

此类是认证信息匹配器,此方法用来判断当前用户提交的登陆信息是否可以被校验,返回true则会使用该类的实现类进行匹配,返回false则跳过

代码

创建UsernamePasswordLoginTypeToken,继承UsernamePasswordToken

/**
 * Created by Lancelot on 2017/3/17.
 * 重写{@link #UsernamePasswordToken},增加{@link #loginType}属性,该属性是在登陆界面form表单中传递过来的,定义了用户使用的登陆类型
 */
public class UsernamePasswordLoginTypeToken extends UsernamePasswordToken {

    private static final long serialVersionUID = 7134536615448037793L;
    /**
    *登陆类型
    */
    private String loginType;

    public UsernamePasswordLoginTypeToken(String username, String password, boolean rememberMe, String host, String loginType) {
        super(username, password, rememberMe, host);
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }
}

创建MyFormAuthenticationFilter,继承FormAuthenticationFilter,并重写createToken():

public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
    public static final String DEFAULT_LOGIN_TYPE_PARAM = "loginType";
    private boolean kickOutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认1

    private CacheManager cacheManager;
    private SessionManager sessionManager;
    private String kickOutSessionCacheName;
    private Cache<String, Deque<Serializable>> cache;
    private ReentrantLock reentrantLock = new ReentrantLock();
    private String loginTypeParamName = DEFAULT_LOGIN_TYPE_PARAM;

    @PostConstruct
    public void init() {
        this.cache = cacheManager.getCache(kickOutSessionCacheName);
    }


    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        //如果其他过滤器已经,验证失败了,则禁止登陆,不再进行身份验证
        if (request.getAttribute(getFailureKeyAttribute()) != null) {
            return true;
        }
        return super.onAccessDenied(request, response, mappedValue);
    }

    /**
     * 重写登陆成功后的处理方法,使其跳转到指定的页面,这里是successUrl
     *
     * @param token    令牌
     * @param subject  用户信息
     * @param request  请求
     * @param response 响应
     * @return true 继续过滤,false 跳过之后的过滤;
     * @throws Exception 异常
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        Session session = subject.getSession();
        String username = (String) subject.getPrincipal();
        Serializable sessionId = session.getId();
        reentrantLock.lock();
        Deque<Serializable> deque = cache.get(username);
        if (deque == null) {
            deque = new LinkedList<>();
        }

        //如果队列里没有此sessionId,且用户没有被踢出;放入队列
        if (!deque.contains(sessionId)) {
            deque.push(sessionId);
        }

        //如果队列里的sessionId数超出最大会话数,开始踢人
        while (deque.size() > maxSession) {
            Serializable kickOutSessionId;
            if (kickOutAfter) { //如果踢出后者
                kickOutSessionId = deque.removeFirst();
            } else { //否则踢出前者
                kickOutSessionId = deque.removeLast();
            }
            try {
                Session kickOutSession = sessionManager.getSession(new DefaultSessionKey(kickOutSessionId));
                if (kickOutSession != null) {
                    //设置会话的 kickOut 属性表示踢出了
                    kickOutSession.setAttribute("kickOut", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }

        cache.put(username, deque);
        reentrantLock.unlock();
        WebUtils.getAndClearSavedRequest(request);
        WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
        return true;
    }

    @Override
    protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
        request.setAttribute(getFailureKeyAttribute(), ae);
    }

    /**
     * 重写该方法,为了将loginType参数保存到token中
     *
     * @param request  请求
     * @param response 响应
     * @return
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String username = getUsername(request);
        String password = getPassword(request);
        String loginType = getLoginType(request);
        return createToken(username, password, request, response, loginType);
    }

    private AuthenticationToken createToken(String username, String password, ServletRequest request, ServletResponse response, String loginType) {
        boolean rememberMe = isRememberMe(request);
        String host = getHost(request);
        return createToken(username, password, rememberMe, host, loginType);
    }

    private AuthenticationToken createToken(String username, String password, boolean rememberMe, String host, String loginType) {
        return new UsernamePasswordLoginTypeToken(username, password, rememberMe, host, loginType);
    }

    private String getLoginType(ServletRequest request) {
        return WebUtils.getCleanParam(request, getLoginTypeParamName());
    }

    .....省略getter/setter方法    
}

创建MyModularRealmAuthenticator,继承自ModularRealmAuthenticator,重写doMultiRealmAuthentication()方法

/**
 * Created by Lancelot on 2017/3/17.
 * 重写模块化用户验证器,根据登录界面传递的loginType参数,获取唯一匹配的realm
 */
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
    private final Logger log = LoggerFactory.getLogger(MyModularRealmAuthenticator.class);


    @Override
    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException {
        Realm uniqueRealm = getUniqueRealm(realms, token);
        if (uniqueRealm == null) {
            throw new UnsupportedTokenException("没有匹配类型的realm");
        }
        return uniqueRealm.getAuthenticationInfo(token);
    }

    /**
     * 判断realms是否匹配,并返回唯一的可用的realm,否则返回空
     *
     * @param realms realm集合
     * @param token  登陆信息
     * @return 返回唯一的可用的realm
     */
    private Realm getUniqueRealm(Collection<Realm> realms, AuthenticationToken token) {
        for (Realm realm : realms) {
            if (realm.supports(token)) {
                return realm;
            }
        }
        log.error("一个可用的realm都没有找到......");
        return null;
    }

}

创建UserRealm,继承自AuthorizingRealm,并重写里面的三个方法

/**
 * Created by Lancelot on 2017/3/17.
 * 从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作,可以把 Realm 看成 DataSource,即安全数据源。
 * 从数据库中获取认证信息及授权信息
 */
public class UserRealm extends AuthorizingRealm {
    /**
     * 用户数据DAO
     */
    @Autowired
    private BiUserMapper userDao;
    /**
     * 支持的登陆类型
     */
    private String supportedLoginType;

    /**
     * 授权验证
     *
     * @param principals 认证人
     * @return 授权信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.setRoles(userDao.findRoles(username));
        authorizationInfo.setStringPermissions(userDao.findPermissions(username));

        return authorizationInfo;
    }


    /**
     * 用户认证
     *
     * @param token 令牌
     * @return 认证信息
     * @throws AuthenticationException 认证失败
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String username = (String) token.getPrincipal();

        BiUser user = userDao.selectByLoginName(username);

        if (user == null) {
            throw new UnknownAccountException();//没找到帐号
        }

        if (user.getLockTime() != null && user.getLockTime().after(new Date())) {
            throw new LockedAccountException(); //帐号锁定
        }

        //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
        return new SimpleAuthenticationInfo(
                user.getLoginName(), //用户名
                user.getLoginPwd(), //密码
                getName()  //realm name
        );
    }

    public boolean supports(AuthenticationToken token) {
        if (token instanceof UsernamePasswordLoginTypeToken) {
            UsernamePasswordLoginTypeToken usernamePasswordLoginTypeToken = (UsernamePasswordLoginTypeToken) token;
            return getSupportedLoginType().equals(usernamePasswordLoginTypeToken.getLoginType());
        }
        return false;
    }

    public String getSupportedLoginType() {
        return supportedLoginType;
    }
   /**
   *spring注入
   */
    public void setSupportedLoginType(String supportedLoginType) {
        this.supportedLoginType = supportedLoginType;
    }
}

创建VerifyCodeRealm,继承自UserRealm,重写里面的两个方法

/**
 * Created by Lancelot on 2017/3/16.
 * 使用验证码登陆
 */
public class VerifyCodeRealm extends UserRealm {

    @Autowired
    private BiUserMapper userDao;
    @Autowired
    private ShiroCacheUtils shiroCacheUtils;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        BiUser user = userDao.selectByLoginName(username);

        if (user == null) {
            throw new UnknownAccountException();//没找到帐号
        }

        if (user.getLockTime() != null && user.getLockTime().after(new Date())) {
            throw new LockedAccountException(); //帐号锁定
        //从缓存中跟用户名取出验证码信息(验证码+到期时间)
        CaptchaVo captcha = shiroCacheUtils.getCaptcha(username);
        //判断验证码是否过期,如果过期,从缓存中移除并抛出一个自定义异常
        if (captcha != null && captcha.isExpired()) {
            shiroCacheUtils.clearUserCaptcha(username);
            throw new CaptchaExpiredException("验证码已过期");
        }
        //验证码不存在,抛出一个自定义异常
        if (captcha == null) {
            throw new AuthenticationException("尚未发送验证码,请先获取");
        }

        //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
        return new SimpleAuthenticationInfo(
                user.getLoginName(), //用户名
                captcha.getCaptcha(), //验证码
                getName()  //realm name
        );
    }

    /**
    *重写断言验证码是否一致,为了方便区别错误原因,抛出一个自定义异常
    */
    @Override
    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        try {
            super.assertCredentialsMatch(token, info);
        } catch (IncorrectCredentialsException e) {
            throw new IncorrectCaptchaException();
        }

    }
}

附上spring-shiro的配置文件中需要注意的地方:

    <!-- Realm 实现 -->
    <bean id="userRealm" class="com.seawave.shiro.realm.UserRealm">
        <property name="authenticationCachingEnabled" value="true"/>
        <property name="authenticationCacheName" value="${authenticationCacheName}"/>
        <property name="authorizationCachingEnabled" value="true"/>
        <property name="authorizationCacheName" value="${authorizationCacheName}"/>
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
        <property name="cachingEnabled" value="true"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="supportedLoginType" value="usernameAndPassword"/>
    </bean>
    <bean id="verifyCodeRealm" class="com.seawave.shiro.realm.VerifyCodeRealm">
        <property name="authenticationCachingEnabled" value="true"/>
        <property name="authenticationCacheName" value="${userAndCaptchaCacheName}"/>
        <property name="authorizationCachingEnabled" value="true"/>
        <property name="authorizationCacheName" value="${authorizationCacheName}"/>
        <!--这里使用shiro自带的一个凭证验证器,根据字节码判断是否一致-->
        <property name="credentialsMatcher" ref="simpleCredentialsMatcher"/>
        <property name="cachingEnabled" value="true"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="supportedLoginType" value="usernameAndCaptcha"/>
    </bean>

    <!--凭证匹配器-->
    <bean id="authenticator" class="com.seawave.shiro.authc.pam.MyModularRealmAuthenticator">
        <property name="realms">
            <list>
                <ref bean="verifyCodeRealm"/>
                <ref bean="userRealm"/>
            </list>
        </property>
    </bean>

    <!--权限匹配器-->
    <bean id="authorizer" class="org.apache.shiro.authz.ModularRealmAuthorizer">
        <property name="realms">
            <list>
                <ref bean="verifyCodeRealm"/>
                <ref bean="userRealm"/>
            </list>
        </property>
    </bean>

    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"/>
        <property name="authorizer" ref="authorizer"/>
    </bean>

    <!-- 基于Form表单的身份验证过滤器 -->
    <!-- shiro的身份验证成功后的逻辑是这样的:如果原来的请求里面存在了一个请求地址,如:http://localhost/a/b/c,那么此时-->
    <!-- shiro将自动跳转到a/b/c页面或对应的Controller,这时候自定义的successUrl是不生效的-->
    <!-- 如果想要跳转到指定的页面只能重写onLoginSuccess()方法:-->
    <!-- 1.进行重定向-->
    <!-- 2.清空请求里面的请求地址,此时因为请求地址为空,则会使用我们自定义的successUrl-->
    <bean id="formAuthenticationFilter"
          class="com.seawave.shiro.captcha.MyFormAuthenticationFilter">
        <property name="usernameParam" value="name"/>
        <property name="passwordParam" value="pwd"/>
        <!--该参数定义了用户表单中提交的'登陆类型'这个参数的名称-->
        <property name="loginTypeParamName" value="loginType"/>
        <property name="rememberMeParam" value="rememberMe"/>
        <property name="successUrl" value="/homePage/gotoHomePage"/>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="kickOutSessionCacheName" value="${kickOutSessionCacheName}"/>
        <property name="kickOutAfter" value="false"/>
        <property name="maxSession" value="1"/>
    </bean>

总结

多看源码

耐心细致

  • 11
    点赞
  • 6
    收藏
  • 打赏
    打赏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 23

打赏作者

Lancelot_DL

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值