Shiro系列(二)shiro身份认证

在上篇Shiro系列(一)shiro简介中,我们了解了Shiro的工作原理和组成架构,这篇我们将利用spring 、spring mvc整合shiro,了解shiro的身份认证。

推荐书目:张开涛《跟我学shiro》

一、认证流程

在Shiro中,用户需要提供principals(身份) 和 credentials(证明),一般是用户名/密码来验证用户的身份;

认证流程如图所示:
流程图
流程如下:

1、首先调用 Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils. setSecurityManager()设置;

2、SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;

3、Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自 定义插入自己的实现;

4、Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;

5、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返 回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进 行访问。

二、示例解析

Spring+Spring MVC+Shiro

新建一个Web Project(基础框架参考使用IDEA整合Spring+Spring MVC+Mybatis+Maven+Jetty框架的搭建),我们使用maven构建项目,添加shiro的依赖包:

        <!--核心包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>1.3.2</version>
        </dependency>
        
        <!--缓存包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.2.2</version>
        </dependency>
        
        <!-- spring结合 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        

web.xml配置

在web.xml中 配置:

    <!-- spring shiro 过滤器  -->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

Shiro配置文件:spring-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context-4.0.xsd
	http://www.springframework.org/schema/tx
	http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
	http://www.springframework.org/schema/data/jpa">

    <description>The Configuration of Shiro </description>
    
    <!-- 配置权限管理器(核心) -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="adminRealm"/>
        <property name="cacheManager" ref="cacheManager"/>
    </bean>

  <!-- 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的ShiroDbRealm.java -->
    <bean id="adminRealm" class="com.swc.base.filter.SystemAuthorizingRealm"/>
    
    <!-- 缓存管理 用户授权/认证信息Cache, 采用redis 缓存 -->
    <bean id="cacheManager" class="com.swc.base.pojo.RedisCacheManager"/>
  


    <!-- Shiro主过滤器本身功能十分强大,其强大之处就在于它支持任何基于URL路径表达式的、自定义的过滤器的执行 -->
    <!-- Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截,Shiro对基于Spring的Web应用提供了完美的支持 -->
    <!-- id 必须和web.xml 那种配置的delegatingFilterProxy的<filter-name> 保持一致 -->
    
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全接口,这个属性是必须的 -->
        <property name="securityManager" ref="securityManager"/>
        <!-- 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
        <property name="loginUrl" value="/login"/>
        <property name="successUrl" value="/main"/>
        <!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码为main.jsp了) -->
        <!-- <property name="successUrl" value="/system/main"/> -->
        <!-- 用户访问未对其授权的资源时,所显示的连接 -->
        <property name="unauthorizedUrl" value="/login"/>
        <!-- Shiro连接约束配置,即过滤链的定义 -->
        <!-- 下面value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 -->
        <!-- anon:anonymous  可以被匿名访问它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 -->
        <!-- authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截org.apache.shiro.web.filter.authc.FormAuthenticationFilter -->
        <property name="filters">
            <map>
                <entry key="logout" value-ref="systemLogoutFilter"/>
            </map>
        </property>
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionClass" />
    </bean>
    <!-- url 权限采用第一次匹配优先的方式-->
    <bean id="filterChainDefinitionClass" class="com.swc.base.filter.ShirofilterChainDefinitions" scope="singleton">
        <property name="filterChainDefinitions">
            <value>
                <!--/=anon-->
                /login=anon
                /logout=logout
                /register=anon
                /oauth/**=anon
                /resources/**=anon
                /iridium/testGet=anon
                /**=authc
            </value>
        </property>
    </bean>


    <bean id="systemLogoutFilter" class="com.swc.base.filter.SystemLogoutFilter">
        <property name="redirectUrl" value="/login"/>
    </bean>

    <!-- 保证实现了Shiro内部lifecycle函数的bean执行,可以自动的调用springioc容器中shiro 备案的生命周期方法 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    
    <!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 -->
    <!-- 配置以下两个bean即可实现此功能 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>
    
</beans>

从配置文件中,我们也可以看出Shiro本身不提供维护用户、权限,而是通过Realm让开发人员自己注入到SecurityManager,从而让SecurityManager能得到合法的用户以及权限进行判断;

自定义系统安全认证实现类

因此我们要自己定义系统安全认证实现类:SystemAuthorizingRealm ,继承AuthorizingRealm ,实现它的两个方法:
doGetAuthenticationInfo()和doGetAuthorizationInfo();

doGetAuthenticationInfo是当我们调用subject.login进行认证的方法;
doGetAuthorizationInfo方法是进行用户授权的时候调用的方法;暂且不讨论;

package com.swc.base.filter;


import com.swc.base.utils.StrUtil;
import com.swc.user.entity.User;
import com.swc.user.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 系统认证实现类
 * @Auther: songweichao
 * @Date: 2018/12/24 14:51
 */
@Service
public class SystemAuthorizingRealm extends AuthorizingRealm {

    /**
     * 认证回调函数, 登录时调用
     */
    @Resource
    private UserService userService;


    @Resource
    private RoleService roleService;

    @Resource
    private FunctionService functionService;


    /**
     * 用户认证
     *
     * @param authcToken 含登录名密码的信息
     * @return 认证信息
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
        if (authcToken == null)
            throw new AuthenticationException("parameter token is null");
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        // 校验用户名密码

        String password=String.copyValueOf(token.getPassword());
        User user= userService.getUserByName(token.getUsername());

        if (user!=null) {
           if(!password.equals(user.getPassword())){
                throw new IncorrectCredentialsException();
            }
            //这样前端页面可取到数据
            SecurityUtils.getSubject().getSession().setAttribute("user",user);
            SecurityUtils.getSubject().getSession().setAttribute("userId",user.getId());
            // 注意此处的返回值没有使用加盐方式,如需要加盐,可以在密码参数上加
            return new SimpleAuthenticationInfo(user.getId(), token.getPassword(), token.getUsername());
        }
        throw new UnknownAccountException();
    }


    /**
     * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用 shiro 权限控制有三种
     * 1、通过xml配置资源的权限
     * 2、通过shiro标签控制权限
     * 3、通过shiro注解控制权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        // 因为非正常退出,即没有显式调用 SecurityUtils.getSubject().logout()
        // (可能是关闭浏览器,或超时),但此时缓存依旧存在(principals),所以会自己跑到授权方法里。
        if (!SecurityUtils.getSubject().isAuthenticated()) {
            doClearCache(principals);
            SecurityUtils.getSubject().logout();
            return null;
        }

        if (principals == null) {
            throw new AuthorizationException("parameters principals is null");
        }
        //获取已认证的用户名(登录名)
        String userId=(String)super.getAvailablePrincipal(principals);
        if(StrUtil.isEmpty(userId)){
            return null;
        }
       /* Set<String> roleCodes=roleService.getRoleCodeSet(userId);
        //默认用户拥有所有权限
        Set<String> functionCodes=functionService.getAllFunctionCode();*/
       /* Set<String> functionCodes=functionService.getFunctionCodeSet(roleCodes);*/
        SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
        /*authorizationInfo.setRoles(roleCodes);
        authorizationInfo.setStringPermissions(functionCodes);*/
        return authorizationInfo;
    }
}

缓存实现类:

package com.swc.base.pojo;


import com.swc.base.dao.RedisDao;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @Auther: songweichao
 * @Date: 2018/12/24 14:51
 */
public class RedisCacheManager implements CacheManager {


    private static final Logger logger = LoggerFactory.getLogger(RedisCacheManager.class);

    // fast lookup by name map
    private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();

    @Resource
    private RedisDao redisDao;

    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        Cache c = caches.get(name);
        if (c == null) {
            c = new RedisCache<K, V>(redisDao);
            caches.put(name, c);
        }
        return (Cache<K, V>) c;
    }
}

Controller控制器:

package com.swc.user.controller;

import com.swc.base.utils.EncryptUtil;
import com.swc.base.utils.ResultCode;
import com.swc.base.utils.StrUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

/**
 * @Auther: songweichao
 * @Date: 2018/12/27 11:15
 * @Description:
 */
@Controller
public class LoginController {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);

    private final static String MAIN_PAGE = "main";
    private final static String LOGIN_PAGE = "login";

    @RequestMapping(value = "/login")
    private String login(HttpServletRequest request, Model model) {
        //model.addAttribute("oAuthServices", oAuthServices.getAllOAuthServices());
        //已经登录过,直接进入主页
        Subject subject = SecurityUtils.getSubject();
        if (subject != null && subject.isAuthenticated()) {
            Object authorized = subject.getSession().getAttribute("isAuthorized");
            //boolean isAuthorized = Boolean.valueOf(subject.getSession().getAttribute("isAuthorized").toString());
            if (authorized != null && Boolean.valueOf(authorized.toString()))
                return MAIN_PAGE;
        }
        String userName = request.getParameter("name");
        
        if (StrUtil.isEmpty(userName)) {
            return LOGIN_PAGE;
        }
        String password = request.getParameter("password");
        
        password = EncryptUtil.getPassword(password, userName);
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
        token.setRememberMe(true);
        subject = SecurityUtils.getSubject();
        String msg;
        try {
            subject.login(token);
            if (subject.isAuthenticated()) {
                return MAIN_PAGE;
            } else {
                return LOGIN_PAGE;
            } 
        } catch (IncorrectCredentialsException e) {
            msg = "登录密码错误. ";
            model.addAttribute("message", new ResultCode("2", msg));
            LOGGER.error(msg);
        } catch (ExcessiveAttemptsException e) {
            msg = "登录失败次数过多";
            model.addAttribute("message", new ResultCode("3", msg));
            LOGGER.error(msg);
        } catch (LockedAccountException e) {
            msg = "帐号已被锁定.";
            model.addAttribute("message", new ResultCode("1", msg));
            LOGGER.error(msg);
        } catch (DisabledAccountException e) {
            msg = "帐号已被禁用";
            model.addAttribute("message", new ResultCode("1", msg));
            LOGGER.error(msg);
        } catch (ExpiredCredentialsException e) {
            msg = "帐号已过期.";
            model.addAttribute("message", new ResultCode("1", msg));
            LOGGER.error(msg);
        } catch (UnknownAccountException e) {
            msg = "帐号不存在 " + token.getPrincipal();
            model.addAttribute("message", new ResultCode("1", msg));
            LOGGER.error(msg);
        } catch (UnauthorizedException e) {
            msg = "您没有得到相应的授权!" + e.getMessage();
            model.addAttribute("message", new ResultCode("1", msg));
            LOGGER.error(msg);
        }
        return LOGIN_PAGE;
    }

}

注: 在LoginController中调用 subject.login 方法进行登录,其会自动委托给 SecurityManager.login 方法进行登录,SecurityManager将所传参数UsernamePasswordToken 传给SystemAuthorizingRealm的AuthenticationInfo()方法,调用userService,从数据库中获取用户名和密码,进行比对;一致返回true,不一致则返回相应AuthenticationException 异常;

在浏览器端访问任意地址:
在这里插入图片描述
dbug断点可以看到进入了login方法:

login
地址栏重定向到/login,进入到登录页面:

登录页面
输入用户名song和密码111111,可以看到通过验证进入主页,也可输入错误密码,日志打印异常信息;

主页

三、带你出坑

整个过程中,界面上访问没有问题,但在访问任何一个url时,控制台都会报如下错误,但是所有url又能访问正常:

2019-01-15 17:41:51,787 WARN[org.apache.shiro.mgt.AbstractRememberMeManager:449]- There was a failure while trying to retrieve remembered principals.  This could be due to a configuration problem or corrupted principals.  This could also be due to a recently changed encryption key, if you are using a shiro.ini file, this property would be 'securityManager.rememberMeManager.cipherKey' see: http://shiro.apache.org/web.html#Web-RememberMeServices. The remembered identity will be forgotten and not used for this request.
2019-01-15 17:41:51,789 WARN[org.apache.shiro.mgt.DefaultSecurityManager:609]- Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals().
org.apache.shiro.crypto.CryptoException: Unable to execute 'doFinal' with cipher instance [javax.crypto.Cipher@724e319e].
	at org.apache.shiro.crypto.JcaCipherService.crypt(JcaCipherService.java:462)
	at org.apache.shiro.crypto.JcaCipherService.crypt(JcaCipherService.java:445)
	at org.apache.shiro.crypto.JcaCipherService.decrypt(JcaCipherService.java:390)
	at org.apache.shiro.crypto.JcaCipherService.decrypt(JcaCipherService.java:382)
	at org.apache.shiro.mgt.AbstractRememberMeManager.decrypt(AbstractRememberMeManager.java:482)
	at org.apache.shiro.mgt.AbstractRememberMeManager.convertBytesToPrincipals(AbstractRememberMeManager.java:419)
	at org.apache.shiro.mgt.AbstractRememberMeManager.getRememberedPrincipals(AbstractRememberMeManager.java:386)
	at org.apache.shiro.mgt.DefaultSecurityManager.getRememberedIdentity(DefaultSecurityManager.java:604)
	at org.apache.shiro.mgt.DefaultSecurityManager.resolvePrincipals(DefaultSecurityManager.java:492)
	at org.apache.shiro.mgt.DefaultSecurityManager.createSubject(DefaultSecurityManager.java:342)
	at org.apache.shiro.subject.Subject$Builder.buildSubject(Subject.java:846)
	at org.apache.shiro.web.subject.WebSubject$Builder.buildWebSubject(WebSubject.java:148)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.createSubject(AbstractShiroFilter.java:292)
	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:359)
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1645)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:564)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:578)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:221)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1111)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:498)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:183)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1045)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
	at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:199)
	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:109)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:98)
	at org.eclipse.jetty.server.Server.handle(Server.java:461)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:284)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:244)
	at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:534)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:607)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:536)
	at java.lang.Thread.run(Thread.java:745)
Caused by: javax.crypto.BadPaddingException: Given final block not properly padded
	at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:966)
	at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:824)
	at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:436)
	at javax.crypto.Cipher.doFinal(Cipher.java:2165)
	at org.apache.shiro.crypto.JcaCipherService.crypt(JcaCipherService.java:459)
	... 36 more

从warn信息看出,应该是没有配置RememberMeManager 的缘故,将以下配置添加到spring-shiro.xml中,解决了这个问题。

 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="adminRealm"/>
        <property name="cacheManager" ref="cacheManager"/>
        <!-- 定义RememberMe的管理器 -->
        <property name="rememberMeManager" ref="rememberMeManager"/>
    </bean>

    <!-- 定义RememberMe功能的程序管理类 -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <!-- 定义在进行RememberMe功能实现的时候所需要使用到的Cookie的处理类 -->
        <property name="cookie" ref="rememberMeCookie"/>
    </bean>

    <!-- 配置需要向Cookie中保存数据的配置模版(RememberMe) -->
    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <!-- 设置Cookie在浏览器中保存内容的名字,由用户自己来设置 -->
        <constructor-arg value="MLDNJAVA-RememberMe"/>
        <!-- 保证该系统不会受到跨域的脚本操作供给 -->
        <property name="httpOnly" value="true"/>
        <!-- 定义Cookie的过期时间为一小时 -->
        <property name="maxAge" value="3600"/>
    </bean>

Shiro系列文章:

Shiro系列(一)shiro简介

Shiro系列(二)shiro身份认证

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值