基本使用

image-20220928090001625
  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份
  • Authorization:权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作(如:验证某个用户是否拥有某个角色)
  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中会话可以是普通 JavaSE 环境,也可以是 Web 环境的
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
  • Web Support:Web支持,可以非常容易的集成到 Web 环境
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
  • Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去
  • Testing:提供测试支持
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
  • Remember Me:记住我,即一次登录后,下次再来的话不用登录

依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--Shiro依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>

登录认证

概念

  • 身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供 email,用户名/密码来证明(在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而验证用户身份)

  • principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可

  • 注意:一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/ 邮箱/手机号

  • credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等(最常见的principals和credentials组合就是用户名/密码)


基本流程

1、收集用户身份/凭证

2、调用 Subject.login 进行登录,如果失败就根据异常提示用户错误信息

3、创建自定义的 Realm 类,继承 org.apache.shiro.realm.AuthenticatingRealm 类,实现 doGetAuthenticationInfo() 方法

先在classpath下创建了shiro.ini文件

image-20221013161110064
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//1、初始化获取SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//2、获取Subject对象
Subject subject = SecurityUtils.getSubject();
//3、创建token对象,web应用用户名密码从页面传递
AuthenticationToken token = new UsernamePasswordToken("zhangsan","z3");
//4、完成登录
try {
subject.login(token);
System.out.println("登录成功");
}
catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在");
}
catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
catch (AuthenticationException e) {
e.printStackTrace();
}

(1)首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
(2)SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份
验证;
(3)Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此
处可以自定义插入自己的实现;
(4)Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份
验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm
身份验证;
(5)Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如
果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序
及策略进行访问


角色授权

概念

1、授权:也叫访问控制,即在应用中控制谁访问哪些资源

2、主体:访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。

3、资源:在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑 某些 数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问

4、权限:代表了用户有没有操作某个资源的权利。

5、角色:权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。不同的角色拥有一组不同的权限


示例

在ini文件中先进行定义

image-20221013164621783
1
2
3
4
5
6
7
8
//判断角色
boolean hasRole = subject.hasRole("role1");
System.out.println("是否拥有此角色 = " + hasRole);
//判断权限
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有此权限 = " + permitted);
//也可以用checkPermission方法,但没有返回值,没权限抛AuthenticationException
subject.checkPermission("user:select");

加密示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
//密码明文
String password = "z3";
//使用md5加密
Md5Hash md5Hash = new Md5Hash(password);
System.out.println("md5加密 = " + md5Hash.toHex());
//带盐的md5加密
Md5Hash md5Hash2 = new Md5Hash(password,"salt");
System.out.println("带盐的md5加密 = " + md5Hash2.toHex());
//为了保证安全,避免被破解还可以多次迭代加密,保证数据安全
Md5Hash md5Hash3 = new Md5Hash(password,"salt",3);
System.out.println("md5带盐的3次加密 = " + md5Hash3.toHex());
//也使用父类进行加密
SimpleHash simpleHash = new SimpleHash("MD5",password,"salt",3);
System.out.println("父类带盐的3次加密 = " + simpleHash.toHex());
}

自定义登录认证

Shiro 默认的登录认证是不带加密的,如果想要实现加密认证需要自定义登录认证, 自定义Realm

通过继承AuthenticatingRealm类,重写doGetAuthenticationInfo方法进行自定义操作

注意:shiro的login方法底层会调用该类的认证方法进行认证,需要配置自定义的realm生效,在ini文件中配置,在Springboot中配置。该方法只是获取进行对比的信息,认证逻辑还是按照shiro底层认证逻辑完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyRealm extends AuthenticatingRealm {
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次迭代的密码
String pwdInfo = "7174f64b13022acd3c56e2781e098a5f";
/**
* 4、创建封装校验逻辑对象,封装数据返回
* 参数一:身份信息
* 参数二:密码信息
* 参数三:盐信息
* 参数四:realmName
*/
AuthenticationInfo info = new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),
pwdInfo,
ByteSource.Util.bytes("salt"),
authenticationToken.getPrincipal().toString()
);
return info;
}
return null;
}
}

结合springboot实现登录认证

1、首先创建出mapper,entity,service,并设计好数据库

image-20221013213528729 image-20221013213758830

2、创建controller,以便进行测试

image-20221013214551412

3、创建自定义Realm

1)继承AuthorizingRealm类,重写doGetAuthorizationInfo方法(一个是自定义授权方法,一个是自定义登录认证方法)

2)在自定义登录认证方法中获取用户身份信息

3)从数据库中获取用户信息,并将数据封装到SimpleAuthenticationInfo类中

4)最后返回该类即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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;
}
}

4、创建config配置类,配置 SecurityManager

1)创建defaultWebSecurityManager对象

2)创建加密对象,设置属性

3)将加密对象存储到 myRealm 中

4)将MyRealm存储到defaultWebSecurityManager对象中并返回

5、配置 Shiro 内置过滤器拦截范围

1)创建DefaultShiroFilterChainDefinition对象

2)调用addPathDefinition方法设置不认证就可以进行访问的资源

3)调用addPathDefinition方法设置需要进行登录认证的拦截范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@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;
}
}

多个realm实现原理(*)

配置多个realm,Shiro会使用内部的 AuthenticationStrategy 组件判断认证是成功还是失败

AuthenticationStrategy 在身份验证尝试中被询问4次

1、在所有 Realm 被调用之前

2、在调用 Realm 的 getAuthenticationInfo 方法之前

3、在调用 Realm 的 getAuthenticationInfo 方法之后

4、在所有 Realm 被调用之后


Remember-Me功能

1、在登录接口额外记录一个参数

image-20221013224956521

2、设置cookie 属性

1
2
3
4
5
6
7
8
9
public SimpleCookie rememberMeCookie(){
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置跨域
//cookie.setDomain(domain);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}

3、创建 Shiro 的 cookie 管理对象

1
2
3
4
5
6
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}

4、在config配置SecurityManager时,设置 rememberMe

5、添加存在用户的过滤器

image-20221013230639582

登录认证后登出

image-20221013231343571 image-20221013231247886

授权,角色登录

后端接口服务注解

@RequiresAuthentication 验证用户是否登录,等同于方法subject.isAuthenticated()

@RequiresUser 验证用户是否被记忆: 登录认证成功subject.isAuthenticated()为true 登录后被记忆subject.isRemembered()为true

@RequiresGuest 验证是否是一个guest的请求,是否是游客的请求 此时subject.getPrincipal()为null

@RequiresRoles 验证subject是否有相应角色,有角色访问方法,没有则会抛出异常AuthorizationException。

@RequiresPermissions 验证subject是否有相应权限,访问方法,没有则会抛出异常AuthorizationException


角色认证

1
2
3
4
5
6
7
8
//登录认证验证角色
@RequiresRoles("admin")
@GetMapping("userLoginRoles")
@ResponseBody
public String userLoginRoles(){
System.out.println("登录认证验证角色");
return "验证角色成功";
}
image-20221014104625974 image-20221014105034118 image-20221014105014501

在自定义授权方法内,存储当前登录用户的权限和角色,此时再次进行权限认证时,认证成功

image-20221014105741185

权限认证

创建数据库权限表和角色权限映射表

image-20221014200134469

修改Myrealm类的自定义授权方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected AuthorizationInfo
doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("进入自定义授权方法");
//获取当前用户身份信息
String principal = principalCollection.getPrimaryPrincipal().toString();
//调用接口方法获取用户的角色信息
List<String> roles = userService.getUserRoleInfo(principal);
System.out.println("当前用户角色信息:"+roles);
//创建对象,存储当前登录的用户的权限和角色
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//存储角色
info.addRoles(roles);
//返回
return info;
}

添加controller的认证验证权限

1
2
3
4
5
6
7
@RequiresPermissions("user:delete")
@GetMapping("userPermissions")
@ResponseBody
public String userLoginPermissions(){
System.out.println("登录认证验证权限");
return "验证权限成功";
}

Shiro整合EhCache

1、引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

2、添加相关的配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="ehcache" updateCheck="false">
<!--磁盘的缓存位置-->
<diskStore path="java.io.tmpdir"/>
<!--默认缓存-->
<defaultCache
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="false">
</defaultCache>
<!--登录认证信息缓存:缓存用户角色权限-->
<cache name="loginRolePsCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true"/>
</ehcache>

3、修改配置类 ShiroConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获取缓存管理器
public EhCacheManager getEhCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
InputStream is = null;
try {
is = ResourceUtils.getInputStreamForPath(
"classpath:ehcache/ehcache-shiro.xml");
} catch (IOException e) {
e.printStackTrace();
}
CacheManager cacheManager = new CacheManager(is);
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}

并在配置SecurityManager中,设置缓存管理器

image-20221014204657355

会话管理

SessionManager

会话管理器,负责创建和管理用户的会话(Session)生命周期,它能够在任何环境中 在本地管理用户会话,即使没有Web/Servlet/EJB容器,也一样可以保存会话


会话管理实现

SessionManager由SecurityManager管理。Shiro提供了三种实现

image-20221014204904753

DefaultSessionManager:用于JavaSE环境

ServletContainerSessionManager:用于web环境,直接使用Servlet容器的会话

DefaultWebSessionManager:用于web环境,自己维护会话(不使用Servlet容器的 会话管理)


获取Session的方式

1
2
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute(“key”,”value”)

注意:Controller 中的 request,在 shiro 过滤器中的 doFilerInternal 方法,被包装成 ShiroHttpServletRequest

SecurityManager和SessionManager会话管理器决定session来源于ServletRequest还是由Shiro管理的会话

无论是通过 request.getSession 或 subject.getSession 获取到 session,操作 session,两者都是等价的