在使用shiro实现认证之前,我们首先要加入shiro的jar包,这里引入了五个包:
shiro-core-1.3.2.jar,junit-4.10.jar,commons-logging-1.2.jar
slf4j-api-1.7.25.jar,slf4j-nop-1.7.25.jar
其中slf4j的两个包是shiro-core包要求引入的
这里主要通过简单的程序来实现shiro认证,授权,自定义Realm和加密
shiro认证
1.认证的概念
在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份:
- principals:身份,即主体的标识属性,可以是任何东西,如用户名,邮箱等,只要保证 唯一即可。一个主体可以有多个principles,但只有一个primary principl e,一般是用户名/手机号。
- credentials:证明/凭证,即只有主体知道的安全值,如密码/安全证书等。
最常见的principles和credentials组合是用户名/密码组合。
2.shiro认证过程
- 1.创建security manager对象
- 2.主体提交认证
- 3.security manager进行认证
- 4.Authenticator进行认证
- 5.Realm认证
具体流程如下:
1.首先调用Subject.login(token)进行登录,其会自动委托给SecurityManager,调用之前必须通过SecurityUtils. setSecurityManager()设置。
2.SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证。
3.Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现。
4.Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证
5.Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
3.编写shiro认证程序
在src中创建AuthenticatorTest.java1
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
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
public class AuthenticationTest {
// 这里构建一个简单的数据源
SimpleAccountRealm simpleAccountRealm=new SimpleAccountRealm();
@Before
public void addUser(){
// 为数据源中添加账户(模拟数据库中存放的账号密码)
simpleAccountRealm.addAccount("admin","123456");
}
@Test
public void testAuthentication(){
// 构造security manager对象
DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
// 将上面编写的数据源Realm配置到SecurityManager中
defaultSecurityManager.setRealm(simpleAccountRealm);
//主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); //将SecurityManager设置到当前运行环境中
Subject subject= SecurityUtils.getSubject();
// 设置登录令牌
// 这里模拟了用户名和密码,正规操作是从页面接收传过来的用户名和密码
UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
try{
// 执行登录提交
subject.login(token);
}catch (AuthenticationException e){
// 认证失败
e.printStackTrace();
}
// 输出是否匹配成功
System.out.println("是否匹配成功:"+subject.isAuthenticated());
// 退出操作
subject.logout();
}
}
程序里使用shiro提供的SimpleAccountRealm来创建简单的数据源Realm,并且在数据源中加入用户来模拟数据库中存放的账号和密码,与用户传过来的用户密码进行比对,通过subject.isAuthenticated()方法返回Boolean值,比对正确则返回true,否则返回false。使用junit测试可以在控制台看到相应结果。
代码解读:
1.首先通过new IniSecurityManagerFactory并指定一个ini配置文件来创建一个SecurityManager工厂。
2.接着获取SecurityManager并绑定到SecurityUtils,这是一个全局设置,设置一次即可。
3.通过SecurityUtils得到Subject,其会自动绑定到当前线程;如果在web环境在请求结束时需要解除绑定;然后获取身份验证的Token(即用户输入的信息),如用户名/密码。
4.调用subject.login方法进行登录,其会自动委托给SecurityManager.login方法进行登录。
5.如果身份验证失败请捕获AuthenticationException或其子类异常,常见的如 :DisabledAccountException(禁用的帐号)
、LockedAccountException(锁定的帐号)
、UnknownAccountException(错误的帐号)
、ExcessiveAttemptsException(登录失败次数过多)
、IncorrectCredentialsException (错误的凭证)
、ExpiredCredentialsException(过期的凭证)
等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库。
6.最后可以调用subject.logout退出,其会自动委托给SecurityManager.logout方法退出。
通过以上代码总结身份验证步骤:
1.收集用户身份/凭证,如用户名/密码。
2.调用Subject.login进行登录,如果失败将得到相应的AuthenticationException异常,根据异常提示用户错误信息;否则登录成功。
3.最后调用Subject.logout进行退出操作。
4.自定义Realm认证
实际开发中我们使用realm从数据库中查询用户信息,即realm的作用为:根据token中的身份信息去查询数据库是否有匹配的用户。若有,则返回认证信息,否则返回null。
在shiro中,realm接口中的方法如下:1
2
3
4
5
6String getName(); //返回一个唯一的Realm名字
boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException; //根据Token获取认证信息
这里我们自定义一个UserRealm并让它继承AuthorizingRealm抽象类。在src下新建realm包,在realm包中创建自定义的UserRealm1
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
package realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class UserRealm extends AuthorizingRealm {
// 设置realm的名字
@Override
public void setName(String name) {
super.setName("UserRealm");
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// token是用户输入的
// 第一步:从token中获取用户的身份信息,需要转换为String类型
String username=(String)authenticationToken.getPrincipal();
// 第二步:从数据库中查询用户密码
// 这里模拟从数据库查询到的密码为
String password="123456";
// 如果查询不到则返回null(上面已经设置查询到密码为123456)
// 查询到则返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(username,password,this.getName());
return simpleAuthenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 到后面写授权模块时补充
return null;
}
}
自定义realm后,我们还需要对这个realm进行配置。在src包下创建一个shiro-reaml.ini文件,内容如下:1
2
3
4
5
6[main]
# 声明并定义一个realm,属性等于我们自定义的UserRealm
userRealm=realm.UserRealm
# 将realm设置到security manager中,相当于spring中的注入,使用$userRealm来引用上面realm的定义
securityManager.realms=$userRealm
接下来让我们测试一下,在src/realm中创建testUserRealm.java1
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
package realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class testUserRealm {
@Test
public void userRealmTest(){
// UserRealm数据源
UserRealm userRealm=new UserRealm();
// 创建security manager对象
DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
// 设置数据源到security manager中
defaultSecurityManager.setRealm(userRealm);
// 将security manager设置到运行环境中
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject=SecurityUtils.getSubject();
// 设置登录令牌
// 在UserRealm中设置了匹配密码为123456,必须填写123456为密码才能匹配成功
UsernamePasswordToken token=new UsernamePasswordToken("老王","123456");
try{
subject.login(token);
}catch (AuthenticationException e){
e.printStackTrace();
}
System.out.println("是否匹配成功"+subject.isAuthenticated());
}
}
5.加密算法
实际开发中为了保护用户信息的安全,我们需要对用户在注册时输入的密码进行加密后再保存到数据库,当用户登录时我们也要将用户输入的密码进行加密后再与数据库中的密码进行比对。即需要对密码进行散列,常用的散列方法有md5、sha。
用md5算法对密码进行散列的问题:如果知道散列后的值可以通过穷举算法得到md5密码对应的明文。解决方法:建议对md5进行散列时加salt(盐),进行加密相当于对原始密码+盐进行散列。
接下来让我们使用MD5算法进行加密
在realm包下新建UserRealmMd5.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
package realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class UserRealmMd5 extends AuthorizingRealm{
// 设置realm的名字
@Override
public void setName(String name) {
super.setName("UserRealm");
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// token是用户输入的
// 第一步:从token中获取用户的身份信息,需要转换为String类型
String username=(String)authenticationToken.getPrincipal();
// 第二步:从数据库中查询用户密码
// 模拟根据用户名从数据库查询到的密码,散列值
String password = "f3694f162729b7d0254c6e40260bf15c";
// 从数据库获取salt
String salt = "qwerty";
//上边散列值和盐对应的明文:111111
// 查询到则返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(username,password,this.getName());
return simpleAuthenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 到后面写授权模块时补充
return null;
}
}
在realm中配置凭证匹配器
,在src包下创建shiro-realm-md5.ini文件1
2
3
4
5
6
7
8
9
10
11
12[main]
#自定凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列的算法
credentialsMatcher.hashAlgorithmName=md5
#散列的次数
credentialsMatcher.hashIterations=1
#将凭证匹配器设置到我们定义的realm
UserRealm=realm.UserRealmMd5
UserRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$UserRealm
最后,使用junit编写测试类,查看经过Md5加密后密文是否和UserRealmMd5中定义的一致
在src/realm包下新建TestMd5.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package realm;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.junit.Test;
public class TestMd5 {
@Test
public void test(){
// 模拟输入密码
String password="111111";
// md5算法的盐
String salt="qwerty";
//密码111111经过散列1次得到的密码:f3694f162729b7d0254c6e40260bf15c
int hashIterations=1;
Md5Hash md5Hash=new Md5Hash(password,salt,hashIterations);
System.out.println("加密后密文:"+ md5Hash);
}
}
shiro授权
1.授权的概念
授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
主体:即访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。
资源:在应用中用户可以访问的任何东西,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面、查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)、打印文档等等。
如上可以看出,权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro不会去做这件事情,而是由实现人员提供。
角色:角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。包括隐式角色和显示角色。
隐式角色:即直接通过角色来验证用户有没有操作权限,如在应用中CTO、技术总监、开发工程师可以使用打印机,假设某天不允许开发工程师使用打印机,此时需要从应用中删除相应代码;再如在应用中CTO、技术总监可以查看用户、查看权限;突然有一天不允许技术总监查看用户、查看权限了,需要在相关代码中把技术总监角色从判断逻辑中删除掉;即粒度是以角色为单位进行访问控制的,粒度较粗;如果进行修改可能造成多处代码修改。
显式角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设哪个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无须修改多处代码;即粒度是以资源/实例为单位的;粒度较细。
授权的核心是权限控制,而权限控制分为基于角色的访问控制和基于资源的访问控制,实际开发中都是采用的基于资源的访问控制。在编写shiro授权程序
中会涉及两种控制方式,而在自定义Realm
中则只涉及基于资源的访问控制
。
2.shiro授权过程
授权过程和认证过程类似,参考下图:
Shiro支持三种授权的方式。
第一种:编程式:通过写if/else授权代码块完成:1
2
3
4
5
6Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
第二种:注解式:通过在执行的Java方法上放置相应的注解完成(没有权限将抛出相应的异常):1
2
3
4@RequiresRoles("admin")
public void hello() {
//有权限
}
第三种:JSP/GSP标签:在JSP/GSP页面通过相应的标签完成:1
2
3<shiro:hasRole name="admin">
<!— 有权限 —>
</shiro:hasRole>
对于上述授权的三种实现方式,我们现在只使用第一种方式,在实际开发中我们使用后两种方式进行用户的授权判断。
3.编写shiro授权程序
在src中配置user.ini数据源(即user.ini文件)1
2
3
4
5
6
7
8
9
10
11
12
# 基于用户的访问控制
[users]
# 用户admin的密码是123456,拥有角色admin
admin=123456,role1,role2
# 基于授权的访问控制
[roles]
# 管理员admin具有创建,更新,删除权限
role1=user:create,user:update,user:delete
# role1具有创建功能
role2=user:create
对配置文件中的解释如下:
权限标识符号规则:资源:操作:实例(中间使用半角:分隔)。如下:
user:create:01
,表示对用户资源的01实例进行create操作。user:create
,表示对用户资源进行create操作,相当于user:create:*
,对所有用户资源实例进行create操作。user:*:01
,表示对用户资源实例01进行所有操作。
修改之前的shiro认证程序,即AuthenticationTest.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
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
public class AuthenticationTest {
@Test
public void testAuthentication(){
// 使用ini文件作为数据源
IniRealm iniRealm=new IniRealm("classpath:user.ini");
// 构造security manager对象
DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
// 将上面编写的数据源Realm配置到SecurityManager中
defaultSecurityManager.setRealm(iniRealm);
//主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); //将SecurityManager设置到当前运行环境中
Subject subject= SecurityUtils.getSubject();
// 设置登录令牌
// 这里模拟了用户名和密码,正规操作是从页面接收传过来的用户名和密码
UsernamePasswordToken token=new UsernamePasswordToken("admin","123456");
try{
// 执行登录提交
subject.login(token);
}catch (AuthenticationException e){
// 认证失败
e.printStackTrace();
}
// 输出是否匹配成功
System.out.println("是否匹配成功:"+subject.isAuthenticated());
// 基于角色的访问控制(查看是否有admin角色权限)
System.out.println("单个角色判断:"+ subject.hasRole("role1"));
System.out.println("多个角色判断:"+ subject.hasAllRoles(Arrays.asList("role1","role2")));
// 基于资源的访问控制
subject.checkRole("role1");
System.out.println("单个权限判断"+ subject.isPermitted("user:create"));
System.out.println("多个权限判断"+ subject.isPermittedAll("user:create:1","user:delete"));
// 退出操作
subject.logout();
}
}
```
### 4.自定义Realm授权
在之前的`shiro认证自定义的UserRealm.java`中,修改doGetAuthorizationInfo()方法
这里将全部代码贴出(修改相应的授权方法即可)
package realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.*;
public class UserRealm extends AuthorizingRealm {
// 设置realm的名字
@Override
public void setName(String name) {
super.setName("UserRealm");
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// token是用户输入的
// 第一步:从token中获取用户的身份信息,需要转换为String类型
String username=(String)authenticationToken.getPrincipal();
// 第二步:从数据库中查询用户密码
// 这里模拟从数据库查询到的密码为
String password="123456";
// 如果查询不到则返回null(上面已经设置查询到密码为123456)
// 查询到则返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(username,password,this.getName());
return simpleAuthenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 从principalCollection中获取用户的身份信息,需要转换为String类型
//将getPrimaryPrincipal方法返回值转为真实身份类型(在上边的goGetAuthenticationInfo认证通过填充到SimpleAuthenticationInfo)
String username=(String)principalCollection.getPrimaryPrincipal();
//根据身份信息获取权限信息,
//模拟从数据库中获取到的动态权限数据
List<String> permissions=new ArrayList<>();
permissions.add("user:create");//模拟user的创建权限
permissions.add("items:add");//模拟商品的添加权限
//查到权限数据,返回授权信息(包括上边的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
//将上边查询到授权信息填充到simpleAuthorizationInfo对象中
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
}1
2
上面的testUserRealm.java相应改为
package realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class testUserRealm {
@Test
public void userRealmTest(){
// UserRealm数据源
UserRealm userRealm=new UserRealm();
// 创建security manager对象
DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
// 设置数据源到security manager中
defaultSecurityManager.setRealm(userRealm);
// 将security manager设置到运行环境中
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject=SecurityUtils.getSubject();
// 设置登录令牌
// 在UserRealm中设置了匹配密码为123456,必须填写123456为密码才能匹配成功
UsernamePasswordToken token=new UsernamePasswordToken("老王","123456");
try{
subject.login(token);
}catch (AuthenticationException e){
e.printStackTrace();
}
System.out.println("是否匹配成功"+subject.isAuthenticated());
//基于资源的权限控制
System.out.println("单个权限判断"+ subject.isPermitted("user:create"));
System.out.println("多个权限判断"+ subject.isPermittedAll("user:create:1","user:delete"));
// 退出操作
subject.logout();
}
}`
文章中部分内容参考codingxiaxw的博客