Java 第三方包-Apache Shiro

wiki

1. 核心名词

  • subject : 可以是当前操作用户,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物,即当前跟软件交互的东西
  • securityManager : 典型的 Facade 模式,它管理内部组件实例,并通过它来提供安全管理的各种服务
  • realm : 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”,即当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。Realm 实质上是一个安全相关的 DAO,它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。当配置 Shiro 时,你必须至少指定一个Realm,用于认证和(或)授权。

2. 实战

1. 添加依赖

1
2
3
4
5
6
<!-- Shiro 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>

2. 配置 Shiro

配合 Spring 使用

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
/**
* Spring 配置类.
*
*/
@Configuration
public class ShiroConfig {

/**
* 配置 Shiro 的安全管理器.
*/
@Bean
public SecurityManager securityManager(Realm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置一个 Realm,这个 Realm 是最终用于完成我们的认证号和授权操作的具体对象
securityManager.setRealm(myRealm);
// 也可以使用配置文件
// securityManager.setRealm(iniRealm);
return securityManager;
}

/**
* 读取配置文件
*/
// @Bean
// public IniRealm getIniRealm(){
// IniRealm iniRealm=new IniRealm("classpath:shiro.ini");
// return iniRealm;
// }

/**
* 配置一个自定义的 Realm 的 bean,最终将使用这个 bean 返回的对象来完成我们的认证和授权
*/
@Bean
public MyRealm myRealm(){
MyRealm myRealm = new MyRealm();
return myRealm;
}

/**
* 配置一个 Shiro 的过滤器 bean,它将配置 Shiro 相关的一个规则的拦截
* 如什么样的请求可以访问,什么样的请求不可以访问等等
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
// 创建 Shiro 的拦截器 ,用于拦截用户请求
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();

// 设置 Shiro 的安全管理,设置管理的同时也会指定某个 Realm 用来完成权限分配
shiroFilter.setSecurityManager(securityManager);

//设置 Shiro 的拦截规则
// 用于设置一个登录的请求地址,这个地址可以是一个 html 或 jsp 的访问路径,也可以是一个控制器的路径
// 作用是通知 Shiro 我们可以使用这里路径转向到登录页面,但 Shiro 判断到我们当前的用户没有登录时就会自动转换到这个路径要求用户完成
shiroFilter.setLoginUrl("/login.html");

// 登录成功后转向页面,由于用户的登录后需要交给 Shiro 完成,因此就需要通知 Shiro 登录成功之后返回到那个位置
shiroFilter.setSuccessUrl("/success");

// 用于指定没有权限的页面,当用户访问某个功能时,如果 Shiro 判断这个用户没有对应的操作权限,那么就会将请求转向到这个位置,用于提示用户没有操作权限
shiroFilter.setUnauthorizedUrl("/noPermission");

// 定义一个集合存放规则,用于设置通知 Shiro 什么样的请求可以访问,什么样的请求不可以访问
Map<String,String> filterChainMap = new LinkedHashMap<String,String>();

/**
/login 表示某个请求的名字
/admin/** 表示一个请求名字的通配, 以 admin 开头的任意子路径下的所有请求
anon 表示可以使用游客进行登录(不需要登录)
authc 表示这个请求需要进行认证(登录),只有认证(登录)通过才能访问
注:
** 表示任意子路径
* 表示任意的一个路径
? 表示任意的一个字符
*/
filterChainMap.put("/login","anon");
filterChainMap.put("/admin/**","authc");
filterChainMap.put("/user/**","authc");
filterChainMap.put("/**","authc");

shiroFilter.setFilterChainDefinitionMap(filterChainMap);
return shiroFilter;
}
}

3. 自定义 Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 自定义 Realm,用来实现用户的认证和授权
*/
public class MyRealm implements Realm {

@Override
public String getName() {
return null;
}

@Override
public boolean supports(AuthenticationToken authenticationToken) {
return false;
}

@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}

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
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
/**
* 自定义Realm,用来实现用户的认证和授权
* AuthenticatingRealm 只负责认证(登录)的 Realm实现父类
*/
public class MyAuthenticatingRealm extends AuthenticatingRealm {
/**
* Shiro 的认证方法,我们需要在这个方法中来获取用户的信息.
*
* @param authenticationToken 用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
* @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证 密码是由Shiro进行验证是否合法
* @throws AuthenticationException 如果认证失败,Shiro就会抛出AuthenticationException 也可以手动抛出这个AuthenticationException
* 以及它的任意子异常类,不同的异常类型对应认证过程中的不同错误情况,我们需要根据异常类型来为用户返回特定的响应数据
* AuthenticationException 异常的子类 可以自己抛出
* AccountException 账号异常 可以自己抛出
* UnknownAccountException 账号不存在的异常 可以自己抛出
* LockedAccountException 账号异常锁定异常 可以自己抛出
* IncorrectCredentialsException 密码错误异常(这个异常会在Shiro进行密码验证时抛出)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 将 AuthenticationToken 强转成 UsernamePasswordToken ,这样获取账号和密码更加方便
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

// 获取用户在浏览器中输入的账号
String userName = token.getUsername();

// 认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
String dbUserName = userName;

if(!"admin".equals(dbUserName) && !"zhangsan".equals(dbUserName)) { //判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}
if("zhangsan".equals(userName)){
throw new LockedAccountException("账号被锁定");
}
//定义一个密码(这个密码应该来自数据库)
String dbpassword = "123456";

/**
* 数据密码加密主要是防止数据在浏览器访问后台服务器之间进行数据传递时被篡改或被截获,因此应该在前端到后台的过程中
* 进行加密,而这里的加密方式是将浏览器中获取后台的明码加密和对数据库中的数据进行加密
* 这就丢失了数据加密的意义 因此不建议在这里进行加密,应该在页面传递传递时进行加密
* 注:
* 建议浏览器传递数据时就加密数据,数据库中存在的数据也是加密数据,必须保证前端传递的数据
* 和数据主库中存放的数据加密次数以及盐规则都是完全相同的,否则认证失败
*/
//设置让当前登录用户中的密码数据进行加密
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1);
this.setCredentialsMatcher(credentialsMatcher);

/**
* 密码加密
* 参数 1 为加密算法,我们选择MD5加密
* 参数 2 为被加密的数据的数据
* 参数 3 为加密时的盐值 ,用于改变加密后数据结果,通常这个盐值需要选择一个表中唯一的数据,如表中的账号
* 参数 4 为需要对数据使用指定的算法加密多少次
*/
//对数据库中的密码进行加密
Object obj = new SimpleHash("MD5",dbpassword,"",1);

/**
* 创建密码认证对象,由Shiro自动认证密码
* 参数1 数据库中的账号(页面账号也可)
* 参数2 数据库中的密码
* 参数3 当前Relam的名字
* 如果密码认证成功,会返回一个用户身份对象;如果密码验证失败则抛出异常
*/
//认证密码是否正确
return new SimpleAuthenticationInfo(dbUserName,obj.toString(),getName());
}
}

5. 使用

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
// 创建一个 Shiro 的 Subject 对象,利用这个对象来完成用户的登录认证
Subject subject = SecurityUtils.getSubject();

//判断当前用户是否已经认证过,如果已经认证过着不需要认证;如果没有认证过则完成认证
if(!subject.isAuthenticated()) {
//创建一个用户账号和密码的Token对象,并设置用户输入的账号和密码,这个对象将在 Shiro 中被获取
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

try {
// 调用login后,Shiro就会自动执行自定义的Realm中的认证方法
subject.login(token);
// 如账号不存在或密码错误等,需要根据不同的异常类型来判断用户的登录状态,并给予友好的信息提示
} catch (UnknownAccountException e) {
// 表示用户的账号错误,这个异常是在后台抛出
System.out.println("---------------账号不存在");
}catch (LockedAccountException e){
// 表示用户的账号被锁定,这个异常是在后台抛出
System.out.println("===============账号被锁定");
}catch (IncorrectCredentialsException e){
// 表示用户的密码错误,这个异常是 shiro 在认证密码时抛出
System.out.println("***************密码不匹配");
}
}

//登出方法调用,用于清空登录时的缓存信息,否则无法重复登录
subject.logout();

6. 基于代码的权限控制

判断用户哪个权限是否可以使用,可以先为用户分配权限

继承 Realm 的实现一个类 AuthorizingRealm,并添加角色信息

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
/**
* 自定义 Realm,用来实现用户的认证和授权
* AuthenticatingRealm 只负责认证(登录)的Realm
* AuthorizingRealm 负责认证(登录)和授权 的Realm父类
*/
public class MyAuthRealm2 extends AuthorizingRealm {

/**
* Shiro 用户授权的回调方法.

* @param principalCollection
* @return
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 从 Shiro 中获取用户名
Object username = principalCollection.getPrimaryPrincipal();

// 创建一个 SimpleAuthorizationInfo 对象,利用这个对象需要设置当前用户的权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

// 创建角色信息的集合
Set<String> roles = new HashSet<String>();

// 增加角色信息,并初始化到 roles 集合中
if ("admin".equals(username)) {
roles.add("admin");
roles.add("user");
} else if ("zhangsan".equals(username)) {
roles.add("user");
}

// 增加权限信息
Set<String> permission = new HashSet<String>();
if ("admin".equals(username)) {
permission.add("admin:add");
}

// 设置角色信息
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permission);
return simpleAuthorizationInfo;
}
}

修改 ShiroConfig

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
@Configuration
public class ShiroConfig {

...

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {

ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setSuccessUrl("/success");
shiroFilter.setUnauthorizedUrl("/noPermission");

Map<String,String> filterChainMap = new LinkedHashMap<String,String>();
filterChainMap.put("/login","anon");
/**
roles[admin] 表示 以 /admin/** 开头的请求需要拥有 admin 角色才可以访问,否则返回没有权限的页面
perms[admin:add] 表示 /admin/test 的请求需要拥有 admin:add 权限才可访问
注:admin:add 仅仅是一个普通的字符串用于标记某个权限功能
*/
filterChainMap.put("/admin/test","authc,perms[admin:add]");
filterChainMap.put("/admin/**","authc,roles[admin]");
filterChainMap.put("/user/**","authc,roles[user]");

shiroFilter.setFilterChainDefinitionMap(filterChainMap);
return shiroFilter;
}
}

6. 基于注解的权限控制

修改 ShiroConfig,此时需要删除拦截器的相关配置,如 filterChainMap

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
@Configuration
public class ShiroConfig {

...

/**
* 开启Shiro注解支持(例如@RequiresRoles()和@RequiresPermissions())
* shiro的注解需要借助Spring的AOP来实现
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}

/**
* 开启AOP的支持
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}

使用

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
@RequiresRoles(value = {"admin"})
@RequestMapping("/admin/test")
public @ResponseBody String adminTest(){
return "这个adminTest请求";
}

//@RequiresPermissions 注解用于指定当前请求必须要拥有指定的权限名字为 admin:add才能访问
//admin:add 只是一个普通的权限名称字符串,表示admin下的add功能
@RequiresPermissions(value = {"admin:add"})
@RequestMapping("/admin/add")
public @ResponseBody String adminAdd(){
Subject subject = SecurityUtils.getSubject();
//验证当前用户是否拥有这个权限
//subject.checkPermission();
//验证当前用户是否拥有这个角色
//subject.checkRole();
return "这个adminAdd请求";
}

//配置一个Spring的异常监控,当工程抛出了value所指定的所以异常类型以后将直接进入到当前方法中
@ExceptionHandler(value = {Exception.class})
public String myError(Throwable throwable){
//获取异常的类型,应该根据不同的异常类型进入到不通的页面显示不同提示信息
System.out.println(throwable.getClass());
System.out.println("---------------------------------");
return "noPermission";
}

7. 无 spring 使用

添加依赖后,可以创建 shiro 的配置文件,以 ini 为后缀的文件

1
2
3
4
5
6
7
8
9
[users]
zhangsan=123456,seller
lisi=123123,dba
admin=admin,admin

[roles]
admin=*
seller=order-add,order-del,order-list
ckmgr=dba-add,dba-del,dba-list

使用

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
public class ShiroDemo {

public static void main(String[] args) {
String username = "";
String password = "";

// 1.创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2.创建 realm
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
// 3.将 realm 设置给安全管理器
securityManager.setRealm(iniRealm);
// 4.将 Realm 设置给 SecurityUtil 工具
SecurityUtils.setSecurityManager(securityManager);
// 5.通过 SecurityUtil 工具类获取 subject 对象
Subject subject = SecurityUtils.getSubject();

// 认证流程
// 将认证账号和密码封装到 token 中
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
// 通过 subject 对象调用 login 方法进行认证
boolean flag = false;
try {
subject.login(token);
flag = true;
} catch (IncorrectCredentialsException e) {
flag = false;
}
System.out.println(flag?"登录成功":"登录失败");

// 授权
// 判断是否有某个角色
System.out.println(subject.hasRole("seller"));

// 判断是否有某个权限
System.out.println(subject.isPermitted("order-del"));
}
}