第十节 Shiro集成SSM框架

一、搭建基本的SSM框架

       首先告诉大家一个好消息,现在github的私有仓库免费咯。

       好,开始本小节的教学。首先需要搭建一个SSM框架。如何搭建SSM框架,不在本文的讨论范围之内。你可以下载我的已经搭建好的基本的SSM框架,地址:点击我下载源码。我提供了一个基本的SSM框架与集成Shiro后的SSM框架。如果你时间比较急,你可以直接查看搭建后的代码并进入第三步中直接查看我的源码分析,省略搭建过程。

       基本的SSM框架导入到你的开发工具里后,你需需要稍作修改,将jdbc.properties里面配置的数据库信息,改为你的本机的MySQL数据库的帐号密码。

       基本的SSM框架启动系统后的基本页面如图所示。

       数据库t_user表的数据如图所示

  

       点击测试超链接,得到数据库中的所有USER的JSON数据。  

二、开始集成Shiro

(1)第一步,我们需要配置web.xml,首先在web.xml中定义一个shiroFilter。在web.xml中加入如下代码。

         在下述配置中,从这个filter-class的名字就能够知道,DelegatingFilterProxy这个类是一个代理类。代理谁呢?它会代理Spring容器中bean的id为"shiroFilter"的类。

         PS:Shiro的依赖我已经编写在了基本的SSM框架中。

    <!-- Shiro框架入口 -->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/actions/*</url-pattern>
    </filter-mapping>

(2)第二步,编写shiro.xml配置文件,编译器会帮你找到你需要编写的自定义类。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

    <!-- 自定义Realm -->
    <bean id="userRealm" class="com.jay.shiro.UserRealm"/>

    <!-- securityManager 对象-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 引入UserRealm -->
        <property name="realm" ref="userRealm"/>
    </bean>

    <!-- 这个bean的id与web.xml中shiro相关配置保持一致 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!-- 没认证后重定向的位置 -->
        <property name="loginUrl" value="/login.jsp"/>
        <!-- 登录成功跳转的位置 -->
        <property name="successUrl" value="/home.jsp"/>
        <!-- 没有权限跳转的位置 -->
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        <!-- 拦截请求-->
        <property name="filterChainDefinitions">
            <value>
                <!-- 登录请求不拦截 -->
                /actions/security/login = anon
                <!-- 访问admin相关的请求,需要认证,
                     且经过自定义拦截器permissionFilter,最后还需要coder权限-->
                /actions/admin/** = authc,permissionFilter,roles[coder]
                /actions/logout = logout
                /actions/** = authc
            </value>
        </property>
        <!-- 用户自定义的过滤器 -->
        <property name="filters">
            <map>
                <entry key="permissionFilter" value-ref="userAccessControlFilter"/>
            </map>
        </property>
    </bean>

    <!-- Shiro 生命周期处理器-->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

</beans>

在applicationContext.xml中导入刚才编写的 shiro.xml 

    <!-- 导入shiro的配置文件 -->
    <import resource="shiro.xml"/>

(3)编写自定义Realm与拦截器。相信你已经发现了,你的编译器已经报警,说找不到com.jay.shiro.UserRealm与userAccessControlFilter这两个类,不要着急,放轻松,这两个类让我们慢慢的开始编写。

        首先编写的是UserRealm,这个是自定义Realm。如何自定义Realm,在前置章节中已经详细的讨论过,也追踪过其源码,如果你有点生疏,可以查看前面的一些文章哦。在本小节大宇不做过多阐述如何自定义Realm。

package com.jay.shiro;

/**
 * @author jay.zhou
 * @date 2019/1/10
 * @time 14:23
 */

import com.jay.service.UserService;
import org.apache.shiro.authc.*;
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.beans.factory.annotation.Autowired;

import java.util.Arrays;
import java.util.List;

/**
 * @author jay.zhou
 * @date 2019/1/10
 * @time 10:57
 */
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 强制重写的认证方法
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        //还记得吗,token封装了客户端的帐号密码,由Subject拉客并最终带到此处
        String clientUsername = (String) token.getPrincipal();
        //从数据库中查询帐号密码
        String passwordFromDB = userService.findPasswordByName(clientUsername);
        if (passwordFromDB == null) {
            //如果根据用户输入的用户名,去数据库中没有查询到相关的密码
            throw new UnknownAccountException();
        }

        /**
         * 返回一个从数据库中查出来的的凭证。用户名为clientUsername,密码为passwordFromDB 。封装成当前返回值
         * 接下来shiro框架做的事情就很简单了。
         * 它会拿你的输入的token与当前返回的这个数据库凭证SimpleAuthenticationInfo对比一下
         * 看看是不是一样,如果用户的帐号密码与数据库中查出来的数据一样,那么本次登录成功
         * 否则就是你密码输入错误
         */
        return new SimpleAuthenticationInfo(clientUsername, passwordFromDB, "UserRealm");
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String yourInputUsername = (String) principals.getPrimaryPrincipal();
        //构造一个授权凭证
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //通过你的用户名查询数据库,得到你的权限信息与角色信息。并存放到权限凭证中
        info.addRole(getYourRoleByUsernameFromDB(yourInputUsername));
        info.addStringPermissions(getYourPermissionByUsernameFromDB(yourInputUsername));
        //返回你的权限信息
        return info;
    }

    private String getYourRoleByUsernameFromDB(String username) {
        return "coder";
    }

    private List<String> getYourPermissionByUsernameFromDB(String username) {
        return Arrays.asList("code:insert", "code:update");
    }

}

(4)第四步,编写剩下的一个自定义类:用户自定义拦截器。此类可用于判断用户是否已经拥有相关的角色或者权限。

package com.jay.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
 * @author jay.zhou
 * @date 2019/1/10
 * @time 14:34
 */
@Component("userAccessControlFilter")
public final class UserAccessControlFilter extends AccessControlFilter {

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

    /**
     * 即是否允许访问,返回true表示允许.
     * 如果返回false,则进入本类的onAccessDenied方法中进行处理
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object object) 
throws Exception {
        final Subject subject = SecurityUtils.getSubject();

        //判断用户是否进行过登录认证,如果没经过认证则返回登录页
        if (subject.getPrincipal() == null || !subject.isAuthenticated()) {
            return Boolean.FALSE;
        }

        final String requestURI = this.getPathWithinApplication(request);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("请求URL为:{}", requestURI);
        }
        final String requestHeader = ((HttpServletRequest) request).getHeader("Referer");

        //防盗链处理
        if (requestHeader == null || "".equals(requestHeader)) {
            return Boolean.FALSE;
        }

        //此处可以编写用于判断用户是否有相关权限的代码
        //subject.hasRole("需要的角色");
        //subject.isPermitted("需要的权限");
        return Boolean.TRUE;
    }

    /**
     * 如果返回true,则继续执行其它拦截器
     * 如果返回false,则表示拦截住本次请求,且在代码中规定处理方法为重定向到登录页面
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) 
throws Exception {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("当前帐号没有相应的权限!");
        }

        //重定向到登录页面
        this.redirectToLogin(servletRequest, servletResponse);
        return Boolean.FALSE;
    }
}

编写完自定义Realm与自定义Shiro拦截器,再次查看shiro.xml配置文件,发现编译器已经不报红。

(5)第五步,编写JSP来验证SSM框架是否成功集成Shiro。

        编写login.jsp页面。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="<c:url value="/actions/security/login"/>" method="post">
    用户名<input type="text" name="username"><br>
    密码<input type="password" name="password"><br>
    <input type="submit" value="提交">
</form>
</body>
</html>

       login.jsp发送的表单请求需要编写Java代码来处理。处理/actions/security/login请求的Controller代码如下。

    @RequestMapping(value = "security/login", method = {RequestMethod.POST})
    public String login(@RequestParam("username") String userName, @RequestParam("password") String password) {
        //获取到Subject门面对象
        Subject subject = getSubject();
        try {
            //将用户数据交给Shiro框架去做
            //你可以在自定义Realm中的认证方法doGetAuthenticationInfo()处打个断点
            subject.login(new UsernamePasswordToken(userName, password));
        } catch (AuthenticationException exception) {
            if (!subject.isAuthenticated()) {
                //登录失败
                return "fail";
            }
        }
        //登录成功
        return "home";

        接下来编写登录成功的 home.jsp 与登录失败的fail.jsp 

        在web-inf/pages/目录下,编写home.jsp页面。编写一个进入管理员页面的超链接,再编写一个退出登录的超链接。 

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>主页</title>
</head>
<body>
您已经登录成功<br>
<a href="<c:url value="/index.jsp"/>">回到首页</a><br>
<a href="<c:url value="/actions/admin"/> ">进入管理员页面</a><br>
<a href="<c:url value="/actions/logout"/> ">退出,此请求会被shiro的退出拦截器捕获</a>
</body>
</html>

        在web-inf/pages/目录下,编写登录失败的fail.jsp页面

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录失败</title>
</head>
<body>
<a href="<c:url value="/login.jsp"/> ">重新登录</a>
</body>
</html>

 (6)第六步, 在web-inf/pages/目录下编写管理员页面 admin.jsp,并在Controller中编写处理代码。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
管理员页面<br>
<a href="<c:url value="/index.jsp"/>">回到首页</a>
</body>
</html>
    @RequestMapping(value = "admin")
    public String enterAdmin() {
        //跳转到 web-inf/pages/admin.jsp页面
        return "admin";
    }

        最后,把index.jsp页面修改一下

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title></title>
</head>
<body>
hello world
<h1>下面的第一个超链接测试是否返回JSON数据</h1>
<a href="<c:url value="/actions/obtainAllUsers"/> ">测试超链接</a><br>
<a href="<c:url value="/actions/admin"/> ">进入管理员页面</a><br>
<a href="<c:url value="/actions/logout"/> ">退出</a>
</body>
</html>

         至此,整个搭建环节就已经完毕了。

三、代码分析

        启动集成Shiro后的框架,让我们回到最初的页面。

        首先,我们在浏览器上输入http://localhost:8080/actions/obtainAllUsers请求,显然,此请求在没有登录的情况下,会被Shiro拦截。为什么会被拦截呢,我们查看shiro.xml配置文件,它有如下的一段配置。/actions/**说明除了特殊规定的URL以外,使用/actions开头的请求都需要认证。所以,这次我们就请求不到JSON数据啦。本次请求会被重定向到/login.jsp页面。

        在login.jsp页面中,我们输入 jay/123456,点击提交。这个form表单的请求是/actions/security/login,此请求不会被拦截,因为使用的是anon拦截器。

        我们再找到处理此请求的Controller代码。显然,此请求将用户名密码封装成了一个Token对象,交给了Subject门面对象去做登录。

        Subject门面对象将数据拿给谁去校验呢?没错,就是我们在配置文件中自定义的Realm对象。

        在自定义Realm中的认证方法doGetAuthenticationInfo()中打一个断点。在页面上点击提交,将会进入此断点中。如果你已经提交了,那么请退出登录或者重启项目哦。

        我点击了提交,来到了断点处。本项目是SSM框架,所以userService是自动注入进来的哦。从数据库中查询到的密码是123456,最后把这个从数据库中查到的密码封装成一个凭证,与用户输入的对比。发现是正确的。所以,再回头看Controller,它的处理逻辑是登录成功,就跳转home.jsp页面。

        下面是登录成功后的home.jsp页面。

        接着,我们再来点击页面上的"进入管理员页面"的超链接。这个超链接发送的请求是"/actions/admin"。

        我们得去shiro.xml中看看shiro的过滤器是怎么规定/admin开头的请求的。如图,我们规定/admin相关的请求首先需要登录,然后需要经过我们自定义的拦截器permissionFilter,最后需要判断这个登录人是否含有coder角色。

        我们已经登录了,所以authc拦截器不会拦截我们的请求。接着,我们的请求会到permissionFilter中。我们找到自定义过滤器的相关配置,permissionFilter其实就是UserAccessControlFilter类。我们在这个类的isAccessAllowed方法上打一个断点,看看请求会不会过来。

        OK,这个时候,点击home.jsp的“进入管理员页面”超链接。 如图,在点击超链接后,请求已经成功进入isAccessAllowed方法。

        为什么我在这里编写了这个拦截器呢,因为在真实项目中,访问某个资源需要前置判断当前登录的人是否含有某种角色信息或者权限信息。所以,我们可以在这个拦截器里面做这些操作。

        还有一个好处就是,我之前也碰到过一些测试人员提出的建议,其中有个妹子就对我说能不能在客户访问某个页面之前判断这个用户是否已经过期。比如你的身份证到期了,这个时候就可以在这个拦截器里面查一下这个人的身份证是否过期。如果过期了,那么就进行拦截。

         哦,差点忘了正事,本拦截器继承的是AccessControlFilter类,如果isAccessAllowed方法返回false,那么就会进入本类的另外一个方法中:onAccessDenied。从这个方法的名字也可以知道,一旦(on)请求(Access)被拒绝(Denied)。我的处理方式是如果请求被拦截,那么直接重定向到登录页面。

//重定向到登录页面
this.redirectToLogin(servletRequest, servletResponse);

        我们接着往下走,刚才isAccessAllowed方法返回的是true,说明此次请求已经成功放行。不过在图中,我们知道,还有最后一个限制:是否含有coder权限。shiro对权限的校验会拿到自定义Realm中的doGetAuthorizationInfo方法中去对比。

        OK,我们最后再在UserRealm的doGetAuthorizationInfo方法处打个断点。如果你已经成功到达admin.jsp页面,可以退出后重新操作一波,或者直接重启项目。打了断点后,请求会来到我们的doGetAuthorizationInfo方法处。从图中可以清楚的看到,本次请求执行了自定义Realm的授权操作。Shiro会问你,这个登录的人它有什么权限呀?我们可以从数据库中查询当前登录人的权限信息与角色信息。当然了,我这里偷了个懒,getYourRoleByUsernameFromDB方法直接返回了一个"coder",表明从数据中查询出来的角色信息是coder。

        然后Shiro就会进行判断,Shiro发现本次需要的请求角色信息是coder,这个人的角色信息包含了coder,所以验证通过。最终此请求成功来到了admin.jsp页面。

       

        我们再点击回到首页后,点击退出。没反应?没事,那是因为我忘了配置退出后重定向的位置了o(∩_∩)o ,有点小尴尬。不过没有关系,实际上已经退出了哦。不信你再点击"进入管理员页面"超链接,因为你退出了,所以你会被重定向到login.jsp页面。

        关于上面退出后应该跳转哪个页面的问题,你可以新增像这样配置,规定退出后跳转 /index.jsp页面。

       好了,本期SSM框架集成Shiro就结束了。如果你有问题,欢迎留言哦。

四、源码下载

       本章节项目源码:点击我下载源码

----------------------------------------------------分割线------------------------------------------------------- 

        下一篇:第十一节 Remember Me

        阅读更多:跟着大宇学Shiro目录贴

如果本文对你有帮助,不妨请我喝瓶可乐吧!

你的打赏是对我最好的支持!

                    

posted @ 2022-07-17 12:12  小大宇  阅读(21)  评论(0编辑  收藏  举报