Shiro

Shiro介绍

​ Apache Shiro(发音为“shee-roh”,日语中“城堡”的意思)是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理,可用于保护任何应用程序 -从命令行应用程序、移动应用程序到最大的 Web 和企业应用程序。

Shiro 提供应用程序安全 API 来执行以下方面(我喜欢将它们称为应用程序安全的 4 个基石):

  • 身份验证 - 证明用户身份,通常称为用户“登录”。
  • 授权-访问控制
  • 密码学 - 保护或隐藏数据免遭窥探
  • 会话管理 - 每个用户的时间敏感状态

Shiro 还支持一些辅助功能,例如 Web 应用程序安全性、单元测试和多线程支持,但这些功能的存在是为了加强上述四个主要问题。

官网QuickStart

  1. Tutorial.java
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Tutorial {

private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);

public static void main(String[] args) {
log.info("My First Apache Shiro Application");

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

//1. 获取当前的用户对象Subject
Subject currentUser = SecurityUtils.getSubject();

// Do some stuff with a Session (no need for a web or EJB container!!!)

//2. 通过当前用户拿到session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}

// let's login the current user so we can check against roles and permissions:
//3. 判断当前的用户是被认证
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true); //设置记住我

try {
currentUser.login(token); //执行了登录操作(暂时看不到 )
} catch (UnknownAccountException uae) {
//未知的账户
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
//
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}

//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

//test a role:
//4. 测试当前用户是否有角色
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}

//test a typed permission (not instance-level)
//粗粒度 (暂时为止)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//细粒度
/**
* 这些权限都是在shiro.ini中的
* admin = *
* schwartz = lightsaber:*
* goodguy = winnebago:drive:eagle5
*/
//是否拥有更高的权限
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}

//all done - log out!注销
currentUser.logout();

//结束
System.exit(0);
}
}

我们当前使用的shiro很多的用法都可以从QuickStart中了解到

  1. shiro.ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

实现

导入依赖

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!--shiro重点依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<!--shiro 和前端的交互-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>



<!--基础开发依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>



<!--sql驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

项目实现背景介绍

 我们日常使用的所有应用程序都存在着很多不安全的问题,为了解决这些问题,我们学习SpringSecurity、shiro等技术,为实现密码安全问题,我们会使用md5,md5盐值加密....一系列操作

​ 在应用程序中,为了更好的盈利,我们会将普通用户和会员用户进行区分,同时,对于两者之间展现的页面,功能也会有所不同。那么如何实现这种不同

​ 本次练习项目就会通过shiro来实现这些操作

功能实现介绍

  • vip1身份的用户会展示有关vip1的界面,以及普通用户界面
  • vip2身份的用户会展示有关vip1的界面,以及普通用户界面
  • 普通用户只会展示普通用户界面
  • 登录、退出
  • 提示用户名或者密码错误
  • ….

数据库信息展示

image

未登录界面展示

image

登录及其错误提示

image

vip1用户所在页面展示

image

普通用户页面展示

image

关键代码详解

首先,我们导入以来完成后,需要进行配置自定义配置shiro类,同时用@Configuration注解标注

1
2
3
4
5
6
7
@Configuration
//自定义配置类,实现shiro的配置
public class shiroConfig{
//1. shiroFilterFactoryBean
//2. DefaultWebSecurityManager 安全管理器(关联Realm)
//3. 创建realm对象
}

在shiro配置类中有三个内置类是十分重要的,他们分别是

  • ShiroFilterFactoryBean
  • DefaultWebSecurityManager
  • realm对象

他们决定了shiro的工作机制及流程

image

如图所示

Realm :

Realm : 作为接收需要接受的安全数据的对象,起着与我们底层的数据交互的作用,它通过封装安全数据,然后放入在Spring的容器中

1
2
3
4
5
//3. 创建realm对象,需要自定义类,然后交给spring托管(放到bean中)
@Bean(name = "userRealm")
public UserRealm userRealm(){
return new UserRealm();
}

DefaultWebSecurityManager :

DefaultWebSecurityManager : 作为安全管理员,他的作用就是来关联我们放置在容器中Realm对象,同时,将自身再封装成为Bean交给spring容器来托管,然后等待ShiroFilterFactoryBean的调用 。可以看出他是shiro的核心,相当于我们springMVC中的DispatcherServlet

1
2
3
4
5
6
7
8
9
10
//2. DefaultWebSecurityManager 安全管理器(关联Realm)
//@Qualifier("userRealm")中的内容就是下面第三步中的@Bean中name属性
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//用securityManager来关联realm
securityManager.setRealm(userRealm);
//因为我们第三步已经将userRealm交给了spring接管,所以需要传参数来得到,而不是直接new
return securityManager;
}

ShiroFilterFactoryBean :

​ **ShiroFilterFactoryBean : 作用就是继续关联安全管理器DefaultWebSecurityManager,然后同时将自己也封装成bean **

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
//1. shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//关联DefaultWebSecurityManager安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
/**
* 添加内置过滤器
* ● anon : 无需认证即可访问
* ● authc :必须认证才能访问
* ● user :必须拥有记住我才能访问
* ● perms : 拥有对某个资源的权限才能访问
* ● role : 拥有某个角色权限才能访问
*/
//拦截的请求-----------------------------------
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/vipFirst","perms[vip1]");
filterMap.put("/vipSecond","perms[vip2]");

bean.setFilterChainDefinitionMap(filterMap);//他的参数是从map集合中拿的,所以需要提前设置一个集合
//如果没有权限,设置跳转页面(登录的请求)
bean.setLoginUrl("/login");
//未授权的请求
//bean.setUnauthorizedUrl("");

//-------------------------------------------
return bean;
}

他的拦截请求会通过一个map集合来操作,但是shiro也存在很多的内置过滤器 , 通过这些过滤器,我们就可以实现请求过滤操作

 * ● anon : 无需认证即可访问
 * ● authc :必须认证才能访问
 * ● user :必须拥有记住我才能访问
 * ● perms  : 拥有对某个资源的权限才能访问
 * ● role  : 拥有某个角色权限才能访问
 

​ 在ShiroFilterFactoryBean中我们可以做很多需要的操作比如:

  • 请求的拦截
  • 未授权用户页面的跳转
  • ….

上述三者的执行顺序虽然是从:S - > D - > R (简写)

但是我们在按逻辑写的时候确实 从 : R- > D -> S

对外核心Subject

与应用程序的代码直接进行交互的对象就是Subject, 它代表的是当前用户,这个用户不仅仅值得是一个具体的人,而是与当前应用交互的任何事物。与Subject进行交互,他就会将所有的东西全都委托给我们的安全管理员(DefaultWebSecurityManager ),他才是真正的执行者。

对内核心Realm

为什么这里我将他作为对内核心,因为我们所有需要进行安全操作的事情都在他的实现类中完成

1
2
3
4
5
6
7
8
//3. 创建realm对象
//实现的两个方法就是 springsecurity中授权和认证

public class UserRealm extends AuthorizingRealm {
//认证
// ....
//授权
}

比如:

  • 对用户进行授权
  • 认证
  • 密码保护

认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("===> 执行了认证方法");
//获取当前的令牌,及其其中的信息
UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

// 获取当前的用户 , 封装用户的登录数据(在controller的登录方法中实现)
User user = userService.selectUser(userToken.getUsername());

//判断是否与数据库中的相同,如果不相同
if (!user.getUsername().equals(userToken.getUsername())){
return null; //就会抛出异常
}
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
session.setAttribute("loginUser",user);
//密码认证, shrio来帮助我们实现
AuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),user.getDept());
return info;
}

授权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("===> 执行了授权方法");
//执行授权的功能
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//将用户所具有的权限存放在数据库中,然后登录时获取用户的全部信息
Subject subject = SecurityUtils.getSubject();
//在用户进行认证时我们进行这个操作【AuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),user.getDept());】
//通过他我们就可以拿到数据
User user = (User) subject.getPrincipal();
//设置当前用户的权限
authorizationInfo.addStringPermission(user.getDept());
//authorizationInfo.addRole("vip1");

return authorizationInfo;
}