0%

Shiro基础

什么是Shiro

Shiro是一个强大的简单易用的Java安全框架,主要用来更便捷的认证,授权,加密,会话管理。Shiro首要的和最重要的目标就是容易使用并且容易理解。Shiro官网

Shiro是一个有许多特性的全面的安全框架,下面这幅图可以了解Shiro的特性:

img

可以看出shiro除了基本的认证,授权,会话管理,加密之外,还有许多额外的特性。

Shiro架构

Shiro有三个主要的概念:SubjectSecurityManagerRealms,下面这幅图可以看到这些原件之间的交互。

img

  • Subject:翻译为主角,当前参与应用安全部分的主角。可以是用户,可以试第三方服务,可以是cron 任务,或者任何东西。主要指一个正在与当前软件交互的东西。
    所有Subject都需要SecurityManager,当你与Subject进行交互,这些交互行为实际上被转换为与SecurityManager的交互
  • SecurityManager:安全管理员,Shiro架构的核心,它就像Shiro内部所有原件的保护伞。然而一旦配置了SecurityManager,SecurityManager就用到的比较少,开发者大部分时间都花在Subject上面。
    请记得,当你与Subject进行交互的时候,实际上是SecurityManager在背后帮你举起Subject来做一些安全操作。
  • Realms:Realms作为Shiro和你的应用的连接桥,当需要与安全数据交互的时候,像用户账户,或者访问控制,Shiro就从一个或多个Realms中查找。
    Shiro提供了一些可以直接使用的Realms,如果默认的Realms不能满足你的需求,你也可以定制自己的Realms

img

核心概念

  1. 主体:即访问应用的用户,在Shiro中使用Subject代表用户,用户只有授权后才允许访问相应的资源。
  2. 资源:在应用中用户可以访问的任何东西。比如页面、图片、信息等等的一些数据。
  3. 权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力,即对资源的CRUD。
  4. 角色:代表了操作的集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限。

认证

创建一个maven项目,引入如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--        shiro的核心依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<!-- slf4j的依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.21</version>
<scope>compile</scope>
</dependency>

然后再创建一个子项目shiro_01_Authentication_ini,在资源文件夹下创建一个shiro.ini文件,使用ini文件来模拟数据库。

1
2
3
4
#users表示用户
[users]
zhangsan = 123456
lisi = 123456
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
public class TestAuthenticationApp {
//日志输出工具
private static final transient Logger log = LoggerFactory.getLogger(TestAuthenticationApp.class);
public static void main(String[] args) {
log.info("My First Shiro App");
String password = "123456";
String username = "zhangsan";
//读取ini文件,创建一个安全管理器的工厂对象
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//使用工厂创建安全管理器
SecurityManager securityManager = factory.getInstance();
//把当前的安全管理器绑定到当前线程
SecurityUtils.setSecurityManager(securityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//给主体对象设置用户名和密码
AuthenticationToken token = new UsernamePasswordToken(username, password);
//认证
try{
subject.login(token);
log.info("认证通过");
}catch (AuthenticationException e){
log.info("认证不通过");
}
}
}

其中的setSecurityManager是把当前的安全管理器绑定到当前线程,这样每一个用户的认证操作就对应一个线程,就不会反复地创建线程;

AuthenticationExceptionUnknownAccountException(用户不存在)和IncorrectCredentialsException(密码错误)的父类,所以也可以改成

1
2
3
4
5
6
7
8
9
try {
//用户进行登录
subject.login(token);
System.out.println("认证成功");
}catch (UnknownAccountException e){
System.out.println("用户不存在");
}catch (IncorrectCredentialsException e){
System.out.println("密码不正确");
}

授权

创建一个子项目shiro_02_Authorization_ini,修改ini文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[users]
zhangsan = 123456, role1
lisi = 123456, role2
wangwu = 123456, role3
zhaoliu = 123456, role2, role3
sunqi = 123456, role4
#角色,权限的集合
[roles]
role1 = user:query, user:update, user:delete, user:add, user:export
role2 = user:query, user:add
role3 = user:query, user:export
#表示所有权限
role4 = *:*

可以在上面认证代码后面添加如下代码:

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
//isAuthenticated:用户是否通过验证
System.out.println("是否认证:"+subject.isAuthenticated());
if (subject.isAuthenticated()){
System.out.println("==========权限判断==========");
boolean permitted = subject.isPermitted("user:query");
System.out.println("是否有user:query的权限:"+permitted);
boolean[] permitted1 = subject.isPermitted("user:query", "user:add", "user:export", "user:delete");
for (boolean b : permitted1) {
System.out.println(b);
}
System.out.println("==========角色判断==========");
//判断用户是否有某种角色
boolean role2 = subject.hasRole("role2");
System.out.println("是否有role2的角色:"+role2);

List<String> roles = Arrays.asList("role1", "role2", "role3", "role4");
//判断用户是否有角色集合里的角色
boolean[] hasRoles = subject.hasRoles(roles);
for (boolean hasRole : hasRoles) {
System.out.println(hasRole);
}
}
//用户登出
subject.logout();
System.out.println("是否认证:"+subject.isAuthenticated());

使用自定义的Realm

创建UserRealm,并用Service来模拟从数据库中取值。

realm

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
public class UserRealm extends AuthorizingRealm {
private UserService userService = new UserServiceImpl();
private RoleService roleService = new RoleServiceImpl();
private PermissionService permissionService = new PermissionServiceImpl();

/**
* 认证,当使用login时会调用此逻辑
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//token传入的用户名
String username = token.getPrincipal().toString();
//token传入的密码
Object credentials = token.getCredentials();
System.out.println(username);
System.out.println(credentials);
/**
* shiro是根据用户名把用户对象查询出来。在来做密码的匹配
*/
User user = userService.queryUserByName(username);
//如果查询到用户,则进行认证逻辑
if (user!=null){
List<String> roles = roleService.queryRolesByName(username);
List<String> permissions = permissionService.queryPermissionsByName(username);
ActiveUser activeUser = new ActiveUser(user, roles, permissions);
/**
*进行密码匹配,并将认证后的结果返回
* 参数说明:
* 参数1:可以传入任意对象,一般为用户名或用户对象
* 参数2:从数据库中查询出来的密码
* 参数3:当前的类名
*/
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(activeUser, user.getPwd(), this.getName());
return info;
}else{
//当返回null,即代表用户不存在时,shiro会抛出UnknowAccountException异常
return null;
}
}

/**
* 授权,即给用户添加权限或角色的逻辑
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//添加权限
// info.addStringPermission("user:query");
// Collection<String> permissions = new ArrayList<>();
// Collections.addAll(permissions, "user:add", "user:export");
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
List<String> permissions = activeUser.getPermissions();
if (permissions.size()>0)
info.addStringPermissions(permissions);
//添加角色
// info.addRole("role1");
// Collection<String> list = new ArrayList<>();
// Collections.addAll(list, "role2", "role3");
List<String> roles = activeUser.getRoles();
if (roles.size()>0)
info.addRoles(roles);

return info;
}
}
  1. 修改主函数中的代码,在安全管理器中设置Realm,把自定义的UserRealm对象注入。
  2. 当主函数进行身份认证时,则会走doGetAuthenticationInfo里的逻辑,在创建SimpleAuthenticationInfo对象时,会进行密码匹配,然后返回匹配结果,如果返回null则会抛出UnknownAccountException异常,如果匹配不成功则会抛出IncorrectCredentialsException异常。
  3. 当认证通过后就会进行授权,则会走doGetAuthorizationInfo里的逻辑,在这里面则可以为用户添加角色、权限。
  4. doGetAuthorizationInfo传入的参数就是在认证逻辑里面SimpleAuthenticationInfo的第一个参数。

entity

1
2
3
4
5
6
7
8
9
10
11
12
13
//User
public class User {
private Integer id;
private String username;
private String pwd;
private Date createTime;
}
//ActiveUser
public class ActiveUser {
private User user = new User();
private List<String> roles = new ArrayList<>();
private List<String> permissions = new ArrayList<>();
}

service

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
//UserServiceImpl
public class UserServiceImpl implements UserService {
@Override
public User queryUserByName(String username) {
switch (username){
case "zhangsan":
return new User(1, username, "123456", new Date());
case "lisi":
return new User(2, username, "123456", new Date());
case "sunqi":
return new User(3, username, "123456", new Date());
}
return null;
}
}
//PermissionServiceImpl
public class PermissionServiceImpl implements PermissionService {
@Override
public List<String> queryPermissionsByName(String name) {
return Arrays.asList("user:query", "user:add", "user:export");
}
}
//RoleServiceImpl
public class RoleServiceImpl implements RoleService {
@Override
public List<String> queryRolesByName(String name) {
return Arrays.asList("role1", "role2", "role3");
}
}

加密

出于安全着想,我们对用户的密码可以使用某种算法进行加密,加密后的密文存储在数据库中,用户通过输入明文,由后端进行散列加密后,与数据库中的密文进行匹配,如果匹配成功则认证成功。在Shiro中,有几种常见的加密算法:Md2、Md5、Sha1、Sha256等,它们都是可以加入散列值和增加散列次数使密文更难破解。创建一个子项目shiro_05_Authorization_md5:

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
public class MD5Utils {
public static void main(String[] args) {
String source = "123456";
Md5Hash hash = new Md5Hash(source);
System.out.println("使用Md5加密后的结果:"+hash.toString());

Md5Hash hash1 = new Md5Hash(source, "北京武汉");
System.out.println("Md5加密并散列后的结果:" + hash1.toString());

Md5Hash hash2 = new Md5Hash(source, "我最帅", 2);
System.out.println("Md5加密并散列2次后的结果:"+hash2.toString());
//登录后的明文进行加密,然后和数据库加密的密文进行匹配,如果匹配成功则认证成功
}
/**
*用Md5算法进行加密
* @param source 明文
* @param salt 散列值
* @param hashIterations 散列次数
* @return
*/
public static String md5(String source, Object salt, Integer hashIterations){
return new Md5Hash(source, salt, hashIterations).toString();
}
/**
*用Sha1算法进行加密
* @param source 明文
* @param salt 散列值
* @param hashIterations 散列次数
* @return
*/
public static String sha1(String source, Object salt, Integer hashIterations){
return new Sha1Hash(source, salt, hashIterations).toString();
}
}

在认证和授权中集成加密

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
public class UserRealm extends AuthorizingRealm {
private UserService userService = new UserServiceImpl();
private RoleService roleService = new RoleServiceImpl();
private PermissionService permissionService = new PermissionServiceImpl();
public UserRealm() {
//设置加密规则
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//指定加密算法
credentialsMatcher.setHashAlgorithmName("md5");
//指定散列次数
credentialsMatcher.setHashIterations(2);
setCredentialsMatcher(credentialsMatcher);
}
/**
* 认证,当使用login时会调用此逻辑
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//token传入的用户名
String username = token.getPrincipal().toString();
//token传入的密码
Object credentials = token.getCredentials();
System.out.println(username);
System.out.println(credentials);
/**
* shiro是根据用户名把用户对象查询出来。在来做密码的匹配
*/
User user = userService.queryUserByName(username);
//如果查询到用户,则进行认证逻辑
if (user!=null){
List<String> roles = roleService.queryRolesByName(username);
List<String> permissions = permissionService.queryPermissionsByName(username);
ActiveUser activeUser = new ActiveUser(user, roles, permissions);
/**
*进行密码匹配,并将认证后的结果返回
* 参数说明:
* 参数1:可以传入任意对象,一般为用户名或用户对象
* 参数2:从数据库中查询出来的密码
* 参数3:当前的类名
*/
// SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(activeUser, user.getPwd(), this.getName());
ByteSource credentialsSalt = ByteSource.Util.bytes("我最帅");
/**
*进行密码匹配,并将认证后的结果返回
* 参数说明:
* 参数1:可以传入任意对象,一般为用户名或用户对象
* 参数2:从数据库中查询出来的密码
* 参数3:散列值
* 参数4:当前的类名
*/
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(activeUser, user.getPwd(), credentialsSalt, this.getName());
return info;
}else{
//当返回null,即代表用户不存在时,shiro会抛出UnknowAccountException异常
return null;
}
}
/**
* 授权,即给用户添加权限或角色的逻辑
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//添加权限
// info.addStringPermission("user:query");
// Collection<String> permissions = new ArrayList<>();
// Collections.addAll(permissions, "user:add", "user:export");
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
List<String> permissions = activeUser.getPermissions();
if (permissions.size()>0)
info.addStringPermissions(permissions);
//添加角色
// info.addRole("role1");
// Collection<String> list = new ArrayList<>();
// Collections.addAll(list, "role2", "role3");
List<String> roles = activeUser.getRoles();
if (roles.size()>0)
info.addRoles(roles);
return info;
}
}

在UserRealm的构造器中设置了加密算法和散列次数,而散列值是在SimpleAuthenticationInfo中的credentialsSalt添加的,一般散列值都会存在数据库中,而加密的规则会在后端代码中设置,这里有三种设置方式:1、如上在构造器中

  1. 在主函数中设置加密规则
1
2
3
4
5
UserRealm userRealm = new UserRealm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(2);
userRealm.setCredentialsMatcher(credentialsMatcher);
  1. 在ini文件中设置规则,即把加密规则保存在数据库中。

参考网址:简书

-------------本文结束感谢您的阅读-------------