四、认证

身份验证

一般需要提供如身份 ID 等一下标识信息来表名登录者的身份,如提供email,用户名/密码来证明。
在shiro中,用户需要提供 principals(身份)和 credentials(证明)给shiro,从而应用能够验证用户身份。

  • principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个 Primary principals,一般是用户名/邮箱/手机号;
  • credentials 凭证,即只有主体知道的安全值,如密码等;
    最常见的 principals 和 credentials 组合就是 用户名/密码了。
基本流程
  1. 获取当前的Subject,调用SecurityUtils.getSubject();
  2. 测试当前用户是否已经被认证.即是否已经登录,调用Subject#isAuthencticated()方法;
  3. 若没有被认证,则把用户名和密码封装为UsernamePasswordToken对象;
  4. 执行登录,调用Subject#login(AuthenticationToken token)方法,其会自动委托给SecurityManager;
  5. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  6. 自定义Realm方法,从数据库中获取对应的记录,返回给Shiro;
    -a.实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类;
    -b.实现 doGetAuthenticationInfo(AuthenticationToken token) 方法.
  7. 由shiro完成对密码的比对。
实现认证流程
  1. 将ShiroRealm修改为以下代码:
public class ShiroRealm extends AuthenticatingRealm {
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("2. " + token.hashCode());
        return null;
    }
}
  1. 实现请求Controller编码
@Controller
@RequestMapping("/shiro")
public class ShiroController {

    @RequestMapping("/shiroLogin")
    public String login(@RequestParam("username") String username,@RequestParam("password") String password){
        Subject subject = SecurityUtils.getSubject();

        if (!subject.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(username,password);
            token.setRememberMe(true);
            try {
                System.out.println("1. " + token.hashCode());
                subject.login(token);
            } catch (AuthenticationException e) {
                e.printStackTrace();
            }
        }

        return "redirect:/list.jsp";
    }
}
  1. login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>login</title>
</head>
<body>
    <h4>login page</h4>

    <form action="shiro/shiroLogin" method="POST">
        username:<input type="text" name="username"/><br><br>
        password:<input type="password" name="password"/><br><br>
        <input type="submit" value="Submit"/>
    </form>

</body>
</html>

4.filterChainDefinitions配置

<property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                /shiro/shiroLogin = anon

                /** = authc
            </value>
        </property>
  1. 运行项目
    在登录页面随意输入用户名和密码,登录失败,两次打印的token#hashCode()相等,后台出现"org.apache.shiro.authc.UnknownAccountException"异常,这是因为没有具体实现认证过程,接下来将完成认证的Realm。
实现认证Realm

例子:

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.把AuthenticationToken转换为UserNamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //2.从UserNamePasswordToken中获取username
        String username = upToken.getUsername();
        //3.调用dao层方法,从数据库中查询username对应的用户记录
        System.out.println("从数据库中获取username:" + username + " 所对应的用户信息。");
        //4.若用户不存在,则可以抛出 UnknownAccountException 异常
        if ("unknown".equals(username)) {
            throw  new UnknownAccountException("用户不存在");
        }

        //5.根据用户信息的情况,觉得是否需要抛出其他的异常.
        if ("monster".equals(username)) {
            throw  new LockedAccountException("用户被锁定");
        }

        //6.根据用户的情况,来构造 AuthenticationInfo 对象并返回
        //principal认证实体,可以是username,也可以是数据表对应的用户的实体类的对象
        Object principal = username;
        //credentials:密码
        Object credentials = "123456";
        //realmName:当前realm对象的name.调用父类的getName()
        String realmName = getName();
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName);

        return info;
    }

启动项目:

  • 在登录页面输入"unknown"作为用户名时,抛出用户不存在异常;
  • 输入"monster"作为用户名时,抛出用户被锁定异常;
  • 输入其他用户名+密码非"123456"时,认证不通过;
  • 输入其他用户名+密码"123456"时,认证通过,重定向到list.jsp页面;
密码比对

通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对!

  1. 如何把一个字符串加密为MD5
    public static void main(String[] args) {
        String hashAlgorithmName = "MD5";
        Object source = "123456";
        Object salt = null;
        int hashIterations = 3;
        SimpleHash hash = new SimpleHash(hashAlgorithmName, source, salt, hashIterations);
        System.out.println(hash.toString());
    }
  1. 替换当前 Realm 的 credentialsMatcher 属性,直接使用 HashedCredentialsMatcher 对象,并设置加密算法即可。
    <bean id="shiroRealm" class="org.keyhua.shiro.ShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <!--加密算法-->
                <property name="hashAlgorithmName" value="MD5"></property>
                <!--加密次数-->
                <property name="hashIterations" value="3"></property>
            </bean>
        </property>
    </bean>
  1. MD5盐值加密
  • 认证时返回 SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 构造器
  • 使用 ByteSource.Util.bytes() 来计算盐值
  • 盐值需要唯一:一般使用随机字符串或用户id
  • 使用 new SimpleHash(hashAlgorithmName, source, salt, hashIterations) 来计算盐值加密后的密码
    修改认证实现:
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.把AuthenticationToken转换为UserNamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //2.从UserNamePasswordToken中获取username
        String username = upToken.getUsername();
        //3.调用dao层方法,从数据库中查询username对应的用户记录
        System.out.println("从数据库中获取username:" + username + " 所对应的用户信息。");
        //4.若用户不存在,则可以抛出 UnknownAccountException 异常
        if ("unknown".equals(username)) {
            throw  new UnknownAccountException("用户不存在");
        }

        //5.根据用户信息的情况,觉得是否需要抛出其他的异常.
        if ("monster".equals(username)) {
            throw  new LockedAccountException("用户被锁定");
        }

        //6.根据用户的情况,来构造 AuthenticationInfo 对象并返回
        //principal认证实体,可以是username,也可以是数据表对应的用户的实体类的对象
        Object principal = username;
        //credentials:密码
        Object credentials = null;
        if ("admin".equals(username)){
            credentials = "9aa75c4d70930277f59d117ce19188b0";
        } else if ("user".equals(username)) {
            credentials = "dd957e81b004227af3e0aa4bde869b25";
        }
        //realmName:当前realm对象的name.调用父类的getName()
        String realmName = getName();
        //盐值
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);

        SimpleAuthenticationInfo info = null;
        //info = new SimpleAuthenticationInfo(principal, credentials, realmName);
        info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);

        return info;
    }

启动项目,用户名 user/admin,密码123456,认证通过。

多Realm验证

再自定义一个relam:

public class SecondRealm extends AuthenticatingRealm {

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("[SecondRealm]  doGetAuthenticationInfo");

        //1.把AuthenticationToken转换为UserNamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //2.从UserNamePasswordToken中获取username
        String username = upToken.getUsername();
        //3.调用dao层方法,从数据库中查询username对应的用户记录
        System.out.println("从数据库中获取username:" + username + " 所对应的用户信息。");
        //4.若用户不存在,则可以抛出 UnknownAccountException 异常
        if ("unknown".equals(username)) {
            throw  new UnknownAccountException("用户不存在");
        }

        //5.根据用户信息的情况,觉得是否需要抛出其他的异常.
        if ("monster".equals(username)) {
            throw  new LockedAccountException("用户被锁定");
        }

        //6.根据用户的情况,来构造 AuthenticationInfo 对象并返回
        //principal认证实体,可以是username,也可以是数据表对应的用户的实体类的对象
        Object principal = username;
        //credentials:密码
        Object credentials = null;
        if ("admin".equals(username)){
            credentials = "28078bcf86c16b80329aa523afb74da57ffb8a11";
        } else if ("user".equals(username)) {
            credentials = "393c16607f34db540e1ec19ab2829044e98efaca";
        }
        //realmName:当前realm对象的name.调用父类的getName()
        String realmName = getName();
        //盐值
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);

        SimpleAuthenticationInfo info = null;
        //info = new SimpleAuthenticationInfo(principal, credentials, realmName);
        info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);

        return info;
    }
}

修改applicationContext.xml配置文件:

<!-- 1.配置SecurityManager -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager" />
        <property name="authenticator" ref="authenticator" />
        <property name="realms">
            <list>
                <ref bean="shiroRealm" />
                <ref bean="secondRealm"/>
            </list>
        </property>
    </bean>

    <!--2.配置CacheManager
        2.1.需要引入ehcache的jar及配置文件
    -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    </bean>

    <!--3.配置realm
        3.1 直接实现了Realm接口的bean
    -->
    <bean id="shiroRealm" class="org.keyhua.shiro.ShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <!--加密算法-->
                <property name="hashAlgorithmName" value="MD5"></property>
                <!--加密次数-->
                <property name="hashIterations" value="3"></property>
            </bean>
        </property>
    </bean>

    <bean id="secondRealm" class="org.keyhua.shiro.SecondRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <!--加密算法-->
                <property name="hashAlgorithmName" value="SHA1"></property>
                <!--加密次数-->
                <property name="hashIterations" value="3"></property>
            </bean>
        </property>
    </bean>

多个Realm进行验证时,验证规则通过 AuthenticationStrategy 接口指定
AuthenticationStrategy接口的默认实现:
-FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
-AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息;
-AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了;

  • ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy 策略

认证策略的设置:

<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <!-- 设置认证策略 -->
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
        </property>
        <property name="realms">
            <list>
                <ref bean="shiroRealm" />
                <ref bean="secondRealm"/>
            </list>
        </property>
    </bean>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容