依赖

  • shiro-web

    <!--shiro项目至少的jar-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.2</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.2.2</version>
    </dependency>
    
  • Servlet3:编译与测试时的web环境

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
        <scope>provided</scope>
    </dependency>
    
  • tomcat7-maven-plugin插件: 运行时的web环境

    <build>
        <finalName>shiroHelloWorld-chapter7</finalName>
          <plugins>
              <plugin>
                  <groupId>org.apache.tomcat.maven</groupId>
                  <artifactId>tomcat7-maven-plugin</artifactId>
                  <version>2.2</version>
                  <configuration>
                      <path>/${project.build.finalName}</path>
                  </configuration>
              </plugin>
          </plugins>
    </build>
    

web.xml配置、ShiroFilter

  • Shiro提供了与web的集成,通过一个ShiroFilter入口来拦截需要安全控制的url,默认实现类是IniShiroFilter。
  • shiro 1.1版本的ShiroFilter:

    <filter>
        <filter-name>iniShiroFilter</filter-name> <!--使用IniShiroFilter作为shiro安全控制入口点-->
        <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
    
        <init-param>
            <param-name>configPath</param-name> <!--configPath:指定ini配置文件位置;固定-->
            <param-value>classpath:shiro.ini</param-value> <!--默认先从/WEB-INF/shiro.ini加载,
                没有再classpath:shiro.ini-->
        </init-param>
        <!-- 或不指定ini位置,直接按下面这样
        <init-param>
            <param-name>config</param-name>
            <param-value>
                [main]
                authc.loginUrl=/login
                [users]
                zhang=123,admin
                [roles]
                admin=user:*,menu:*
                [urls]
                /login=anon
                /static/**=anon
                /authenticated=authc
                /role=authc,roles[admin]
                /permission=authc,perms["user:create"]
            </param-value>
        </init-param>
        -->
    </filter>
    
    <filter-mapping>
        <filter-name>iniShiroFilter</filter-name>
        <url-pattern>/*</url-pattern> <!--指定需要拦截的url-->
    </filter-mapping>
    
  • 使用IniShiroFilter作为Shiro安全控制的入口点。
  • 可以使用configPath指定ini配置文件路径,默认先从/WEB-INF/shiro.ini加载(/表示根目录,为webapp),没有再是classpath:shiro.ini。也可以直接内嵌配置文件内容。
  • shiro 1.2版本开始引入Environment/WebEnvironment,由它们的实现类提供相应的SecurityManager及其相应的依赖。ShiroFilter会自动找到Environment然后获取相应依赖。

    <listener>
        <!--通过EnvironmentLoaderListener创建相应的WebEnvironment,并自动绑定到ServletContext,
        默认使用IniWebEnvironment实现类。-->
        <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
    </listener>
    
    <!--可通过如下配置修改默认配置-->
    <context-param>
        <param-name>shiroEnvironmentClass</param-name>
        <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
    </context-param>
    <context-param>
        <param-name>shiroConfigLocations</param-name>
        <param-value>classpath:shiro.ini</param-value> <!--默认先/WEB-INF/shiro,没有再classpath:shiro.ini-->
    </context-param>
    
  • 以上俩版本的配置相当于以下代码:
//1、获取SecurityManager工厂,并使用ini配置文件初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory=
          new IniSecurityManagerFactory("classpath:config/shiro.ini");
//2、得到SecurityManager实例,并绑定SecurityUtils
SecurityManager securityManager=factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
  • 那么以后登录直接从第3步:Subject subject=SecurityUtils.getSubject();写起。

  • 与Spring集成:使用DelegatingFilterProxy,作用是自动到Spring容器查找名为filter-name的bean,并把所有Filter的操作委托给它,所以需要将shiroFilter注册到Spring。

    <filter>
        <!--DelegatingFilterProxy作用是自动到Spring容器查找名为filter-name的bean,并把所有
        Filter的操作委托给它,所以需要将shiroFilter注册到Spring。-->
        <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>/*</url-pattern>
    </filter-mapping>
    
  • ShiroFilter的bean:主要是绑定SecurityManager。需要使用org.springframework.web.context.ContextLoaderListener加载所在配置文件。

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!—忽略其他,详见与Spring集成部分 -->
    </bean>
    
  • 如果不与Spring集成,则直接声明ShiroFilter即可:

    <!--绑定SecurityManager-->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    

常用的身份验证

  • shiro.ini配置文件
[main]
#默认是/login.jsp
authc.loginUrl=/login #未登录会自动跳转登录页面
roles.unauthorizedUrl=/unauthorized
perms.unauthorizedUrl=/unauthorized

[users]
zhang=123,admin
wang=123

[roles]
admin=user:*,menu:*

[urls]
;格式:url=拦截器[参数]。anon拦截器表示匿名访问,无需登录;authc则需要登录;
;roles[admin]需要admin角色;perms["user:create"]需要有user:create权限
/login=anon
/unauthorized=anon
/static/**=anon
/authenticated=authc
/role=authc,roles[admin]
/permission=authc,perms["user:create"]
  • urls格式:url=拦截器[参数]。anon拦截器表示匿名访问,无需登录,相当于不需拦截的资源;authc则需要登录;roles[admin]需要admin角色;perms[“user:create”]需要有user:create权限.
  • 按照顺序进行匹配,只采用第一个匹配的进行拦截。

  • 使用Ant风格模式

    • ?:匹配一个字符,如”/admin?”将匹配/admin1,但不匹配/admin或/admin2;
    • :匹配零个或多个字符串,如/admin将匹配/admin、/admin123,但不匹配/admin/1;
    • :匹配路径中的零个或多个路径,如/admin/将匹配/admin/a或/admin/a/b。
  • 接收到请求后,先到ini查看是否需要拦截,不需要则查找url匹配的servlet类,需要则在ini中查找登录的url,再查找匹配的servlet类。

  • LoginServlet:登录Servlet。get请求时展示登录页面;post请求进行登录验证。

//相当于web.xml配置<servlet>
@WebServlet(name="loginServlet",urlPatterns = "/login") 
//url:http://localhost:8080/shiroHelloWorld-chapter7/login
public class LoginServlet extends HttpServlet {
    /**
     * @Author haien
     * @Description get请求时展示登录页面
     * @Date 2019/2/28
     * @Param [req, resp]
     * @return void
     **/
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        req.getRequestDispatcher("/jsp/login.jsp").forward(req,resp);
    }

    /**
     * @Author haien
     * @Description post请求进行登录验证
     * @Date 2019/2/28
     * @Param [req, resp]
     * @return void
     **/
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String error = null;
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            error = "用户名/密码错误";
        } catch (IncorrectCredentialsException e) {
            error = "用户名/密码错误";
        } catch (AuthenticationException e) {
            //其他错误,比如锁定,如果想单独处理请单独catch处理
            error = "其他错误:" + e.getMessage();
        }
        if(error != null) {//出错了,返回登录页面
            req.setAttribute("error", error);
            req.getRequestDispatcher("/jsp/login.jsp").forward(req, resp);
        } else {//登录成功
            req.getRequestDispatcher("/jsp/loginSuccess.jsp").forward(req, resp);
        }
    }
}
  • 登录成功不能老是跳到成功页面,而是要跳到之前请求的页面,可以在登录时把当前请求保存下来,登录成功再重定向到该请求即可。

基于Basic的拦截器身份验证

  • 同样是做安全控制的拦截器,但是它要求登录的方式不是跳转登录页面,而是弹出登录窗口。
  • shiro-basicfilterlogin.ini:
[main]
;authBasic是org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter类的实例,
;用于实现基于Basic的身份验证;applicationName属性显示在弹出的登录框
authcBasic.applicationName=please login

perms.unauthorizedUrl=/unauthorized
roles.unauthorizedUrl=/unauthorized
[users]
zhang=123,admin
wang=123

[roles]
admin=user:*,menu:*

[urls]
;/role需要走authcBasic拦截器,即如果访问/role时未登录则弹出对话框进行登录
/role=authcBasic,roles[admin]
  • 需要将web.xml中配置文件更改为shiro-basicfilterlogin.ini。
  • 未登录访问/role将弹出窗口。再次测试未登录却没弹窗可能是因为Chrome记住了登录信息。

基于表单的拦截器身份验证

  • 和第一种类似,但是更简单,因为已经实现了登录信息与用户库的匹配,我们只需要写登录页面的映射、登录失败处理即可。
  • 原理大致如下,详情参见笔记:Shiro第八章二-自定义拦截器
public class FormLoginFilter extends PathMatchingFilter {
    private String loginUrl = "/login.jsp";
    private String successUrl = "/";

    /**
    * 总方法
    */
    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, 
      Object mappedValue) throws Exception {

        //1、判断是否已登录,是则直接进入过滤链下一步
        if(SecurityUtils.getSubject().isAuthenticated()) {
            return true;//已经登录过
        }

        //2、未登录则判断是否为登录请求,是,则若是get请求,继续过滤链(跳转登录
        //页面),若是post请求,认为是表单验证请求,进行表单验证,执行subject.login();
        //否,若是get方法的其他页面请求则保存当前请求并重定向到登录页面,非get请求可能报错吧
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        if(isLoginRequest(req)) { //是登录请求
            if("post".equalsIgnoreCase(req.getMethod())) { //form表单提交
                boolean loginSuccess = login(req); //登录
                if(loginSuccess) { //登录成功
                    return redirectToSuccessUrl(req, resp);
                }
            }

            //是get请求|登录失败,继续过滤器链,可能是被分配到controller处理/login
            return true;
        } 
        else { //不是登录请求,保存当前地址并重定向到登录界面
            saveRequestAndRedirectToLogin(req, resp);
            return false;
        }
    }

    /**
    * 登录成功后调用,若有之前的请求则重定向到它,否则到默认成功页面
    */
    private boolean redirectToSuccessUrl(HttpServletRequest req, 
        HttpServletResponse resp) throws IOException {
        WebUtils.redirectToSavedRequest(req, resp, successUrl);
        return false;
    }

    /**
    * 保存当前请求并跳转登录页面
    */
    private void saveRequestAndRedirectToLogin(HttpServletRequest req,
      HttpServletResponse resp) throws IOException {
        WebUtils.saveRequest(req);
        WebUtils.issueRedirect(req, resp, loginUrl);
    }

    /**
    * 表单验证,执行登录方法
    */
    private boolean login(HttpServletRequest req) {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        try {
            SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));
        } catch (Exception e) {
            req.setAttribute("shiroLoginFailure", e.getClass());
            return false;
        }
        return true;
    }

    /**
    * 判断是否登录请求
    */
    private boolean isLoginRequest(HttpServletRequest req) {
        return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));
    }
}
  • shiro-formfilterlogin.ini:
[main]
;authc是org.apache.shiro.web.filter.authc.FormAuthenticationFilter类的实例;
;loginUrl:指定登录地址、表单提交地址;successUrl:默认是/,
          如果有上一个地址会自动重定向到该地址;
;failureKeyAttribute:登录失败信息的在request中的key,默认为shiroLoginFailure,
                      内容为异常类型名。
authc.loginUrl=/formfilterlogin //get请求即为请求登录页面,由controller映射页面;
    post为表单验证请求,FormAuthenticationFilter处理
authc.usernameParam=username
authc.passwordParam=password
authc.successUrl=/
authc.failureKeyAttribute=shiroLoginFailure

perms.unauthorizedUrl=/unauthorized
roles.unauthorizedUrl=/unauthorized

[users]
zhang=123,admin
wang=123

[roles]
admin=user:*,menu:*

[urls]
/static/**=anon
/formfilterlogin=authc
/role=authc,roles[admin]
/permission=authc,perms["user:create"]
  • 然后web.xml改为shiro-formfilterlogin.ini。

  • formfilterlogin.jsp:登录页面。

    <body>
        <div class="error">${error}</div>
        <form action="${pageContext.request.contextPath}/formfilterlogin" method="post">
            用户名:<input type="text" name="username"><br/>
            密码:<input type="password" name="password"><br/>
            <input type="submit" value="登录">
        </form>
    </body>
    
  • FormFilterLoginServlet:登录失败处理

@WebServlet(name = "formFilterLoginServlet", urlPatterns = "/formfilterlogin")
public class FormFilterLoginServlet extends HttpServlet {
    /**
     * @Author haien
     * @Description get请求的话FormAuthenticationFilter不做任何处理
     直接结束当前过滤链,进入下一过滤链,应该是Spring自己的过滤链了,
     应该就是分发给controller处理,所以这里应该是映射登录页面才对,不过交给doPost做
     也获取不到shiroLoginFailure,也是直接返回登录页面了
     * 
     * @Date 2019/3/15
     * @Param [req, resp]
     * @return void
     **/
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doPost(req, resp);
    }

    /**
     * @Author haien
     * @Description 身份验证和成功跳转已被处理,如果失败FormAuthenticationFilter
     * 并不会返回登录页面或做任何处理,只是结束当前过滤链而进入下一过滤链,
     * 而下一过滤链应该就是Spring自己的过滤链,也就是将原请求分发到controller,
     * 所以要准备一个处理登录失败情况的跳转这里只处理失败情况
     * 
     * @Date 2019/2/28
     * @Param [req, resp]
     * @return void
     **/
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String errorClassName = (String)req.getAttribute("shiroLoginFailure"); //异常类型名
        if(UnknownAccountException.class.getName().equals(errorClassName)) {
            req.setAttribute("error", "用户名/密码错误");
        } else if(IncorrectCredentialsException.class.getName().equals(errorClassName)) {
            req.setAttribute("error", "用户名/密码错误");
        } else if(errorClassName != null) {
            req.setAttribute("error", "未知错误:" + errorClassName);
        }
        req.getRequestDispatcher("/jsp/formfilterlogin.jsp").forward(req, resp); //返回登录页面
    }
}
  • 测试:登录/role,会跳转/formfilterlogin登录页面,登陆成功会跳转/role而不是默认登录成功页面。

权限验证

  • shiro.ini
[main]
roles.unauthorizedUrl=/unauthorized
perms.unauthorizedUrl=/unauthorized
 [urls]
/role=authc,roles[admin]
/permission=authc,perms["user:create"]
  • unauthorizedUrl: 验证失败重定向到的地址。
  • roles: org.apache.shiro.web.filter.authz.RolesAuthorizationFilter类的实例,通过[参数]指定访问时需要的权限,如有多个使用”,”分隔,验证需要都通过。
  • perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter类实例,验证权限字符串。

  • PermissionServlet类:只处理权限验证成功情况,失败情况ini已处理。

@WebServlet(name = "permissionServlet", urlPatterns = "/permission")
public class PermissionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
        Subject subject = SecurityUtils.getSubject();
        subject.checkPermission("user:create");
        req.getRequestDispatcher("/WEB-INF/jsp/hasPermission.jsp").forward(req, resp);
    }
}
  • RoleServlet类:角色单位的权限验证,也是只处理成功情况,主要逻辑如下:
subject.checkRole("admin");
  • 测试:访问/login,使用zhang/123登录后访问/role或/permission,跳转授权成功页面;使用wang/123登录则跳转/unauthorized无授权页面。

  • Shiro也提供了logout拦截器用于退出,它是org.apache.shiro.web.filter.authc.LogoutFilter类的实例

[main]
;logout是org.apache.shiro.web.filter.authc.LogoutFilter类实例,Shiro内置logout拦截器
logout.redirectUrl=/login
[urls]
;指定退出url是/logout2;使用Shiro内置的logout拦截器退出,logout配置在上面main中
/logout2=logout
  • 则无需写Servlet类处理退出请求,只要把退出链接的url改成/logout2即可

    <a href="${pageContext.request.contextPath}/logout2">退出</a>
    

    代码实例

  • ideaProjects/shiroHelloWorldchapter7
  • 参考文章