Shiro安全框架
Shiro 是 apache 旗下一个开源安全框架,它对软件系统中的安全认证相关功能进行了封装,实现了用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
Shiro框架的详细架构
- Subject : 主体对象,负责提交用户认证和授权信息。
- SecurityManager(安全管理器) : Shiro 的核心,安全管理器,负责认证,授权等业务实现。
- Authenticator(认证管理器):负责执行认证操作。
- Authorizer(授权管理器):负责授权检测。
- Cryptography(加密管理器):提供了加密方式的设计及管理
- Realms(领域对象): 负责从数据层获取业务数据。
Shiro配置
1.下载依赖
<!--可在官网下载最新版本-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>
2.Shiro 核心对象配置
- 推荐在service的realm包下创建一个 Realm 类型的实现类(基于此类通过 DAO 访问数据库)
//定义shiro realm对象,基于此对象获取用户认证和授权信息,
//假如将来你的项目只做认证,不做授权,则继承继承AuthenticatingRealm对象即可
public class ShiroRealm extends AuthorizingRealm {
/**此方法负责获取并封装授权信息*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
return null;
}
/**此方法负责获取并封装认证信息*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken)
throws AuthenticationException {
return null;
}
}
- 在启动类同级下创建一个ShiroConfig配置类,添加 Realm 对象配置
//定义Realm对象,通过此对象访问数据库中的用户和权限信息,并进行封装。
//@Bean注解描述方法时,表示方法的返回要交给spring管理,这个bean的名字默认为方法名。
//还可以自己起名,例如@Bean("shiroRealm")*/
@Bean
public Realm realm() {
return new ShiroRealm();
}
- 在启动类中定义过滤规则(哪些访问路径要进行认证才可以访问)
Shiro验证URL时的顺序
- URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)
- 故filterChainDefinitions的配置顺序为自上而下,以最上面的为准
user和authc都是认证过滤器,但不同的地方是:
- 当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的。
//基于此对象定义过滤规则,例如哪些请求必须要认证,哪些请求不需要认证
//ShiroFilterChainDefinition 此对象中定义了若干过滤器Filter
//基于这些过滤器以及我们定义的过滤规则对业务进行实现。
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition =
new DefaultShiroFilterChainDefinition();
//设置登录操作不需要认证,anon表示可以匿名访问(不用登录就可以访问),
//其中 anon 为 shiro 框架指定的 匿名过滤器
chainDefinition.addPathDefinition("/user/login/**","anon");
//配置登出操作,登出以后要跳转到登录页面(其中,logout表示系统登出处理)
chainDefinition.addPathDefinition("/user/logout","logout");
//设置哪些资源需要认证才可访问
//其中 authc 为 shiro 框架指定的 认证过滤器
//chainDefinition.addPathDefinition("/**", "authc");
//当启用rememberme时,设置为user过滤器,因为authc是需要重新认证的。
chainDefinition.addPathDefinition("/**", "user");
return chainDefinition;
}
- 在spring 的配置文件(application.yml)中,添加登录页面的配置
shiro:
loginUrl: /login.html
Shiro认证业务实现
业务流程逻辑分析
- 系统调用 subject 的 login 方法将用户信息提交给 SecurityManager
- SecurityManager 将认证操作委托给认证器对象 Authenticator
- Authenticator 将用户输入的身份信息传递给 Realm。
- Realm 访问数据库获取用户信息然后对信息进行封装并返回。
- Authenticator 对 realm 返回的信息进行身份认证。
业务逻辑实现
1.在 SysUserDao 中定义基于用户名查询用户信息的方法
@Select("select * from sys_users where username=#{username}")
SysUser selectUserByUsername(String username);
2.修改 ShiroRealm 中获取认证信息的方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取客户端提交的用户名
String username = ((UsernamePasswordToken)authenticationToken).getUsername();
//2.基于用户名查询用户信息并校验
SysUser sysUser = sysUserDao.selectUserByUsername(username);
if(sysUser==null) throw new UnknownAccountException();
if(sysUser.getValid()==0) throw new LockedAccountException();
//3.封装用户信息
ByteSource credentialsSalt = ByteSource.Util.bytes(sysUser.getSalt());
String realmName= this.getName();
//4.返回认证信息给Authenticator进行身份认证。
return new SimpleAuthenticationInfo(
sysUser,sysUser.getPassword(),credentialsSalt,realmName);
}
3.在 ShiroRealm 中重谢获取凭证加密算法的方法
//重写此方法的目的是,底层对用户输入的登录密码进行加密,需要算法
public CredentialsMatcher getCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher =
new HashedCredentialsMatcher("MD5");//MD5算法
credentialsMatcher.setHashIterations(1);//加密次数1次
return credentialsMatcher;
}
4.在 SysUserController 中添加处理登录请求的方法
@GetMapping("/login/{username}/{password}")
public JsonResult doLogin(@PathVariable String username,
@PathVariable String password){
//将账号和密码封装 token 对象
UsernamePasswordToken token = //参考官网
new UsernamePasswordToken(username, password);
//基于 subject 对象将 token 提交给 securityManager
token.setRememberMe(true);//设置记住我
Subject subject = SecurityUtils.getSubject();
subject.login(token);//提交给 securityManager
return new JsonResult("login ok");
}
5.处理异常:统一异常处理类中添加 shiro 异常处理代码
@ExceptionHandler(ShiroException.class)
public JsonResult doShiroException(ShiroException e){
JsonResult r=new JsonResult();
r.setState(0);
e.printStackTrace();
log.error("exception {}",e.getMessage());
if(e instanceof UnknownAccountException){
r.setMessage("用户名不存在");
}else if(e instanceof IncorrectCredentialsException){
r.setMessage("密码不正确");
}else if(e instanceof LockedAccountException){
r.setMessage("账户被锁定");
}else if(e instanceof AuthorizationException){
r.setMessage("没有此权限");
}else{
r.setMessage("认证或授权失败");
}
return r;
}
6.:在 spring 的配置文件(application.yml)中,添加登录页面的配置
shiro:
loginUrl: /login.html
Shiro授权业务实现
业务流程逻辑分析
- 系统调用subject相关方法将用户信息(例如isPermitted)递交给SecurityManager。
- SecurityManager 将权限检测操作委托给 Authorizer 对象。
- Authorizer 将用户信息委托给 realm。
- Realm 访问数据库获取用户权限信息并封装。
- Authorizer 对用户授权信息进行判定。
业务逻辑实现
1.在 SysMenuDao 中定义基于用户名查询用户信息的方法
//用set集合接收permission字符串目的是去重,也可在sql语句中加入distinct
Set<String> selectUserPermissions(Integer userId);
2.在 SysMenuMapper 中添加查询用户权限标识的 SQL 映射
<select id="selectUserPermissions"
resultType="string">
select distinct permission
from sys_user_roles ur join sys_role_menus rm join sys_menus m
on ur.role_id=rm.role_id and rm.menu_id=m.id
where ur.user_id =# {userId} and m.permission!=''and m.permission is not null
</select>
3.修改 ShiroRealm 中获取权限并封装权限信息的方法
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
//1.获取登录用户(登录时传入的用户身份是谁)
SysUser user= (SysUser) principalCollection.getPrimaryPrincipal();
//2.基于登录用户 id 获取用户权限标识
Set<String> permissions=
sysMenuDao.selectUserPermissions(user.getId());
//3.封装用户权限信息并返回
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.setStringPermissions(permissions);
return info;
}
4.对需要授权登录的业务方法使用切入点注解描述
- 在 shiro 框架中,授权切入点方法需要通过@RequiresPermissions 注解进行描述
//此注解的参数将会与用户的permission集合中的String对比 //,如果有此参数则拥有访问此方法的权限。 @RequiresPermissions("sys:user:update") public int validById(Integer id,Integer valid)
Shiro框架与其他API的不兼容
Shiro与PageHelper不兼容
不兼容案例:
- 在Service层里使用shiro的@RequiresPermissions注解定义切入点方法。
- 在Controller层中此方法使用了分页查询功能
结果:
- 当客户端访问时,先通过Controller层调用Service层,而此时权限校验还未开启,
- 所以当Service权限不通过时,页面上仍然会出现分页查询,但值为空。
- 这是因为:SpringBoot先调用Controller层时不需要权限认证,分页查询仍然会被启动
解决方案1:在Service层启动分页拦截查询,Controller层调用此方法返回的结果
@RequiresPermissions("sys:user:view")
@Override
public PageInfo<SysUser> findUsers(SysUser sysUser) {
return PageUtil.startPage().doSelectPageInfo(()->{
sysUserDao.selectUsers(sysUser);
});
}
- 这个方法的缺点就是,需要改动的代码较大,不仅要将Service层的方法返回值更换,还要更换抽象类和数据层的方法返回值
解决方案2:仍在Controller层中执行分页查询,但把@RequiresPermissions注解放在Controller的方法上
- 如果把@RequiresPermissions注解放在Controller的方法上,此方法的其他注解会失效,这将导致GetMapper失效从而访问不到此url;
- 这时可以在ShiroConfig配置类中配置此对象
//当将shiro中的注解RequiresPermissions放到控制层方法时需要配置此对象
//并设置对控制层方法上的注解有效(例如@GetMapping),即setUserPrefix为true
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=
new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setUsePrefix(true);
return advisorAutoProxyCreator;
}
Shiro框架提供的其他功能
配置Session会话
- 在ShrioConfig配置类中配置
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager=
new DefaultWebSessionManager();
//session 超时自动关闭的时间
sessionManager.setGlobalSessionTimeout(1000*60*60);//1 个小时
// sessionManager.setGlobalSessionTimeout(2*60*1000);//2 分钟
//删除无效 session
sessionManager.setDeleteInvalidSessions(true);
//当客户端 cookie 被禁用是否要设置 url 重写
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
配置"记住我"
记住我功能是要在用户登录成功以后,假如关闭浏览器,下次再访问系统资源时,无需再执行登录操作
"记住我"的本质是将Cookie持久化
在Controller的doLogin方法中对 UsernamePasswordToken token 对象进行配置:
- token.setRememberMe(true);
在ShiroConfig配置类中配置
@Bean public RememberMeManager rememberMeManager(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); SimpleCookie cookie = new SimpleCookie("rememberMe"); //将Cookie持久化 cookie.setMaxAge(7*24*60*60);//设置为7天 cookieRememberMeManager.setCookie(cookie); //当项目中通过shiro实现了记住我功能,假如此时客户端登录成功,服务端重启 //再通过客户端访问,服务端就会出现问题,此时加入以下代码 cookieRememberMeManager.setCipherKey(Base64 .decode("FBenXrLADHpyCrwQ0+RI8Q==")); /* base64Encoded码在test中由以下代码生成 @Test void testGeneratorKey() throws NoSuchAlgorithmException { KeyGenerator keyGenerator= KeyGenerator.getInstance("AES"); SecretKey secretKey = keyGenerator.generateKey(); String key= Base64.encodeToString(secretKey.getEncoded()); System.out.println(key); } */ return cookieRememberMeManager; }
当设置了记住我时有一个漏洞。当客户端用户登陆成功后,服务器重启会让客户端再次访问时出现错误
- 解决方案:生成一个decode码 --- Base64.encodeToString(secretKey.getEncoded());
在Shiro配置类中修改认证拦截器中的过滤配置,将 authc 替换为 user
//当启用rememberme时,设置为user过滤器,因为authc是需要重新认证的 chainDefinition.addPathDefinition("/**", "user");
授权缓存配置
Shiro框架默认客户端每次访问都会进行权限比对但也给出了减少数据库的访问压力, 同时提高授权性能的对应方案
在Shiro配置类中配置缓存
@Bean protected CacheManager shiroCacheManager() { return new MemoryConstrainedCacheManager(); }
配置好以后,重启服务,登陆成功以后,反复访问授权方法,检测 realm 中权限信息 的查询。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。