目录
一、URL 基本配置
二、身份认证
三、身份认证流程
四、AuthenticationStrategy 接口的默认实现
五、授权
六、Permissions 规则(资源标识符)
七、ModularRealmAuthorizer 进行多 Realm 匹配流程
八、JSTL 标签
九、权限注解
十、自定义拦截器
十一、会话管理
十二、缓存:CacheManagerAware 接口
十三、RememberMe
【格式】URL=拦截器[参数],例如:
/login.jsp = anon
■ anno :(anonymous)拦截器表示匿名访问(不登录即可访问);
■ authc:(authentication)拦截器表示需要身份认证后才能访问;
【URL 配置模式使用了Ant 风格模式 】:Ant 路径通配符支持?,*,** 需要注意的是匹配符不包含目录分割符/
■ _?:匹配一个字符。
■ _* :匹配零个或多个字符串。
■ _** :匹配路径中的零个或多个路径。
【注意】:URL 权限采取第一次匹配优先的方式。
在 Shrio 中用户需要提供 principals(身份)和 credentials(证明)给 Shiro,从而能验证用户身份。
基本流程:【1】调用 SecurityManager 获取 subject;
【2】验证当前用户是否登录,subject.isAuthenticated();
【3】若未验证,则将用户名和密码封装为 UsernamePasswordToken 对象;
【4】调用 subject.login(AutenticationToken)方法验证,会调用5中的方法,实现用户认证操作;
【5】自定义 Realm,从数据库中获取用户记录,返回给 Shiro;
1)实际上只需继承 org.apache.shiro.realm.AuthenticatingRealm 类。
2)实现 doGetAuthenticationInfo(AuthenticationToken) 方法。---->方法中的 Token==login 方法传过来的 Token。
【6】由 Shiro 完成密码比对;
因为 Shiro 缓存的原因,当用户 Token 登录成功之后,你回退重新登入 Token 但是密码错误时,发现扔能够登录成功。所以当每次结束时需要出发 logout 命令,简单办法如下:在 Shiro 拦截其中添加如下代码。/shiro/logout==需要发送的URL。
/shiro/logout = logout
【1】首先调用 Subject.login(token) 进行登录,其会委托给 SecurityManager;
【2】securityManager 负责真正的身份验证逻辑,他会委托给 Authenticator 进行身份认证;
【3】Authentication 才是真正的身份验证者,Shrio API 中核心的身份认证入口点,此处可以插入自己的实现;
【4】Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份认证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Real 身份认证;
【5】Authenticator 会把相应的 token 传入 Realm,从 realm 获取身份认证信息,如果没有返回/抛出异常表示认证失败。此处可以配置多个 realm,将按照相应的顺序及策略进行访问;
Realm:Shiro 从 Realm 中获取安全数据(如用户,角色,权限)即 SecurityManager 要验证用户身份,需要从 Realm 中获取相应的用户进行比较以确定用户的身份是否合法;需要从用户中得要相应的角色和权限判断用户是否可以操作。Realm 一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份认证),而他也间接继承了CachingRealm(带有缓存实现)。
Authenticator:职责是验证用户账号,是 Shiro API 中身份认证的核心入口点,如果验证成功。将返回 AuthenticationInfo 验证信息;此信息中包含身份及凭证;如果异常则会抛出相应的 AuthenticationException异常。
【1】FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份认证成功的认证信息,其它忽略;
【2】AtLeastOneSuccessfulStrategy:只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy 不同的是。将返回所有认证成功的 Realm 信息。
【3】AllSuccessfulStrategy:所有 Realm 认证成功才算成功,且返回所有 Realm 身份认证信息,如果一个失败就是失败。
ModularRealmAuthenticator 默认是 AtLeastOneSusccessfulStrategy 策略
修改认证策略:
当用户输入密码时,我们通常会对密码进行加密:在 Shiro 中我们需要对输入的密码加密后认证比较时,只需要在配置文件中配置密码的加密方式:替换当前 Realm 的 credentialsMatcher 属性. 直接使用 HashedCredentialsMatcher 对象, 并设置加密算法即可。
//加密方式
//加密次数
如何使两个原始密码相同的密码,加密后不一样。(MD5盐值加密)我们将用户名看做是唯一的元素。
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null;
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
realmName:当前 realm 对象的 name. 调用父类的 getName() 方法即可:String realmName = getName();
【1】使用 ByteSource.Util.bytes() 来计算盐值;
【2】盐值需要唯一:一般使用随机字符串或 user id;
【3】使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 来计算盐值加密后的密码的值.
String hashAlgorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("user");;
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
有时候我们会将用户密码存入多个库,用不同的加密方式。例如:
当配置多个 Realm 时,需要配置 autenticator 来存放。
最后将 authentication 配置在 SecurityManager 中。
通常我们会将多个 Reals 配置在 SecurityManager 中:(因为系统初始化的时候,会通过认证是否为ModularRealmAuthenticator,如果是则将 Realms 的值赋给 Authencator)
【1】也叫访问控制,即在应用中控制谁访问那些资源(如访问页面、编辑数据、页面操作等)在授权中需要了解基本关键对象:
● 主体(Subject):访问应用的用户;
● 资源(Resource):在应用中用户可以访问的 URL;
● 权限(Permission):在应用中用户能不能访问某个资源;
● 角色(Role):权限的集合;
【2】Shrio 支持三种授权方式:
● 编程式:通过 if/else代码块来完成;
● 注解式:通过在执行的方法上放置相应的注解来完成,没有权限则抛出相应异常。(@RequiresRoles("admin"));
● JSP/GSP:在 JSP/GSP页面通过相应的标签完成。(
Shrio 内置了许多默认拦截器,比如身份认证、授权等相关的。(可参考org.apache.shiro.web.filter.mgt.DefaultFilter中的枚举拦截器)。授权相关拦截器如下:
对象实例ID:即对那个资源的那个实例可以进行什么样的操作。默认支持通配符:(:表示资源/操作/实例的分割)
■ ,表示操作的分割。(user:query, edit)实例访问控制(user:edit:manager)
■ *表示任意资源/操作/实例。(user:*)实例访问使用通配符(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表示授权失败。
授权需要继承 AuthorizingRealm 类,并实现其 doGetAuthorizationInfo 方法。AuthorizingRealm 类继承自 AuthenticatingRealm, 但没有实现 AuthenticatingRealm 中的 doGetAuthenticationInfo, 所以认证和授权只需要继承 AuthorizingRealm 就可以了. 同时实现他的两个抽象方法.
【1】首先检查相应的 Realm 是否实现了实现了 Authorizer;
【2】如果实现了 Authorizer,那么接着调用其相应的 isPermitted*/hasRole* 接口进行匹配;
【3】如果有一个 Realm 匹配那么将返回 true,否则返回 false。
//授权会被 shiro 回调的方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
Object principal = principals.getPrimaryPrincipal();
//2. 利用登录的用户的信息来伪造当前用户的角色或权限(可能需要查询数据库)
Set roles = new HashSet<>();
roles.add("user");
if("admin".equals(principal)){
roles.add("admin");
}
//3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//4. 返回 SimpleAuthorizationInfo 对象.
return info;
}
【1】guest 标签:用户没有登录时显示相应信息,即游客访问信息。
【2】user 标签:用户已经认证/记住我登录之后显示相应信息。
【3】authenticated 标签:用户已经身份认证通过,即shiro.login登录成功,不是记住我登录的。
【4】notAuthenticated 标签:用户未进行身份认证,即没有调用Subject.login登录,包括记住我登录的也输入未认证登录。
【5】pincipal标签:显示用户身份信息,默认调用Subject.getPrincipal()获取,即Primary Principal。
【6】hasRole标签:如果当前Subject有角色,将显示body体内容:
【7】hasAnyRole标签:如果Subject有任意一个角色(或的关系)将显示body中的内容。
【8】lacksRole标签:如果当前Subject没有角色将显示body中的内容。
【9】hasPermission:如果当前Subject有权限将显示body中的内容。
【10】lacksPermission:如果当前Subject没有权限将显示body中的内容
@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(即设置通用数据)、验证码验证、在线用户信息的保存等。
我们一般在获取用户权限的时候,都是从数据库中获取的。所以在配置文件中会通过工厂方式创建出需要的对象。根据代码跟踪,我们发现获取到用户权限的时候会调用 filterChainDefinitionMap 方法,得到我们在配置中配置的所有权限,已LinkedHashMap 的方式收集。所以我们需要重新配置 filterChainDefinitionMap。如下:
创建相应的实体类,创建一个方法用来返回我们需要的 LinkedHashMap 实体。在方法中通过获取数据库,拿到对应用户的角色/权限。
package com.yintong.shiro.factory;
import java.util.LinkedHashMap;
public class FilterChainDefinitionMapBuilder {
public LinkedHashMap buildFilterChainDefinitionMap(){
LinkedHashMap 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;
}
}
Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对Web 的透明支持、SSO 单点登录的支持等特性。
【1】Subject.getSession():即可获取会话;
【2】session.getId():获取当前会话的唯一标识;
【3】session.getHost():获取当前Subject的主机地址;
【4】session.getTimeout() & session.setTimeout(毫秒):获取/设置当前Session的过期时间;
【5】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):设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作
主要特点:web 层的 session 通常在 service 层不能够获取,但是 shiro 的 session 能够在 service 层获取到。
SessionDao 的种类:
【1】AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话ID等;
【2】CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager;
【3】MemorySessionDAO 直接在内存中进行会话维护;
【4】EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用ConcurrentHashMap 保存缓存的会话;
会话验证:
【1】Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;
【2】出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器SessionValidationScheduler;
【3】Shiro 也提供了使用Quartz会话验证调度器:QuartzSessionValidationScheduler;
Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了CacheManagerAware 并自动注入相应的CacheManager。Shiro 提供了 CachingRealm,其实现了CacheManagerAware 接口,提供了缓存的一些基础实现;AuthenticatingRealm 及 AuthorizingRealm 也分别提供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓存。
Session缓存:
【1】如 SecurityManager 实现了 SessionSecurityManager,其会判断 SessionManager 是否实现了CacheManagerAware 接口,如果实现了会把CacheManager 设置给它;
【2】SessionManager 也会判断相应的 SessionDAO(如继承自CachingSessionDAO)是否实现了CacheManagerAware,如果实现了会把 CacheManager设置给它;
【3】设置了缓存的 SessionManager,查询时会先查缓存,如果找不到才查数据库
Shiro 提供了记住我(RememberMe)的功能,基本流程如下:
【1】首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并保存下来;
【2】关闭浏览器再重新打开;会发现浏览器还是记住你的;
【3】访问一般的网页服务器端还是知道你是谁,且能正常访问;
【4】但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。
认证和记住我的区别:
【1】subject.isAuthenticated() 表示用户进行了身份验证登录的,即使有 Subject.login 进行了登录;
【2】subject.isRemembered():表示用户是通过记住我登录的,此时可能并不是真正的你(如你的朋友使用你的电脑,或者你的cookie 被窃取)在访问的;
【3】两者二选一,即 subject.isAuthenticated()==true,则subject.isRemembered()==false;反之一样;
建议:
【1】访问一般网页:如个人在主页之类的,我们使用user 拦截器即可,user 拦截器只要用户登录(isRemembered() || isAuthenticated())过即可访问成功;
【2】访问特殊网页:如我的订单,提交订单页面,我们使用authc 拦截器即可,authc 拦截器会判断用户是否是通过Subject.login(isAuthenticated()==true)登录的,如果是才放行,否则会跳转到登录页面叫你重新登录。
实现:如果要自己做RememeberMe,需要在登录之前这样创建Token:UsernamePasswordToken(用户名,密码,是否记住我),且调用UsernamePasswordToken 的:token.setRememberMe(true); 方法。
默认拦截器 user 表示记住我:表示 list.jsp 可以通过记住我登录后访问。
map.put("/list.jsp", "user");
设置记住我的有效时间:
----关注公众号,获取更多内容----