[shiro] - 加入rememberMe功能

Posted ukzq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[shiro] - 加入rememberMe功能相关的知识,希望对你有一定的参考价值。

 

shiro不加入rememberMe没事,一加入就出错.

RememberMeAuthenticationToken : 

public interface RememberMeAuthenticationToken extends AuthenticationToken {

    /**
     * Returns {@code true} if the submitting user wishes their identity (principal(s)) to be remembered
     * across sessions, {@code false} otherwise.
     *
     * @return {@code true} if the submitting user wishes their identity (principal(s)) to be remembered
     *         across sessions, {@code false} otherwise.
     */
    boolean isRememberMe();

}

该接口只有一个isRememberMe() 来判定是否选定了记住我选项.

UsernamePasswordToken : 该类实现了RememberMeAuthenticationToken

public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken
/**
     * Whether or not ‘rememberMe‘ should be enabled for the corresponding login attempt;
     * default is <code>false</code>
     */
    private boolean rememberMe = false;

默认参数false,

/**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted, as well as if the user
     * wishes their identity to be remembered across sessions.
     * <p/>
     * <p>This is a convience constructor and maintains the password internally via a character
     * array, i.e. <tt>password.toCharArray();</tt>.  Note that storing a password as a String
     * in your code could have possible security implications as noted in the class JavaDoc.</p>
     *
     * @param username   the username submitted for authentication
     * @param password   the password string submitted for authentication
     * @param rememberMe if the user wishes their identity to be remembered across sessions
     * @since 0.9
     */
    public UsernamePasswordToken(final String username, final String password, final boolean rememberMe) {
        this(username, password != null ? password.toCharArray() : null, rememberMe, null);
    }

里面的可以传入rememberMe的一个构造器,最后一个null是host

/**
     * Simply returns {@link #getUsername() getUsername()}.
     *
     * @return the {@link #getUsername() username}.
     * @see org.apache.shiro.authc.AuthenticationToken#getPrincipal()
     */
    public Object getPrincipal() {
        return getUsername();
    }
/**
     * Returns the {@link #getPassword() password} char array.
     *
     * @return the {@link #getPassword() password} char array.
     * @see org.apache.shiro.authc.AuthenticationToken#getCredentials()
     */
    public Object getCredentials() {
        return getPassword();
    }

这些晦涩的getPricipal(), getCredentials()其实就是获取用户名,密码的Object表示

登录的一个方法 (控制层)

 @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(Model model, User user,boolean rememberMe) {
        System.err.println("login");
        System.err.println("rememberMe:"+rememberMe);
//        UsernamePasswordToken token = new UsernamePasswordToken(user.getName(),user.getPassword());
        System.err.println("rememberMe:"+rememberMe);
        UsernamePasswordToken token = new UsernamePasswordToken(user.getName(),user.getPassword(), rememberMe);
        Subject subject = SecurityUtils.getSubject();

        try {
            subject.login(token);
        } catch (AuthenticationException e) {
//            e.printStackTrace();
            System.err.println("login异常");
        }
        Session session = subject.getSession();
        session.setAttribute("subject",subject);
        return "redirect:/admin/";//该路径应该为后台的首页页面,为login之后的redirect路径
    }

可以看出来调用了构造器UsernamePasswordToken,传入三个参数,分别是前端form过来的name,password,rememberMe,

但是会报异常.

2019-01-17 15:51:10,281 - Authentication failed for token submission [org.apache.shiro.authc.UsernamePasswordToken - null, 
rememberMe=false (0:0:0:0:0:0:0:1)]. Possible unexpected error? (Typical or expected login exceptions should extend from AuthenticationException).

这表示身份验证失败了,但是报错之后又查询数据库还是会通过登录.这就奇怪了.

如果只是传入name,password,没有错.

将身份验证信息方法改为数据库里的用户名(写死)

/*获取身份验证信息*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 获取账号密码
//        Object userName = token.getPrincipal();
//        String userName = token.getPrincipal().toString();
        // 获取数据库中的密码
        User user = userService.getByName("zhang3");
        String passwordInDB = user.getPassword();
        String salt = user.getSalt();
        // 认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
        // 盐也放进去
        // 这样通过applicationContext-shiro.xml里配置的 HashedCredentialsMatcher 进行自动校验
        SimpleAuthenticationInfo a = new SimpleAuthenticationInfo("zhang3", passwordInDB, ByteSource.Util.bytes(salt),
                getName());
        return a;
    }

竟然登录成功了.

技术分享图片

可以看到时间也是设置的30天.

/**
     * cookie对象,会话Cookie模板,默认为:JSESSIONID
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie(){
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/admin/**");
        simpleCookie.setMaxAge(60*60*24*30);//一个月cookie保留时间
        return simpleCookie;
    }

 继续测试

技术分享图片

现在是没有rememberMe这个session的,因为还没登录.

当不选择记住我时,

技术分享图片

当选择了记住我时,除了有上面这个,还有,

技术分享图片

也就是说rememberMe功能实现了,那么获取名称当时为什么会报空指针异常?

将数据改回来,肯定是依旧报错,但是用户名不能写死,那是不是当UsernamePasswordToken转换为AuthenticationToken出错的呢?

在AuthenticationToken中,不存在对rememberMe的参数判定和其它方法,那么能不能直接对

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.shiro.authc;

import java.io.Serializable;

/**
 * <p>An <tt>AuthenticationToken</tt> is a consolidation of an account‘s principals and supporting
 * credentials submitted by a user during an authentication attempt.
 * <p/>
 * <p>The token is submitted to an {@link Authenticator Authenticator} via the
 * {@link Authenticator#authenticate(AuthenticationToken) authenticate(token)} method.  The
 * Authenticator then executes the authentication/log-in process.
 * <p/>
 * <p>Common implementations of an <tt>AuthenticationToken</tt> would have username/password
 * pairs, X.509 Certificate, PGP key, or anything else you can think of.  The token can be
 * anything needed by an {@link Authenticator} to authenticate properly.
 * <p/>
 * <p>Because applications represent user data and credentials in different ways, implementations
 * of this interface are application-specific.  You are free to acquire a user‘s principals and
 * credentials however you wish (e.g. web form, Swing form, fingerprint identification, etc) and
 * then submit them to the Shiro framework in the form of an implementation of this
 * interface.
 * <p/>
 * <p>If your application‘s authentication process is  username/password based
 * (like most), instead of implementing this interface yourself, take a look at the
 * {@link UsernamePasswordToken UsernamePasswordToken} class, as it is probably sufficient for your needs.
 * <p/>
 * <p>RememberMe services are enabled for a token if they implement a sub-interface of this one, called
 * {@link RememberMeAuthenticationToken RememberMeAuthenticationToken}.  Implement that interfac if you need
 * RememberMe services (the <tt>UsernamePasswordToken</tt> already implements this interface).
 * <p/>
 * <p>If you are familiar with JAAS, an <tt>AuthenticationToken</tt> replaces the concept of a
 * {@link javax.security.auth.callback.Callback}, and  defines meaningful behavior
 * (<tt>Callback</tt> is just a marker interface, and of little use).  We
 * also think the name <em>AuthenticationToken</em> more accurately reflects its true purpose
 * in a login framework, whereas <em>Callback</em> is less obvious.
 *
 * @see RememberMeAuthenticationToken
 * @see HostAuthenticationToken
 * @see UsernamePasswordToken
 * @since 0.1
 */
public interface AuthenticationToken extends Serializable {

    /**
     * Returns the account identity submitted during the authentication process.
     * <p/>
     * <p>Most application authentications are username/password based and have this
     * object represent a username.  If this is the case for your application,
     * take a look at the {@link UsernamePasswordToken UsernamePasswordToken}, as it is probably
     * sufficient for your use.
     * <p/>
     * <p>Ultimately, the object returned is application specific and can represent
     * any account identity (user id, X.509 certificate, etc).
     *
     * @return the account identity submitted during the authentication process.
     * @see UsernamePasswordToken
     */
    Object getPrincipal();

    /**
     * Returns the credentials submitted by the user during the authentication process that verifies
     * the submitted {@link #getPrincipal() account identity}.
     * <p/>
     * <p>Most application authentications are username/password based and have this object
     * represent a submitted password.  If this is the case for your application,
     * take a look at the {@link UsernamePasswordToken UsernamePasswordToken}, as it is probably
     * sufficient for your use.
     * <p/>
     * <p>Ultimately, the credentials Object returned is application specific and can represent
     * any credential mechanism.
     *
     * @return the credential submitted by the user during the authentication process.
     */
    Object getCredentials();

}

再次测试中,可以获取name,but...还是空指针异常.

但是奇怪的是,一直有rememberMe 30day的cookie存在...

可以看到

public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
public interface HostAuthenticationToken extends AuthenticationToken {

也就是说明了为什么可以将UsernamePasswordToken类型的token传入到AuthenticationToken的参数中了

最终决定采用捕获异常来处理.奇葩的是能获取名称的同时会报null异常:

zhang3
Creating a new SqlSession
SqlSession [[email protected]] was not registered for synchronization because synchronization is not active
JDBC Connection [[email protected] wrapping [email protected]] will not be managed by Spring
==>  Preparing: select ‘false‘ as QUERYID, id, name, password, salt from user WHERE ( name = ? ) 
==> Parameters: zhang3(String)
<==    Columns: QUERYID, id, name, password, salt
<==        Row: false, 1, zhang3, a7d59dfc5332749cb801f86a24f5f590, e5ykFiNwShfCXvBRPr3wXg==
<==      Total: 1
Closing non transactional SqlSession [[email protected]]

同样,获取不到的时候:

null
Creating a new SqlSession
SqlSession [[email protected]] was not registered for synchronization because synchronization is not active
2019-01-17 16:58:43,509 - HikariPool-1 - Starting...
2019-01-17 16:58:43,605 - HikariPool-1 - Start completed.
JDBC Connection [[email protected] wrapping [email protected]] will not be managed by Spring
==>  Preparing: select ‘false‘ as QUERYID, id, name, password, salt from user WHERE ( name = ? ) 
==> Parameters: li4(String)
<==    Columns: QUERYID, id, name, password, salt
<==        Row: false, 2, li4, 43e28304197b9216e45ab1ce8dac831b, jPz19y7arvYIGhuUjsb6sQ==
<==      Total: 1
Closing non transactional SqlSession [[email protected]]

但最终会去数据库查找.

最后得出结论,当用户退出时,数据还会存在缓存,没清理干净.

比如这次我用户名输入li,而输出了上次的li4

Creating a new SqlSession
SqlSession [[email protected]] was not registered for synchronization because synchronization is not active
li4
JDBC Connection [[email protected] wrapping [email protected]] will not be managed by Spring
==>  Preparing: select ‘false‘ as QUERYID, id, name, password, salt from user WHERE ( name = ? ) 
==> Parameters: li(String)
<==      Total: 0
Closing non transactional SqlSession [[email protected]]
li
login异常

具体参考这个

重登录:(数据库不存在的123)

2019-01-17 17:05:08,820 - Initializing Servlet ‘dispatcherServlet‘
2019-01-17 17:05:08,826 - Completed initialization in 6 ms
exception:userName->null
Creating a new SqlSession
SqlSession [[email protected]] was not registered for synchronization because synchronization is not active
2019-01-17 17:05:11,200 - HikariPool-1 - Starting...
2019-01-17 17:05:11,384 - HikariPool-1 - Start completed.
JDBC Connection [[email protected] wrapping [email protected]] will not be managed by Spring
==>  Preparing: select ‘false‘ as QUERYID, id, name, password, salt from user WHERE ( name = ? ) 
==> Parameters: 123(String)
<==      Total: 0
Closing non transactional SqlSession [[email protected]]
exception:userName->123
login异常

不管怎样,能正常使用,可能也是登出用户时存在的问题.

Creating a new SqlSession
SqlSession [[email protected]] was not registered for synchronization because synchronization is not active
exception:userName->14221
JDBC Connection [[email protected] wrapping [email protected]] will not be managed by Spring
==>  Preparing: select ‘false‘ as QUERYID, id, name, password, salt from user WHERE ( name = ? ) 
==> Parameters: zhang3(String)
<==    Columns: QUERYID, id, name, password, salt
<==        Row: false, 1, zhang3, a7d59dfc5332749cb801f86a24f5f590, e5ykFiNwShfCXvBRPr3wXg==
<==      Total: 1
Closing non transactional SqlSession [[email protected]]

14221是错误的用户名,

而之后的zhang3是存在的,但是不进入exception了.

是否应该自己写一个logout.

看到一个比较像的问题的博客

todo

 

以上是关于[shiro] - 加入rememberMe功能的主要内容,如果未能解决你的问题,请参考以下文章

Shiro RememberMe 1.2.4 反序列化命令执行漏洞复现

Shiro学习(13)RememberMe

第十三章 RememberMe——《跟我学Shiro》

Shiro中的Rememberme后出现浏览器500错误

spring 配置 shiro rememberMe

shiro中记住我功能