实现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
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
### 回答1: Shiro 提供了多种登录方式,包括: 1. 基于表单的登录:用户输入用户名和密码,然后提交表单进行身份验证。 2. 基于记住我功能的登录:在用户登录成功后,将用户的身份信息(如用户名和密码的哈希值)保存在 cookie 中。当用户再次访问网站时,可以通过 cookie 中的身份信息自动登录。 3. 基于 OAuth2 的登录:使用 OAuth2 协议进行身份验证,允许用户使用第三方身份提供者(如 Google、Facebook 等)进行登录。 4. 基于 JWT 的登录:使用 JSON Web Token(JWT)进行身份验证,JWT 包含了用户的身份信息和签名,可以通过密钥进行验证。 5. 基于单点登录(SSO)的登录:允许用户在多个应用程序之间共享身份信息,只需要登录一次即可在所有应用程序中使用。 以上是 Shiro 可以支持的一些常见登录方式,可以根据具体需求选择合适的登录方式。 ### 回答2: Shiro是一个强大的Java身份验证和授权框架,支持多种登录方式。下面是几种常见的登录方式: 1. 用户名密码登录:用户通过输入用户名和密码进行登录验证。Shiro提供了内置的UsernamePasswordToken来封装用户名和密码信息。 2. 第三方账号登录:用户可以通过第三方账号,如微信、QQ、微博等登录Shiro可以通过OAuth2等认证协议来实现第三方账号登录。 3. 邮箱登录:用户可以通过输入注册时绑定的邮箱和密码登录Shiro可以使用EmailToken来验证邮箱登录。 4. 手机号码登录:用户可以通过输入注册时绑定的手机号码和密码进行登录Shiro可以使用PhoneToken来验证手机号码登录。 5. 非交互式登录:用户可以通过预先授权、使用API Token等方式进行非交互式登录。在这种方式下,用户不需要提供用户名和密码,而是使用事先生成的Token进行验证。 6. 单点登录:用户可以通过在主系统登录后,在其他系统中无需再次输入密码直接登录Shiro支持集成开源的单点登录框架,如CAS、OAuth等。 这些登录方式都能够通过Shiro的身份验证功能进行集中管理和控制。通过配置不同的Realm和AuthenticationStrategy,可以灵活地适应各种登录需求,并且可以根据具体情况进行扩展和定制。总之,Shiro提供了多种登录方式的支持,可以方便地满足不同系统的用户认证需求。 ### 回答3: Shiro 是一个流行的 Java 安全框架,提供了多种登录方式,可以根据业务需求和用户实际情况选择合适的方式进行用户认证。 1. 用户名密码登录:这是最常见的登录方式,用户需要输入用户名和密码来进行认证。Shiro 提供了相应的认证策略和相关 API,可以对用户输入的用户名和密码进行校验,验证用户身份。 2. RememberMe 登录:RememberMe 是一种方便的登录方式,用户在成功登录后,可以选择“记住我”选项,下次再访问时可以直接通过验证,无需再次输入用户名和密码。Shiro 提供了 RememberMe 的功能支持,通过在 Cookie 中存储一个加密的记住我标记来实现。 3. 第三方登录Shiro 也支持第三方登录方式,例如使用社交媒体账号(如微信、QQ、微博)进行登录。用户可以选择使用第三方平台的账号来进行认证,Shiro 则需要与相应的第三方平台进行集成,完成认证流程。 4. 单点登录(SSO):单点登录是一种方便用户的登录方式,用户只需登录一次,即可访问多个相关系统Shiro 提供了相应的 SSO 功能支持,可以通过与其他系统集成,共享用户登录状态和认证信息,实现单点登录的效果。 5. 客户端证书登录:对于某些需要较高安全级别的系统,可以使用客户端证书登录方式。用户需要使用自己的数字证书进行登录认证,Shiro 提供了相关的认证策略和证书管理机制,以确保用户身份的安全认证。 总结来说,Shiro 提供了多种登录方式,包括用户名密码登录、RememberMe 登录、第三方登录、单点登录和客户端证书登录。开发者可以根据实际需求选择合适的登录方式,确保系统的安全性和用户体验。
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值