红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 其它
  3. 正文

Shiro 学习笔记2:Web 应用集成

2023年9月19日 934点热度 0人点赞 0条评论

需要先进行一些准备工作:

  1. 创建一个 Maven Web 项目,具体方式可以参考这篇文章。

  2. 将上篇文章项目中的依赖拷贝进当前项目。

  3. 将上篇文章项目中的代码拷贝进当项目。

这里提供一个整合后的示例项目 web-demo,内含 SQL。

Web 集成 Shiro

web.xml

现在这个示例项目存在一个问题,读取 Shiro 配置并创建 SecurityManager 的代码需要手动调用,虽然我们已经封装成了工具类:

public class SubjectUtil {
    private static SecurityManager securityManager;
​
    public static Subject getSubject(){
        SecurityManager securityManager = getSecurityManager();
        // 将 SecurityManager 关联到 SecurityUtils
        SecurityUtils.setSecurityManager(securityManager);
        // 获取 subject
        return SecurityUtils.getSubject();
    }
​
    private static SecurityManager getSecurityManager(){
        if (securityManager!=null){
            return securityManager;
        }
        // 对安全管理器初始化
        // 从配置文件加载 SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        return securityManager;
    }
}

实际上如果我们开发的是 Java Web 应用,可以在 web.xml 配置中集成 Shiro。这样在 Web 服务器启动后就可以自动读取配置并创建一个全局的 SecurityManager,我们只需要直接使用即可。

用 Maven 创建的 Web 项目默认使用 2.3 版本的 web.xml 配置:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
​
<web-app>
  <display-name>Archetype Created Web Application</display-name>
</web-app>

修改为新版本:

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
  <display-name>Archetype Created Web Application</display-name>
</web-app>

要在 Web 应用中集成 Shiro,需要添加一个依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.3.2</version>
</dependency>

在 web.xml 中添加以下内容:

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
  <display-name>Archetype Created Web Application</display-name>
  <!-- 初始化SecurityManager对象所需要的环境-->
  <context-param>
    <param-name>shiroEnvironmentClass</param-name>
    <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value>
  </context-param>
​
  <!-- 指定Shiro的配置文件的位置 -->
  <context-param>
    <param-name>shiroConfigLocations</param-name>
    <param-value>classpath:shiro.ini</param-value>
  </context-param>
​
  <!-- 监听服务器启动时,创建shiro的web环境。
       即加载shiroEnvironmentClass变量指定的IniWebEnvironment类-->
  <listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
  </listener>
​
  <!-- shiro的l过滤入口,过滤一切请求 -->
  <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>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
    <dispatcher>ASYNC</dispatcher>
  </filter-mapping>
</web-app>

关于 Web 集成 Shiro 后的启动过程分析可以观看这个视频。

默认过滤器

Shiro 默认提供了一些常用的过滤器,这些过滤器定义在org.apache.shiro.web.filter.mgt包下的枚举类型DefaultFilter中:

public enum DefaultFilter {
    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);
    // ...
}

这些过滤器分为两种:认证相关和授权相关。

认证相关的过滤器:

过滤器 过滤器类 说明 默认
authc FormAuthenticationFilter 基于表单的过滤器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录 无
logout LogoutFilter 退出过滤器,主要属性:redirectUrl:退出成功后重定向的地址,如“/logout=logout” /
anon AnonymousFilter 匿名过滤器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon” 无

授权相关的过滤器:

过滤器 过滤器类 说明 默认
roles RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]” 无
perms PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms["user:create"]” 无
port PortFilter 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 无
rest HttpMethodPermissionFilter rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll) 无
ssl SslFilter SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样;

案例:通过 web.xml 实现权限控制

这里继续完善 Web 应用。

应用的表现层使用 Servlet + JSP,所以这里需要添加相关依赖:

<!-- servlet + JSP 的相关依赖 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
</dependency>

添加示例页面的 JSP 和 Servlet,主要包含下面几个链接:

  • http://localhost/jsp/user/login.jsp,登录页

  • http://localhost/jsp/user/user/login,执行登录动作

  • http://localhost/jsp/user/home.jsp,个人主页

  • http://localhost/brand/list,商品列表页

  • http://localhost/jsp/brand/add.jsp,新增商品页

  • http://localhost/brand/add,执行新增商品动作

  • http://localhost/user/logout,执行注销账户动作

相关页面和 Servlet 的实现可以参考 web-demo。

当然,这些页面现在是没有登录状态检查和权限控制的。如果是以往,我们需要用 Session 的方式实现登录状态检查,但现在我们可以使用 Shiro 实现。

登录和注销

先添加登录和注销的逻辑:

public class UserServiceImpl implements UserService {
    @Override
    public void login(String username, String password) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        subject.login(token);
        if (!subject.isAuthenticated()){
            // 没有抛出异常但是没有登录成功(不可能出现的错误)
            throw new AuthenticationException("未知的登录错误");
        }
    }
​
    @Override
    public void logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
    }
}

因为前边说的,Shiro 的 SecurityManager 已经利用事件监听器,在 Tomcat 启动时完成了配置读取和初始化,所以这里可以直接使用SecurityUtils.getSubject获取Subject对象并执行登录和注销动作。

用于登录的 Servlet:

@WebServlet("/user/login")
public class LoginServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Map<String, String> utf8Params = ServletUtil.getUTF8Params(req);
        String username = utf8Params.get("username");
        String password = utf8Params.get("password");
        // 执行登录操作
        try {
            userService.login(username, password);
            //登录成功,跳转到用户主页
            resp.sendRedirect("/jsp/user/home.jsp");
        }
        catch (Exception e){
            e.printStackTrace();
            // 登录失败
            // 跳转到登录页面
            resp.sendRedirect("/jsp/user/login.jsp");
        }
    }
}

用于注销的 Servlet:

@WebServlet("/user/logout")
public class LogoutServlet extends HttpServlet {
    private UserService userService = new UserServiceImpl();
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 执行注销动作
        userService.logout();
        // 注销成功,跳转到登录页
        resp.sendRedirect("/jsp/user/login.jsp");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }
}

登录检查

现在虽然实现了登录和注销,但是依然可以直接访问相应的页面链接打开页面,要实现登录检查,只允许登录的用户才能访问某些页面,需要修改 Shiro 的配置文件 shiro.ini:

[main]
definitionRealm=cn.icexmoon.webdemo.realm.MyRealm
securityManager.realms=$definitionRealm
#用户退出后跳转指定JSP页面
logout.redirectUrl=/jsp/user/login.jsp
#若没有登录,则被authc过滤器重定向到login.jsp页面
authc.loginUrl = /jsp/user/login.jsp
[urls]
# 登录页面任何人都可以访问
/jsp/user/login.jsp=anon
/user/login=anon
# 需要登录才能访问的页面
/jsp/**=authc
/user/**=authc
/brand/**=authc

authc.loginUrl的用途是指定一个登录页面,如果用户没有登录却访问了需要登录才能访问的页面,就会跳转到相应的页面。在[urls]属性组里,可以用前边说的 Shiro 过滤器配置页面的访问权限。这些权限是有前后顺序区分的,比如将/jsp/user/login.jsp=anon设置在第一个,虽然/jsp/user/login.jsp这个 url 也同样符合后边/jsp/**=authc的规则,但是只要符合第一个规则,就不会再按后边的规则处理,也就是说登录页是任何人可以访问(匿名访问)的。

需要注意的是,除了登录页设置为可以匿名访问,登录表单提交的 url 同样要设置为可以匿名访问,否则就无法正常登录。

角色过滤

现在已经可以正常登录和注销了,且只有登录的用户可以访问相关页面。如果我们需要对页面进行更细粒度的权限控制,比如拥有某种角色或权限的用户才能访问相应的页面或 url,就需要使用相应的过滤器进行设置:

/brand/** = authc, roles[sys_manager]

此时如果请求路径是/brand或其子路径,就会触发一个过滤器链,在这个过滤器链上,会先检查是否登录状态(authc过滤器),再检查是否拥有某个角色(roles过滤器)。

如果同时要拥有多个角色才能访问,可以:

/brand/** = authc, roles[sys_manager,dep_manager]

此时只有同时拥有系统管理员和部门经理两个角色的用户才能访问相关页面。

自定义过滤器

但roles这个过滤器并不是很实用,因为通常我们需要的是满足任意一个角色就可以访问,此时默认的过滤器不能满足要求,就需要我们添加一个自定义过滤器。

Shiro 的官方文档告诉我们,添加的自定义过滤器需要继承自 org.apache.shiro.web.filter.PathMatchingFilter 这个接口。实际上观察类的继承层次可以发现,我们使用的默认过滤器roles正是它的子类:

image-20230919133221613

也就是说我们只需要照着RoleAuthorizationFilter抄一个用于检查是否满足任意一个角色的过滤器即可:

public class OrRolesAuthorizationFilter extends AuthorizationFilter {
    /**
     * 拥有指定角色列表中的任意一个角色就视为通行
     *
     * @param request     请求对象
     * @param response    响应对象
     * @param mappedValue 指定的角色列表
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            // 指定的角色列表为空,放行
            return true;
        }

        Set<String> roles = CollectionUtils.asSet(rolesArray);
        // 只要满足任意角色,就放行
        for (String role : roles) {
            if (subject.hasRole(role)) {
                return true;
            }
        }
        // 所有角色都不满足,禁止通过
        return false;
    }
}

在 Shiro 的配置文件 shiro.ini 中使用这个过滤器:

[main]
# ...
#自定义过滤器
orRoles = cn.icexmoon.webdemo.shiro.filter.OrRolesAuthorizationFilter
[urls]
# ...
# 访问品牌相关页面需要管理员或者部门经理的角色
/brand/** = authc, orRoles[sys_manager,dep_manager]

可以创建两个账户进行测试,会发现只有员工角色的账户不能访问品牌列表页,但是拥有部门经理角色的账户可以访问。

权限过滤

只检查角色是不够的,我们还需要对页面按照功能划分权限,比如将品牌相关页面划分为以下权限:

image-20230919135813473

修改 Shiro 配置文件:

[urls]
# 登录页面任何人都可以访问
/user/login = anon
# 访问品牌相关页面需要管理员或者部门经理的角色
/brand/list = authc, orRoles[sys_manager,dep_manager], perms["brand:view"]
/brand/add = authc, orRoles[sys_manager,dep_manager], perms["brand:add"]
/jsp/brand/add.jsp = authc, orRoles[sys_manager,dep_manager], perms["brand:add"]
/brand/** = authc, orRoles[sys_manager,dep_manager]
# 需要登录才能访问的页面
/jsp/** = authc
/user/** = authc

现在只有拥有相应权限的账号才能访问相关页面或 url,否则会返回一个 401 错误。

比较特别的是,我们可以给某个账号赋予brand:*这样带通配符的权限,此时该账户就可以看作是拥有了匹配的任意权限,比如brand:view、brand:add等brand:开头的权限。

关于权限相关的更多说明可以阅读官方文档。

案例:用编程的方式实现权限控制

虽然 Shiro 官方建议通过配置 web.xml 实现对 Web 应用的权限控制,这样做的好处是简洁高效。但也可以通过编程的方式实现权限控制。

删除 web.xml 中的权限控制相关内容,只需要保留以下内容:

[main]
definitionRealm = cn.icexmoon.webdemo.shiro.realm.MyRealm
securityManager.realms = $definitionRealm

添加一个过滤器,用于处理登录状态检查:

@WebFilter("/*")
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!(request instanceof HttpServletRequest)
                || !(response instanceof HttpServletResponse)) {
            throw new RuntimeException("不是 Web 请求");
        }
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String uri = httpServletRequest.getRequestURI();
        // 不需要登录就可以访问的链接
        String[] noNeedLoginUris = {"/user/login"};
        for (String u : noNeedLoginUris) {
            if (uri.indexOf(u) == 0) {
                chain.doFilter(request, response);
                return;
            }
        }
        // 其余 url 都视作需要登录访问
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            // 已经登录,放行
            chain.doFilter(request, response);
            return;
        }
        // 没有登录,跳转到登录页
        httpServletResponse.sendRedirect("/user/login");
    }

    @Override
    public void destroy() {

    }
}

内容和以前编写的没有根本区别,只不过在检查是否登录的时候使用 Shiro 的 API。

在需要权限检查的相关 Servlet 中实现具体的权限检查逻辑:

@WebServlet("/brand/list")
public class BrandListServlet extends HttpServlet {
    private BrandService brandService = new BrandServiceImpl();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 品牌列表页只有系统管理员或部门经理可以查看
        Subject subject = SecurityUtils.getSubject();
        if (!subject.hasRole(Role.ROLE_SYS_MANAGER)
                && !subject.hasRole(Role.ROLE_DEP_MANAGER)) {
            response.sendError(401, "缺少相关权限");
            return;
        }
        // 品牌列表必须要有查看品牌的权限
        if (!subject.isPermitted("brand:view")){
            response.sendError(401, "缺少相关权限");
            return;
        }
        //准备品牌数据
        List<Brand> brands = brandService.getAllBrands();
        request.setAttribute("brands", brands);
        //加载品牌列表页
        request.getRequestDispatcher("/jsp/brand/list.jsp").forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }
}

对其他页面的权限检查方式是相似的,这里不再赘述。

JSP 标签

Shiro 为方便在 JSP 中进行权限检查并控制 Html 元素是否加载提供了一套标签(类似于之前学过的 c 标签):

标签 说明
< shiro:guest > 验证当前用户是否为“访客”,即未认证(包含未记住)的用户
< shiro:user > 认证通过或已记住的用户
< shiro:authenticated > 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在
< shiro:notAuthenticated > 未认证通过用户。与guest标签的区别是,该标签包含已记住用户
< shiro:principal /> 输出当前用户信息,通常为登录帐号信息
< shiro:hasRole name="角色"> 验证当前用户是否属于该角色
< shiro:lacksRole name="角色"> 与hasRole标签逻辑相反,当用户不属于该角色时验证通过
< shiro:hasAnyRoles name="a,b"> 验证当前用户是否属于以下任意一个角色
<shiro:hasPermission name=“资源”> 验证当前用户是否拥有指定权限
<shiro:lacksPermission name="资源"> 与permission标签逻辑相反,当前用户没有制定权限时,验证通过

案例

之前的示例实现了权限控制,但依然有一些瑕疵,比如在用户主页,有的用户没有品牌列表的权限,但是可以在导航栏看到相应的链接,这样是不合适的。

虽然可以通过在请求域中添加相应的属性进行判断来控制链接是否显示,但使用 Shiro 标签是更简单直接的方式。

要使用 Shiro 标签,需要先在 JSP 上添加标签库的引用:

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

修改个人主页 home.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h2>个人主页</h2>
<h3>导航栏 <a href="/user/home">个人主页</a>
    <shiro:authenticated>
        <shiro:hasAnyRoles name="sys_manager,dep_manager">
            <shiro:hasPermission name="brand:view">
                <a href="/brand/list">品牌列表页</a>
            </shiro:hasPermission>
        </shiro:hasAnyRoles>
    </shiro:authenticated>
    <a href="/user/logout">注销</a></h3>
欢迎登录,${username}
</body>
</html>

这里用三种 Shiro 标签包裹品牌列表页的超链接,结果是只有在用户登录、且拥有管理员或部门经理角色,且有品牌查看权限的情况下才会显示超链接。

实际上这里不需要最外层的shiro:authenticated标签,因为个人主页是要登录后才能查看的。这里只是展示多种标签的用途。

类似的,可以控制品牌列表页的新增品牌超链接只有在有相关权限时才显示:

<shiro:hasPermission name="brand:add">
    <a href="/jsp/brand/add.jsp">新增</a>
</shiro:hasPermission>

The End,谢谢阅读。

本文的完整示例可以从这里获取。

参考资料

  • 黑马程序员Java高级工程师技术栈-由浅入深掌握Shiro权限框架

  • Apache Shiro Web支持

  • JavaWeb 学习笔记 5:JSP - 红茶的个人站点 (icexmoon.cn)

  • Apache Shiro授权

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: shiro web
最后更新:2023年9月19日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号