官网地址
Apache Shiro
是Java的一个安全(权限)框架,对比Spring Security
,没有Spring Security做的功能强大,但小而简单;
Shiro
可以完成:认证、授权、加密、会话管理、与Web集成、缓存等;
应用代码直接交互的对象是 Subject
,也就是说 Shiro 的对外 API 核心就是 Subject;
Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;与 Subject 的所有交互都会委托给 SecurityManager; Subject
其实是一个门面,SecurityManager
才是实际的执行者;
所有与安全有关的操作都会与 SecurityManager
交互,且其管理着所有 Subject
;
它负责与 Shiro
的其他组件进行交互,相当于SpringMVC 中的 DispatcherServlet,是 Shiro 的核心; 所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证、授权、会话及缓存的管理;
SecurityManager
接口继承了 Authenticator
,另外还有一个 ModularRealmAuthenticator
实现,其委托给多个Realm 进行验证,验证规则通过 AuthenticationStrategy
接口指定;
Authenticator
:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authenticator 的职责是验证用户帐号,是 Shiro API 中身份验证核心的入口点:如果验证成功,将返回AuthenticationInfo 验证信息,此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException 异常;
Authentication
:身份认证/登录,验证用户是不是拥有相应的身份;
使用Shiro完成用户的密码匹配以此来完成用户的登录;
Authorizer
:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Authorization
:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作;
如:验证某个用户是否拥有某个角色;或者细粒度的验证某个用户对某个资源是否具有某个权限;
Shiro
从 Realm
获取安全数据(如用户、角色、权限);认证或授权的时候,可以有 1 个或多个 Realm,用来访问持久化的数据;
就是说 SecurityManager
要验证用户身份,那么它需要从 Realm
获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource;
可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的 Realm;
Realm接口常用方法:
String getName(); // 返回一个唯一的Realm名字;
boolean supports(AuthenticationToken token); // 判断此Realm是否支持此token;
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token); // 根据token获取认证信息;
一般继承 AuthorizingRealm
(授权)即可;其继承了 AuthenticatingRealm
(即身份验证),而且也间接继承了 CachingRealm
(带有缓存实现);
SessionManager
:Session管理器,管理 Session 生命周期的组件,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境;
Session可以是普通 JavaSE 环境的Shiro提供的Session,也可以是 Web 环境的HttpSession;
CacheManager
:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
**Caching**
:缓存;
比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Cryptography
:加密,保护数据的安全性;
如密码盐值加密存储到数据库,而不是明文存储;
Shiro 提高了一些常见的加密组件用于如密码加密/解密;
可以非常容易的集成到Web 环境;
Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
这个是非常常见的功能,即一次登录后,下次再来的话不用登录了;
public static void main(String[] args) {
// 获取当前的Subject:使用SecurityUtils的方法;
Subject currentUser = SecurityUtils.getSubject();
// 获取Session:使用Subject的方法;
Session session = currentUser.getSession();
// 设置Session属性
session.setAttribute("name", "zxj");
// 从Session中获取数据
String name = session.getAttribute("name");
// 测试当前用户是否已经被认证:是否已经登录;使用Subject实例的方法;
if(!currentUser.isAuthenticated()){
// 把用户名和密码封装为UsernamePasswordToken对象;用户名、密码从页面表单传入;
UsernamePasswordToken token = new UsernamePasswordToken("zxj", "123456");
token.setRememberMe(true);
try{
// 使用Subject实例的login方法,传入token,执行登录
// 能都登录成功取决于配置文件中是否配置了这个用户,以及密码是否正确;
currentUser.login(token);
}catch (UnknownAccountException e){ // 账户不存在
log.info("There is no user with username of " + token.getPrincipal());
return;
}catch (IncorrectCrendentialsException e){ // 密码错误
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
return ;
}catch (LockedAccountException e){ // 用户被锁定
log.info("用户被锁定,请联系管理员解锁!");
return ;
}catch (AuthenticationException e){
// 所有认证时异常的父类
}
}
// 测试用户是否有某个角色;
if(currentUser.hasRole("root")){...}
// 测试用户是否有某个权限;
if(currentUser.isPermitted("某个权限:..")){...}
// 用户可以对某个用户执行某个操作;
if(currentUser.isPermitted("user:delete:zhangsan")){...}
// 执行登出
currentUser.logout();
}
1、创建Maven项目,配置好Spring和SpringMVC;
2、在web.xml
中配置spring容器的监听器和SpringMVC前端控制器:
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:applicationContext.xmlparam-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<servlet>
<servlet-name>springmvcservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:springmvc.xmlparam-value>
init-param>
servlet>
<servlet-mapping>
<servlet-name>springmvcservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
<filter>
<filter-name>characterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>UTF-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>characterEncodingFilterfilter-name>
<url-pattern>*.htmlurl-pattern>
filter-mapping>
<filter-mapping>
<filter-name>characterEncodingFilterfilter-name>
<url-pattern>*.jspurl-pattern>
filter-mapping>
3、springmvc.xml
:配置视图解析器、批量扫描包…
<beans xmlns="......"
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"/>
<property name="suffix" value=".jsp"/>
bean>
beans>
4、在web.xml
中配置ShiroFilter
:
DelegatingFilterProxy
实际上是Filter的一个代理对象,作用是自动到 Spring 的IOC容器中查找查找和
对应的 filter bean 并把所有 Filter 的操作委托给它;也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id;
<filter>
<filter-name>shiroFilterfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
<init-param>
<param-name>targetFilterLifecycleparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>shiroFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
3、在Spring的配置文件applicationContext.xml
中配置Shiro:
4、创建自定义ShiroRealm
类实现Realm
接口:
Shiro 提供了与 Web 集成的支持,其通过一个 ShiroFilter
入口来拦截需要安全控制的URL,然后进行相应的控制;
ShiroFilter
类似于如 Strut2/SpringMVC 这种 web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件),然后判断URL 是否需要登录/权限等工作;
1、在web.xml
文件中配置的shiroFilter是一个入口,它可以拦截所有请求资源;
有两种页面可以直接访问:在applicationContext.xml
的shiroFilter的filterChainDefinitions属性中配置的过滤器是anon的页面,和在这里面没有进行配置的页面;
2、请求不能直接访问的资源,会被重定向到shiroFilter的loginUrl属性定义的页面中;
<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>
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
value>
property>
bean>
org.apache.shiro.web.filter.mgt.DefaultFilter
这时shiro的一个枚举类,里面列举了shiro的默认过滤器;
在spring的配置文件applicationContext.xml
的配置的
的属性filterChainDefinitions
用来配置哪些页面需要受保护,以及访问这些页面需要的权限;
1、配置的格式是:url = 拦截器[参数]
,如果当前请求的 url 匹配某个 url 模式,将会执行其配置的拦截器;
2、匹配模式: url 模式使用 Ant 风格模式,Ant 路径通配符支持?(匹配一个字符)
、* (匹配零个或多个字符串)
、** (匹配多重路径)
;
3、URL匹配顺序: URL权限采取第一次匹配优先的方式,即从头开始,使用第一个匹配的 url 模式对应的拦截器链;
4、 身份认证相关的过滤器
anon(anonymous)
: 拦截器表示匿名访问(即不需要登 录即可访问);authc (authentication)
:认证过滤器;拦截器表示需要身份认证通过后才能访问对应的资源;logout
:登出过滤器;5、授权相关的过滤器:
roles
:角色授权过滤器;有对应的权限,才能访问对应的资源;/user.jsp = roles[user]
/admin.jsp = roles[admin]
6、Permissions配置:
(1)规则:资源标识符:操作:对象实例ID
,即对哪个资源的哪个实例可以进行什么操作;默认支持通配符权限字符串:冒号:
表示资源/操作/实例的分割,逗号,
表示操作的分割,星号*
表示任意资源/操作/实例;
(2)多层次管理:
user:query
、user:edit
user:query
和 user:edit
权限外,也可以简单地授予他们一个:user:query, edit
*
号代替所有的值,如:user:*
, 也可以写:*:query
,表示某个用户在所有的领域都有 query 的权限;(3)实例级访问控制:
user:edit:manager
user:edit:*
、user:*:*
、user:*:manager
user:edit
等价于 user:edit :*
、 user
等价于 user:*:*
user:edit
并不等价于 user:*:edit
**身份验证:**一般需要提供如身份 ID 等一些标识信息来表明登录者的身份,如提供 email,用户名/密码来证明;
在 shiro 中,用户需要提供 principals
(身份)和 credentials
(证明)给 shiro
,从而应用能验证用户身份;最常见的 principals
和 credentials
组合就是用户名/密码;
principals
:身份,即主体的标识属性,可以是任何属性,如用户名、 邮箱等,唯一即可;一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名/邮箱/手机号;credentials
:证明/凭证,即只有主体知道的安全值,如密码/数字证书等;【前三步在handler中完成;】
1、调用 SecurityUtils.getSubject()
方法获取当前的Subject
;
2、调用 subject.isAuthenticated()
方法测试当前的用户是否已经被认证,即是否已经登录;
3、若返回false,表示没有被认证, 则把从页面表单传来的 用户名和密码封装为 UsernamePasswordToken
对象;
创建一个表单页面,把请求提交到 SpringMVC 的 Handler,然后获取用户名和密码;
4、执行登录: 调用 Subject的login(AuthenticationToken)
方法; UsernamePasswordToken类是接口AuthenticationToken的实现类;
5、自定义Realm的方法, 从数据库中获取对应的记录, 返回给Shiro;
自定义Realm需要继承 org.apache.shiro.realm.AuthenticatingRealm
类,实现 doGetAuthenticationInfo(AuthenticationToken)
方法;
6、由 shiro 完成对密码的比对;
Subject.login(token)
进行登录,其会自动委托给 SecurityManager
;SecurityManager
负责真正的身份验证逻辑;它会委托给 Authenticator
进行身份验证;Authenticator
才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;Authenticator
可能会委托给相应的 AuthenticationStrategy
进 行多 Realm 身份验证,默认 ModularRealmAuthenticator
会调用 AuthenticationStrategy
进行多 Realm 身份验证;1、创建一个表单页面;
<form action="shiro/login" method="POST">
username: <input type="text" name="username"/><br><br>
password: <input type="password" name="password"/><br><br>
<input type="submit" value="Submit"/>
form>
2、在applicationContext.xml
中的shiroFilter
Bean处添加配置/shiro/login = anon
,使表单提交的请求可以匿名访问;
3、创建handler接收表单提交请求,获取用户名、密码,判断是否登录;
@Controller
@RequestMapping("/shiro")
public class ShiroHandler {
@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);
token.setRememberMe(true);
try {
System.out.println("1. " + token.hashCode());
currentUser.login(token); // 这个token传给了自定义Realm的doGetAuthenticationInfo方法入参;
}catch (AuthenticationException ae) {
System.out.println("登录失败: " + ae.getMessage());
}
}
return "redirect:/list.jsp";
}
}
4、创建自定义Realm
继承AuthorizingRealm
类,实现doGetAuthenticationInfo
方法:
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1、把 AuthenticationToken 转换为 UsernamePasswordToken;
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 2、从 UsernamePasswordToken 中来获取 username;(表单传入的)
String username = upToken.getUsername();
// 3、根据用户名从数据库中获取用户信息,然后根据用户信息的获取情况决定是否要抛异常;
System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");
// 4、若用户不存在, 则抛出 UnknownAccountException 异常;
if("unknown".equals(username)){ throw new UnknownAccountException("用户不存在!"); }
// 5、根据用户信息的情况,决定是否需要抛出其他的 AuthenticationException 异常;
if("monster".equals(username)){ throw new LockedAccountException("用户被锁定"); }
// 6、根据用户的情况,来构建 AuthenticationInfo 对象并返回;通常使用的实现类为: SimpleAuthenticationInfo;
Object principal = upToken.getPrincipal(); // 认证的实体信息;可以是username,也可以是数据表对应的用户的实体类对象;从数据库中获取的;
Object credentials = "从数据库中获取的密码"; // 密码,从数据库中获取的;
String realmName = getName(); // 当前realm对象的name,调用父类的getName()方法即可;
ByteSource credentialsSalt = ByteSource.Util.bytes(username); // 盐值:防止相同的密码加密之后的值相同;
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}
5、 Shiro 比对密码:
UsernamePasswordToken
保存的是从表单获取的用户名和密码,SimpleAuthenticationInfo
保存的是从数据库中查询的密码,shiro 通过 AuthenticatingRealm
的 credentialsMatcher
属性来完成这两个对象中保存的密码的比对;
6、登出:直接在applicationContext.xml
中的shiroFilter
Bean处添加配置/shiro/logout = logout
;
public static void main(String[] args) {
String hashAlgorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("user");;
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
1、如何把一个字符串加密为 MD5 :
在applicationContext.xml
中配置的自定义Realm的Bean,替换Bean的 credentialsMatcher
属性,直接使用 HashedCredentialsMatcher
对象,指定凭证匹配器,给凭证匹配器指定使用MD5加密算法;指定凭证匹配器之后,shiro会自动将表单输入的密码进行MD5加密;
<bean id="jdbcRealm" class="com.zxj.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="1024"/> // 加密次数
bean>
property>
bean>
2、为什么使用 MD5 盐值加密: 防止相同的密码加密之后的值相同;
3、如何做到盐值加密:
(1)在自定义Realm的 doGetAuthenticationInfo
方法返回值创建 SimpleAuthenticationInfo
对象的时候,使用
SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName)
构造器;
(2)使用 ByteSource.Util.bytes()
来计算盐值;
(3)盐值需要唯一:一般使用随机字符串或 user id;
(4)使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
来计算盐值加密后的密码的值;
在某些特定的场合,可能将安全数据放在不同的数据库中,比如:MySQL中使用MD5加密算法,Oracle数据库使用SHA1加密算法;这时候进行数据认证的时候就要同时访问这两个数据库,就需要使用多个Realm;
1、创建多个自定义Realm,继承AuthorizingRealm
类,实现doGetAuthenticationInfo
方法:
2、在applicationContext.xml
配置文件中配置Bean:
<bean id="jdbcRealm" class="com.zxj.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="1024"/> // 加密次数
bean>
property>
bean>
<bean id="secondRealm" class="com.atguigu.shiro.realms.SecondRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="SHA1"/>
<property name="hashIterations" value="1024"/>
bean>
property>
bean>
3、在applicationContext.xml
配置文件中配置认证器:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
list>
property>
<property name="rememberMeManager.cookie.maxAge" value="10"/>
bean>
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
property>
bean>
定义多个Realm,指定认证策略,来决定怎么样才叫认证成功;
认证策略接口AuthenticationStrategy
有三个默认实现:
FirstSuccessfulStrategy
:只要有一个 Realm 验证成功即可,只返回第 一个 Realm 身份验证成功的认证信息,其他的忽略;AtLeastOneSuccessfulStrategy
:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy
不同,将返回所有Realm身份验证成功的认证信 息;AllSuccessfulStrategy
:所有Realm验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了;ModularRealmAuthenticator
认证器默认是 AtLeastOneSuccessfulStrategy
策略;
授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作等);在授权中需了解的几个关键对象:
主体(Subject)
:访问应用的用户,在 Shiro 中使用 Subject 代表该用户;用户只有授权后才允许访问相应的资源;
资源(Resource)
:在web应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源;用户只要授权后才能访问;
权限(Permission)
:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力;即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)等;
权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许;
Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限, 即实例级别的);
角色(Role)
:权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便;典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限;
– 编程式
:通过写if/else 授权代码块完成;
– 注解式
:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相 应的异常;
– JSP/GSP 标签
:在JSP/GSP 页面通过相应的标签完成;(隐藏没有访问权限的资源)
1、授权需要继承 AuthorizingRealm
类,并实现其 doGetAuthorizationInfo
方法;
2、AuthorizingRealm
类继承自 AuthenticatingRealm
,但没有实现 AuthenticatingRealm
中的
doGetAuthenticationInfo
,所以认证和授权只需要继承 AuthorizingRealm
就可以了,同时实现他的两个抽象方法;
public class ShiroRealm extends AuthorizingRealm {
@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;
}
}
Subject.isPermitted*/hasRole*
接口,其会委托给SecurityManager,而 SecurityManager
接着会委托给 Authorizer
;Authorizer
是真正的授权者,如果调用如 isPermitted(“user:view”)
,其首先会通过PermissionResolver
把字符串转换成相应的 Permission
实例;isPermitted*/hasRole*
会返回true,否则返回false表示,授权失败;isPermitted*/hasRole*
接口进行匹配;如果有一个Realm匹配那么将返回 true,否则返回 false;Shiro 提供了 JSTL 标签用于在 JSP 页面进行权限控制,如:根据登录用户显示相应的页面按钮;
在jsp页面引入shiro标签:<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
guest 标签
:用户没有身份验证时显示相应信息,即游客访问信息;欢迎游客访问,登录
user 标签
:用户已经经过认证/记住我登录后显示相应的信 息;欢迎【】登录,退出
authenticated 标签
:用户已经身份验证通过,即Subject.login
登录成功,不是记住我登录的;用户【】身份已经验证通过;
notAuthenticated 标签
:用户未进行身份验证,即没有调 用Subject.login
进行登录,包括记住我自动登录的也属于未进行身份验证;未身份验证(包括记住我)
pincipal 标签
:显示用户身份信息,默认调用 Subject.getPrincipal()
获取,即 Primary Principal
hasRole 标签
:如果当前 Subject 有角色将显示 body 体内容;Admin Page
hasAnyRoles 标签
:如果当前Subject有任意一个角色(或的关系)将显示body体内容;用户拥有user或admin角色
lacksRole
:如果当前 Subject 没有角色将显 示 body 体内容;用户没有admin角色
hasPermission
:如果当前 Subject 有权限 将显示 body 体内容;用户拥有权限user:create
lacksPermission
:如果当前Subject没有权限将显示body体内容;用户没有权限user:create
可以写在Controller层,也可以写在service层;
注意:若service的类上添加的事务的注解,那么就不能再在该类上添加权限注解,添加了也无效,这时需要将权限注解加到Controller上;
@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
;@Controller
@RequestMapping("/shiro")
public class ShiroHandler {
@Autowired
private ShiroService shiroService;
@RequestMapping("/testShiroAnnotation")
public String testShiroAnnotation(HttpSession session){
session.setAttribute("key", "value12345");
shiroService.testMethod();
return "redirect:/list.jsp";
}
}
public class ShiroService {
@RequiresRoles({"admin"})
public void testMethod(){
System.out.println("testMethod, time: " + new Date());
Session session = SecurityUtils.getSubject().getSession();
Object val = session.getAttribute("key");
System.out.println("Service SessionVal: " + val);
}
}
受保护的资源跟对应的权限关系,有两种配置方式;
1、在applicationContext.xml
文件中配置shiroFilter
Bean,在Bean中配置filterChainDefinitions
属性,属性的值配置资源与权限的对应关系;
2、资源与权限的对应关系写到数据库中,通过sql获取;
(1)shiroFilter
Bean使用filterChainDefinitionMap
属性,引用一个Map类型的Bean;
(2)构建一个实例工厂类,类中方法返回LinkHashMap类型的集合;
(3)通过实例工厂类的Bean,配置实例Bean;
<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"/>
bean>
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"/>
<bean id="filterChainDefinitionMapBuilder" class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder">bean>
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("/**", "authc");
return map;
}
}
Shiro的Session和Http的Session是一致的,都表示是一个客户端和服务器的一次会话;
Shiro提供了完整的企业级会话管理功能,不依赖于底层的容器,就是说它没有web服务器,没有tomcat也可以使用Session;So不管是JavaSE、JavaEE环境下都可以使用Shiro的Session;提供了会话管理
、会话事件监听
、会话存储/ 持久化
、容器无关的集群
、失效/过期支持
、对Web 的透明支持
、SSO 单点登录的支持
等特性;
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)
:设置/获取/删除会话属性:在整个会话范围内都可以对这些属性进行操作;
会话监听器用于监听会话创建onStart(Session)
、过期onExpiration(Session)
及停止onStop(Session)
事件;和Http的Session监听器类似;
在Web层建议使用Http的Session,但是在传统的web应用中,是无法在Service中访问HttpSession的,但是可以使用Shiro的Session在Service中访问session中属性值;
SessionDao
可以将Session写到数据库中,然后就可以对Session进行增删改查操作;开发时一般继承EnterpriseCacheSessionDao
;
• AbstractSessionDAO
提供了 SessionDAO 的基础实现,如生成会话ID等;
• CachingSessionDAO
提供了对开发者透明的会话缓存的功能,需要设置相应CacheManager
;
• MemorySessionDAO
直接在内存中进行会话维护;
• EnterpriseCacheSessionDAO
提供了缓存功能的会话维护,默认情况下使用 MapCache
实现,内部使用 ConcurrentHashMap
保存缓存的会话;
该类提供了doCreate()
、doReadSession()
、doUpdate()
、doDelete
四个方法;
doCreate()
方法创建SessionDao
时需要获取sessionId
,generateSessionId方法返回sessionID
时中需要用到sessionIdGenerator
属性,所以在配置文件中配置EnterpriseCacheSessionDAO
的时候,要设置sessionIdGenerator
这个属性;
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
return sessionId;
}
protected Serializable generateSessionId(Session session) {
if (this.sessionIdGenerator == null) {
String msg = "sessionIdGenerator attribute has not been configured.";
throw new IllegalStateException(msg);
}
return this.sessionIdGenerator.generateId(session);
}
配置好SessionDao以后,就可以对Session进行增删改查操作了;需要使用对象输入/输出流进行读写操作;
步骤:
创建类继承EnterpriseCacheSessionDAO
类,实现四个方法;需要读写、更新的时候,需要对进行Session序列化/反序列化操作,利用对象输入输出流进行读写,对象输入输出流需要包装成字节数组输入输出流;
Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;
出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler
;
Shiro 也提供了使用Quartz会话验证调度器: QuartzSessionValidationScheduler
Shiro 内部相应的组件(DefaultSecurityManager
)会自动检测相应的对象(如Realm)是否实现了 CacheManagerAware
接口,并自动注入相应的 CacheManager
;就是说如果实现了这个接口的话,Shiro会自动将CacheManager注入给这个对象,这时这个对象就可以使用缓存了;
Realm实现的接口中,就实现了CacheManager
这个接口了,所以Realm是有缓存的;缓存的实际意义是:用户登录的时候,会进入到授权的方法,再点登录的时候,如果使用了缓存,就不再进授权的方法了,不使用缓存的话,每次点击登录都会授权一次;
Shiro 提供了 CachingRealm
,其实现了 CacheManagerAware
接口,提供了缓存的一些基础实现;
• AuthenticatingRealm 及 AuthorizingRealm 也分别提供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓 存;
在配置Realm的时候,可以设置一些缓存的属性:指定缓存的名字、指定是否使用缓存等;可以在ehcache.xml
中分别为授权、认证设置不同的缓存策略;
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)
登录的,如 果是才放行,否则会跳转到登录页面叫你重新登录;
实现:
user
过滤器:用户拦截器,用户已经身份认证
,或是通过记住我
登录的,都可以,/** = user
;
(1)在登录页面添加“记住我”checkbox,checkbox的值会传给Controller,在Controller的方法中判断传过来的值是否为空,若不为空,则设置token.setRememberMe(true);
,若为空,就不用设置这个了;
(2)设置“记住我”的时间:
......
如果要自己做RememeberMe
,需要在登录之前这样创建Token: UsernamePasswordToken(用户名,密码,是否记住我)
,且调用 UsernamePasswordToken 的token.setRememberMe(true);
方法;
<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-4.0.xsd ">
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
list>
property>
<property name="sessionManager" ref="sessionManager">property>
// <property name="rememberMeManager.cookie.maxAge" value="10"/>
<property name="rememberMeManager" ref="rememberMeManager"/>
bean>
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
<property name="globalSessionTimeout" value="1800000"/>
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionDAO" ref="sessionDAO"/>
bean>
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="2592000"/>
bean>
<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>
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
<bean id="sessionDAO" class="com.atguigu.shiro.realms.MySessionDao">
<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
bean>
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
bean>
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
property>
bean>
<bean id="jdbcRealm" class="com.zxj.shiro.realms.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="1024"/> // 加密次数
bean>
property>
bean>
<bean id="secondRealm" class="com.atguigu.shiro.realms.SecondRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="SHA1"/>
<property name="hashIterations" value="1024"/>
bean>
property>
bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.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>
<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 name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/login = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
# everything else requires authentication:
/** = authc
value>
property>
bean>
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"/>
<bean id="filterChainDefinitionMapBuilder" class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder">bean>
<bean id="shiroService" class="com.atguigu.shiro.services.ShiroService">bean>
beans>
public class MySessionDao extends EnterpriseCacheSessionDAO {
@Autowired
private JdbcTemplate jdbcTemplate = null;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
String sql = "insert into sessions(id, session) values(?,?)";
jdbcTemplate.update(sql, sessionId,SerializableUtils.serialize(session));
return session.getId();
}
@Override
protected Session doReadSession(Serializable sessionId) {
String sql = "select session from sessions where id=?";
List<String> sessionStrList = jdbcTemplate.queryForList(sql,String.class, sessionId);
if (sessionStrList.size() == 0) return null;
return SerializableUtils.deserialize(sessionStrList.get(0));
}
@Override
protected void doUpdate(Session session) {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
String sql = "update sessions set session=? where id=?";
jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId());
}
@Override
protected void doDelete(Session session) {
String sql = "delete from sessions where id=?";
jdbcTemplate.update(sql, session.getId());
}
}
public class SerializableUtils {
public static String serialize(Session session) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(session);
return Base64.encodeToString(bos.toByteArray());
} catch (Exception e) {
throw new RuntimeException("serialize session error", e);
}
}
public static Session deserialize(String sessionStr) {
try {
ByteArrayInputStream bis = new ByteArrayInputStream(
Base64.decode(sessionStr));
ObjectInputStream ois = new ObjectInputStream(bis);
return (Session) ois.readObject();
} catch (Exception e) {
throw new RuntimeException("deserialize session error", e);
}
}
}