shiro框架整合spring boot及登录身份验证源码的解析
shiro核心架构
- Subject(主体)
- 需要认证的内容主体
- Principals (代表身份):用户名、邮件、手机号码等
- Credentials(代表凭证):密码,数字证书等
- SecurityManager(安全管理器)
- Shiro 架构的核心
- 与 Subject 交互的实际安全操作,由 SecurityManager 完成
- SecurityManager是一个接口,继承了Authenticator(认证器),Authorizer(授权器),SessionManager(会话管理器)这三个接口
- Authenticator(认证器)
- 对用户进行身份认证
- Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,基本可实现大多数需求,也可自定义认证器
- Authorizer(授权器)
- 用户通过认证后,通过授权器判断用户是否有相应功能的操作权限
- Realm(域)
- 连接 Shiro 和具体应用的桥梁,相当于datasource数据源
- SecurityManager进行安全认证时需要通过Realm获取用户权限数据
- Realm中还包含了认证授权校验相关代码
- SessionManager(会话管理)
- shiro框架定义了一套会话管理,不依赖web容器的session,可将shiro使用在非web应用上
- 可将分布式应用的会话集中在一点管理,实现单点登录
- SessionDAO(会话DAO)
- 对会话操作的一套接口
- CacheManager(缓存管理)
- 将用户权限数据存储在缓存,可提供性能
- Cryptography(密码管理)
- shiro框架提供了一套加密/解密组件,方便开发
身份认证
-
认证流程图
-
登录身份认证过程
- 应用调用 Subject.login(token)
- 将 Subject 实例委托给应用程序的 SecurityManager
- SecurityManager 根据具体的 Realm 进行安全认证
- 关键点
- 用户名校验 :该过程默认最终由SimpleAccountRealm(如果配置了自定义Realm,则由自定义Realm)中的doGetAuthenticationInfo方法 完成
- 密码校验:该过程最终由AuthenticatingRealm的assertcredentialsMatch方法完成
权限授权
- 授权流程图
- 授权方式
- 基于角色的访问控制
if(subject.hasRole("角色名")){
//操作某种资源
}
- 基于资源的访问控制
if(subject.IsPermission("资源标志符:操作:资源实例标识符")){
//操作某种资源
}
- shiro中授权编程方式
- 编程式
Subject subject = SecurityUtils.getSubject();
if((subject.hasRole("admin")){
}
- 注解式
@RequiresRoles("admin")
public void canDoSomething(){
}
- 标签式
<shiro:hasRole name="admin">
</shiro:hasRole>
Shiro集成Spring Boot流程源码分析
关键模块类图
-
自定义域 MyRealm
-
自定义密码匹配器MyHashedCredentialsMatcher
-
安全管理器SecurityManager
登录代码示例
/**
* 账号(用户名或手机号)密码登录
* @param userMsg
* @return
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public JSONObject login(@RequestBody JSONObject userMsg) {
JSONObject jsonObject = new JSONObject();
JSONObject data = new JSONObject();
Subject subject = SecurityUtils.getSubject();
try {
String account = userMsg.getString("account");
String password = userMsg.getString("password");
SysUserInfo sysUserInfo;
//先查手机号,有的话就手机号密码登陆
sysUserInfo = userInfoService.getByPhone(account);
if (null == sysUserInfo) {
//用用户名查找
sysUserInfo = userInfoService.getByUserName(account);
}
if (null == sysUserInfo) {
jsonObject.put("code", SystemConstants.CODE_FAILURE);
jsonObject.put("msg", "该用户不存在");
data.put("type", "username");
jsonObject.put("data", data);
log.info(jsonObject.toString());
return jsonObject;
}
MyUsernamePasswordToken token = new MyUsernamePasswordToken(sysUserInfo.getUserName(), password);
subject.login(token);
//每次登录更新token
String sessionId = (String) subject.getSession().getId();
String token1 = userInfoService.updateUserToken(sysUserInfo.getGuid(), sessionId);
JSONObject userInfo = JSON.parseObject(JSON.toJSONString(sysUserInfo));
userInfo.put("token",token1);
userInfo.remove("salt");
userInfo.remove("password");
userInfo.remove("credentialsSalt");
jsonObject.put("code", SystemConstants.CODE_SUCCESS);
jsonObject.put("msg", "登录成功");
jsonObject.put("data", userInfo);
} catch (IncorrectCredentialsException e) {
jsonObject.put("code", SystemConstants.CODE_FAILURE);
jsonObject.put("msg", "密码错误");
data.put("type", "password");
jsonObject.put("data", data);
log.error(jsonObject.toString(),e);
} catch (LockedAccountException e) {
jsonObject.put("code", SystemConstants.CODE_FAILURE);
jsonObject.put("msg", "登录失败,该用户已被冻结");
data.put("type", "username");
jsonObject.put("data", data);
log.error(jsonObject.toString(),e);
} catch (AuthenticationException e) {
jsonObject.put("code", SystemConstants.CODE_FAILURE);
jsonObject.put("msg", "该用户不存在");
data.put("type", "username");
jsonObject.put("data", data);
log.error(jsonObject.toString(),e);
} catch (Exception e) {
jsonObject.put("code", SystemConstants.CODE_ERROR);
jsonObject.put("msg", "登陆失败");
jsonObject.put("data", data);
log.error("login is failed", e);
}
log.info(jsonObject.toString());
return jsonObject;
}
登录过程步骤源码解析
- 启动应用时,自动加载ShiroConfig,并注入和初始化相关Bean
- AuthFilter(自定义url访问控制过滤器)
- ShiroFilterFactoryBean(入口拦截并进行安全控制)
- SecurityManager(安全管理器)
- MyRealm(自定义Realm)
- HashedCredentialsMatcher(密码匹配器)
- SessionManager(Session管理器)
- RedisCacheManager(Cache管理器)
- CookieRememberMeManager(记住密码cookie管理)
-
调用登录接口/login,此时会调用AuthFilter的doFilterInternal方法进行url鉴定,发现此url已被放行,直接通过
-
根据当前登录用户创建token对象,调用subject.login(token),进入实现类
-
进入securityManager.login(this, token)的实现类
-
进入authenticate(token)实现类
-
进入doAuthenticate(token)实现类
-
我这里是单域,所以进入doSingleRealmAuthentication实现类
-
进入realm.getAuthenticationInfo(token)实现类
-
缓存中没有时,进入doGetAuthenticationInfo
-
关键点1:调用实现类MyRealm的doGetAuthenticationInfo方法,验证用户名,并将(从库里查出的)用户信息封装进AuthenticationInfo对象,返给shiro
-
关键点2:8步骤中,返回getAuthenticationInfo后进入assertCredentialsMatch(token, info)进行密码校验
-
关键3:进入doCredentialsMatch,此时调用自定义MyHashedCredentialsMatcher类的doCredentialsMatch,并最终验证密码
-
进入HashedCredentialsMatcher类的doCredentialsMatch,将用户输入密码与库里查的密码进行对比校验
总结:登录身份校验的关键是将用户输入的密码与从库里查出的密码进行对比校验,通过上述类图和源码方法的调用关系可以很好的帮助我们理解验证过程的原理。
我的项目地址