Shiro在Web中的应用

Scroll Down

Shiro在Web中的应用

参考 :http://shiro.apache.org/webapp-tutorial.html#project-setup

在web中使用shiro的步骤分为7步

  1. 启用Shiro
  2. 绑定数据库
  3. 启用登录登出
  4. 区别用户的界面
  5. 仅登录用户可访问
  6. 基于角色的访问控制
  7. 基于权限的访问控制

搭建一个web应用程序

添加maven依赖

<properties>
    <shiro.version>1.3.2</shiro.version>
</properties>
<dependencies>

    <!-- Logging API + implementation: -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.21</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.21</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.1.7</version>
        <scope>runtime</scope>
    </dependency>

    <!-- Shiro dependencies: -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>${shiro.version}</version>
    </dependency>

    <!-- Stormpath support for quick Realm deployment: -->
    <dependency>
        <groupId>com.stormpath.shiro</groupId>
        <artifactId>stormpath-shiro-core</artifactId>
        <version>0.7.0</version>
    </dependency>
    <dependency>
        <groupId>com.stormpath.sdk</groupId>
        <artifactId>stormpath-sdk-httpclient</artifactId>
        <version>1.0.4</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

步骤1:启用Shiro

添加一个shiro.ini文件

无论shiro在哪一个环境中使用,都需要一个ini或者别的类型的文件作为配置,

src/main/webapp/WEB-INF/shiro.ini创建文件

[main]

# Let's use some in-memory caching to reduce the number of runtime lookups against a remote user store.
# A real application might want to use a more robust caching solution (e.g. ehcache or a
# distributed cache).  When using such caches, be aware of your cache TTL settings: too high
# a TTL and the cache won't reflect any potential changes in Stormpath fast enough.  Too low
# and the cache could evict too often, reducing performance.
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager

这个.ini有一个简单的[main]区块

  • 它定义了一个cacheManager实例。缓存(Caching)是Shiro的一个重要的结构,它减少了持续不断地去查询数据库中的信息,这个例子中使用了MemoryConstrainedCacheManager
  • 它还把这个cacheManager,放到了Shiro的securityManager中,这个securityManager是已经出现的(已有对象),我们不用去刻意定义它

在web.xml中开启Shiro

我们需要在环境中去加载它

<listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

<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>
</filter-mapping>
  • <listener>声明了一个ServletContextListener对象,这个对象启动这个Shiro的环境,包括SecurityManager,默认回去在WEB-INF目录下寻找shiro.ini作为配置
  • <filter>声明了ShiroFilter,这个过滤器会过滤所有的request请求,Shiro就可以去做身份验证和访问控制了
  • <filter-mapping> 保证了过滤器会过滤所有请求。通常,<filter-mapping>不需要配置dispatcher,但是Shiro需要定义他们,这样Shiro就可以过滤并处理各种类型的请求

步骤2:绑定数据库

现在Shiro已经集成在在web应用中了,但是我们还没有告诉Shiro它要做什么,在我们能登录,登出之前我们需要用户

我们需要配置Shiro去绑定一个数据库,这样它就可以和数据库查询用户,检查角色和权限信息

Shiro通过Realm去做到这一切,所以我们要去配置Realm

我们使用的Realm是JdbcRealmRealm的一个实现

设置数据库

添加mysql连接的jar包依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.22</version>
</dependency>

在数据库中建表等操作

drop database if exists shiro;
create database shiro;
use shiro;

create table users (
  id bigint auto_increment,
  username varchar(100),
  password varchar(100),
  password_salt varchar(100),
  constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_users_username on users(username);

create table user_roles(
  id bigint auto_increment,
  username varchar(100),
  role_name varchar(100),
  constraint pk_user_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_user_roles on user_roles(username, role_name);

create table roles_permissions(
  id bigint auto_increment,
  role_name varchar(100),
  permission varchar(100),
  constraint pk_roles_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_roles_permissions on roles_permissions(role_name, permission);

insert into users(username,password)values('test','123');

在配置文件中配置

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC
dataSource.username=root
dataSource.password=root
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm

ini配置分析

  • 变量名 = 全限定类名会自动创建一个实例

  • 变量名.属性 = 值自动调用相应的setter方法进行赋值

  • $变量名引用之前的一个对象实例

JdbcRealm源码分析

几个查询语句

/**
 * The default query used to retrieve account data for the user.
 */
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";

/**
 * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
 */
protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";

/**
 * The default query used to retrieve the roles that apply to a user.
 */
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";

/**
 * The default query used to retrieve permissions that apply to a particular role.
 */
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";

从此分析,我们需要三张表

  1. users
  2. user_roles
  3. roles_permissions

源码中通过JdbcRealm的两个方法,来实现验证,获取角色,获取权限

  1. doGetAuthenticationInfo
  2. doGetAuthorizationInfo

测试

IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("test","123");
Subject user = SecurityUtils.getSubject();
user.login(usernamePasswordToken);

因为是测试,我把shiro.ini复制了一份放到classpath下

如果不报错就成功了

步骤3:启用登录登出

编写一个Login界面

几个要求

  1. 表单的action属性置空

    当表单属性置空时,浏览器会提交表单并请求相同的URL,我们会告诉Shiro哪个URL要去处理登录请求,所以提交到哪不必担心

  2. 表单中有一个username表单项

    Shiro过滤器会自动找一个username的请求参数

  3. 表单中含有一个password表单项

    Shiro过滤器回去寻找一个password请求参数

  4. 表单中含有一个rememberMe复选框

    它的选中状态可以是一个“真”的值(true, t, 1, enabled, y, yes, 或者 on

  5. 表单的提交方式为post

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>login</title>
</head>
<body>
    <form action="" method="post">
        <input type="text" name="username"><br/>
        <input type="password" name="password"><br/>
        <input type="checkbox" name="rememberMe" value="true">记住我<br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>

在ini中配置

增加以下配置

[main]

shiro.loginUrl = /login.jsp


[urls]
/login.jsp = authc
/logout = logout

[main]中添加登录的url,这个配置告诉Shiro,给所有的过滤器的loginUrl属性赋值/login.jsp,这让authc过滤器知道了登录页面

[urls]中添加的内容

  1. 当Shiro看见/login.jsp,Shiro就会开启authc过滤器
  2. 当Shiro看见/logout,Shiro开启logout过滤器

登录成功后就会跳转到index.jsp

登录成功界面定制

Shiro默认跳转到index.jsp,但是这可以改变

如果我添加success.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>

并且更改配置authc.successUrl = /success.jsp

步骤4:区别用户的界面

这个demo界面定制是用jsp做的

所以要引入一些标签库

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

使用shiro标签库

<body>
  <p>Hi <shiro:guest>Guest</shiro:guest><shiro:user>
    <%
      //This should never be done in a normal page and should exist in a proper MVC controller of some sort, but for this
      //tutorial, we'll just pull out Stormpath Account data from Shiro's PrincipalCollection to reference in the
      //<c:out/> tag next:

      request.setAttribute("account", org.apache.shiro.SecurityUtils.getSubject().getPrincipals().oneByType(java.util.Map.class));

    %>
    <c:out value="${account.givenName}"/></shiro:user>!
    ( <shiro:user><a href="<c:url value="/logout"/>">Log out</a></shiro:user>
    <shiro:guest><a href="<c:url value="/login.jsp"/>">Log in</a></shiro:guest> )
  </p>
</body>
  • <shiro:guest>:这个标签里的内容只会在当前Subject是“guest”的时候展示
  • <shiro:user>:这个标签中的内容只会在当前Subject是一个用户的时候展示

步骤5:仅登录用户可访问

添加一个受限的区域

添加web/account这个文件夹

在account文件夹中创建index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>account</title>
</head>
<body>
<h2>Users only</h2>

<p>You are currently logged in.</p>

<p><a href="<c:url value="/home.jsp"/>">Return to the home page.</a></p>

<p><a href="<c:url value="/logout"/>" onclick="document.getElementById('logout_form').submit();return false;">Log out.</a></p>
<form id="logout_form" action="<c:url value="/logout"/>" method="post"></form>
</body>
</html>

在ini中配置

在ini中的[url]添加以下配置

/account/** = authc

每个访问account的用户必须是认证过了的

如果没有登录/验证,就会被自动重定向到设置的

shiro.loginUrl = /login.jsp

步骤6:基于角色的访问控制

添加一些角色

加入以下三个角色:

  • Captains
  • Officers
  • Enlisted

在数据库中进行添加

insert into user_roles(username, role_name) values
('test','Captains'),('test','Officers');

在视图中体现

<h2>Roles</h2>

<p>Here are the roles you have and don't have. Log out and log back in under different user
    accounts to see different roles.</p>
    
<h3>Roles you have:</h3>

<p>
    <shiro:hasRole name="Captains">Captains<br/></shiro:hasRole>
    <shiro:hasRole name="Officers">Bad Guys<br/></shiro:hasRole>
    <shiro:hasRole name="Enlisted">Enlisted<br/></shiro:hasRole>
</p>

<h3>Roles you DON'T have:</h3>

<p>
    <shiro:lacksRole name="Captains">Captains<br/></shiro:lacksRole>
    <shiro:lacksRole name="Officers">Officers<br/></shiro:lacksRole>
    <shiro:lacksRole name="Enlisted">Enlisted<br/></shiro:lacksRole>
</p>
  • <shiro:hasRole>中的内容当subject有这个角色的时候才展示
  • <shiro:lacksRole>中的内容当subject中没有这个角色的时候展示

步骤7:基于权限的访问控制

在数据库中插入权限

insert into roles_permissions(role_name, permission) values
('Captains','ship:NCC-1701-D:command'),('Officers','user:jlpicard:edit');

此处注意检查配置ini中有没有:jdbcRealm.permissionsLookupEnabled = true

如果没有是不会查询数据库的

在视图中体现

<h2>Permissions</h2>

<ul>
    <li>You may <shiro:lacksPermission name="ship:NCC-1701-D:command"><b>NOT</b> </shiro:lacksPermission> command the <code>NCC-1701-D</code> Starship!</li>
    <li>You may <shiro:lacksPermission name="user:${account.username}:edit"><b>NOT</b> </shiro:lacksPermission> edit the ${account.username} user!</li>
</ul>
  • <shiro:lacksPermission>标签中的内容在没有权限的时候显示
  • <shiro:hasPermission>标签中的内容在拥有权限时进行展示