Shiro 那点事儿

来源:互联网 发布:恩牛网络小贷 编辑:程序博客网 时间:2024/04/28 04:48

前几天我遇见了一位美女,真有种相逢恨晚的感觉。她皮肤白皙、气质优雅、楚楚动人,拥有苗条的身材,却又不失丰满之躯,正所谓“该大的地方大,该小的地方小”,她就是我朝思梦想的情人。

她就是 Apache 组织下的名媛 —— Shiro(希罗),一款轻量级 Java 安全框架。如果您已经玩腻了丰满的 Spring Security,想品尝一下新口味的话,建议先到她的裙下一睹风光吧:

Apache Shiro 官网: http://shiro.apache.org/

从官网上,我们基本上可以了解到,她提供的服务非常明确:

  1. Authentication(认证)

  2. Authorization(授权)

  3. Session Management(会话管理)

  4. Cryptography(加密)

首先,她提供了 Authentication(认证)服务,也就是说,通过她可以完成身份认证,让她去判断您是否为真实的会员。

其次,她还提供了 Authorization(授权)服务,其实说白了就是“访问控制”服务,也就是让她来识别您是否可以做某件事情,毕竟不同的会员是拥有不同的权限的。

更有特色的是,她还提供了 Session Management(会话管理)服务,这个就厉害了,这并不是您熟知的 HTTP Session,而是一个独立的 Session 管理框架,不管是否为 Web 应用,都可以用这套框架。

最后(但并不是最不重要的),她还提供了 Cryptography(加密)服务,封装了许多密码学算法,有您知道的,也有您不知道的,总之琳琅满目,应有尽有。

除了以上 4 个基本服务以外,她也提供很好的系统集成方案,您可以轻松将其运用到 Web 应用中,可能这也是您最关心的。此外,还可以集成第三方框架,例如:Spring、Guice、CAS 等。

想必您已经了解了 Shiro 的主要功能,那么如何才能真正占有她呢?不妨先主动跟她打声招呼吧:Hello Shiro!

Hello Shiro

如果您与我一样,也使用 Maven 的话,可以将以下配置复制到您的 pom.xml 文件中:

?
1
2
3
4
5
6
7
8
9
10
11
12
<!-- SLFJ -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.6.4</version>
</dependency>
<!-- Shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.2.3</version>
</dependency>

需要说明的是,Shiro 依赖于 SLFJ 日志框架,而 SLFJ 只是一个接口,并没有提供具体的实现,您可以选择 Log4J 作为它的实现,正好 SLFJ 也提供了一个 slf4j-log4j12 的 Artifact,所以这就用上了。

下面是 hello 项目的 Maven 依赖图:

既然使用了 Log4J,那么就应该在 classpath 下提供一个 log4j.properties 文件:

?
1
2
3
4
5
log4j.rootLogger = INFO, console
 
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %-5p %c(%L) - %m%n

通过上面的配置将日志输出到控制台上,并配置了日志输出格式。

同样,既然使用了 Shiro,那么就应该在 classpath 下提供一个 shiro.ini 文件:

?
1
2
[users]
shiro = 201314

很简单的配置,我们配置了一个用户名为 shiro,密码为“爱你一生一世”的用户。当然,这里仅为演示,在您的实际项目中肯定不会把用户信息定义在配置文件中,除非这个项目的用户只有您自己。

我们就用这个用户来见识一下 Shiro 的认证服务功能吧,不妨写一个 main 方法试试:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class HelloShiro {
 
    private static final Logger logger = LoggerFactory.getLogger(HelloShiro.class);
 
    public static void main(String[] args) {
        // 初始化 SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
 
        // 获取当前用户
        Subject subject = SecurityUtils.getSubject();
 
        // 登录
        UsernamePasswordToken token = new UsernamePasswordToken("shiro""201314");
        try {
            subject.login(token);
        catch (AuthenticationException ae) {
            logger.info("登录失败!");
            return;
        }
        logger.info("登录成功!Hello " + subject.getPrincipal());
 
        // 注销
        subject.logout();
    }
}

我们分析一下这个 HelloShiro 吧:

  1. 需要读取 classpath 下的 shiro.ini 配置文件,并通过工厂类创建 SecurityManager 对象,最终将其放入 SecurityUtils 中,供 Shiro 框架随时获取。

  2. 同样通过 SecurityUtils 类获取 Subject 对象,其实就是当前用户,只不过在 Shiro 的世界里优雅地将其称为 Subject(主体)。

  3. 首先使用一个 Username 与 Password,来创建一个 UsernamePasswordToken 对象,然后我们通过这个 Token 对象调用 Subject 对象的 login 方法,让 Shiro 进行用户身份认证。

  4. 当登录失败时,您可以使用 AuthenticationException 来捕获这个异常;当登录成功时,您可以调用 Subject 对象的 getPrincipal 方法来获取 Username,此时 Shiro 已经为您创建了一个 Session。

  5. 最后还是通过 Subject 对象的 logout 方法来注销本次 Session。

感觉还不错吧?您只需要知道以上几个 Shiro 的核心成员的基本用法,Shiro 就是您的了。

其实,Shiro 的内部调用流程也不难理解:

通过 Subject 调用 SecurityManager,通过 SecurityManager 调用 Realm。这个 Realm 感觉有点生僻,其实就是提供用户信息的数据源了,在以上例子在 shiro.ini 里配置的用户信息就是一种 Realm,在 Shiro 中叫做 IniRealm。除此以外,Shiro 还提供了其它几种 Realm:PropertiesRealm、JdbcRealm、JndiLdapRealm、ActiveDirectoryReam 等,当然也可以定制 Realm 来满足您的业务需求。

不难发现,SecurityManager 是才是 Shiro 的真正的核心,您只需通过 Subject 就可以操作 SecurityManager,尤其是在 Web 应用中,您甚至都可以忘记 SecurityManager 的存在。

那么,在 Web 中应该如何使用 Shiro 呢?我们继续吧!

在 Web 开发中使用 Shiro

您可以直接在 Web 应用中使用 Shiro 官方提供的 Web 模块 —— shiro-web。

只需在您的 pom.xml 文件中增加如下配置:

?
1
2
3
4
5
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.2.3</version>
</dependency>

随后,在您的 web.xml 中添加一个 Listener 与一个 Filter:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
 
    <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>
    </filter-mapping>
 
</web-app>

实际上就是通过 EnvironmentLoaderListener 这个监听器来初始化 SecurityManager 的,并且通过 ShiroFilter 来完成认证与授权的。

可以使用以下数据表结构来存放您的用户及其权限相关数据,这就是传说中的 RBAC 模型:

然后通过 Shiro 的 JdbcReam 来进行认证与授权,这一切都是那么的简单,只需您在 shiro.ini 中做如下配置:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[main]
authc.loginUrl = /login
 
ds = org.apache.commons.dbcp.BasicDataSource
ds.driverClassName = com.mysql.jdbc.Driver
ds.url = jdbc:mysql://localhost:3306/sample
ds.username = root
ds.password = root
 
jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource = $ds
jdbcRealm.authenticationQuery = select password from user where username = ?
jdbcRealm.userRolesQuery = select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ?
jdbcRealm.permissionsQuery = select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ?
jdbcRealm.permissionsLookupEnabled = true
securityManager.realms = $jdbcRealm
 
[urls]
/ = anon
/space/** = authc

对以上配置解释如下:

首先,在 [main] 片段中,我们定义了一个“authc.loginUrl=/login”,用于配置当需要认证时需要跳转的 URL 地址,这里表示重定向到 /login 请求,通过 Servlet 映射后可定位到 login 页面。

然后,定义了一个 DBCP 的 DataSource,用于获取 JDBC 数据库连接。

然后,定义 JdbcRealm 并指定 DataSource,通过配置以下几条 SQL 来完成认证与授权:

  • authenticationQuery:该 SQL 语句用于提供身份认证,即通过 username 查询 password。

  • userRolesQuery:该 SQL 语句用于提供基于角色的授权验证(属于粗粒度级别),即通过 username 查询 role_name。

  • permissionQuery:该 SQL 语句用于提供基于权限的授权验证(属于细粒度级别),即通过 role_name 查询 permission_name,此时需要开启 permissionsLookupEnabled 开关,默认是关闭的。

最后,在 [urls] 片段中,我们定义了一系列的 URL 过滤规则,Shiro 已经提供了一些默认的 Filter(过滤器),便于我们随时使用,当然您也可以扩展其它的过滤器。就本例配置而言,解释如下:

  • / = anon:对于“/”请求(首页)可以匿名访问的。

  • /space/** = authc:对于以“/space/”开头的请求,均由 authc 过滤器处理,也就是完成身份认证操作。

还需要补充说明的是,在 permission 表中存放了所有的权限名,实际上是一个权限字符串,推荐使用“资源:操作”这种格式来命名,例如:product:view(查看产品权限)、product:edit(产品编辑权限)、product:delete(产品删除权限)等。

这些默认的过滤器包括:

过滤器名称功能配置项(及其默认值)anon
确保未登录(匿名)的用户发送的请求才能通过
authc
确保已认证的用户发送的请求才能通过(若未认证,则跳转到登录页面)
authc.loginUrl = /login.jsp
authc.successUrl = /
authc.usernameParam = username
authc.passwordParam = password
authc.rememberMeParam = rememberMe
authc.failureKeyAttribute = shiroLoginFailure
authcBasic
提供 Basic HTTP 认证功能(在浏览器中弹出一个登录对话框)
authcBasic.applicationName = application
logout
接收结束会话的请求logout.redirectUrl = /
noSessionCreation
提供 No Session 解决方案(若有 Session 就会报错)

perms
确保拥有特定权限的用户发送的请求才能通过
port
确保特定端口的请求才能通过
port = 80
rest
提供 REST 解决方案(根据 REST URL 计算权限字符串)
roles
确保拥有特定角色的用户发送的请求才能通过

ssl
确保只有 HTTPS 的请求才能通过
user
确保已登录的用户发送的请求才能通过(包括:已认证或已记住)

可以在 index.jsp(首页)中这样来判断该用户是否已游客还是已登录的用户:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<html>
<head>
    <title>首页</title>
</head>
<body>
 
<h1>首页</h1>
 
<shiro:guest>
    <p>身份:游客</p>
    <a href="<c:url value="/login"/>">登录</a>
    <a href="<c:url value="/register"/>">注册</a>
</shiro:guest>
 
<shiro:user>
    <p>身份:<shiro:principal/></p>
    <a href="<c:url value="/space"/>">空间</a>
    <a href="<c:url value="/logout"/>">退出</a>
</shiro:user>
 
</body>
</html>

需要使用 Shiro 提供的 JSP 标签:

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

随后就可以使用 shiro 标签的相关功能了,它们包括:

标签功能<shiro:guest>...</shiro:guest>判断当前用户是否为游客<shiro:user>...</shiro:user>判断当前用户是否已登录(包括:已认证或已记住)<shiro:authenticated>...</shiro:authenticated>判断当前用户是否已认证通过(不包括已记住)<shiro:notAuthenticated>...</shiro:notAuthenticated>判断当前用户是否未认证通过<shiro:principal />获取当前用户的相关信息,例如:用户名<shiro:hasRole name="foo">...</shiro:hasRole>判断当前用户是否具有某种角色<shiro:lacksRole name="foo">...</shiro:lacksRole>判读当前用户是否缺少某种角色<shiro:hasAnyRoles name="foo, bar">...< /shiro:hasAnyRoles>判断当前用户是否具有任意一种角色(foo 或 bar)
<shiro:hasPermission name="foo">...</shiro:hasPermission>判断当前用户是否具有某种权限<shiro:lacksPermission name="foo">...</shiro:lacksPermission>判断当前用户是否缺少某种权限

如果 Shiro 再提供如下几个 JSP 标签那就完美了:

标签功能<shiro:hasAllRoles name="foo, bar">...< /shiro:hasAllRoles>
判断当前用户是否具同时具有每种角色(foo 与 bar)
<shiro:hasAnyPermission name="foo, bar">...</shiro:hasAnyPermission>
判断当前用户是否具有任意一种权限(foo 或 bar)
<shiro:hasAllPermission name="foo, bar">...</shiro:hasAllPermission>
判断当前用户是否具同时具有每种权限(foo 与 bar)

实现以上这些标签并不是一件困难的事情,Shiro 最迷人的地方就是扩展了,有时候我们需要看源码并“依葫芦画瓢”的,相信这一定是一件非常有趣的事情。

除了 JSP 标签以外,Shiro 还为您提供了 Java 注解,只需将这些注解定义在您想要安全控制的方法上即可。

注解功能RequiresGuest
确保被标注的方法可被匿名用户访问RequiresUser
确保被标注的方法只能被已登录的用户访问(包括:已认证或已记住)RequiresAuthentication
确保被标注的方法只能被已认证的用户访问(不包括已记住)
RequiresRoles
确保被标注的方法仅被指定角色的用户访问
RequiresPermissions
确保被标注的方法仅被指定权限的用户访问

注意,当您使用了 RequiresRoles 与 RequiresPermissions 注解,也就意味着您把代码写死了,这样如果数据库里的 Role 或 Permission 更改了,您的代码也就无效了,这或许是 Shiro 的一点点不完美的地方吧,不过瑕不掩瑜了。

每次认证与授权都需要与数据库打交道,这会对性能产生一定的开销,关于这一点 Shiro 也为我们想到了,您只需在 [main] 片段中增加如下配置即可:

?
1
2
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager

此时 Shiro 就会在内存使用一个 Map 来缓存您的查询结果,从而减少了数据库的操作次数,提高了查询方面的性能。Shiro 也提供了 EhCache 的扩展,为缓存提供了更加高大上的解决方案。

目前在数据库里保存的是明文的密码,这样不太安全,如何将其加密呢?Shiro 同样提供了非常优雅的解决方案,您只需在 [main] 片段下增加如下配置即可:

?
1
2
passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
jdbcRealm.credentialsMatcher = $passwordMatcher

其实,Shiro 对密码的加密与解密提供了非常强大的支持,这里仅仅是一种最简单的情况。需要确保在创建密码的时候使用对应的加密算法,Shiro 给我们提供了 PasswordService 接口,您可以这样来使用:

?
1
2
PasswordService passwordService = new DefaultPasswordService();
String encryptedPassword = passwordService.encryptPassword(plaintextPassword);

只需将这个 encryptedPassword 存入数据库即可。

其实关于 Shiro 的那点事儿还有很多,不可能通过一篇文章就能完全覆盖,后续我会继续与大家分享,包括:Shiro 架构分析,在 Shiro 中使用 EhCache,将 Shiro 与 Spring 集成,将 Shiro 与 CAS 集成,将 Shiro 与 Smart 集成,等等。

本文示例代码:http://git.oschina.net/huangyong/shiro_demo

欢迎阅读《Shiro 源码分析》:http://my.oschina.net/huangyong/blog/209339

1 0
原创粉丝点击