写点什么

Apache Shiro Realm 实战及认证授权源码解读

作者:编程江湖
  • 2022 年 3 月 30 日
  • 本文字数:11532 字

    阅读完需:约 38 分钟

之前文章我们讲解了 Apache Shiro 的一些基础知识,今天我们会进行 Shiro Realm 实战以及对 Shiro 认证授权源码进行解读。

1. Shiro 安全数据来源之 Realm 实战

从之前章节的讲解我们了解到实际进行权限信息验证的是我们的 Realm,Shiro 从 Realm 获取安全数据,Shiro 框架内部默认提供了两种实现,一种是查询.ini 文件的 IniRealm,另一种是查询数据库的 JdbcRealm,除此之外,我们还可以根据自身的需求进行自定义 Realm,这其中有两个概念需要先了解一下:

  1. principal : 主体的标示,可以有多个,但是需要具有唯一性,常见的有用户名,手机号,邮箱等

  2. credential:凭证, 一般就是密码

接下来我们分别来看如何使用 Shiro 默认实现的 realm 以及如何自定义 realm:

尚硅谷 Java 开发培训全新体系

尚硅谷 2020Java 全新课程体系,项目实战,培养 Java 开发实战人才!

尚硅谷 IT 培训

查看

1.1 Shiro 默认实现的 realm 实操和常见使用方法

1.1.1 Shiro 内置 realm 之 IniRealm 实操

1)首先我们需要创建一个.ini 配置文件,并按照相应的语法格式进行配置:

# 格式 username=password,role1,role2,...roleN

[users]

atguigu=123456,user

tom=456789,root,admin

# 格式 role=permission1,permission2...permissionN 也可以用通配符

# 下面配置 user 角色的权限为所有 video:find,video:buy,如果需要配置 video 全部操作 crud 则 user=video:*

# 权限都是自行配置,一般格式是:资源名:操作,比如对视频的更新操作可以定义为:video:update

[roles]

user = video:find,video:buy

admin = video:update,video:delete,comment:*

# root 角色有所有的权限,可以用通配符*来表示

root=*

2)新建一个【IniRealmTest】测试类:

package com.atguigu.shiro.demo;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.UsernamePasswordToken;

import org.apache.shiro.config.IniSecurityManagerFactory;

import org.apache.shiro.mgt.SecurityManager;

import org.apache.shiro.subject.Subject;

import org.apache.shiro.util.Factory;

import org.junit.Test;

public class IniRealmTest {

@Test

public void test() {

//创建 SecurityManager 工厂,并且读取配置文件 shiro.ini

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

SecurityManager securityManager = factory.getInstance();

//将 securityManager 设置到当前运行环境中

SecurityUtils.setSecurityManager(securityManager);

//获取当前主体

Subject subject = SecurityUtils.getSubject();

//用户输入的账号密码

UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("atguigu", "123456");

//主体提交登录认证

subject.login(usernamePasswordToken);

System.out.println("认证结果:"+subject.isAuthenticated());

System.out.println("是否有对应的 user 角色:" + subject.hasRole("user"));

System.out.println("是否有对应的 root 角色:" + subject.hasRole("root"));


System.out.println("当前用户的用户名为:" + subject.getPrincipal());


System.out.println("当前用户是否有 video:find 权限:" + subject.isPermitted("video:find"));


System.out.println("当前用户是否有 video:delete 权限:" + subject.isPermitted("video:delete"));

}

}

3)运行测试用例,可以看到结果如下:


1.1.2 Shiro 内置 realm 之 JdbcRealm 实操

1)由于 JdbcRealm 需要操作数据库,所以在编写测试用例之前需要先导入连接数据库的相关依赖包:

<!--mysql 驱动包-->

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<!--注释掉-->

<!--<scope>runtime</scope>-->

</dependency>

<!--这里使用阿里巴巴 druid 数据源-->

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>druid</artifactId>

<version>1.1.6</version>

</dependency>

2)创建相应的数据库表进行用户权限信息的存储,由于 JdbcRealm 提供了默认编写的 SQL 进行数据库查询,所以创建数据库表时表名和字段名都需要对应上,下面提供创建数据库表所需的 SQL 脚本并插入相应的测试数据:

# 角色-权限对应关系表

DROP TABLE IF EXISTS `roles_permissions`;

CREATE TABLE `roles_permissions` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`role_name` varchar(100) DEFAULT NULL,

`permission` varchar(100) DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `idx_roles_permissions` (`role_name`,`permission`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `roles_permissions` WRITE;

# 插入相应的测试数据

INSERT INTO `roles_permissions` (`id`, `role_name`, `permission`)

VALUES

(4,'admin','video:*'),

(3,'role1','video:buy'),

(2,'role1','video:find'),

(5,'role2','video:list'),

(1,'root','*');

UNLOCK TABLES;

# 用户-角色对应关系表

DROP TABLE IF EXISTS `user_roles`;

CREATE TABLE `user_roles` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`username` varchar(100) DEFAULT NULL,

`role_name` varchar(100) DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `idx_user_roles` (`username`,`role_name`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user_roles` WRITE;

# 插入测试数据

INSERT INTO `user_roles` (`id`, `username`, `role_name`)

VALUES

(1,'jack','role1'),

(2,'jack','role2'),

(4,'atguigu','admin'),

(3,'atguigu','root');

UNLOCK TABLES;

# 用户信息表

DROP TABLE IF EXISTS `users`;

CREATE TABLE `users` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`username` varchar(100) DEFAULT NULL,

`password` varchar(100) DEFAULT NULL,

`password_salt` varchar(100) DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `idx_users_username` (`username`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `users` WRITE;

# 插入测试数据

INSERT INTO `users` (`id`, `username`, `password`, `password_salt`)

VALUES

(1,'jack','123',NULL),

(2,'atguigu','123456',NULL);

UNLOCK TABLES;

3)运行 SQL 脚本创建完相应的数据表之后,需要进行工程代码的编写,JdbcRealm 提供了 2 种方式进行实现,下面分别对两种方式进行介绍及代码实操:

方式一:使用.ini 配置文件进行配置

编写配置文件 jdbcRealm.ini

#注意 文件格式必须为 ini,编码为 ANSI

#声明 Realm,指定 realm 类型

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm

#配置数据源 由于我们引入了 druid 数据源依赖,所以这里使用 druid 数据源

dataSource=com.alibaba.druid.pool.DruidDataSource


# mysql-connector-java 5 用的驱动 url 是 com.mysql.jdbc.Driver,mysql-connector-java6 以后用的是 com.mysql.cj.jdbc.Driver

dataSource.driverClassName=com.mysql.cj.jdbc.Driver


#数据源链接

dataSource.url=jdbc:mysql://192.168.200.128:3306/atguigu_shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false


dataSource.username=root

dataSource.password=123456


#指定数据源

jdbcRealm.dataSource=$dataSource


#开启查找权限, 默认是 false,如果不开启不会去查找角色对应的权限,这是一个坑!!!!!

jdbcRealm.permissionsLookupEnabled=true


#指定 SecurityManager 的 Realms 实现,设置 realms,可以有多个,用逗号隔开

securityManager.realms=$jdbcRealm


新建一个【JdbcRealmTest】测试类:

@Test

public void test() {

//创建 SecurityManager 工厂,并加载 jdbcrealm.ini 配置文件

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:jdbcrealm.ini");

SecurityManager securityManager = factory.getInstance();

//将 securityManager 设置到当前运行环境中

SecurityUtils.setSecurityManager(securityManager);

Subject subject = SecurityUtils.getSubject();

//用户输入的账号密码

UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("jack", "123");

subject.login(usernamePasswordToken);


System.out.println(" 认证结果:"+subject.isAuthenticated());


System.out.println(" 是否有对应的 role1 角色:"+subject.hasRole("role1"));


System.out.println("是否有 video:find 权限:"+ subject.isPermitted("video:find"));

}

运行测试用例,结果如下:


方式二:自行配置数据源及 JdbcRealm

依赖引入与方式一一致

在【JdbcRealmTest】测试类中编写一个测试用例:

@Test

public void test2(){

DefaultSecurityManager securityManager = new DefaultSecurityManager();

DruidDataSource ds = new DruidDataSource();

ds.setDriverClassName("com.mysql.cj.jdbc.Driver");

ds.setUrl("jdbc:mysql://192.168.200.128:3306/atguigu_shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false");

ds.setUsername("root");

ds.setPassword("123456");

JdbcRealm jdbcRealm = new JdbcRealm();

jdbcRealm.setPermissionsLookupEnabled(true);

jdbcRealm.setDataSource(ds);


securityManager.setRealm(jdbcRealm);


//将 securityManager 设置到当前运行环境中

SecurityUtils.setSecurityManager(securityManager);


Subject subject = SecurityUtils.getSubject();


//用户输入的账号密码

UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("jack", "123");


subject.login(usernamePasswordToken);


System.out.println(" 认证结果:"+subject.isAuthenticated());


System.out.println(" 是否有对应的 role1 角色:"+subject.hasRole("role1"));


System.out.println(" 是否有 video:find 权限:"+ subject.isPermitted("video:find"));


System.out.println(" 是否有任意权限:"+ subject.isPermitted("aaaa:xxxxxxxxx"));

}

运行测试用例,结果如下:


1.2 Shiro 自定义 realm 实战

1.2.1 自定义 realm 实战基础

1)步骤:

创建一个类 ,继承 AuthorizingRealm,AuthorizingRealm 继承关系如下图:


由上图可见继承关系为:AuthorizingRealm->AuthenticatingRealm->CachingRealm->Realm

重写授权方法 doGetAuthorizationInfo


重写认证方法 doGetAuthenticationInfo


方法:

当用户登陆的时候会调用 doGetAuthenticationInfo,获取认证信息进行用户身份认证

进行权限校验的时候会调用: doGetAuthorizationInfo,获取授权信息进行用户授权

对象介绍

UsernamePasswordToken : 对应的是用户输入的账号密码信息组成的,token 中有 Principal 和 Credential,UsernamePasswordToken 继承关系图如下:


由上图可见继承关系为:UsernamePasswordToken->HostAuthenticationToken->AuthenticationToken

  • SimpleAuthorizationInfo:代表用户角色权限信息

  • SimpleAuthenticationInfo :代表该用户的认证信息

1.2.2 自定义 realm 代码实操

有了上面的对基础知识以及认证授权的理解,我们先在合适的包下创建一个【CustomRealm】类,继承 Shiro 框架的 AuthorizingRealm 类,并实现默认的两个方法:

package com.atguigu.shiro.demo;

import org.apache.shiro.authc.*;

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.HashMap;

import java.util.HashSet;

import java.util.Map;

import java.util.Set;

public class CustomRealm extends AuthorizingRealm {

//此处用 Map 集合模拟用户-角色-权限之间的关联关系,实际开发中会从数据库进行查询

private final Map<String,String> userInfoMap = new HashMap<>();

{

userInfoMap.put("jack","123");

userInfoMap.put("atguigu","123456");

}


//role -> permission

private final Map<String, Set<String>> permissionMap = new HashMap<>();

{


Set<String> set1 = new HashSet<>();

Set<String> set2 = new HashSet<>();


set1.add("video:find");

set1.add("video:buy");


set2.add("video:add");

set2.add("video:delete");


permissionMap.put("jack",set1);

permissionMap.put("atguigu",set2);


}


//user -> role

private final Map<String,Set<String>> roleMap = new HashMap<>();

{


Set<String> set1 = new HashSet<>();

Set<String> set2 = new HashSet<>();


set1.add("role1");

set1.add("role2");


set2.add("root");


roleMap.put("jack",set1);

roleMap.put("atguigu",set2);


}

//进行权限校验的时候会调用

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

System.out.println("权限 doGetAuthorizationInfo");


String name = (String)principals.getPrimaryPrincipal();


Set<String> permissions = getPermissionsByNameFromDB(name);


Set<String> roles = getRolesByNameFromDB(name);


SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();


simpleAuthorizationInfo.setRoles(roles);

simpleAuthorizationInfo.setStringPermissions(permissions);


return simpleAuthorizationInfo;

}


//当用户登陆的时候会调用

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {


System.out.println("认证 doGetAuthenticationInfo");


//从 token 获取身份信息,token 代表用户输入的信息

String name = (String)token.getPrincipal();


//模拟从数据库中取密码

String pwd = getPwdByUserNameFromDB(name);


if( pwd == null || "".equals(pwd)){

return null;

}


SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, pwd, this.getName());


return simpleAuthenticationInfo;

}

/**

* 模拟从数据库获取用户角色集合

* @param name

* @return

*/

private Set<String> getRolesByNameFromDB(String name) {

return roleMap.get(name);


}


/**

* 模拟从数据库获取权限集合

* @param name

* @return

*/

private Set<String> getPermissionsByNameFromDB(String name) {

return permissionMap.get(name);

}


private String getPwdByUserNameFromDB(String name) {


return userInfoMap.get(name);

}

}

然后我们编写测试类,来验证是否正确:

package com.atguigu.shiro.demo;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.UsernamePasswordToken;

import org.apache.shiro.mgt.DefaultSecurityManager;

import org.apache.shiro.subject.Subject;

import org.junit.Test;

public class AuthenticationTest {

private CustomRealm customRealm = new CustomRealm();

private DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();

@Before

public void init(){

//构建环境

defaultSecurityManager.setRealm(customRealm);

SecurityUtils.setSecurityManager(defaultSecurityManager);

}


@Test

public void testAuthentication() {


//获取当前操作的主体

Subject subject = SecurityUtils.getSubject();


//用户输入的账号密码

UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("jack", "123");


subject.login(usernamePasswordToken);


//登录

System.out.println(" 认证结果:"+subject.isAuthenticated());


//拿到主体标示属性

System.out.println(" getPrincipal=" + subject.getPrincipal());


subject.checkRole("role1");


System.out.println("是否有对应的角色:"+subject.hasRole("role1"));


System.out.println("是否有对应的权限:"+subject.isPermitted("video:add"));


}

}

运行测试用例,结果如下:


2. 深入 Shiro 源码解读认证授权流程

2.1 认证流程源码解读

1)我们以 subject.login(token)为起点进行断点调试

2)DelegatingSubject.login(token)会将身份认证请求委托给 DefaultSecurityManager 的 login()方法进行处理,接着继续进入 DefaultSecurityManager 的 login()方法

public class DelegatingSubject implements Subject {

......

public void login(AuthenticationToken token) throws AuthenticationException {

this.clearRunAsIdentitiesInternal();

//调用 DefaultSecurityManager 的 login 方法进行身份信息认证

Subject subject = this.securityManager.login(this, token);

......

}

......

}

3)DefaultSecurityManager 的 login()方法会调用 AuthenticatingSecurityManager 的 authenticate()方法进行验证,接着继续进入 AuthenticatingSecurityManager 的 authenticate()方法

public class DefaultSecurityManager extends SessionsSecurityManager {

......

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {

AuthenticationInfo info;

try {

//调用 AuthenticatingSecurityManager 的 authenticate()方法进行验证

info = this.authenticate(token);

} catch (AuthenticationException var7) {

......

}

Subject loggedIn = this.createSubject(token, info, subject);

this.onSuccessfulLogin(token, info, loggedIn);

return loggedIn;

}

......

}

4)AuthenticatingSecurityManager 的 authenticate()方法会调用 AbstractAuthenticator 的 authenticate()方法进行验证,接着继续进入 AbstractAuthenticator 的 authenticate()方法

public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {

......

public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {

//调用 AbstractAuthenticator 的 authenticate()方法进行验证

return this.authenticator.authenticate(token);

}

......

}

5)AbstractAuthenticator 的 authenticate()方法会调用 ModularRealmAuthenticator 的 doAuthenticate()方法进行验证,接着继续进入 ModularRealmAuthenticator 的 doAuthenticate()方法

public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {

......

public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {

if (token == null) {

throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");

} else {

log.trace("Authentication attempt received for token [{}]", token);

AuthenticationInfo info;

try {

//调用 ModularRealmAuthenticator 的 doAuthenticate()进行验证

info = this.doAuthenticate(token);

if (info == null) {

String msg = "No account information found for authentication token [" + token + "] by this " + "Authenticator instance. Please check that it is configured correctly.";

throw new AuthenticationException(msg);

}

} catch (Throwable var8) {

......

}

log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);

this.notifySuccess(token, info);

return info;

}

}

......

}

6)ModularRealmAuthenticator 的 doAuthenticate()方法会获取相应的 realm,由于我们目前只配置了一个 realm,所以会执行 doSingleRealmAuthentication()方法,接着会 AuthenticatingRealm 的 getAuthenticationInfo()方法,接着继续进入 AuthenticatingRealm 的 getAuthenticationInfo()方法

public class ModularRealmAuthenticator extends AbstractAuthenticator{

......

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {

this.assertRealmsConfigured();

//获取当前配置的 realm

Collection<Realm> realms = this.getRealms();


//如果当前配置 realm 个数为 1,则执行 doSingleRealmAuthentication()方法,否则执行 doMultiRealmAuthentication()

return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);

}


protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {

if (!realm.supports(token)) {

String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type.";

throw new UnsupportedTokenException(msg);

} else {

AuthenticationInfo info = realm.getAuthenticationInfo(token);

if (info == null) {

String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "].";

throw new UnknownAccountException(msg);

} else {

return info;

}

}

}

......

}

7)在此可以看到,AuthenticatingRealm 的 getAuthenticationInfo()方法会调用我们自定义配置 realm 的 doGetAuthenticationInfo(token)获取认证信息,并且调用 AuthenticatingRealm 的 assertCredentialsMatch 方法进行密码匹配认证

public abstract class AuthenticatingRealm extends CachingRealm implements Initializable{

......

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

AuthenticationInfo info = this.getCachedAuthenticationInfo(token);

if (info == null) {

// 调用自定义配置 realm 的 doGetAuthenticationInfo(token)方法

info = this.doGetAuthenticationInfo(token);

log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);

if (token != null && info != null) {

this.cacheAuthenticationInfoIfPossible(token, info);

}

} else {

log.debug("Using cached authentication info [{}] to perform credentials matching.", info);

}


if (info != null) {

// 对获取的认证信息进行密码验证

this.assertCredentialsMatch(token, info);

} else {

log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);

}


return info;

}

......

}

到此,我们的认证流程就结束了,通过源码可以看到认证流程到最后就是调用的我们自定义 realm 并重写的 doGetAuthenticationInfo(token)方法,从以上的源码分析,可以得到以下方法调用的时序图:


2.2 授权流程源码解读

1)授权流程我们以 subject.checkRole()方法为起点进行断点调试

2)DelegatingSubject 的 checkRole()方法会将授权请求委托给 AuthorizingSecurityManager 的 checkRole()方法进行处理,接着继续进入 AuthorizingSecurityManager 的 checkRole()方法

public class DelegatingSubject implements Subject {

......

public void checkRole(String role) throws AuthorizationException {

this.assertAuthzCheckPossible();

//将请求委托给 AuthorizingSecurityManager 的 checkRole()方法

this.securityManager.checkRole(this.getPrincipals(), role);

}

......

}

3)AuthorizingSecurityManager 的 checkRole()方法会将请求委托给 ModularRealmAuthorizer 的 checkRole()方法进行处理,接着继续进入 ModularRealmAuthorizer 的 checkRole()方法

public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager {

......

public void checkRole(PrincipalCollection principals, String role) throws AuthorizationException {

//将请求委托给 ModularRealmAuthorizer 的 checkRole()方法

this.authorizer.checkRole(principals, role);

}

......

}

4)ModularRealmAuthorizer 的 checkRole()方法会调用 AuthorizingRealm 的 hasRole()方法验证当前用户是否有相应的权限,若没有相应的权限,则会抛出异常,接着继续进入 AuthorizingRealm 的 hasRole()方法

public class ModularRealmAuthorizer implements Authorizer, PermissionResolverAware, RolePermissionResolverAware {

......

public void checkRole(PrincipalCollection principals, String role) throws AuthorizationException {

this.assertRealmsConfigured();

//验证当前用户是否有相应的权限,如果没有,则会抛出异常

if (!this.hasRole(principals, role)) {

throw new UnauthorizedException("Subject does not have role [" + role + "]");

}

}


public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {

this.assertRealmsConfigured();

Iterator var3 = this.getRealms().iterator();


Realm realm;

do {

if (!var3.hasNext()) {

return false;

}


realm = (Realm)var3.next();

//调用 AuthorizingRealm 的 hasRole()方法进行验证,如果有相应权限则返回 true,否则返回 false

} while(!(realm instanceof Authorizer) || !((Authorizer)realm).hasRole(principals, roleIdentifier));


return true;

}

......

}

5)AuthorizingRealm 的 hasRole()方法会通过 getAuthorizationInfo()方法进行调用自定义 realm 并重写的 doGetAuthorizationInfo()方法获取当前用户拥有的授权信息

public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {


......


public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {

AuthorizationInfo info = this.getAuthorizationInfo(principal);

return this.hasRole(roleIdentifier, info);

}


protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {

if (principals == null) {

return null;

} else {

AuthorizationInfo info = null;

......


if (info == null) {

//调用自定义 realm 并重写的 doGetAuthorizationInfo()方法获取当前用户的授权信息

info = this.doGetAuthorizationInfo(principals);

//如果授权信息存在并且存在缓存,则将当前授权信息进行缓存

if (info != null && cache != null) {

if (log.isTraceEnabled()) {

log.trace("Caching authorization info for principals: [" + principals + "].");

}


key = this.getAuthorizationCacheKey(principals);

cache.put(key, info);

}

}


return info;

}

}

......

}

到此,我们的授权流程就结束了,通过源码可以看到授权流程到最后就是调用的我们自定义 realm 并重写的 doGetAuthorizationInfo()方法,从以上的源码分析,可以得到以下方法调用的时序图:


至此,我们就通过源码把 Shiro 的认证以及授权流程解读完了,java培训由上边分析我们可以知道 Shiro 认证授权的时候,会调用我们自定义 Realm 时重写的 doGetAuthenticationInfo()方法以及 doGetAuthorizationInfo()方法进行获取当前主体的认证以及授权信息,所以我们需要根据项目的具体情况在自定义 Realm 中定义获取认证以及授权信息的逻辑。

3. Shiro 数据安全之数据加解密

  • 为什么需要加解密?

  • 在之前的学习中,我们在数据库中保存的密码都是明文的,一旦数据库数据泄露,那就会造成不可估算的损失。

  • 什么是散列算法?

  • 一般叫 hash,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数,适合存储密码,比如 MD5

  • 什么是 salt(盐) ?

  • 如果直接通过散列函数得到加密数据,容易被对应解密网站暴力破解,一般会在应用程序里面加特殊的字段进行处理,比如用户 id,例子:加密数据 = MD5(明文密码+用户 id), 破解难度会更大,也可以使用多重散列,比如多次 md5

  • Shiro 如何根据用户传过来的密码进行加密与数据库密码进行匹配?

  • Shiro 会使用 AuthenticatingRealm 的 assertCredentialsMatch()方法进行验证密码是否正确

  • 因此,在 Shiro 中我们一般会自定义密码验证规则,示例如下:

@Bean

public HashedCredentialsMatcher hashedCredentialsMatcher(){

HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();


//散列算法,使用 MD5 算法;

hashedCredentialsMatcher.setHashAlgorithmName("md5");


//散列的次数,比如散列两次,相当于 md5(md5("xxx"));

hashedCredentialsMatcher.setHashIterations(2);


return hashedCredentialsMatcher;

}

自定义验证规则之后,Shiro 进行密码验证时就会调用相应的逻辑对用户输入的密码进行加密之后与数据库存储的密码进行匹配。

总结一下:今天这篇文章我们进行 Shiro Realm 实战以及对 Shiro 认证授权源码进行解读,之后我们将会对 Shiro 的缓存、Session 模块进行讲解,以及会整合 SpringBoot2.x 进行综合实战,敬请期待。

关键词:java培训

用户头像

编程江湖

关注

IT技术分享 2021.11.23 加入

关注【IT云文化】微信公众号,获取学习资源

评论

发布
暂无评论
Apache Shiro Realm实战及认证授权源码解读_编程江湖_InfoQ写作平台