ApachShiro 一个系统 两套验证方法-(后台管理员登录、前台App用户登录)同一接口实现、源码分析
需求:
在公司新的系统里面博主我使用的是ApachShiro 作为安全框架、作为后端的鉴权以及登录、分配权限等操作 管理员的信息都是存储在管理员表
前台App 用户也需要校验用户名和密码进行登录、但是用户的信息却是存在另一张表里面、如何给这两个不同的数据表进行登录?鉴权呢?
当然 按照Shiro的强大,我们完全可以用一个接口作为登录的验证、不同的Realm 来执行不同的逻辑即可
相关知识储备 Realm

用最简单的话来说 一个Realm就是一个检验用户身份的组件,但这里这个组件需要我们继承去重写,因为每个系统有各自不同的业务逻辑,这些事情是Shiro所不能了解的,我们得通过这个
Realm 告诉Shiro 我们的密码是怎么加密得到的,还有用户名是哪个,以及加密的方式是啥
————————————————————————————————————————————
了解这些需要了解的东西之后,我们模仿现有的Realm,照猫画虎的来一个
@Component
public class WeChatRealm extends AuthorizingRealm { @Autowired
private VehicleOwnerService vehicleOwnerService; @Autowired
private SysUserService sysUserService; /**
* 授权 微信接口没有权限
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//没有权限机制返回Null即可
return null;
} /*
* @Author MRC
* @Description 认证
* @Date 11:36 2019/9/11
* @Param [token]
* @return org.apache.shiro.authc.AuthenticationInfo
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("微信登录认证"); //登录用户名
String username = (String) token.getPrincipal(); Wrapper<VehicleOwner> vehicleOwnerWrapper = new EntityWrapper<>();
vehicleOwnerWrapper.eq("phone",username); VehicleOwner vehicleOwner = vehicleOwnerService.selectOne(vehicleOwnerWrapper); if (vehicleOwner == null) {
//找不到这个用户直接返回null
return null;
} //构造一个简单的认证信息
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
vehicleOwner, //用户名
vehicleOwner.getPassword(), //密码
ByteSource.Util.bytes(username + vehicleOwner.getSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
}
- 继承 AuthorizingRealm 重写 doGetAuthorizationInfo()鉴权方法 以及 doGetAuthenticationInfo()认证方法
- 按照传入的用户名查找这个用户是否存在,不存在就返回null即可
- 这里不校验密码,直接把用户名和密码以及盐值(如果存在加盐机制)就一起传递过去 交给Shiro去校验
加入到Shiro SecurityManager当中
/**
* 前端验证Realm
* @return
*/
public WeChatRealm getWeChatRealm() {
//使用MD5凭证管理器
weChatRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return weChatRealm;
}
通过@Bean 的方式注入一个SecurityManager 对象 并且加入多个Realm
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // securityManager.setRealm(myShiroRealm());
List<Realm> list = new ArrayList<>();
list.add(myShiroRealm());
list.add(getWeChatRealm()); //设置多个Realm
securityManager.setRealms(list);
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
配置是配置好了 那他们两个是如何工作的呢?
debug 走你~
从前台拿到的用户名和密码封装成 token 传递到login方法内
Subject subject = SecurityUtils.getSubject();
//封装toKen
UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUsername(), sysUser.getPassword());
//这里会抛出异常
subject.login(token);

继续跟进 ,进入用户名校验的过程。

token 里面封装了我们传递过来的admin用户名和密码
继续跟进,进入到login方法。 跳转到authenticate(token)方法 这里才是真正意义上验证方法

跟进到 authenticate(AuthenticationToken token) 方法

doAuthenticate(token) 才是要进行验证的方法,继续跟进,进入到
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
//初始化Realms
assertRealmsConfigured();
//取出我们多个Realm
Collection<Realm> realms = getRealms();
//一个或者多个执行不同的方法
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}

这里取出我们配置的两个Realm

我们配置里两个控制器,需要去做两个不同的校验,我们继续跟进。
这里我把这个方法的源代码贴出来,分析一下
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
//获取验证策略
AuthenticationStrategy strategy = getAuthenticationStrategy();
//获取到一个简单的验证信息(图1)
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
//开始循环拿出所有的Realm
for (Realm realm : realms) {
//这里返回的是传入的 aggregate ,不知道这个是干嘛的(图2)
aggregate = strategy.beforeAttempt(realm, token, aggregate);
//判断是否支持toKen
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
//关键 开始获取验证信息 开始用户名和密码的比对
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isWarnEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.warn(msg, t);
}
}
//验证完成后
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
//将最终的验证信息返回出去 图7
//如果aggregate(我们验证的用户信息)为空则抛出一个异常
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取缓存里面的验证信息
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//缓存里面没有,开始验证,跳转到我们自己写的逻辑 (图3)
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
//这里把我们前台传递过来的token 以及从数据库查询出来的对象要进行一个对比
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
//终于是密码的比对
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
//密码正确 返回info
return info;
}
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
### 获取密码凭证管理器
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
//检验密码正确性(图6)
if (!cm.doCredentialsMatch(token, info)) {
//not successful - throw an exception to indicate this:
//密码错误,抛出异常
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
"credentials during authentication. If you do not wish for credentials to be examined, you " +
"can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
}
}
图一
在所有的尝试之前,它先New 出一个简单的验证对象

图二
返回的依旧是一个传入的一个Aggregate对象

图3 跳转到我们自己写的逻辑层 返回一个用户名和密码的包装体

图4 这里没有开启缓存,直接跳出,不走下面的缓存相关的方法

图5 拿到我们配置的凭证管理器,配置的MD5以及加密次数

图6 检验密码的正确性 使用equals方法进行比对两个密码的方法

第二遍循环,因为在第一个循环(第一个Realm)里面已经匹配到,第二个肯定匹配不到,我们继续跟进
返回了一个NULL

图7
如果两个都匹配不到,就会抛出一个异常,账号不存在的异常,我们捕获即可

ApachShiro 一个系统 两套验证方法-(后台管理员登录、前台App用户登录)同一接口实现、源码分析的更多相关文章
- [Abp 源码分析]十一、权限验证
0.简介 Abp 本身集成了一套权限验证体系,通过 ASP.NET Core 的过滤器与 Castle 的拦截器进行拦截请求,并进行权限验证.在 Abp 框架内部,权限分为两块,一个是功能(Featu ...
- 开源网站流量统计系统Piwik源码分析——参数统计(一)
Piwik现已改名为Matomo,这是一套国外著名的开源网站统计系统,类似于百度统计.Google Analytics等系统.最大的区别就是可以看到其中的源码,这正合我意.因为我一直对统计的系统很好奇 ...
- 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入
使用react全家桶制作博客后台管理系统 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...
- ABP源码分析九:后台工作任务
文主要说明ABP中后台工作者模块(BackgroundWorker)的实现方式,和后台工作模块(BackgroundJob).ABP通过BackgroundWorkerManager来管理Backgr ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...
- [Abp 源码分析]十二、多租户体系与权限验证
0.简介 承接上篇文章我们会在这篇文章详细解说一下 Abp 是如何结合 IPermissionChecker 与 IFeatureChecker 来实现一个完整的多租户系统的权限校验的. 1.多租户的 ...
- 【精】EOS智能合约:system系统合约源码分析
系统合约在链启动阶段就会被部署,是因为系统合约赋予了EOS链资源.命名拍卖.基础数据准备.生产者信息.投票等能力.本篇文章将会从源码角度详细研究system合约. 关键字:EOS,eosio.syst ...
- 一个由正则表达式引发的血案 vs2017使用rdlc实现批量打印 vs2017使用rdlc [asp.net core 源码分析] 01 - Session SignalR sql for xml path用法 MemCahe C# 操作Excel图形——绘制、读取、隐藏、删除图形 IOC,DIP,DI,IoC容器
1. 血案由来 近期我在为Lazada卖家中心做一个自助注册的项目,其中的shop name校验规则较为复杂,要求:1. 英文字母大小写2. 数字3. 越南文4. 一些特殊字符,如“&”,“- ...
- [Abp vNext 源码分析] - 7. 权限与验证
一.简要说明 在上篇文章里面,我们在 ApplicationService 当中看到了权限检测代码,通过注入 IAuthorizationService 就可以实现权限检测.不过跳转到源码才发现,这个 ...
随机推荐
- 【LA 3942】 Remember the word
题意 给定一个字符串和若干个单词,询问能把字符串分解成这些单词的方案数.比如abcd ,有单词a,b,ab,cd:就可以分解成a+b+cd或者ab+cd. 分析 trie树—>DP 代码 (感谢 ...
- solr(一) 单节点安装部署
一.solr简介 1.什么是solr? Solr是一个独立的企业级搜索应用服务器,它对外提供类似于Web-service的API接口.用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件 ...
- linux修改固定IP
点击虚拟机菜单栏的编辑,选择虚拟网络编辑器 选择Vmnet8 NAT模式,查看子网ip:192.168.233.0 我们的虚拟机网络模式也需要选择NAT模式 打开虚拟机,输入:ip addr 查看当前 ...
- httpd.exe你的电脑中缺失msvcr110.dll怎么办(WIN2008服务器环境装WAMP2.5出现的问题)
httpd.exe你的电脑中缺失msvcr110.dll怎么办 去微软官方下载相应的文件 1 打开上面说的网址 Download and install, if you not have it alr ...
- HBase(一) —— 基本概念及使用
一.安装&启动 下载 https://mirrors.tuna.tsinghua.edu.cn/apache/hbase/2.1.8/ 快速开始文档,HBase2.1.8 http://hba ...
- 剑指offer:两个链表的第一个公共结点
题目描述: 输入两个链表,找出它们的第一个公共结点. 解题思路: 这道题一开始的题意不太理解,这里的公共结点,实际上指结点指相同,在题目不存在结点值相同的不同结点. 1. 最直接的思路是对链表一的每个 ...
- IDEA启动Springboot时,解决报错java.lang.NoClassDefFoundError: javax/servlet/Filter
如下所示,将spring-boot-starter-tomcat依赖中的<scope>provided</scope>注释掉 <dependency> <gr ...
- Tomcat中加载不到项目 项目构建Deployment Assembly报错:The given project is not a virtual component project
转: The given project is not a virtual component project The given project is not a virtual compone ...
- Qt布局 tab-widget-layout
QHBoxLayout *horizontalLayout_6 = new QHBoxLayout(main_ui.tab_5); horizontalLayout_6->setSpacing( ...
- 系统重装之认识UEFI
UEFI是一种新型的引导方式?他与传统的BIOS引导不同,传统BIOS引导需要经过(开机→BIOS初始化→BIOS自检→引导系统→进入系统)五个步骤来完成引导操作,UEFI只需要(开机→UEFI初始化 ...