自定义 Realm
『认证』和『授权』是 Shiro 要进行的两大操作:
- 认证,是判断用户的身份是否合法。
- 类比于,判断一个人是否是本公司员工,他能否进公司办公大楼。
- 授权,是判断用户所能做哪些操作。
- 类比于,判断一个人<small>(本公司员工)</small>的身份,他能进办公大楼的哪些层,哪些个办公区。
在 Shiro 的使用过程中,Shiro 需要从某处获得『标准答案』,以便于来验证当前用户的身份和权限。Shiro 获取标准答案的来源就是 Realm 。
以 IniRealm 为例,『标准答案』在 .ini
文件中,Realm 就是去获取这个『标准答案』的途径,Shiro 就是通过 IniRealm 从 .ini 文件中获取有关用户认证和授权的『标准答案』。
我们的应用程序中要做的就是自定义一个 Realm 类,继承 AuthorizingRealm
抽象类,并重写其中两个必要方法:
方法 | 说明 |
---|---|
doGetAuthenticationInfo() |
认证用户,判断用户身份的合法性 |
doGetAuthorizationInfo() |
授权用户,判断用户的使用权限 |
-
Shiro 进行『认证校验』时,以 Realm 的
doGetAuthenticationInfo()
方法的返回值作为『标准答案』,以校验当前用户是否是合法用户。 -
Shiro 进行『授权校验』时,以 Realm 的
doGetAuthorizationInfo()
方法的返回值作为『标准答案』,以校验当前用户是否有权限执行当前操作。
doGetAuthenticationInfo 方法
什么时候会触发 doGetAuthenticationInfo 方法
当你直接<small>(或间接)</small>调用 subject.login(token)
方法时,Shiro 的 Security Manager 就回去调用 doGetAuthenticationInfo 方法。
<el-divider></el-divider>
doGetAuthenticationInfo()
方法的功能是:提供用于用户登录认证用到的相关信息的『标准答案』。
Shiro<small>(的 Sercurity Manager)</small>在从 Realm 的 doGetAuthenticationInfo 获得『标准答案』后,在进行比对、判断工作<small>(这就无需我们程序员参与了)。</small>
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
if (username == null) {
throw new UnknownAccountException(); // 没找到帐号
}
// 这里的密码“123”应该是从数据库中查询出来的结果
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username, "123", getName());
return authenticationInfo;
}
- 判断用户传入的用户名不为null
- 判断用户传入的用户名确实是存在的
- 查询用户名『配套』的密码
- 将用这套户名-密码组成一个『标准答案』返回给Shiro,让 Shiro 去和用户实际输入的密码匹配。
doGetAuthorizationInfo 方法
什么时候会触发 doGetAuthorizationInfo 方法
当你直接<small>(或间接)</small>调用 subject.hasRole(...)
或 subject.isPermitted(...)
等方法时,Shiro 的 Security Manager 就回去调用 doGetAuthorizationInfo 方法。
<el-divider></el-divider>
同理,doGetAuthorizationInfo()
方法的功能是:提供用于用户访问 URI 时用到的相关信息的『标准答案』。
Shiro<small>(的 Sercurity Manager)</small>在从 Realm 的 doGetAuthorizationInfo 获得『标准答案』后,在进行比对、判断工作<small>(这就无需我们程序员参与了)。</small>
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Set<String> roles = new HashSet<String>();
roles.add("admin");
roles.add("user");
Set<String> permissions = new HashSet<String>();
permissions.add("insert");
permissions.add("delete");
permissions.add("update");
permissions.add("select");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
info.setStringPermissions(permissions);
return info;
/*
// 标准代码
String username = (String) principals.getPrimaryPrincipal();
username = MoreObjects.firstNonNull(username, Strings.EMPTY);
ShiroUser user = userMapper.selectByUsername(username);
// 从数据库或者缓存中获取角色数据
Set<String> roleNames= user.getRoleNameSet();
// 从数据库或者缓存中获取权限数据
Set<String> permissionNames = user.getPermissionNameSet();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(roleNames);
authorizationInfo.setStringPermissions(permissionNames);
log.info("[用户 [{}] 的角色是 [{}]", username, roleNames);
log.info("[用户 [{}] 的权限是 [{}]", username, permissionNames);
return authorizationInfo;
*/
}
在实际项目中,这些 标准答案 必然是来源于数据库中,而非在代码中写死的!
Shiro 中的加密功能
从数据安全的角度来看,不应该将用户的密码以明文的形式存储于数据库中,以免数据的泄露,从而造成用户的损失。
通常数据库中存放的是用户密码的加密形式<small>(甚至,其中还可以加盐 Salt)</small>。
在自定义 Realm 的基础上加上密码加密功能:
public class CustomRealm extends AuthorizingRealm {
/* 因演示需要,简化代码,此处并未连接真实数据库。使用 Map 模拟数据库中的数据 */
private static Map<String, String> userMap = new HashMap<>(16);
static {
// 原始密码是 123 ,通过 main 方法运算得到加密后的字符串。
userMap.put("tom", "5caf72868c94f184650f43413092e82c");
}
public CustomRealm() {
super.setName(CustomRealm.class.getName());
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken t) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) t;
String username = token.getUsername();
// 2、通过用户名到数据库中获取凭证信息
String password = getPasswordByUsername(username);
if (password == null)
return null;
// 注意,对于密码,字符串“123”和数字123是不同的密码。一定要注意。
SimpleAuthenticationInfo authenticationInfo
= new SimpleAuthenticationInfo("tom", password, CustomRealm.class.getName());
// 在返回之前,要将盐值加进来
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("tom"));
return authenticationInfo;
}
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 为演示,简化代码,授权逻辑略
return null;
}
public static void main(String[] args) {
// 第一个是原密码,第二个是加盐的值
Md5Hash md5Hash = new Md5Hash("123","tom");
System.out.println(md5Hash);
System.out.println(md5Hash.toBase64());
}
}
测试代码:
@Test
public void test() {
/* “告知” Shiro 从自定义的 Realm 中获取 『标准答案』*/
CustomRealm customRealm = new CustomRealm();
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(customRealm);
/*
* “告知” Shiro『标准答案』中的密码使用 md5 加密算法加密过。
* 让它在进行比对时,把用户传入的密码也用 md5 加密后,再进行比对。否则,是肯定不一样的。
*/
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");
// 加密操作可以反复执行。设置加密次数。
matcher.setHashIterations(1);
customRealm.setCredentialsMatcher(matcher);
/* 让 Shiro 正式开始生效/工作 */
SecurityUtils.setSecurityManager(defaultSecurityManager);
/* 模拟用户登录 */
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("tom", "123");
subject.login(token);
// 判断用户是否登录成功
logger.info("isAuthenticated: [{}]", subject.isAuthenticated());
}
使用 SimpleHash 加密
上面我们用到的 Md5Hash 是 SimpleHash 的实现类。除了 Md5Hash,SimpleHash 的实现类还有 5 个:
- Md2Hash
- Md5Hash
- Sha1Hash
- Sha256Hash
- Sha384Hash
- Sha512Hash
Shiro 和 SpringMVC 整合
-
pom.xml
<shiro.version>1.5.0</shiro.version>
<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> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency>
ShiroFilter 和 DelegatingFilterProxy
Shiro 和 Servlet 或 Spring MVC 整合的核心在于一个 Filter。<small>由于 Filter 的处理流程是链式的,所以这一个 Filter 的背后实际上是一串 Filter。</small>。
ShiroFilter
在用户的请求走到请求处理程序<small>(Controller)</small>之前,会经过这个 Filter <small>链</small>。Filter <small>链</small>来判断当前的请求是否对用户的 登陆状态
、角色
、权限
有所要求,进而再决定是否将请求放行给请求处理程序。
因此,我们在相关的配置文件中要配置出这个单例的 Filter:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
...
</bean>
DelegatingFilterProxy
Shiro 的 Filter 的配置为什么写在了 Spring 配置文件中?难道它们不应该写在
web.xml
中吗?
普通的 Filter(和 Servlet)对象是由 Servlet 容器<small>(例如 Tomcat)</small>来管理的。它们是由 Servlet 容器来负责创建、调用和销毁。当我们在 Web 项目中使用 Spring 框架时,对于 Filter 和 Servlet 这样的单例对象,很自然地我们就会想到交由 Spring IoC 容器来管理,这样就能很方便的使用依赖注入功能,在 Filter 和 Servlet 中注入 Spring Ioc 容器中的其它的 Bean 。
Spring MVC 中的『Servlet』,除了 DispatcherServlet 意外,其它的都是由 Spring IoC 容器创建并管理的?!
表面上看,Spring MVC 中有且仅有 DispacherServlet 一个 Servlet。但是发散一下思维,你很容易想到,Spring MVC 中的 Controller 其实逻辑上就扮演了原来的 Servlet 的角色。
也就是说,在 Spring MVC 中,除了 DispatcherServlet 这个『真·Servlet』是由 Tomcat 容器创建并管理的,其它的『假·Servlet』<small>(也就是 Controller)</small>都是由 Spring IoC 容器创建并管理的。
另外,DispatcherServlet 这个『真·Servlet』它是不负责处理任何具体的逻辑的。真正干活的是各个『假·Servlet』:Controller。
Filter 的情况和 Servlet 的情况类似。Spring 中的 DelegatingFilterProxy 就是在起到类似于 DispatcherServlet 的作用。
DelegatingFilterProxy 是个『真·Filter』,它和 DispatcherServlet 一样由 Tomcat 容器创建并管理。除了这个『真·Filter』之外,还可以有很多逻辑上的扮演 Filter 功能的『假.Filter』。
上一章节中的 Shiro 的 Filter 就是『假·Filter』。
对于这些逻辑上的『假』的Filter,它们是否实现 servlet-api 中的 Filter 接口,Spring 不作要求。<small>因为它们都是由 Spring IoC 容器创建并管理,而非 Tomcat 容器。</small>
这就是为什么 Shiro 的 Filter 的配置写在了 Spring 配置文件中,而非 web.xml
中。
<el-divider></el-divider>
DelegatingFilterProxy 会拦截客户端浏览器发来的请求,并『转交』给 Shiro 的 Filter 处理,根据 Shiro 的 Filter 的处理的结果来决定是回复客户端浏览器,还是放行给 Controller 。
为了能让 DelegatingFilterProxy『找得到』真正干活的 Shiro 的 Filter,web.xml
和 spring-shiro.xml
中有两处内容要『保持一致』。
-
web.xml
<filter> <!-- 注意这里的 filter-name --> <filter-name>shiroFilter</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> ... </filter> <filter-mapping> <!-- 注意这里的 filter-name --> <filter-name>shiroFilter</filter-name> ... </filter-mapping>
-
spring-shiro.xml
<!-- 注意这里的 id --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> ... </bean>
targetFilterLifecycle
<!-- 一般情况下,我们不会真的叫 xxxFitler -->
<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>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
targetFilterLifecycle 配置就是指明 DelegatingFilterProxy 所代理的 Filter 是由 Spring IoC 维护的『假·Filter』。
一个偶发的问题
如果将所有 SSM 和 Shiro 的配置都写在一个配置文件中,并且只利用 Spring MVC 的一次加载机会<small>(DispatcherServlet 加载)</small>加载配置文件,初始化 Spring IoC 容器,那么在现目启动时,会偶发性出现 No bean named 'shiroFilter' is defined
异常。
原因暂时未知。
如果利用 Spring MVC 的两次加载机会,将 SSM 和 Shiro 中与 Spring MVC 无关的配置先由 ContextLoaderListener 加载;Spring MVC 有关的配置由 DispatcherServlet 加载,则不会出现这个问题。
<?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.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- 你也可以写在一起 -->
<param-value>
classpath:spring/spring-shiro.xml,
classpath:spring/spring-dao.xml,
classpath:spring/spring-service.xml
</param-value>
</context-param>
<!-- shiro 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>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
...
</web-app>
Spring Shiro 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 如果没有使用到 Shiro 的加密功能,则不需要这个 bean -->
<!-- 另外,盐的设置不在这里设置。在 myRealm 的代码中返回盐值:
authenticationInfo
.setCredentialsSalt(
ByteSource.Util.bytes("xxx")
);
-->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="1" /> <!-- 设置加密次数 -->
</bean>
<!-- 自定义 Realm 。也可以使用 注解 + 包扫描 创建它。 -->
<bean id="myShiroRealm" class="web.shiro.CustomRealm">
<!-- 根据具体需要,有可能引用到上述的加密器。 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!-- Shiro 核心 Bean -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myShiroRealm"/>
</bean>
<!-- Shiro web 拦截器配置 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- 这些都是位于 context-path 之后的 url 路径。
不一定是 jsp 路径,也可以是 .do。
另外,/ 可省略 -->
<property name="loginUrl" value="/login.jsp" />
<property name="unauthorizedUrl" value="/403.jsp" />
<!-- 定义过滤器链,从上向下进行匹配 -->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/login.do = anon
/logout.do = logout
/** = authc
</value>
</property>
</bean>
<!-- 截止目前为止,SSM + Shiro 的整合即完成。
虽有进一步简化的空间,但至此 SSM + Shiro 功能可用。-->
</beans>
整合后的用户身份校验
为 /login.do
提供一个 Controller,并在 Congroller 中手动调用 subject.login()
再根据结果执行页面跳转:
@Slf4j
@Controller
public class LoginController {
@ExceptionHandler({Exception.class })
public String exception(Exception e) {
if (e instanceof IncorrectCredentialsException) {
log.warn("密码错误", e);
}
else {
log.warn("其它错误", e);
}
return "redirect:failure.jsp";
}
@RequestMapping("/login.do")
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);
log.info("登录成功");
return "redirect:success.jsp";
}
}
整合后的用户权限校验(通过配置文件)
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="login.jsp" />
<property name="unauthorizedUrl" value="403.jsp" />
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/login.do = anon
/logout.do = logout
/role-admin.do = roles["admin"]
/role-user.do = roles["user"]
/opr-1.do = perms["user:add"]
/opr-2.do = perms["user:delete"]
/* = authc
</value>
</property>
</bean>
启用注解简化配置
如果我们要将项目的所有涉及到认证和鉴权的 URL 都写在 .xml 配置文件中,那么整个配置文件的可读性和可维护性就很糟糕。
对此,Shiro 提供了注解,来帮助我们以另一种方式配置认证和鉴权。
提前说明,以下的配置必须和 Spring MVC 的配置放在一起,否则注解功能无效!
Shiro 通过以下的单例对象<small>(以 AOP 的方式)</small>提供对注解功能的支持:
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
理论上,仅需配置上述 Bean,Shiro 的注解功能即可用。不过,这里有个『著名』的 $Proxy40
异常。
Shiro 中大量使用了接口,而 Spring AOP 功能在实现代理类时,如果发现被代理对象实现了某个接口,Spring AOP 就会以 JDK 动态代理方案来实现代理类,而 JDK 动态代理方案在接口/继承体系比较深、复杂的情况下会出问题。
因此,我们在这里需要额外多配置两个 Bean 来要求 Spring AOP 支持、实现 Shiro 注解的时候,必须使用 CGLib 方案<small>(永不出错)</small>。
<!-- 管理 Shiro bean生命周期 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>
<el-divider></el-divider>
<dl>
<dt>@RequiresAuthentication</dt>
<dd>表示当前 Subject 已经通过 login 进行了身份验证;</dd>
<dd>即 Subject.isAuthenticated() 返回 true 。</dd>
<dd></dd>
<dt>@RequiresUser</dt>
<dd>表示当前 Subject 已经身份验证或者通过 记住我 登录的。</dd>
<dt>@RequiresGuest</dt>
<dd>表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。</dd>
<dt>@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)</dt>
<dt>@RequiresRoles(value={“admin”})</dt>
<dt>@RequiresRoles({“admin“})</dt>
<dd>表示当前Subject需要角色admin 和user。</dd>
<dt>@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)</dt>
<dd>表示当前Subject需要权限user:a或user:b。</dd>
</dl>
Shiro 默认过滤器及相关
Shiro 默认过滤器
shiro-filter过滤器 URL 匹配模式
-
url 模式使用 Ant 风格模式:
支持
?
、*
、**
(注意通配符匹配不包括目录分隔符/
) -
?
:匹配一个字符,如
/admin?
将匹配/admin1
,但不匹配/admin
或/admin/
; -
*
:匹配零个或多个字符串,如
/admin*
将匹配/admin
、/admin123
,但不匹配/admin/1
; -
**
:匹配路径中的零个或多个路径,如
/admin/**
将匹配/admin/a
或/admin/a/b
unauthorizedUrl 不跳转问题
Shiro 中的过滤器分为两大类:
-
认证相关过滤器:
包括:anon、authcBasic、authc、user 过滤器
-
授权相关过滤器:
包括:perms、roles、ssl、rest、port 过滤器
要通过授权过滤器<small>(至于是哪一个授权过滤器取决于具体的配置)</small>,就先要通过认证过滤器<small>(至于是哪一个认证过滤器取决于具体的配置)</small>。
顾名思义,unauthorizedUrl 是指请求未能通过授权相关过滤器时的跳转页面。
因此,请求未通过认证相关过滤器时的跳转页面跟 unauthorizedUrl
半毛钱关系都没有(有关的是 loginUrl
)。
RememberMe
Subject 的状态和 authc 过滤器
<small>(在没有引入 RememerMe 的功能的情况下)</small>Shiro 中的 Subject 的登录状态有两种:已登录
和 未登录
:
log.info("{}", subject.isAuthenticated()) // false
subject.login();
log.info("{}", subject.isAuthenticated()) // true
subject.logout();
log.info("{}", subject.isAuthenticated()) // false
Shiro 中的 authc
的工作原理就是:当你指定一个 URL 只有在登陆后才能访问时,Shiro 就会使用 authc
拦截你的请求。在 authc
拦截器中,Shiro 会检查你的 Subject 的登陆状态。逻辑上,当你的 Subject 的 isAuthenticated()
方法返回 true 时,authc 拦截器会放行;如果返回 false
那么 authc 拦截去会返回登录页面。
在你的代码中,你可以通过 Subject 的 login()
和 logout()
方法改变 Subject 的登陆状态。
user 过滤器
当你开启 Shiro 的 RememberMe 功能后,Shiro 在你的 Subject 的登陆状态中就会多引入一种状态:通过 RememeMe 登录
。
这样,Subject 的登录状态逻辑上就有了 3 种:
状态 | 说明 |
---|---|
isAuthenticated() == true |
通过用户名密码等方式登录 |
isRemembered() == true |
通过记住我功能登录 |
isAuthenticated() == false isRemembered() == false
|
未登录 |
user
过滤器的工作原理和 authc
过滤器的工作原理相似。只不过 user
过滤器的要求更低,更宽松。
当你使用 user
过滤器去拦截、保护一个 URL 时,user
过滤器会去判断当前的 Subject 的状态,只有 subject.isRemembered() == true
时,user
过滤器才会放行,否则 user
过滤器会返回登陆页面。
RememberMe 功能的实现原理
user
过滤器本质上就是查看当前请求是否有『附带的用户名和密码的 Cookie』。
这个 Cookie 所记录用户名和密码信息类似如下:
key: rememberMe
value: 6gYvaCGZaDXt1c0xwriXj/Uvz6g8OMT3VSaAK4WL0Fvqvkcm0nf3CfTwkWWTT4EjeSS/EoQjRfCPv4WKUXezQDvoNwVgFMtsLIeYMAfTd17ey5BrZQMxW+xU1lBSDoEM1yOy/i11ENh6eXjmYeQFv0yGbhchGdJWzk5W3MxJjv2SljlW4dkGxOSsol3mucoShzmcQ4VqiDjTcbVfZ7mxSHF/0M1JnXRphi8meDaIm9IwM4Hilgjmai+yzdVHFVDDHv/vsU/fZmjb+2tJnBiZ+jrDhl2Elt4qBDKxUKT05cDtXaUZWYQmP1bet2EqTfE8eiofa1+FO3iSTJmEocRLDLPWKSJ26bUWA8wUl/QdpH07Ymq1W0ho8EIdFhOsELxM66oMcj7a/8LVzypJXAXZdMFaNe8cBSN2dXpv4PwiktCs3J9P9vP4XrmYees5x27UmXNqYFk86xQhRjFdJsw5A9ctDKXzPYvJmWFouo3qT5hugX0uxWALCfWg8MHJnG9w7QgVKM8oy3Xy4Ut8lSvYlA==
这里的 value 的值是用户登录成功后,对代表用户身份信息的 Principal 对象的序列化后再 Base64 的结果。
这就需要我们的程序要让客户端浏览器创建含有用户名和密码的 Cookie ,这样,在客户端浏览器访问我们项目的 URL 时,请求中还会携带这个 Cookie 。否则,subject.isRemembered()
始终将会是 false
,user
过滤器就没有起到应有的作用。
当然,不需要我们亲自去『要求』客户端浏览器创建这个 Cookie ,通过 Shiro ,我们就可以间接地实现这个效果。
只需去设置 UsernamePasswordToken 的 RememberMe 属性值为 true
,表示启用该功能即可:
// 这句话的背后,Shiro 会要求客户端浏览器创建 RememberMe Cookie
token.setRememberMe(true);
currentUser.login(token);
默认情况下,该 Cookie 文件的过期时间为一年。
不过,一般情况下,不会在代码中写死启用 RememberMe 功能。一般情况下,是否启用它,取决于用户是否选中页面上的记住密码 checkbox 。
RememberMe 功能的取消
调用 Shiro 的 Subject#logout 方法, 在实现原有的退出功能<small>(改变 Subject 的状态)</small>之外,Shiro 会再去『通知』客户端浏览器删除那个名为 rememberMe 的 Cookie 文件。 <small>(直到再一次调用 shiro 的 Subject#login 方法,再次创建这个 Cookie 文件,记录登录用户的信息)</small>。
另外,如果在登陆页面上没有选中记住密码的 checkbox ,那么最终调用的就是 token.setRememberMe(false),表示不启用 RememberMe 功能,会使得 Shiro 不会去通知客户端浏览器创建名为 rememberMe 的 cookie 。
Spring MVC 中使用 Shiro 的 RememberMe 功能
<!--Shiro 核心对象-->
<bean id="securityManager" class="...">
...
<property name="rememberMeManager" ref="rememberMeManager"/>
...
</bean>
<!-- rememberMe 管理器,被 securityManager 使用/依赖 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}" />
<property name="cookie" ref="rememberMeCookie"/>
</bean>
<!-- 手动指定 cookie,被 rememberMemanager 使用/依赖-->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="maxAge" value="604800"/> <!-- 7天 -->
</bean>
看起来很美
自动登录功能有个致命的安全缺陷<small>(与 Shiro 无关)</small>就是随便谁把这个 Cookie 值拿到别的浏览器都可以登录。就算你用再厉害的加密,都无法防止表单伪造。
所以自动登录功能仍然只能使用在查看一些无关紧要的信息的功能上。
Shiro 的会话管理
会话管理的基本概念
Shiro 的『野心』很大,它的自我定位并非是一个『Web 安全框架』,而是一个『安全框架』。<small>(当然,Shiro 有点想多了,大家实际上还是只把它当做 『Web 安全框架』)</small>。
因此 Shiro 包装、托管了 Session,这样无论是在 java web 这样天然就有 Session 概念的项目中,还是在 java 这样没有 Session 概念的项目中,你都可以使用 Session 。
简单来说,原本就有 Session 的,Shiro 就利用这个 Session;原本就没有 Session 的,Shiro 就弄出一个 Session 出来给你用。
在 Shiro 中,当你执行 Subject#login() 就意味着会话的开始;当你执行 Subject#logout 就意味着会话的结束。
在会话期内,你可以使用 setAttribute() 和 getAttribute() 方法存取数据。
subject.login(new UsernamePasswordToken("tommy", "123")); // 会话开始
Session session = subject.getSession();
session.setAttribute("username", "tommy"); // 存值
System.out.println(session.getAttribute("username")); // tommy
subject.logout(); // 会话结束
System.out.println(session.getAttribute("username")); // UnknownSessionException
在 Java Web 项目中,Shiro<small>(确切地说是 SecurityManager)</small> 默认使用的是 ServletContainerSessionManager,通过它,你存入 Shiro Session 中的数据,实际上被转存到了 HttpSession 中。也就是说,我们是在直接使用 Shiro Session,而间接使用 HttpSession 。
Session 超时
Shiro Session 的默认超时时间是 30 分钟。如果你对此不满意,可以自己设定合适的超时时间:
Session session = subject.getSession();
session.setTimeout(3000); // 单位微秒
...
如果你觉得每一个 Session 对象都要单独设置,很麻烦。那么,你可以直接在 web.xml
中直接去设置 HttpSession 的超时时间。这是因为在 Java Web 中 Shiro Session 最终利用就是 HttpSession :
<session-config>
<session-timeout>30</session-timeout>
</session-config>
Session DAO (了解、自学)
以 Java Web 项目为例,我们存往 Shiro Session 的键值对最终会被 Shiro 存到 HttpSession ,但是 Shiro Session 并不是直接操作 HttpSession 进行数据的存取的。
这有点类似于 Service > DAO > MySQL,Service 中的数据最终是要存入到数据库,但是 Service 并不直接操作数据库,它是通过 DAO 将它手里的数据存入数据库的。
概念 类比 Shiro Session Service Session DAO DAO HttpSession MySQL
我们这里介绍 SessionDAO 的目的不是为了使用它来实现更复杂的功能,而是为了禁用它。现在流行的分布式的方案<small>(前后端分离的 Restful 方案)</small>中,已经不会在服务端创建 Session,以实现『无状态』化。
以 .ini
配置为例,禁用 SessionDAO 的配置如下:
securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
禁用掉 SessionDAO 功能之后,你在 Shiro Session 中存放的数据,就没有人帮你『送往』HttpSession 或其它地方进行存储。
需要注意的是,此时你只是禁用了 SessionDAO 功能,而不是整个 SessionManager 功能,如果一旦你在代码中调用 Subject#getSession 或 Subject#getSession(true) 这样的代码,Shiro 仍然会在你的内存中创建 Shiro Session 对象。
不过考虑到在无状态的服务中,我们逻辑上不会调用 getSession 方法,所以也无须担心这些 Shiro Session 对象的创建。
<el-divider></el-divider>
<small>(如果你有强迫症)</small> 想彻底禁掉 Shiro Session 整个功能。那么需要自己实现 DefaultWebSubjectFactory 的子类,例如:StatelessDefaultSubjectFactory 。这样,由该 Factory 创建的 Subject 不支持 getSession() 方法。
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
public Subject createSubject(SubjectContext context) {
// 不创建 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
<bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager">
<property name="realm">
<bean class="..." />
</property>
<!-- 注意:不存储 session -->
<property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled" value="false"/>
<property name="sessionManager">
<bean class="org.apache.shiro.session.mgt.DefaultSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false" />
</bean>
</property>
<property name="subjectFactory">
<!-- 不生产会话 -->
<bean class="xxx.xxx.StatelessDefaultSubjectFactory" />
</property>
</bean>
这样再在程序中获得的 Subject 对象调用 getSession()
方法时,Shiro 会直接抛出异常。
网友评论