1、基本介绍
1.1、Shiro 架构:
Subject:任何可以与应用交互的用户
SecurityManager :是 Shiro 的核心
所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进 行认证、授权、会话及缓存的管理
Authenticator:负责 Subject 认证,可以自定义实现;可以使用认证策略( Authentication Strategy),即什么情况下算用户认证通过了
Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的 Realm;
SessionManager:管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web环境,也可以用在如普通的 JavaSE 环境
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块, Shiro 提高了一些常见的加密组件用于如密码加密/解密。
1.2、登录认证
在 shiro 中,用户需要提供 principals(身份)和credentials(证明)给 shiro,从而应用能验证用户身份:
- principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名 / 邮箱 / 手机号
- credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等
- 最常见的 principals 和 credentials 组合就是用户名/密码
登录认证基本流程:
- 收集用户身份/凭证,即如用户名 / 密码
- 用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功
- 创建自定义的 Realm 类,继承 org.apache.shiro.realm.AuthenticatingRealm 类,实现 doGetAuthenticationInfo() 方法
自定义登录认证:
- Shiro 默认的登录认证是不带加密的,如果想要实现加密认证需要自定义登录认证,自定义 Realm。
public class MyRealm extends AuthenticatingRealm {
//自定义的登录认证方法, Shiro 的 login 方法底层会调用该类的认证方法完成登录认证
//需要配置自定义的 realm 生效,在 ini 文件中配置,或 Springboot 中配置
//该方法只是获取进行对比的信息,认证逻辑还是按照 Shiro 的底层认证逻辑完成认证
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//1 获取身份信息
String principal = authenticationToken.getPrincipal().toString();
//2 获取凭证信息
String password = new String((char[])
authenticationToken.getCredentials());
System.out.println("认证用户信息: "+principal+"---"+password);
//3 获取数据库中存储的用户信息
if(principal.equals("zhangsan")){
//3.1 数据库存储的加盐迭代 3 次密码
String pwdInfo = "7174f64b13022acd3c56e2781e098a5f";
//3.2 创建封装了校验逻辑的对象,将要比较的数据给该对象
AuthenticationInfo info = new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),
pwdInfo,
ByteSource.Util.bytes("salt"),
authenticationToken.getPrincipal().toString());
return info;
}
return null;
}
}
在shiro.ini中添加配置信息
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
myrealm=com.atguigu.shirotest.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm
[users]
zhangsan=7174f64b13022acd3c56e2781e098a5f,role1,
role2
lisi=l4
[roles]
role1=user:insert,user:select
1.3、角色授权
在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限 (Permission)、角色(Role)。-
主体(Subject):
访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只
有授权 后才允许访问相应的资源。资源(Resource)) 在应用中用户可以访问的 URL,查看 / 编辑
某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。 即权限表示在应用中用户能不能访问某个资源,(即很多时候都是CRUD(增查改删)式权
限控 制)等。权限代表了用户有没有操作某个资源的权利Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限, 即实例级别的)
角色(Role): 权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有 一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、 CTO、开发工程师等 都是角色,不同的角色拥有一组不同的权限
2、Shiro 与 SprintBoot 整合
相关依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
2.1、自定义 realm
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//自定义授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection
principalCollection) {
return null;
}
//自定义登录认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1 获取用户身份信息
String name = token.getPrincipal().toString();
//2 调用业务层获取用户信息(数据库中)
User user = userService.getUserInfoByName(name);
//3 判断并将数据完成封装
if(user!=null){
AuthenticationInfo info = new SimpleAuthenticationInfo(
token.getPrincipal(),
user.getPwd(),
ByteSource.Util.bytes("salt"),
token.getPrincipal().toString()
);
return info;
}
return null;
}
}
编写配置类
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm myRealm;
//配置 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1 创建 defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new
DefaultWebSecurityManager();
//2 创建加密对象,并设置相关属性
HashedCredentialsMatcher matcher = new
HashedCredentialsMatcher();
//2.1 采用 md5 加密
matcher.setHashAlgorithmName("md5");
//2.2 迭代加密次数
matcher.setHashIterations(3);
//3 将加密对象存储到 myRealm 中
myRealm.setCredentialsMatcher(matcher);
//4 将 myRealm 存入 defaultWebSecurityManager 对象
defaultWebSecurityManager.setRealm(myRealm);
//5 返回
return defaultWebSecurityManager;
}
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition
shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new
DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/login","anon");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
return definition;
}
2.2、多个 realm
当应用程序配置多个 Realm 时,例如:用户名密码校验、手机号验证码校验等等。Shiro 的 ModularRealmAuthenticator 会使用内部的 AuthenticationStrategy 组件判断认证是成功还是失败
AuthenticationStrategy 是一个无状态的组件,它在身份验证尝试中被询问 4 次(这 4 次交互所需的任何必要的状态将被作为方法参数):
- 在所有 Realm 被调用之前
- 在调用 Realm 的 getAuthenticationInfo 方法之前
- 在调用 Realm 的 getAuthenticationInfo 方法之后
- 在所有 Realm 被调用之后
认证策略的另外一项工作就是聚合所有 Realm 的结果信息封装至一个
AuthenticationInfo 实例中,并将此信息返回,以此作为 Subject 的身份信息
Shiro 中定义了 3 种认证策略的实现:
- AtLeastOneSuccessfulStrategy:只要有一个(或更多)的 Realm 验证成功,那么认证将视为成功
- FirstSuccessfulStrategy:第一个 Realm 验证成功,整体认证将视为成功,且后续 Realm 将被忽略
- AllSuccessfulStrategy:所有 Realm 成功,认证才视为成功
ModularRealmAuthenticator 内置的认证策略默认实现是
AtLeastOneSuccessfulStrategy 方式。可以通过配置修改策略
多个 realm 代码实现:
//配置 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1 创建 defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new
DefaultWebSecurityManager();
//2 创建认证对象,并设置认证策略
ModularRealmAuthenticator modularRealmAuthenticator = new
ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new
AllSuccessfulStrategy());
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator)
;
//3 封装 myRealm 集合
List<Realm> list = new ArrayList<>();
list.add(myRealm);
list.add(myRealm2);
//4 将 myRealm 存入 defaultWebSecurityManager 对象
defaultWebSecurityManager.setRealms(list);
//5 返回
return defaultWebSecurityManager;
}
2.3、remember me
Shiro 提供了记住我(RememberMe)的功能,比如访问一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁, 下次访问时无需再登录即可访问。 (一般会把 RememberMe 的 Cookie 写到客户端并保存下来; )
代码实现:
/*
* 配置类
*/
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm myRealm;
//配置 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1 创建 defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new
DefaultWebSecurityManager();
//2 创建加密对象,并设置相关属性
HashedCredentialsMatcher matcher = new
HashedCredentialsMatcher();
//2.1 采用 md5 加密
matcher.setHashAlgorithmName("md5");
//2.2 迭代加密次数
matcher.setHashIterations(3);
//3 将加密对象存储到 myRealm 中
myRealm.setCredentialsMatcher(matcher);
//4 将 myRealm 存入 defaultWebSecurityManager 对象
defaultWebSecurityManager.setRealm(myRealm);
//4.5 设置 rememberMe
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
//5 返回
return defaultWebSecurityManager;
}
//cookie 属性设置
public SimpleCookie rememberMeCookie(){
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置跨域
//cookie.setDomain(domain);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}
//创建 Shiro 的 cookie 管理对象
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new
CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition
shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new
DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/myController/login","anon");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
//添加存在用户的过滤器(rememberMe)
definition.addPathDefinition("/**","user");
return definition;
}
/*
* controller
*/
//登录认证
@GetMapping("userLogin")
public String userLogin(String name, String pwd,@RequestParam(defaultValue =
"false")boolean rememberMe, HttpSession session){
//1 获取 Subject 对象
Subject subject = SecurityUtils.getSubject();
//2 封装请求数据到 token 对象中
AuthenticationToken token = new UsernamePasswordToken(name,pwd,rememberMe);
//3 调用 login 方法进行登录认证
try {
subject.login(token);
session.setAttribute("user",token.getPrincipal().toString());
return "main";
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登录失败");
return "登录失败";
}
}
2.4、授权、角色认证
用户登录后, 需要验证是否具有指定角色指定权限。
通过 Realm 的 doGetAuthorizationInfo 方法进行判断。触发权限判断的有两种方式
- 在页面中通过shiro:****属性判断
- 在接口服务中通过注解@Requires****进行判断
后端接口服务注解
@RequiresAuthentication:验证用户是否登录,等同于方法subject.isAuthenticated()
@RequiresUser:
验证用户是否被记忆:
登录认证成功 subject.isAuthenticated() 为 true
登录后被记忆 subject.isRemembered() 为 true
@RequiresGuest:验证 subject 是否有相应角色,有角色访问方法,没有则会抛出异常 AuthorizationException
@RequiresRoles:
例如: @RequiresRoles(“aRoleName”)
void someMethod();
只有 subject 有 aRoleName 角色才能访问方法 someMethod()@RequiresPermissions:
验证 subject 是否有相应权限,有权限访问方法,没有则会抛出异常
AuthorizationException例如: @RequiresPermissions (“file:read”,”wite:aFile.txt”)
void someMethod();
subject 必须同时含有 file:read 和 wite:aFile.txt 权限才能访问方法someMethod()