shiro安全框架
1.1 什么是shiro
Shiro 是 apache 旗下一个开源安全框架(http://shiro.apache.org/),它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用 shiro 就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
1.2 架构图
1.2.1 简要架构图
- Subject :主体对象,负责提交用户认证和授权信息。
- SecurityManager:安全管理器,负责认证,授权等业务实现。
- Realm:领域对象,负责从数据层获取业务数据。
1.2.2 详细架构图
- Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
- SecurityManager(安全管理器) :Shiro 的核心,用来协调管理组件工作。
- Authenticator(认证管理器):负责执行认证操作。
- Authorizer(授权管理器):负责授权检测。
- SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一
个强有力的 Session 体验。 - SessionDAO:代表 SessionManager 执行 Session 持久(CRUD)动作,它允
许任何存储的数据挂接到 session 管理基础上。 - CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
- Cryptography(加密管理器):提供了加密方式的设计及管理。
- Realms(领域对象):是 shiro 和你的应用程序安全数据之间的桥梁。
1.3 shiro框架认证拦截实现(Filter)
1.3.1 基本环境配置
添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.0</version>
</dependency>
1.3.2 核心对象配置
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
/**
* @Configuration 注解描述的类,为springboot工程中的配置类,此类
* 的实例由spring创建和管理。
*/
@Configuration
public class SpringShiroConfig {
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(2*1800000L);//默认为30分钟
//不重写url
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
/**记住我对象配置*/
@Bean
public RememberMeManager rememberMeManager(){
CookieRememberMeManager rememberMeManager=new CookieRememberMeManager();
//构建Cookie对象,此对象负责存储用户状态信息,并将状态信息写到客户端
SimpleCookie cookie=new SimpleCookie("rememberMe");
cookie.setMaxAge(7*24*60*60);//设置cookie的的生命周期
rememberMeManager.setCookie(cookie);
return rememberMeManager;
}
/**配置shiro缓存管理器,减少授权时,频繁访问数据库查询用户权限信息的过程。*/
@Bean
public CacheManager shiroCacheManager(){
return new MemoryConstrainedCacheManager();
}
/**
* 配置SecurityManager,此对象是Shiro框架的核心,负责认证和授权等业务实现。
* 当由spring框架整合一个第三方的bean对象时,这个类型不是我们自己写的,
* 我们无法在类上直接使用类似@Component的注解进行描述,那么如何去整合
* 这样的bean的呢?
* 解决方案:自己在spring中的配置类(@Configuration)中定义方法,
* 在方法内部构建对象实例,并且由@Bean注解对方法进行描述即可.(记住,这是规则),
* 这个注解描述的方法其返回值会交给spring管理,其bean的名字默认为方法名,
* 当然也可以通过@Bean注解对应bean的名字进行定义(例如@Bean("sManager")).
*/
//站在java多态特性应用的角度分析,方法的返回值,参数列表类型能用抽象尽量使用抽象类型。
@Bean //
public SecurityManager securityManager(Realm realm,
CacheManager cacheManager,
RememberMeManager rememberMeManager,
SessionManager sessionManager){
//在web应用项目中,SecurityManager的具体实现建议使用DefaultWebSecurityManager对象。
DefaultWebSecurityManager securityManager=
new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setCacheManager(cacheManager);
securityManager.setRememberMeManager(rememberMeManager);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* 配置认证过滤规则,例如,哪些资源需要认证访问,哪些资源可以匿名访问。
* 这个规则我们来定义,规则的检验在shiro框架中是借助大量过滤器(Filter)去实现的,
* Shiro框架中提供了过滤器类型,但是基于其类型创建其过滤器实例需要通过滤器工厂,
* 而我们这里配置的FactoryBean对象就是用于创建过滤器工厂的一个对象(spring框架中
* 所有FactoryBean的作用都是用于创建过滤器工厂的)。
* @return
*/
@Bean
//@Autowired 可以省略
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
System.out.println("==shiroFilterFactoryBean===");
ShiroFilterFactoryBean filterFactoryBean=new ShiroFilterFactoryBean();
//在map中存储规则,key为资源名,value为规则
LinkedHashMap<String,String> map=new LinkedHashMap<>();
map.put("/bower_components/**","anon");//anon表示匿名访问,Shiro框架定义的字符串
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
map.put("/user/doLogin", "anon");//放行登录操作,允许登录
map.put("/doLogout", "logout");//当value为logout时,退出时会自动回到登录页面
//除了以上资源,后续所有资源都要认证访问
//map.put("/**", "authc");//authc表示需要认证
map.put("/**", "user");//此方式的认证还可以从客户端的cookie中取用户信息
filterFactoryBean.setFilterChainDefinitionMap(map);
//如何判定你访问这个资源时是否已经认证过了呢?(要通过securityManager实现)
filterFactoryBean.setSecurityManager(securityManager);
//假如访问一个需要认证的资源,但这个用户还没有通过认证,我们要做什么?跳转到指定认证页面(例如登录页面)
filterFactoryBean.setLoginUrl("/doLoginUI");
return filterFactoryBean;
}
/**
* 这里的advisor(顾问)负责找到类中使用此注解RequiresPermissions描述的方法,
* 这些方法为授权访问的切入点方法,当在执行这些方时会由通知(Advice)对象
* 调用SecurityManager对象完成权限检测及授权。
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
1.3.3 编辑PageController类
@GetMapping("/doLoginUI")
public String doLoginUI(){
return "login";
}
1.3.4 测试
1.4 登录认证实现
1.4.1 认证流程图
- 系统调用 subject 的 login 方法将用户信息提交给 SecurityManager
- SecurityManager 将认证操作委托给认证器对象 Authenticator
- Authenticator 将用户输入的身份信息传递给 Realm。
- Realm访问数据库获取用户信息然后对信息进行封装并返回。
- Authenticator 对 realm返回的信息进行身份认证。
1.4.2 API流程图
1.4.3 编辑SysUserDao
/**
* 基于用户名查找数据库中的用户对象
* @param username
* @return
*/
@Select("select * from sys_users where username = #{username}")
SysUser findUserByUsername(String username);
1.4.4 定义ShiroUserRealm类
/**
* 定义realm类型继承授权AuthorizingRealm类型,
* 假如只做认证可以直接继承认证AuthenticatingRealm即可.
*
* Shiro框架中Realm对象可以理解为用户获取认证数据信息和授权数据信息的一个对象。
*/
@Service
public class ShiroUserRealm extends AuthorizingRealm {//AuthorizingRealm 继承了 AuthenticatingRealm
@Autowired
private SysUserDao sysUserDao;
/**
* 此方法获取用户认证信息并进行封装
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取登录时输入的用户名
UsernamePasswordToken upToken= (UsernamePasswordToken) authenticationToken;
String username=upToken.getUsername();
//2.基于用户名查找数据库中的用户信息
SysUser user=sysUserDao.findUserByUsername(username);
//3.校验用户是否存在
if(user==null)
throw new UnknownAccountException();
//4.校验用户是否已被禁用
if(user.getValid()==0) throw new LockedAccountException();
//5.封装用户信息并返回,将信息交给SecurityManager进行认证
ByteSource credentialSalt=ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info=
new SimpleAuthenticationInfo(user,//principal用户身份
user.getPassword(),//hashedCredentials已加密的密码
credentialSalt,//credentialSalt 加密盐
getName());//realmName
return info;//此对象最终会交给securityManager
}
/***
* SecurityManager比对密码时需要调用此方法获取加密算法相关信息
* @return
*/
@Override
public CredentialsMatcher getCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher=
new HashedCredentialsMatcher("MD5");
credentialsMatcher.setHashIterations(1);
return credentialsMatcher;
}
}
//login form->controller-->subject.login(token)-->securityManager-->realm-->dao
1.4.5 编辑SysUserController
@RequestMapping("/doLogin")
public JsonResult doLogin(String username,String password,boolean isRememberMe){
//将用户名和密码封装到令牌对象中
UsernamePasswordToken token=
new UsernamePasswordToken(username, password);
Subject subject=SecurityUtils.getSubject();
//设置记住我
token.setRememberMe(isRememberMe);
//提交用户信息进行登录
subject.login(token);
return new JsonResult("login ok");
}
1.4.6 设置统一异常处理
在common项目中GlobalExceptionHandler类中编辑
/**
* 处理shiro框架异常
* @param e
* @return
*/
@ExceptionHandler(ShiroException.class)
public JsonResult doHandleShiroException(ShiroException e){
JsonResult result=new JsonResult();
result.setState(0);//设置状态
if(e instanceof UnknownAccountException){
result.setMessage("账户不存在");
}else if(e instanceof LockedAccountException){
result.setMessage("账户已被禁用");
}else if(e instanceof IncorrectCredentialsException){
result.setMessage("账户密码不正确");
}else if(e instanceof AuthorizationException){
result.setMessage("没有此操作的权限");
}else{
result.setMessage("系统故障,请稍后访问");
}
e.printStackTrace();
return result;
}
1.5 授权实现
1.5.1 授权流程图
- 系统调用 subject 相关方法将用户信息(例如 isPermitted)递交给 SecurityManager。
- SecurityManager 将权限检测操作委托给 Authorizer 对象。
- Authorizer 将用户信息委托给 realm。
- Realm访问数据库获取用户权限信息并封装。
- Authorizer 对用户授权信息进行判定。
1.5.2 API流程图
1.5.3 编辑Dao
1.5.3.1 SysUserRoleDao
/**
* 基于用户id查找用户对应的角色id
* @param userId
* @return
*/
@Select("select role_id from sys_user_roles where user_id=#{userId}")
List<Integer> findRoleIdsByUserId(Integer userId);
1.5.3.2 SysRoleMenuDao
List<Integer> findMenuIdsByRoleIds(@Param("roleIds")List<Integer> roleIds);
1.5.3.3 SysMenuDao
List<String> findPermissions(List<Integer> menuIds);
1.5.4 编辑Mapper.xml
1.5.4.1 SysRoleMenuMapper
<select id="findMenuIdsByRoleIds" resultType="int">
select menu_id
from sys_role_menus
where role_id in <!--(1,2,3,4,5)-->
<foreach collection="roleIds" open="(" close=")" separator="," item="roleId">
#{roleId}
</foreach>
</select>
1.5.4.2 SysMenuMapper
<select id="findPermissions" resultType="string">
select permission
from sys_menus
where id in
<foreach collection="menuIds" open="(" close=")" separator="," item="menuId">
#{menuId}
</foreach>
</select>
1.5.4 编辑ShiroUserRealm类
@Autowired
private SysUserRoleDao sysUserRoleDao;
@Autowired
private SysRoleMenuDao sysRoleMenuDao;
@Autowired
private SysMenuDao sysMenuDao;
/**
*此方法用于获取用户的权限信息并进行封装。
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
//1.获取登录用户
SysUser user=(SysUser)principalCollection.getPrimaryPrincipal();
//2.获取登录用户的角色id (从用户角色关系表)
List<Integer> roleIds=sysUserRoleDao.findRoleIdsByUserId(user.getId());
if(roleIds==null||roleIds.size()==0)throw new AuthorizationException();
//3.获取角色对应的菜单id(用角色菜单关系表)
List<Integer> menuIds=sysRoleMenuDao.findMenuIdsByRoleIds(roleIds);
if(menuIds==null||menuIds.size()==0)throw new AuthorizationException();
//4.获取菜单id对应授权标识(permisssion)-从菜单表
List<String> permissions=sysMenuDao.findPermissions(menuIds);
if(permissions==null||permissions.size()==0)throw new AuthorizationException();
//5.封装数据并返回,交给securityManager对象
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
Set<String> setPermissions=new HashSet<>();
for(String per:permissions){
if(per!=null&&!"".equals(per)){
setPermissions.add(per);
}
}
System.out.println("setPermissions="+setPermissions);
info.setStringPermissions(setPermissions);
return info;
}
/**
* 这里的advisor(顾问)负责找到类中使用此注解RequiresPermissions描述的方法,
* 这些方法为授权访问的切入点方法,当在执行这些方时会由通知(Advice)对象
* 调用SecurityManager对象完成权限检测及授权。
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
1.5.5 授权方法上添加注解
在需要进行授权访问的业务层(Service)方法上,添加执行此方法需要的权限标识,参考代码@RequiresPermissions(“sys:user:update”)
/**
* @RequiresPermissions 注解描述的方法为授权访问切入点方法,表示
* 访问这个方法需要授权.这个授权的动作由shiro框架中的securityManager
* 对象来实现,请问这个对象如何对用户进行授权?第一,要获取用户有什么权限,
* 第二,要获取访问这个方法需要什么权限(RequiresPermissions内部value属性
* 的值) 第三,检测用户拥有的这些权限中是否包含执行此方法需要的权限,假如
* 包含则授权。
* @param id
* @param valid
* @return
*/
@Transactional //此注解描述的方法为事务切入点方法
@RequiresPermissions(value="sys:user:update")//述的方法为授权访问切入点方法
@RequiredLog //此注解描述的方法为日志切入点方法
@Override
public int validById(Integer id, Integer valid) {
///...............
//1.参数校验
if(id==null||id<1)
throw new IllegalArgumentException("id值无效");
if(valid==null||valid!=0&&valid!=1)
throw new IllegalArgumentException("状态值不正确");
//2.修改状态并校验结果
int rows=sysUserDao.validById(id,valid,"admin");//这里admin代表登录用户
if(rows==0)
throw new ServiceException("记录可能已经不存在");
return rows;
}
1.6 shiro中的缓存配置
1.6.1 背景分析
对于已认证用户而言,默认每次进行授权方法的访问,偶需要从数据库中查询用户的权限信息,而这部分信息相对比较稳定,但是频繁访问数据获取这部分信息可能会在性能上有一定的影响,如何解决这个问题?
– 借助缓存,将查询出来的数据暂时放到缓存中,下次授权可以从缓存中获取
1.6.2 添加配置
/**配置shiro缓存管理器,减少授权时,频繁访问数据库查询用户权限信息的过程。*/
@Bean
public CacheManager shiroCacheManager(){
return new MemoryConstrainedCacheManager();
}
//站在java多态特性应用的角度分析,方法的返回值,参数列表类型能用抽象尽量使用抽象类型。
@Bean
public SecurityManager securityManager(Realm realm,
CacheManager cacheManager,
RememberMeManager rememberMeManager,
SessionManager sessionManager){
//在web应用项目中,SecurityManager的具体实现建议使用DefaultWebSecurityManager对象。
DefaultWebSecurityManager securityManager=
new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setCacheManager(cacheManager);
securityManager.setRememberMeManager(rememberMeManager);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
1.7 “记住我” 功能实现
1.7.1 编辑SpringShiroConfig类
/**记住我对象配置*/
@Bean
public RememberMeManager rememberMeManager(){
CookieRememberMeManager rememberMeManager=new CookieRememberMeManager();
//构建Cookie对象,此对象负责存储用户状态信息,并将状态信息写到客户端
SimpleCookie cookie=new SimpleCookie("rememberMe");
cookie.setMaxAge(7*24*60*60);//设置cookie的的生命周期
rememberMeManager.setCookie(cookie);
return rememberMeManager;
}
1.7 Shiro 会话管理配置
1.7.1 背景分析
web客户端与服务端通讯时可能会产生一些会话状态,这些状态信息无法通过http协议进行存储,因为此协议本生是一个无状态协议,该如何进行存储?
- 在JavaEE中定义了两种对象存储
Cookie(客户端记录)
Session(服务端记录)
1.7.2 编辑 SpringShiroConfig 类
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(2*1800000L);//默认为30分钟
//不重写url
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}