Apache Shiro 是 Java 的一个安全(权限)框架。它可以非常容易的开发出足够安全的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境 。

Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。下载:http://shiro.apache.org/  或  https://github.com/apache/shiro

功能介绍

Shiro目标:Shiro开发团队所称的“应用程序安全”的四个基石——身份验证、授权、会话管理和密码

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户 对某个资源是否具有某个权限;
  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

还有额外的功能来支持和增强:

  • Web Support:Web 支持,可以非常容易的集成到Web 环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
  • Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,就能把权限自动传播过去;
  • Testing:提供测试支持,测试支持的存在是为了帮助您编写单元测试和集成测试,确保您的代码将是安全的。
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

Shiro术语

  • Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject 代表了当前“用户”
  • Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作。

Shiro架构

从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作:

  • Subject(org.apache.shiro.subject.Subject):应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫, 机器人等;与 Subject 的所有交互都会委托给 SecurityManager; Subject 其实是一个门面,SecurityManager 才是实际的执行者;
  • SecurityManager (org.apache.shiro.mgt.SecurityManager):安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;其管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中 DispatcherServlet 的角色
  • Realm (org.apache.shiro.realm.Realm):Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以有一个或多个Realm来自定义,我们在自定义的Realm中获得数据库中真实的用户名和密码,来与传入的用户名和密码进行比较。

从Shiro内部来看:

  • Authenticator(org.apache.shiro.authc.Authenticator):身份验证负责 Subject 认证,执行和对验证用户(登录),可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authorizer(org.apache.shiro.authz.Authorizer):授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • SessionManager (org.apache.shiro.session.mgt.SessionManager):管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境
  • CacheManager(org.apache.shiro.cache.CacheManager):缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据 基本上很少改变,放到缓存中后可以提高访问的性能
  • Cryptography (org.apache.shiro.crypto.*):密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密。

开始使用

我们先导入Shiro的所需jar,我们先通过构建一个JavaSE应用来把Shiro运行起来 ,可以参照Shiro源码包下 (shiro-shiro-root-1.3.2\samples\quickstart)的快速开始

<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.4</version>
</dependency>
</dependencies>

Shiro需要slf4j来做日志,所以slf4j不能缺少

添加配置文件

这些配置文件直接放在Src下,如果是Maven项目就放在resources

  • log4j.properties 文件来打印日志,这里就不多说了
  • shiro.ini 文件是Shiro配置角色和权限的文件,在与Spring整合之后就不用这个文件了,然而这里还是需要的

添加Java代码来运行

我们直接使用下载的Demo中quickstart下面的Quickstart.java类即可,它有一个main方法,我们直接运行看到没有异常并打印出日志信息就是运行成功了

Spring在Web环境整合Shiro

我们一般情况下都是使用Spring来整合Shiro,在Web环境下对URL进行验证等工作。其通过一个 ShiroFilter 入口来拦截需要安全控制的URL,然后进行相应的控制

ShiroFilter 类似于如 Strut2/SpringMVC 这种 web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件或Spring整合之后的filterChainDefinitions属性),然后判断URL是否需要登录/权限等工作。

  1. 加入 Spring 和 Shiro 的 jar 包(这里还需要ehCache的jar和配置文件,因为Shiro可以使用ehCache来做cacheManager)
  2. 配置 Spring 及 SpringMVC

上面两步就不细说了。下面开始配置文件(可以参照:shiro-root-1.3.2-sourcerelease\shiro-root-1.3.2\samples\spring 配置 web.xml 文件和 Spring 的配置文件)

web.xml:主要配置关于Shiro的Filter

    <!-- Filters 配置Shiro过滤器-->
<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>

注意:这个filter-name的值,我们需要它与Spring整合文件的 org.apache.shiro.spring.web.ShiroFilterFactoryBean 这个bean的id值一致

Spring整合文件,主要的配置有

  1. 配置 SecurityManager安全管理器
  2. 配置 CacheManager. (用户授权信息Cache,可以使用ehCache来做缓存)
  3. 配置 Realm (自定义Realm来实现认证)
  4. 配置 LifecycleBeanPostProcessor(Shiro bean在IOC容器的生命周期)
  5. 启用 shiro 的注解
  6. 配置 ShiroFilter(它里面ShiroFilterFactoryBean的id值需要与web.xml中的 DelegatingFilterProxy 这个Filter的名一致;我们在 filterChainDefinitions 的属性可以配置URL过滤)
    <!--
1. 配置 SecurityManager安全管理器
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="sessionMode" value="native"/>
<property name="realm" ref="jdbcRealm"/>
</bean> <!--
2. 配置 CacheManager. (用户授权信息Cache)
2.1 需要加入 ehcache 的 jar 包及配置文件.
-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean> <!--
3. 配置 Realm
3.1 直接配置实现了 org.apache.shiro.realm.Realm 接口的 bean
-->
<bean id="jdbcRealm" class="cn.lynu.realms.ShiroRealm"></bean> <!--
4. 配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法.
-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!--
5. 启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用.
-->
<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> <!--
6. 配置 ShiroFilter.
6.1 id 必须和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
若不一致, 则会抛出: NoSuchBeanDefinitionException. 因为 Shiro 会来 IOC 容器中查找和 <filter-name> 名字对应的 filter bean.
可以设置targetBeanName指定Shiro的filter名
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/index.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/> <!--
配置哪些页面需要受保护.
以及访问这些页面需要的权限.
1). anon 可以被匿名访问
2). authc 必须认证(即登录)后才可能访问的页面.
3). logout 登出.
4). roles 角色过滤器
-->
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
# everything else requires authentication:
/** = authc
</value>
</property>
</bean>

我这里只配了一个login.jsp可以在未登录情况下访问,访问其他的页面都会因为未授权而重定向到login.jsp

URL过滤器

其格式是:“url=过滤器[参数]”。如果当前请求的 url 匹配 [urls] 部分的某个 url 模式,将会执行其配置的过滤器。部分Shiro内置的过滤器:

过滤器 过滤器类 描述 例子
anon org,apache.shiro.web.filter.authc.AnonymousFilter 可以直接访问 /admin=anon
authc org,apache.shiro.web.filter.authc.FormAuthenticationFilter 必须认证(登录)之后才能访问 /admin=authc
logout org,apache.shiro.web.filter.authc.logoutFilter 注销登录:所有的session都会失效,所有身份都会失去关联,remember Me Cookie也会删除 /logout=logout
user org,apache.shiro.web.filter.authc.UserFilter 表示只要有用户存在,无论是认证后或rememberMe都可以访问 /admin=user
roles org,apache.shiro.web.filter.authc.RolesAuthorizcationFilter 角色过滤器,判断当前用户是否为该角色。参数可以有多个,多个参数必须加上引号,多个参数之间用逗号分隔 admin/**=roles["admin,guest"]

URL 匹配模式

url 模式使用 Ant 风格模式 ,Ant 路径通配符支持 ?、 * 、 **,注意通配符匹配不包括目录分隔符“/”:

  • ?:  匹配一个字符,如 /admin? 将匹配 /admin1,但不匹配 /admin 或 /admin/;
  • *:  匹配零个或多个字符串,如 /admin* 将匹配 /admin、 /admin123,但不匹配 /admin/1;
  • **: 匹配路径中的零个或多个路径,如 /admin/** 将匹 配 /admin/a 或 /admin/a/b

编码式配置URL过滤器

其实我们在Spring整合Shiro的配置文件的URL过滤器可以通过编码的方式配置,这样我们可以将URL和过滤器配置到数据表中,通过编码查询数据库的方式获得对应的关系,添加进一个 LinkedHashMap。我们就建一个名为 FilterChainDefinitionMapBuilder 的类,在这个类中写一个 buildFilterChainDefinitionMap方法,我们在这个方法中可以查询数据库得到URL和过滤器的对应关系,这里就用模拟数据:

public class FilterChainDefinitionMapBuilder {

    public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){

        LinkedHashMap<String, String> map = new LinkedHashMap<>();

        map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
map.put("/user.jsp", "authc,roles[user]");
map.put("/admin.jsp", "authc,roles[admin]");
map.put("/list.jsp", "user"); map.put("/**", "authc"); return map;
}

然后修改Spring整合Shiro配置文件的 filterChainDefinitions :

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> </bean> <!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap"
factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"></bean> <bean id="filterChainDefinitionMapBuilder"
class="cn.lynu.factory.FilterChainDefinitionMapBuilder"></bean>

这个 cn.lynu.factory.FilterChainDefinitionMapBuilder 就是我们写的名为 FilterChainDefinitionMapBuilder 的类,buildFilterChainDefinitionMap就是这个类的中写的方法

加密

使用Shiro可以对密码进行加密操作,常见的MD5,SHA1都是支持的。我们在前端获得密码之后,Shiro会根据我们的配置进行对应的加密,并于数据库中的加密后字符串进行比较,比较成功之后就会放行进入受保护的页面,比对失败则会重定向到登录页。

我们先写个Controller,如果访问该请求,handler类比Quickstart.java类访问我们自定义的realm:

package cn.lynu.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; @Controller
@RequestMapping("/shiro")
public class ShiroController { @RequestMapping("/login")
public String login(@RequestParam("userName") String userName,
@RequestParam("password") String password) {
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
//把用户名和密码封装为UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
//Remember Me 操作
token.setRememberMe(true);
try {
System.out.println("token:"+token.hashCode());
//执行登录
currentUser.login(token);
}
//AuthenticationException是所有认证失败的父类
catch (AuthenticationException ae) {
System.err.println("登录失败:"+ae);
}
} return "redirect:/index.jsp";
} }

Subject的login方法就可以访问我们配置的realm,我们还需要在Spring的Shiro整合文件中配置这个URL为anon(未登录也可访问)

再来看看我们配置的realm,我们先使用明文进行测试,这里的自定义realm继承于AuthenticatingRealm,AuthenticatingRealm实现了realm接口:

public class ShiroRealm extends AuthenticatingRealm{

    @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo1:"+token.hashCode());
//1.将AuthenticationToken强转为UsernamePasswordToken
UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
//2.获得用户名
String userName=uspaToken.getUsername();
//3.查询数据库获得真实的用户名或密码(这里模拟)
//3.1若用户不存在,抛出UnknownAccountException
if("unknown".equals(userName)) {
throw new UnknownAccountException("用户不存在");
}
//3.2根据用户信息抛出其他信息(这里使用被锁定,抛出LockedAccountException)
if("lock".equals(userName)){
throw new LockedAccountException("用户被锁定");
} //4.根据用户信息构建AuthenticationInfo,我们常用其子类:
//1).principal 用户实体信息 可以是userName,也可以是数据表对应的实体类信息
Object principal=userName;
//2).credentials 密码
Object credentials="123";
//3).realmName 使用当前realName即可
String realmName=this.getName();
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, realmName);
return info;
}

这里用户名和密码的在真实环境中应该从数据库中查到,为了避免麻烦这里只要用户名不为unknown或lock,密码为123就可以登录成功了。

如果用户名为unknown就抛一个 UnknownAccountException (不存在账户的异常);如果用户名为lock就抛一个LockedAccountException(账户锁定异常),这俩个异常类都是AuthenticationException的子类,所以可以抛出,AuthenticationException还有其他的子类,我们可以根据用户信息来抛异常的方式终止登录。

登录成功之后我们就会根据在Controller中配置的重定向到index.jsp,在之前未登录的情况下,我们是不能访问这个页面的

但是这里有个问题:我们知道了Shiro内部使用缓存(ehCache)来保存验证,认证信息之类的,所以就出现了登录成功之后再次回到登录页,再次登录无论输入什么用户名和密码都会登录成功,因为登录成功的信息已经被缓存,这的时候就需要我们进行登出操作:

<a href="shiro/logout">登出</a>

这里简单的使用一个超链接访问URL->shiro/logout,我们只需要在Spring的Shiro整合文件中配置这个URL为logout即可

只需要在这里配置一下,我们点击登出超链接之后就会重定向到login.jsp要求再次登录

下面来到重头戏:加密。这里以MD5加密为例,首先我们需要在Spring的Shiro整合文件中配置自定义realm指定所需加密方式和加密次数:

加密次数与复杂度成正比,这样配置之后前端传过来的密码就会被Shiro以MD5进行1024次加密

因为存在于加密后的字符串进行比较,所以我们必须先知道加密后的结果,真实的使用当然是从数据库中查出来,我们这里简单点,使用Shiro提供的SimpleHash类来获得加密后的字符串。

在加密之前,我们再来复习密码学中的加盐概念:相同的密码经过加盐加密之后就会得到不同的加密字符串,这样我们将不同的加密字符串保存在数据库中,也不会看得出这是相同密码加密之后的结果,提高安全性。对于盐值的要求:唯一不重复,对于每个用户使用其唯一的属性,例如用户名之类的作为盐值,这样不同用户即使密码相同,加盐加密之后也会得到不同的加密字符串。

    public static void main(String[] args) {
//加密方式
String algorithmName="MD5";
//密码
Object credentials="123";
//盐值(一般将一个唯一值作为盐值)
Object salt=ByteSource.Util.bytes("admin");
//加密次数
int hashIterations=1024;
SimpleHash simpleHash = new SimpleHash(algorithmName, credentials, salt, hashIterations);
System.out.println(simpleHash);
}

simpleHash就可以得到加密之后的字符串,这里盐值使用了Shiro提供的 ByteSource.Util.bytes 方法进行获得盐值,这里使用admin来获得盐值,因为一会我们直接使用用户名为admin(将相当于在使用用户名这个唯一值来生成盐值),密码为123进行登录

修改我们自定义Realm(Shirorealm)的doGetAuthenticationInfo方法:

    @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo1:"+token.hashCode());
//1.将AuthenticationToken强转为UsernamePasswordToken
UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
//2.获得用户名
String userName=uspaToken.getUsername();
//3.查询数据库获得真实的用户名或密码(这里模拟)
//3.1若用户不存在,抛出UnknownAccountException
if("unknown".equals(userName)) {
throw new UnknownAccountException("用户不存在");
}
//3.2根据用户信息抛出其他信息(这里使用被锁定,抛出LockedAccountException)
if("lock".equals(userName)){
throw new LockedAccountException("用户被锁定");
} //4.根据用户信息构建AuthenticationInfo,我们常用其子类:
//1).principal 用户实体信息 可以是userName,也可以是数据表对应的实体类信息
Object principal=userName;
//2).credentials 密码
Object credentials=null; //这里使用加盐密码
if("admin".equals(userName)) {
credentials="c41d7c66e1b8404545aa3a0ece2006ac";
}
//3).realmName 使用当前realName即可
String realmName=this.getName();
//4).盐值(原始密码一致,但是通过加盐加密之后的字符串会不一样,提高安全性)
ByteSource credentialsSalt=ByteSource.Util.bytes(userName);
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}

Ok,现在只有用户名为admin,密码为123的才可以登录成功

多个Realm

如果存在多个Realm,我们应该如何配置呢?

1.这个时候Spring的Shiro整合文件就需要先配置多个Realm,这里我们配两个:

    <bean id="jdbcRealm" class="cn.lynu.realms.ShiroRealm">
<!-- 设置加密方式和加密次数 -->
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean> <!--第二个realm -->
<bean id="secondRealm" class="cn.lynu.realms.ShiroRealm2">
<!-- 设置加密方式和加密次数 -->
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="SHA1"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>

一个id为jdbcRealm,另一个id为secondRealm

2.修改SecurityManager,之前使用一个realm,所以就直接将realm配置在SecurityManager里面了,这里我们使用realms属性替代:

    <!--
1. 配置 SecurityManager安全管理器
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="sessionMode" value="native"/>
<!-- 配置单个realm
<property name="realm" ref="jdbcRealm"/> -->
<!--配置多个realm -->
<property name="authenticator" ref="authenticator"></property>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</property>
</bean>

注意看这个 realms 值使用的是list,所以到验证的时候顺序就根据在这里配置的realm先后顺序进行验证

配置后之后,我们再来看看这个ShiroRealm2如何写的(其实就是根据ShiroRealm改过来的,改为使用SHA1加密方式)

package cn.lynu.realms;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource; public class ShiroRealm2 extends AuthenticatingRealm{ @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo2:"+token.hashCode());
//1.将AuthenticationToken强转为UsernamePasswordToken
UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
//2.获得用户名
String userName=uspaToken.getUsername();
//3.查询数据库获得真实的用户名或密码(这里模拟)
//3.1若用户不存在,抛出UnknownAccountException
if("unknown".equals(userName)) {
throw new UnknownAccountException("用户不存在");
}
//3.2根据用户信息抛出其他信息(这里使用被锁定,抛出LockedAccountException)
if("lock".equals(userName)){
throw new LockedAccountException("用户被锁定");
} //4.根据用户信息构建AuthenticationInfo,我们常用其子类:
//1).principal 用户实体信息 可以是userName,也可以是数据表对应的实体类信息
Object principal=userName;
//2).credentials 密码
Object credentials=null; //这里使用加盐密码
if("admin".equals(userName)) {
credentials="49d9fbf007fd95343492e607aa34455eeb062b26";
}
//3).realmName 使用当前realName即可
String realmName=this.getName();
//4).盐值(原始密码一致,但是通过加盐加密之后的字符串会不一样,提高安全性)
ByteSource credentialsSalt=ByteSource.Util.bytes(userName);
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
} public static void main(String[] args) {
//加密方式
String algorithmName="SHA1";
//密码
Object credentials="123";
//盐值(一般将一个唯一值作为盐值)
Object salt=ByteSource.Util.bytes("admin");
//加密次数
int hashIterations=1024;
SimpleHash simpleHash = new SimpleHash(algorithmName, credentials, salt, hashIterations);
System.out.println(simpleHash);
} }

为了区分,在这两个Realm的开始都打印一句: System.out.println("doGetAuthenticationInfo1:"+token.hashCode());   System.out.println("doGetAuthenticationInfo2:"+token.hashCode()); 用于区分不同的realm

运行之后会发现先打印的是doGetAuthenticationInfo1 后打印doGetAuthenticationInfo2,这与我们在realms属性中配置的顺序有关

多个realm运行策略

多个Realm运行的先后顺序我们已经知道了与realms属性有关,但是多个Realm用于验证,以哪个为主呢?

    <!--配置多realm运行策略  -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>

并加入进 securityManager 中管理

 <property name="authenticator" ref="authenticator"></property>

我们其实在ModularRealmAuthenticator中配置了authenticationStrategy(验证策略)为AtLeastOneSuccessfulStrategy(只要有一个Realm验证成功即可。这其实也是默认的验证策略)。验证策略还有:

  • FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第 一个 Realm 身份验证成功的认证信息,其他的忽略
  • AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息
  • AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了

这些验证策略都是AuthenticationStrategy 接口的实现。我们可以修改验证策略来满足我们的需求

授权

有的时候我们需要给用户分配角色,用于区分不同角色的用户都可以做什么操作。这个时候使用的还是自定义的Realm,之前我们在验证的时候是将自定义的realm继承AuthenticatingRealm类(实现了realm接口),而我们授权是将自定义的realm继承AuthorizingRealm,AuthorizingRealm类是AuthenticatingRealm类的子类。验证使用的是重写AuthenticatingRealm的doGetAuthenticationInfo方法,而AuthorizingRealm类因为是子类也有这个方法,我们也可以使用这个方法完成验证,而授权的方法是doGetAuthorizationInfo方法,我们需要重写它。这两个类和方法都比较接近,注意区分。

public class ShiroRealm extends AuthorizingRealm{

    //用于验证的方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("doGetAuthenticationInfo1:"+token.hashCode());
//1.将AuthenticationToken强转为UsernamePasswordToken
UsernamePasswordToken uspaToken=(UsernamePasswordToken)token;
//2.获得用户名
String userName=uspaToken.getUsername();
//3.查询数据库获得真实的用户名或密码(这里模拟)
//3.1若用户不存在,抛出UnknownAccountException
if("unknown".equals(userName)) {
throw new UnknownAccountException("用户不存在");
}
//3.2根据用户信息抛出其他信息(这里使用被锁定,抛出LockedAccountException)
if("lock".equals(userName)){
throw new LockedAccountException("用户被锁定");
} //4.根据用户信息构建AuthenticationInfo,我们常用其子类:
//1).principal 用户实体信息 可以是userName,也可以是数据表对应的实体类信息
Object principal=userName;
//2).credentials 密码
Object credentials=null; //这里使用加盐密码
if("admin".equals(userName)) {
credentials="c41d7c66e1b8404545aa3a0ece2006ac";
}
if("user".equals(userName)) {
credentials="2bbffae8c52dd2532dfe629cecfb2c85";
}
//3).realmName 使用当前realName即可
String realmName=this.getName();
//4).盐值(原始密码一致,但是通过加盐加密之后的字符串会不一样,提高安全性)
ByteSource credentialsSalt=ByteSource.Util.bytes(userName);
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
} //用于授权的方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
Object principal = principals.getPrimaryPrincipal(); //2. 利用登录的用户的信息来用户当前用户的角色或权限(可能需要查询数据库)
Set<String> roles = new HashSet<>();
roles.add("user");
if("admin".equals(principal)){
roles.add("admin");
} //3. 创建 SimpleAuthorizationInfo, 并设置其 roles 属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); //4. 返回 SimpleAuthorizationInfo 对象.
return info;
} }

doGetAuthorizationInfo方法中默认给所有用户赋予的是user的角色,而如果用户名为admin的用户还会额外赋予admin角色。别忘了在Spring的Shiro的配置文件中配置这个自定义的realm

user和admin角色都可以做什么呢?

我们在Spring与Shiro的整合配置文件中

 /user.jsp=authc,roles[user]
/admin.jsp=authc,roles[admin]

user.jsp只有在认证和拥有user角色的情况下才可以访问,而admin.jsp在认证和拥有admin角色的情况下才可以访问

现在的user.jsp和admin.jsp只有在登录之后并拥有对应权限的用户在可以访问。

Shiro标签

Shiro提供了一套标签供我们在JSP页面进行权限控制,首先在页面导入Shiro标签库

<%-- 使用Shiro标签 --%>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

guest标签:用户没有身份验证时显示对应信息,及游客可以看到的信息

<shiro:guest>
欢迎游客访问,<a href="login.jsp">登录</a>
</shiro:guest>

user标签:用户已经认证/记住我登录之后显示的信息

<shiro:user>
欢迎[<shiro:prinicpal/>]登录,<a href="logout">退出</a>
</shiro:user>

authenticated标签:用户已经身份验证通过,即通过Subject.logon登录成功,而不是通过记住我登录,显示的信息

<shiro:authenticated>
用户[<shiro:principal/>]已经身份验证通过
</shiro:authenticated>

notAuthenticated标签:用户未进行身份认证,即没通过Subject.login进行登录,记住我自动登录也属于为未经过身份验证。

<shiro:notAuthenticated>
未身份验证(包括记住我)
<shiro:notAuthenticated>

principal标签:显示用户身份信息.即调用了Subject.getPrincipal()获得PrimaryPrincipal。

<shiro:principaol/>

hasRole标签:如果当前Subject有角色将显示内容

<shiro:hasRole name="admin">
用户[<shiro:principal/>]拥有admin角色<be>
</shiro:hasRole>

hasAnyRoles标签:如果当前Subject有任意一个角色(或的关系)将显示内容

<shiro:hasAnyRoles name="admin,user">
用户[<shiro:principal/>]拥有角色admin或user<br>
</shiro:hasAnyRoles>

lacksRole标签:如果当前subject没有对应角色将显示的内容

<shiro:lacksRole name="admin">
用户[<shiro:principal/>]没有角黑色admin<br>
</shiro:lacksRole>

hasPermission标签: 如果当前Subject有权限则显示内容

<shiro:hasPermission name="user:create">
用户[<shiro:principal/>]拥有权限user:create<br>
</shiro:hasPermission>

lacksPermission标签:如果当前Subject没有权限将显示内容

<shiro:lacksPermission name="user:create">
用户[<shiro:principal/>]没有权限user:create
</shiro:lacksPermission>

权限注解

@RequiresAuthentication:表示Subject已经通过login进行了身份认证;即Subject.isAuthenticated()返回true

@RequiresUser:表示当前Subject已经通过身份验证或记住我登录

@RequiresGuest:表示是当前Subject没有身份认证或记住我登录过,即是游客身份

@RequiresRoles(values={"admin","user"},logical=Logical.OR):表示当前Subject需要角色admin或user

我在Service层配置一个方法只有拥有admin在可运行:

    @RequiresRoles(value={"admin"})
public void testMethod(){
System.out.println("Now Date:"+new Date());
}

这样只有拥有admin角色的用户调用这个方法才会打印日期,其他角色的用户调用这个方法不会成功,会抛一个异常:

我们可以根据这个异常做一个异常处理:只要出现这个异常就转到一个页面,提示一些东西

但是我在测试的时候遇到一些问题:如果使用注解标注Service那么Shiro就失效了,只有用XML中声明式的配置Service才可以?

会话管理

Shiro提供了完整的会话管理,它不依赖于底层容器(如web容器tomcat),不管JavaSE还是JavaEE都可以使用,提供了会话管理,会话监听,失效/过期支持等等

会话相关的API

  • Subject.getSession():即可获得会话;Subject.getSession(true)如果当前没有会话就创建一个;Subject.getSession(false)如果当前没有Session就返回null
  • session.getId():获得当前会话的唯一标识
  • session.getHost():获取当前Subject的主机地址
  • session.getTimeout()&session.getTimeout(毫秒):获得/设置当前会话session的过期时间
  • session.stop():销毁会话
  • session.setAttribute(key,val) & session.getAttribute(key) & session.removeAttribute(key):设置/获取/删除会话中的属性

这里我们完成一个操作:在Controller层往Session中放入一个名为userName的lz值,并在Service层取出这个userName值,不要将HttpSession传入给Service层避免代码侵入,使用Shiro的Session获得这个值

Controller:

    @RequestMapping("/testShiroAnnotation")
public String testShiroAnnotation(HttpSession session){
session.setAttribute("userName", "lz");
shiroService.testMethod();
return "redirect:/list.jsp";
}

Service:

    public void testMethod(){

        Session session = SecurityUtils.getSubject().getSession();
Object val = session.getAttribute("userName"); System.out.println("Service SessionVal: " + val);
}

RememberMe

Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝 等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁, 下次访问时无需再登录即可访问,基本流程如下:

  1. 首先在登录页面选中 RememberMe 然后登录成功;如果是 浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并保存下来;
  2. 关闭浏览器再重新打开;会发现浏览器还是记住你的;
  3. 访问一般的网页服务器端还是知道你是谁,且能正常访问;
  4. 但是比如我们访问淘宝时,如果要查看我的订单或进行支付 时,此时还是需要再进行身份认证的,以确保当前用户还是你。

我们之前其实一直使用了rememberMe功能:

            //把用户名和密码封装为UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
//Remember Me 操作
token.setRememberMe(true);

我们可以在前端加一个checkbox用于判断是否需要rememberMe,如果勾选,这个设置setRememberMe  为true

因为rememberMe底层使用的是Cookies,我们可以在Spring整合Shiro的配置文件的securityManager配置rememberMeManager.cookie.maxAge

        <!--设置remember Me 的Cookie时间 单位秒  -->
<property name="rememberMeManager.cookie.maxAge" value="1800"></property>

认证和记住我

  • subject.isAuthenticated() 表示用户进行了身份验证登录的, 即使用 Subject.login 进行了登录;
  • subject.isRemembered():表示用户是通过记住我登录的, 此时可能并不是真正的你(如你的朋友使用你的电脑,或者 你的cookie 被窃取)在访问的
  • 两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样。

建议

  • 访问一般网页:如个人在主页之类的,我们使用user 拦截器即可,user 拦截器只要用户登录 (isRemembered() || isAuthenticated())过即可访问成功;
  • 访问特殊网页:如我的订单,提交订单页面,我们使用 authc 拦截器即可,authc 拦截器会判断用户是否是通过 Subject.login(isAuthenticated()==true)登录的,如果是才放行,否则会跳转到登录页面重新登录。

Shiro的学习的更多相关文章

  1. Shiro:学习笔记(2)——授权

    Shiro:学习笔记(2)——授权 Shiro的三种授权方式 编程式: Subject subject = SecurityUtils.getSubject(); if(subject.hasRole ...

  2. Shiro入门学习之shi.ini实现授权(三)

    一.Shiro授权 前提:需要认证通过才会有授权一说 1.授权过程 2.相关方法说明 ①subject.hasRole("role1"):判断是否有该角色 ②subject.has ...

  3. Shiro入门学习与实战(一)

    一.概述 1.Shiro是什么? Apache Shiro是java 的一个安全框架,主要提供:认证.授权.加密.会话管理.与Web集成.缓存等功能,其不依赖于Spring即可使用: Spring S ...

  4. shiro入门学习--使用MD5和salt进行加密|练气后期

    写在前面 在上一篇文章<Shiro入门学习---使用自定义Realm完成认证|练气中期>当中,我们学会了使用自定义Realm实现shiro数据源的切换,我们可以切换成从关系数据库如MySQ ...

  5. 《跟我学Shiro》学习笔记 第一章:Shiro简介

    前言 现在在学习Shiro,参照着张开涛老师的博客进行学习,然后自己写博客记录一下学习中的知识点,一来可以加深理解,二来以后遗忘了可以查阅.没有学习过Shiro的小伙伴,也可以和我一起学习,大家共同进 ...

  6. shiro简单学习的简单总结

    权限和我有很大渊源. 培训时候的最后一个项目是OA,权限那块却不知如何入手,最后以不是我写的那个模块应付面试. 最开始的是使用session装载用户登录信息,使用简单权限拦截器做到权限控制,利用资源文 ...

  7. Shiro:学习笔记(1)——身份验证

    Shiro——学习笔记(1) 1.核心概念 1.Shiro不会自己去维护用户.维护权限:这些需要我们自己去设计/提供:然后通过相应的接口注入给Shiro.2.应用代码直接交互的对象是Subject,也 ...

  8. Shiro入门学习之散列算法与凭证配置(六)

    一.散列算法概述 散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5.SHA等,一般进行散列时最好提供一个salt(“盐”),什么意思?举个栗子 ...

  9. Shiro入门学习之shi.ini实现认证及源码分析(二)

    一.Shiro.ini文件 1.文件说明 ①ini(InitializationFile)初始文件:Window系统文件扩展名 ②Shiro使用时可以连接数据库,也可以不连接数据库(可以使用shiro ...

  10. shiro入门学习--授权(Authorization)|筑基初期

    写在前面 经过前面的学习,我们了解了shiro中的认证流程,并且学会了如何通过自定义Realm实现应用程序的用户认证.在这篇文章当中,我们将学习shiro中的授权流程. 授权概述 这里的授权指的是授予 ...

随机推荐

  1. L156

    China has specified the definition and diagnosis standard for internet addiction in its latest adole ...

  2. Python3 flask nginx uwsgi 环境搭建

    配置项目的时候一般使用虚拟环境,是各个项目的环境独立起来,更多方便管理.至于如何使用搜索即可,并不难 1.安装python3 yum -y install zlib-devel bzip2-devel ...

  3. EasyAACEncoder海思/ARM平台优化G711、G726转AAC的CPU占用高问题

    本文转自EasyDarwin开源团队成员Kim的博客:http://blog.csdn.net/jinlong0603/article/details/75645378 引言 目前EasyDarwin ...

  4. Vim技能修炼教程(5) - 操作符实务

    操作符实务 操作符复习 上次我们讲了操作符与文本对象的组合这样一个vim的强大功能.但是上节的知识点过于密集,可读性可能差了一点.不过没关系,重要的知识点在后面用到的时候我们可以先复习一下. 我们还是 ...

  5. 【Java实战】源码解析为什么覆盖equals方法时总要覆盖hashCode方法

    1.背景知识 本文代码基于jdk1.8分析,<Java编程思想>中有如下描述: 另外再看下Object.java对hashCode()方法的说明: /** * Returns a hash ...

  6. Java从入门到精通全套教程免费分享

    这是我自己早前听课时整理的Java全套知识,适用于初学者,也可以适用于中级进阶的人,你们可以下载,我认为是比较系统全面的,可以抵得上市场上90%的学习资料.讨厌那些随便乱写的资料还有拿出来卖钱的人!在 ...

  7. 《selenium2 python 自动化测试实战》(19)——webdriver错误截图

    webdriver 提供错误截图函数 get_screenshot_as_file(),可以帮助我们跟踪 bug,在脚本无法继续执行时候, get_screenshot_as_file()函数将截取当 ...

  8. 【angularJS】定义模块angular.module

    模块定义了一个应用程序.控制器通常属于一个模块. JavaScript 中应避免使用全局函数.因为他们很容易被其他脚本文件覆盖. AngularJS 模块让所有函数的作用域在该模块下,避免了该问题. ...

  9. 细说VS MSBuild 和 Framework 的区别

    如今已经是 VS2017 横行的时代,而据我所知,大部分人还停留在使用 VS2015 VS2013 或更低的版本,主要是因为他们参与的项目基本使用这几个VS的版本开发的.眼红VS2017却不敢升级,主 ...

  10. 【DUBBO】Dubbo:monitor的配置

    [一]:配置项 <dubbo:monitor protocol="registry"/> [二]:配置解析器-->具体解析器为com.alibaba.dubbo. ...