总文档 :文章目录
Github : github.com/black-ant
一 . 前言
之前说了 SpringSecurity , 也说了 Pac4j , 后续准备把 Shiro 和 CAS 也完善进来 , Shiro 整个框架结构比较简单 , 这一篇也只是简单过一下 , 不深入太多.
1.1 基础知识
Shiro 的基础知识推荐看官方文档 Shiro Doc , 这里就简单的罗列一下
Shiro 具有很简单的体系结构 (Subject,SecurityManager 和 Realms) , 按照流程大概就是这样
ApplicationCode --> Subject (Current User)
|
SecurityManager (Managers all Subject)
|
Realms
复制代码
Shiro 的基石
Shiro 自己内部定义了4个功能基石 , 分为身份验证、授权、会话管理和密码学
- Authentication : 身份认证 , 证明用户身份的行为
- Authorization : 访问控制的过程,即确定谁可以访问什么
- Session Management : 管理特定于用户的会话,即使是在非 web 或 EJB 应用程序中
- Cryptography : 使用加密算法来保证数据的安全,同时仍然易于使用
以及一些额外的功能点:
- Web Support : Shiro 的 Web 支持 api 帮助简单地保护 Web 应用程序
- Caching : 缓存是 Apache Shiro API 中的第一层,用于确保安全操作保持快速和高效
- Concurrency : Apache Shiro 支持多线程应用程序及其并发特性
- Run As : 允许用户假设另一个用户的身份的特性 (我理解这就是代办)
- Remember Me : 记住我功能
补充隐藏概念:
- Permission : 许可
- Role : 角色
二 . 基本使用
Shiro 的使用对我而言第一感觉就是干净 , 你不需要像 SpringSecurity 一样去关注很多配置 ,关注很多Filter , 也不需要像 CAS 源码一样走了很多 WebFlow , 所有的认证都是由你自己去完成的.
2.1 配置类
@Configuration
public class shiroConfig {
/**
* 配置 Realm
*
* @return
*/
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}
/**
* 权限管理,配置主要是Realm的管理认证
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
// logout url
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 注册 SecurityManager
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* AOP 注解冲突解决方式
*
* @return
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
}
复制代码
2.2 发起认证
Shiro 发起认证很简答 , 完全是手动发起
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(),
user.getPassword()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
// subject.checkRole("admin");
// subject.checkPermissions("query", "add");
} catch (UnknownAccountException e) {
log.error("用户名不存在!", e);
return "用户名不存在!";
} catch (AuthenticationException e) {
log.error("账号或密码错误!", e);
return "账号或密码错误!";
} catch (AuthorizationException e) {
log.error("没有权限!", e);
return "没有权限";
}
复制代码
因为是完全手动发起的 , 所以在集成 Shiro 的时候毫无压力 , 可以自行在外层封装任何的接口 , 也可以在接口中做任何的事情.
2.3 校验逻辑
public class CustomRealm extends AuthorizingRealm {
@Autowired
private LoginService loginService;
/**
* @MethodName doGetAuthorizationInfo
* @Description 权限配置类
* @Param [principalCollection]
* @Return AuthorizationInfo
* @Author WangShiLin
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = loginService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permissions permissions : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}
/**
* @MethodName doGetAuthenticationInfo
* @Description 认证配置类
* @Param [authenticationToken]
* @Return AuthenticationInfo
* @Author WangShiLin
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = loginService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), getName());
return simpleAuthenticationInfo;
}
}
}
复制代码
LoginServiceImpl 也简单贴一下 , 就是从数据源中获取用户而已
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private PermissionServiceImpl permissionService;
@Override
public User getUserByName(String getMapByName) {
return getMapByName(getMapByName);
}
/**
* 模拟数据库查询
*
* @param userName 用户名
* @return User
*/
private User getMapByName(String userName) {
// 构建 Role 1
Role role = new Role("1", "admin", getAllPermission());
Set<Role> roleSet = new HashSet<>();
roleSet.add(role);
// 构建 Role 2
Role role1 = new Role("2", "user", getSinglePermission());
Set<Role> roleSet1 = new HashSet<>();
roleSet1.add(role1);
User user = new User("1", "root", "123456", roleSet);
Map<String, User> map = new HashMap<>();
map.put(user.getUserName(), user);
User user1 = new User("2", "zhangsan", "123456", roleSet1);
map.put(user1.getUserName(), user1);
return map.get(userName);
}
/**
* 权限类型一
*/
private Set<Permissions> getAllPermission() {
Set<Permissions> permissionsSet = new HashSet<>();
permissionsSet.add(permissionService.getPermsByUserId("1"));
permissionsSet.add(permissionService.getPermsByUserId("2"));
return permissionsSet;
}
/**
* 权限类型二
*/
private Set<Permissions> getSinglePermission() {
Set<Permissions> permissionsSet1 = new HashSet<>();
permissionsSet1.add(permissionService.getPermsByUserId("1"));
return permissionsSet1;
}
}
复制代码
LoginServiceImpl其实都可以不算是 Shiro 整个认证体系的一员 ,它只是做一个 User 管理的业务而已 , 那么剩下了干了什么?
- 写了一个 API 接口
- 准备了一个 Realm
- 通过 Subject 发起认证
- 在接口上标注相关的注解
整套流程下来 , 就是简单 , 便捷 , 很轻松的就集成了认证的功能.
三 . 源码
按照惯例 , 还是看一遍源码吧 ,我们按照四个维度来分析 :
- 请求的拦截
- 请求的校验
- 认证的过程
- 退出的过程
3.1 请求的拦截
这要从 ShiroFilterFactoryBean 这个类开始
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//....
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
C- ShiroFilterFactoryBean
?- 构建 ShiroFilterFactoryBean 时会为其配置一个 login 地址
M- applyGlobalPropertiesIfNecessary : 配置全局属性
- applyLoginUrlIfNecessary(filter);
- applySuccessUrlIfNecessary(filter);
- applyUnauthorizedUrlIfNecessary(filter);
M- applyLoginUrlIfNecessary
?- 为 Filter 配置 loginUrl
- String existingLoginUrl = acFilter.getLoginUrl();
- acFilter.setLoginUrl(loginUrl)
// 这里对所有的 地址做了拦截
C01- PathMatchingFilter
F- protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();
?- 所有的path均会在这里处理
- 拦截成功了会调用 isFilterChainContinued , 最终会调用 onAccessDenied -> M2_01
C02- FormAuthenticationFilter
M2_01- onAccessDenied
-
// 判断是否需要重定向
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
// 重定向到 login 页
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
// 当然还有已登录得逻辑 , 已登录是在上面之前判断得
C- AccessControlFilter
M- onPreHandle
?- 该方法中会调用其他得 Filter 判断是否登录
// 例如这里就是 AuthenticationFilter 获取 Subject
C- AuthenticationFilter
M- isAccessAllowed
- Subject subject = getSubject(request, response);
- return subject.isAuthenticated() && subject.getPrincipal() != null;
复制代码
整体的调用链大概是
- OncePerRequestFilter # doFilter
- AbstractShiroFilter # call
- AbstractShiroFilter # executeChain
- ProxiedFilterChain # doFilter
- AdviceFilter # doFilterInternal
- PathMatchingFilter # preHandle
最终会因为Filter 链 , 最终由 FormAuthenticationFilter 重定向出去
3.2 拦截的方式
按照我们的常规思路 , 拦截仍然是通过 Filter 来完成
C- AbstractShiroFilter
M- doFilterInternal
- final Subject subject = createSubject(request, response) : 通过 请求构建了一个 Subject
- 调用 Subject 的 Callable 回调
- updateSessionLastAccessTime(request, response);
- executeChain(request, response, chain);
M- executeChain
- 执行 FilterChain 判断
-
// 这里往上追溯 , 可以看到实际上是一个 AOP 操作 : ReflectiveMethodInvocation
// 再往上就是 AopAllianceAnnotationsAuthorizingMethodInterceptor , 注意这里面是懒加载的
C03- AnnotationsAuthorizingMethodInterceptor : 通过 Interceptor 对方法进行拦截
M3_01- assertAuthorized : 断言认证信息
- 获取一个集合 Collection<AuthorizingAnnotationMethodInterceptor>
FOR- 循环 AuthorizingAnnotationMethodInterceptor -> PS301
- assertAuthorized -> M3_05
C- AuthorizingAnnotationMethodInterceptor
M3_05- assertAuthorized(MethodInvocation mi)
- ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi))
- 这里是获取 Method 上面的 Annotation , 再调用 assertAuthorized 验证 -> M5_01
// 补充 : PS301
TODO
C05- RoleAnnotationHandler
M5_01- assertAuthorized(Annotation a)
- 如果不是 RequiresRoles , 则直接返回
- getSubject().checkRole(roles[0]) -> M6_02
- getSubject().checkRoles(Arrays.asList(roles));
?- 注意 , 这里是区别 And 和 Or 将 roles 分别处理
复制代码
请求的逻辑 :