「安全认证」Shiro安全框架入门

1. 初识 Shiro

Apache Shiro是一个强大易用的 Java 安全框架,提供了认证、授权、加密、会话管理、与 Web 集成、缓存等。

  • 具体来说,满足对如下元素的支持:

    • 用户,角色,权限(仅仅是操作权限,数据权限必须与业务需求紧密结合),资源(url)。
    • 用户分配角色,角色定义权限。
    • 访问授权时支持角色或者权限,并且支持多级的权限定义。
  • Shiro 作为一个完善的权限框架,可以应用在多种需要进行身份认证和访问授权的场景,例如:独立应用web应用spring框架中集成等。

2. Shiro 整体架构

在 shiro 架构中,有 3 个最主要的组件:SubjectSecurityManagerRealm

  1. Subject(如图上层部分):”操作用户(主体)”,本质上就是当前访问用户的抽象描述。
  2. SecurityManager(如图中层部分):是 Shiro 架构中最核心的组件(控制器),通过它可以协调其他组件完成用户认证和授权。
    • Authenticator:认证器,协调一个或者多个 Realm,从 Realm 指定的数据源取得数据之后进行执行具体的认证。
    • Authorizer:授权器,用户访问控制授权,决定用户是否拥有执行指定操作的权限。
    • Session Manager:Session 管理器,Shiro 自己实现了一套 Session 管理机制。
    • Session DAO:实现了 Session 的操作,主要有增删改查。
    • CacheManager:缓存管理器,缓存角色数据和权限数据等。
    • Pluggable Realms:数据库与数据源之间的一个桥梁。Shiro 获取认证信息、权限数据、角色数据 通过 Realms 来获取。
    • Cryptography:是用来做加解密,能非常快捷的实现数据加密。
  3. Realm(如图下层部分):定义了访问数据的方式,用来连接不同的数据源,如:LDAP,关系数据库,配置文件等等。

3. Shiro 认证与授权

3.1 Shiro 认证

「创建SecurityManager」>「主体提交请求」>「SecurityManager调用Authenticator去认证」>「Realm验证」

  • 操作用户(主体)提交请求到 Security Manager 调用 Authenticator 去认证,Authenticator 通过 Pluggable Realms 去获取认证信息,Pluggable Realms 是从下面的数据源(数据库)中去获取的认证信息,然后用通过 Pluggable Realms 从数据库中获取的认证信息和主体提交过来的认证数据做比对。
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
/**
* Shiro认证 测试
*/
public class AuthenticationTest {
// 构建一个简单的数据源
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before
public void addUser(){
// 参数分别为:用户名,密码,权限...
simpleAccountRealm.addAccount("chaooo", "123456", "admin","user");
}
/**
* 认证测试方法
*/
@Test
public void testAuthentication(){
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
// 2. 主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
// 3. 调用Subject.login(token)方法开始用户认证流程
UsernamePasswordToken token = new UsernamePasswordToken("chaooo", "123456");
try {
  subject.login(token);
} catch (UnknownAccountException e) {
  logger.error(String.format("用户不存在: %s", username), e);
} catch (IncorrectCredentialsException e) {
  logger.error(String.format("密码不正确: %s", username), e);
} catch (ConcurrentAccessException e) {
  logger.error(String.format("用户重复登录: %s", username), e);
} catch (AccountException e) {
  logger.error(String.format("其他账户异常: %s", username), e);
}
}
}

3.2 Shiro 授权

shiro 访问授权有 3 种实现方式:**api调用java注解jsp标签**。

  1. 通过 api 调用实现:「创建SecurityManager」>「主体授权」>「SecurityManager调用Authorizer授权」>「Realm获取角色权限数据」
    • 大体上和认证操作一样,也是通过 Pluggable Realms 从下面的数据源(数据库)中去获取权限数据,角色数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在执行访问授权验证之前,必须执行用户认证
// 角色验证
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
  //用户属于角色admin
}else{
  //用户不属于角色admin
}
// subject.checkRoles("admin","user");同时check多个角色
// 权限验证
String perm = "log:manage:*";
if(subject.isPermitted(perm)) {
  logger.info(String.format("用户: %s 拥有权限:%s", name, perm));
}else {
  logger.error(String.format("用户:%s 没有权限:%s", name, perm));
}
  1. 在 spring 框架中可以通过 java 注解
1
2
3
4
5
@RequiresPermissions(value={"log:manage:*"})
public ModelAndView home(HttpServletRequest req) {
  ModelAndView mv = new ModelAndView("home");
  return mv;
}
  1. 在 JSP 页面中还可以直接使用 jsp 标签
1
2
3
4
<!-- 使用shiro标签 -->
<shiro:hasPermission name="log:manage:*">
  <a href="<%=request.getContextPath()%>/user/home">操作日志审计</a><br/>
</shiro:hasPermission>

3.3 Quickstart

  1. 新建一个Maven项目,pom导入jar包:shiro-allslf4j-apislf4j-log4j12log4j;
  2. classpath下新建shiro.ini配置文件:
shiro.ini
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
# -----------------------------------------------------------------------------
# Users and their assigned roles
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
# -----------------------------------------------------------------------------
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'chaooo' with password '123456' and roles 'user' and 'guest'
chaooo = 123456, user, guest

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
user = user:*
# The 'goodguy' role is allowed to 'query' (action) the user (type) with license plate 'zhangsan' (instance specific id)
guest = user:query:zhangsan
  1. 启动运行 Quickstart
Quickstart.java
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class Quickstart {

private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

public static void main(String[] args) {
// 构建SecurityManager环境
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(securityManager);

// get the currently executing user:
// 获取当前的 Subject
Subject currentUser = SecurityUtils.getSubject();

// Do some stuff with a Session (no need for a web or EJB container!!!)
// 测试使用 shiro的Session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("---> Retrieved the correct value! [" + value + "]");
}

// let's login the current user so we can check against roles and permissions:
// 测试当前的用户是否已经被认证. 即是否已经登录.
// 调动 Subject 的 isAuthenticated()
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken("chaooo", "123456");
// rememberme
token.setRememberMe(true);
try {
// 执行登录.
currentUser.login(token);
}
// 若没有指定的账户, 则 shiro 将会抛出 UnknownAccountException 异常.
catch (UnknownAccountException uae) {
log.info("----> There is no user with username of " + token.getPrincipal());
return;
}
// 若账户存在, 但密码不匹配, 则 shiro 会抛出 IncorrectCredentialsException 异常。
catch (IncorrectCredentialsException ice) {
log.info("----> Password for account " + token.getPrincipal() + " was incorrect!");
return;
}
// 用户被锁定的异常 LockedAccountException
catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
// 所有认证时异常的父类.
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}

//say who they are:
//print their identifying principal (in this case, a username):
log.info("----> User [" + currentUser.getPrincipal() + "] logged in successfully.");

//test a role:
// 测试是否有某一个角色. 调用 Subject 的 hasRole 方法.
if (currentUser.hasRole("admin")) {
log.info("----> May the Admin be with you!");
} else {
log.info("----> Hello, mere mortal.");
return;
}

//test a typed permission (not instance-level)
// 测试用户是否具备某一个行为. 调用 Subject 的 isPermitted() 方法。
if (currentUser.isPermitted("user:query, edit")) {
log.info("----> You are permitted to 'query' and 'edit' 'user'");
} else {
log.info("Sorry, you don't have permission");
}

//a (very powerful) Instance Level permission:
// 测试用户是否具备某一个行为. 资源标识符:操作:对象实例ID
if (currentUser.isPermitted("user:query:zhangsan")) {
log.info("----> You are permitted to 'delete' 'user' 'zhangsan'");
} else {
log.info("Sorry, you don't have permission!");
}

//all done - log out!
// 执行登出. 调用 Subject 的 Logout() 方法.
System.out.println("---->" + currentUser.isAuthenticated());
currentUser.logout();
System.out.println("---->" + currentUser.isAuthenticated());
System.exit(0);
}
}

4. 在 SpringMVC 框架中集成 Shiro

4.1 配置 Maven 依赖

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
<!-- shiro配置 -->
<dependency>
  <groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${version.shiro}</version>
</dependency>
<!-- Enables support for web-based applications. -->
<dependency>
  <groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${version.shiro}</version>
</dependency>
<!-- Enables AspectJ support for Shiro AOP and Annotations. -->
<dependency>
  <groupId>org.apache.shiro</groupId>
<artifactId>shiro-aspectj</artifactId>
<version>${version.shiro}</version>
</dependency>
<!-- Enables Ehcache-based famework caching. -->
<dependency>
  <groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${version.shiro}</version>
</dependency>
<!-- Enables Spring Framework integration. -->
<dependency>
  <groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${version.shiro}</version>
</dependency>
  • Shiro使用了日志框架slf4j,因此需要对应配置指定的日志实现组件,如:log4jlogback等。
    • 在此,以使用log4j为日志实现为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--
shiro使用slf4j作为日志框架,所以必需配置slf4j。
同时,使用log4j作为底层的日志实现框架。
-->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.25</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.7.25</version>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

4.2 集成 Shiro

Spring框架中集成Shiro,本质上是与Spring IoC容器和Spring MVC框架集成。

4.2.1 ShiroSpring IoC容器集成
  • Spring IoC容器提供了一个非常重要的功能,就是依赖注入,将Bean的定义以及Bean之间关系的耦合通过容器来处理。
  • 也就是说,在Spring中集成Shiro时,Shiro中的相应Bean的定义以及他们的关系也需要通过Spring IoC容器实现。
  • Shiro提供了与Web集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的URL,然后进行相应的控制。
  • ShiroFilter类是安全控制的入口点,其负责读取配置(如ini配置文件),然后判断URL 是否需要登录/权限等工作。
    • [urls] 部分的配置,其格式是:url = 拦截器[参数], 拦截器[参数]
  • shiro中默认的过滤器:
默认拦截器名 拦截器类与说明(括号里的表示默认值)
身份验证相关
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
基于表单的拦截器;如”/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure);
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
Basic HTTP 身份验证拦截器,主要属性:applicationName:弹出登录框显示的信息(application);
logout org.apache.shiro.web.filter.authc.LogoutFilter
退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例”/logout=logout”
user org.apache.shiro.web.filter.authc.UserFilter
用户拦截器,用户已经身份验证/记住我登录的都可;示例”/**=user”
anon org.apache.shiro.web.filter.authc.AnonymousFilter
匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例”/static/**=anon”
授权相关
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
角色授权拦截器,验证用户是否拥有所有角色;主要属性:loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例”/admin/**=roles[admin]”
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例”/user/**=perms[“user:create”]”
port org.apache.shiro.web.filter.authz.PortFilter
端口拦截器,主要属性:port(80):可以通过的端口;示例”/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径/参数等都一样
rest org.apache.shiro.web.filter.authz.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 org.apache.shiro.web.filter.authz.SslFilter
SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样;
其他
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
不创建会话拦截器,调用 subject.getSession(false)不会有什么问题,但是如果 subject.getSession(true)将抛出 DisabledSessionException 异常;
  • URL匹配模式:url 模式使用 Ant 风格模式
    • Ant 路径通配符支持?***,注意通配符匹配不包括目录分隔符“/”:
    • ?:匹配一个字符,如/admin? 将匹配/admin1,但不匹配/admin 或/admin/;
    • *:匹配零个或多个字符串,如/admin 将匹配/admin、/admin123,但不匹配/admin/1;
    • **:匹配路径中的零个或多个路径,如/admin/** 将匹配/admin/a 或/admin/a/b
  • URL匹配顺序:URL 权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的 url 模式对应的拦截器链。如:
    • /bb/**=filter1
    • /bb/aa=filter2
    • /**=filter3
    • 如果请求的 url 是“/bb/aa”,因为按照声明顺序进行匹配,那么将使用 filter1 进行拦截,所以通配符一般写在靠后。
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
38
39
40
41
42
43
44
45
46
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/index"/>
<property name="successUrl" value="/home"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
<!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean -->
<!-- defined will be automatically acquired and available via its beanName in chain -->
<!-- definitions, but you can perform instance overrides or name aliases here if you like: -->
<!-- <property name="filters">
<util:map>
<entry key="logout" value-ref="logoutFilter" />
</util:map>
</property> -->
<property name="filterChainDefinitions">
<value>
# some example chain definitions:
# /admin/** = authc, roles[admin]
# /docs/** = authc, perms[document:read]
/login = anon
/logout = anon
/error = anon
/** = user
# more URL-to-FilterChain definitions here
</value>
</property>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- Single realm app. If you have multiple realms, use the 'realms' property instead. -->
<property name="realm" ref="myRealm" />
<!-- By default the servlet container sessions will be used. Uncomment this line
to use shiro's native sessions (see the JavaDoc for more): -->
<!-- <property name="sessionMode" value="native"/> -->
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- Define the Shiro Realm implementation you want to use to connect to your back-end -->
<!-- security datasource: -->
<bean id="myRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
<property name="dataSource" ref="dataSource"/>
<property name="permissionsLookupEnabled" value="true"/>
</bean>
<!-- Enable Shiro Annotations for Spring-configured beans. Only run after -->
<!-- the lifecycleBeanProcessor has run: -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
4.2.2 与Spring MVC集成
  • 跟在普通Java Web应用中使用Shiro一样,集成ShiroSpring MVC时,实际上就是通过在web.xml中添加指定Filter实现。配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- The filter-name matches name of a 'shiroFilter' bean inside applicationContext.xml -->
<!-- DelegatingFilterProxy作用是自动到Spring 容器查找名字为shiroFilter(filter-name)的bean并把所有Filter 的操作委托给它。 -->
<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>
<!-- Make sure any request you want accessible to Shiro is filtered. /* catches all -->
<!-- requests. Usually this filter mapping is defined first (before all others) to -->
<!-- ensure that Shiro works in subsequent filters in the filter chain: -->
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Spring中集成Shiro的原理就是:通过在web.xml中配置的Shiro FilterSpring IoC中定义的相应的Shiro Bean定义建立关系,从而实现在Spring框架集成Shiro

4.3 数据源配置

Shiro中,Realm定义了访问数据的方式,用来连接不同的数据源,如:LDAP,关系数据库,配置文件等。

  • org.apache.shiro.realm.jdbc.JdbcRealm为例,将用户信息存放在关系型数据库中。
  • 在使用JdbcRealm时,必须要在关系型数据库中存在 3 张表,分别是
    • users表,存放认证用户基本信息,在该表中必须存在 2 个字段:usernamepassword
    • roles_permissions表,存放角色和权限定义,在该表中必须存在 2 个字段:role_namepermission
    • user_roles表,存放用户角色对应关系,在该表中必须存在 2 个字段:usernamerole_name
  • 实际上,在更加复杂的应用场景下,通常需要扩展JdbcRealm

4.4 认证

Shiro中,认证即执行用户登录,读取指定Realm连接的数据源,以验证用户身份的有效性与合法性。

  • 在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:
    • principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名/邮箱/手机号。
    • credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
    • 最常见的 principals 和 credentials 组合就是用户名/密码了
  • 身份认证流程:
    1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
    2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
    3. Authenticator 才是真正的身份验证者,ShiroAPI 中核心的身份认证入口点,此处可以自定义插入自己的实现;
    4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
    5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
      • Realm:一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Subject subject = SecurityUtils.getSubject();
if(!subject.isAuthenticated()) {
  UsernamePasswordToken token = new UsernamePasswordToken(name, password);
try {
  subject.login(token);
} catch (UnknownAccountException e) {
  logger.error(String.format("用户不存在: %s", token.getPrincipal()), e);
} catch (IncorrectCredentialsException e) {
  logger.error(String.format("密码不正确: %s", token.getPrincipal()), e);
} catch (ConcurrentAccessException e) {
  logger.error(String.format("用户重复登录: %s", token.getPrincipal()), e);
} catch (AccountException e) {
  logger.error(String.format("其他账户异常: %s", token.getPrincipal()), e);
}
}

4.5 授权

Shiro 作为权限框架,仅仅只能控制对资源的操作权限,并不能完成对数据权限的业务需求。

  • 而对于 Java Web 环境下 Shiro 授权,包含两个方面的含义。
    • 其一,对于前端来说,用户只能看到他对应访问权限的元素。
    • 其二,当用户执行指定操作(即:访问某个 uri 资源)时,需要验证用户是否具备对应权限。
  • 对于第一点,在 Java Web 环境下,通过 Shiro 提供的 JSP 标签实现。
  • 对于第二点,与在非 Java Web 环境下一样,需要在后端调用 API 进行权限(或者角色)检验。
  • 在 Spring 框架中集成 Shiro,还可以直接通过 Java 注解方式实现
  • Permissions
    • 规则:资源标识符:操作:对象实例ID,即对哪个资源的哪个实例可以进行什么操作.其默认支持通配符权限字符串,: 表示资源/操作/实例的分割;, 表示操作的分割,* 表示任意资源/操作/实例。如:user:edit:manager
      • 也可以使用通配符来定义,如:user:edit:*user:*:*user:*:manager
      • 部分省略通配符:缺少的部件意味着用户可以访问所有与之匹配的值,比如:user:edit等价于user:edit:*user等价于user:*:*
      • 注意:通配符只能从字符串的结尾处省略部件,也就是说user:edit不等价user:*:edit
  • 授权流程:
    1. 首先调用 Subject.isPermitted*/hasRole* 接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
    2. Authorizer 是真正的授权者,如果调用如 isPermitted(“user:view”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
    3. 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
    4. Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。
      • ModularRealmAuthorizer进行多 Realm 匹配流程:
        1. 首先检查相应的 Realm 是否实现了实现了 Authorizer;
        2. 如果实现了 Authorizer,那么接着调用其相应的isPermitted*/hasRole*接口进行匹配;
        3. 如果有一个 Realm 匹配那么将返回 true,否则返回 false。
4.5.1 Shiro 标签
  1. <shiro:guest></shiro:guest>:用户没有身份验证时显示相应信息,即游客访问信息
  2. <shiro:user></shiro:user>:用户已经经过认证/记住我登录后显示相应的信息。
  3. <shiro:authenticated></shiro:authenticated>:用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的
  4. <shiro:notAuthenticated></shiro:notAuthenticated>标签:用户未进行身份验证,即没有调用 Subject.login 进行登录,包括记住我自动登录的也属于未进行身份验证。
  5. <shiro:pincipal></shiro:pincipal>显示用户身份信息,默认调用Subject.getPrincipal()获取,即 Primary Principal。
  6. **<shiro:hasRole></shiro:hasRole>**标签:如果当前 Subject 有角色将显示 body 体内容
  7. <shiro:hasAnyRoles></shiro:hasAnyRoles>标签:如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容
  8. <shiro:lacksRole></shiro:lacksRole>:如果当前 Subject 没有角色将显示 body 体内容
  9. **<shiro:hasPermission></shiro:hasPermission>**:如果当前 Subject 有权限将显示 body 体内容
  10. <shiro:lacksPermission></shiro:lacksPermission>:如果当前 Subject 没有权限将显示 body 体内容
1
2
3
4
5
6
7
8
9
<!-- 在jsp页面中引入shiro标签库 -->
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!-- 权限控制 -->
<shiro:hasRole name="admin">
  <a>用户管理</a>
</shiro:hasRole>
<shiro:hasPermission name="winnebago:drive:eagle5">
  <a>操作审计</a>
</shiro:hasPermission>
4.5.2 调用 API 进行权限(或者角色)检验
1
2
3
4
5
String roleAdmin = "admin";
Subject currentUser = SecurityUtils.getSubject();
if(!currentUser.hasRole(roleAdmin)) {
  //todo something
}
4.5.3 Shiro 权限注解
  • @RequiresAuthentication:表示当前 Subject 已经通过 login 进行了身份验证;即 Subject. isAuthenticated() 返回 true
  • @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的。
  • @RequiresGuest:表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。
  • @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND):表示当前 Subject 需要角色 admin 和 user
  • @RequiresPermissions(value={“user:a”, “user:b”}, logical= Logical.OR):表示当前 Subject 需要权限 user:a 或 user:b。
  • 通过自定义拦截器可以扩展功能,例如:动态 url-角色/权限访问控制的实现、根据 Subject 身份信息获取用户信息绑定到 Request(即设置通用数据)、验证码验证、在线用户信息的保存等
1
2
3
4
5
6
7
8
9
@Controller
public class HomeController {
  @RequestMapping("/home")
  @RequiresPermissions(value={"log:manage:*"})
  public ModelAndView home(HttpServletRequest req) {
    ModelAndView mv = new ModelAndView("home");
    return mv;
  }
}

4.6 Spring 集成 Shiro 注意事项

  1. Spring 4.2.0 RELEASE+Spring 4.1.9 RELEASE**-**版本,配置方式有所不同。
  2. 虽然shiro的注解定义是在Class级别的,但是实际验证只能支持方法级别:@RequiresAuthentication@RequiresPermissions@RequiresRoles

5. Shiro 会话管理

Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。

5.1 会话相关的 API

  • Subject.getSession():即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;Subject.getSession(false),如果当前没有创建 Session 则返回 null
  • session.getId():获取当前会话的唯一标识
  • session.getHost():获取当前 Subject 的主机地址
  • session.getTimeout() & session.setTimeout(毫秒):获取/设置当前 Session 的过期时间
  • session.getStartTimestamp() & session.getLastAccessTime():获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。
  • session.touch() & session.stop():更新会话最后访问时间及销毁会话;当 Subject.logout()时会自动调用 stop 方法来销毁会话。如果在 web 中,调用 HttpSession. invalidate()也会自动调用 Shiro Session.stop 方法进行销毁 Shiro 的会话
  • session.setAttribute(key, val) & session.getAttribute(key) & session.removeAttribute(key):设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作

5.2 会话监听器

会话监听器(SessionListiner):会话监听器用于监听会话创建、过期及停止事件

5.3 SessionDao

  • AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话 ID 等
  • CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager
  • MemorySessionDAO 直接在内存中进行会话维护
  • EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。

5.4 数据表

1
2
3
4
5
create table sessions (
id varchar(200),
session varchar(2000),
constraint pk_sessions primary key(id)
) charset=utf8 ENGINE=InnoDB;

5.5 会话验证

  • Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话
  • 出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler
  • Shiro 也提供了使用 Quartz 会话验证调度器:QuartzSessionValidationScheduler

6. Shiro 缓存

  • CacheManagerAware 接口
    • Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如 Realm)是否实现了 CacheManagerAware 并自动注入相应的 CacheManager。
  • Realm 缓存 + Shiro 提供了 CachingRealm,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现; + AuthenticatingRealm 及 AuthorizingRealm 也分别提供了对 AuthenticationInfo 和 AuthorizationInfo 信息的缓
    存。
  • Session 缓存
    • 如 SecurityManager 实现了 SessionSecurityManager,其会判断 SessionManager 是否实现了 acheManagerAware 接口,如果实现了会把 CacheManager 设置给它。
    • SessionManager 也会判断相应的 SessionDAO(如继承自 CachingSessionDAO)是否实现了 CacheManagerAware,如果实现了会把 CacheManager 设置给它
    • 设置了缓存的 SessionManager,查询时会先查缓存,如果找不到才查数据库。
  • RememberMe
    • Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
      1. 首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把 RememberMe 的 Cookie 写到客户端并保存下来;
      2. 关闭浏览器再重新打开;会发现浏览器还是记住你的;
      3. 访问一般的网页服务器端还是知道你是谁,且能正常访问;
      4. 但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。
  • 认证和记住我
    • subject.isAuthenticated() 表示用户进行了身份验证登录的,即使有 Subject.login 进行了登录;
    • subject.isRemembered():表示用户是通过记住我登录的,此时可能并不是真正的你(如你的朋友使用你的电脑,或者你的 cookie 被窃取)在访问的
    • 两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样。
  • 建议
    • 访问一般网页:如个人在主页之类的,我们使用 user 拦截器即可,user 拦截器只要用户登录(isRemembered() || isAuthenticated())过即可访问成功;
    • 访问特殊网页:如我的订单,提交订单页面,我们使用 authc 拦截器即可,authc 拦截器会判断用户是否是通过 Subject.login(isAuthenticated()==true)登录的,如果是才放行,否则会跳转到登录页面叫你重新登录。
  • 实现
    • 如果要自己做 RememeberMe,需要在登录之前这样创建 Token:UsernamePasswordToken(用户名,密码,是否记住我),且调用 UsernamePasswordToken 的:token.setRememberMe(true); 方法

参考文章: