SpringBoot-Shiro安全权限框架
Apache Shiro是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,授权,企业会话管理和加密。
官网:
http://shiro.apache.org/
源码:
https://github.com/apache/shiro
Subject:代表当前用户或者当前程序,在Shiro中Subject是一个接口,他定义了很多认证授权的方法。
认证就是判断你这个用户是不是合法用户,授权其实就是你认证成功之后,你的权限能访问系统的那些资源。
SecurityManage:安全管理器,Subject去认证的时候,需要通过SecurityManage安全管理器来负责认证和授权
安全管理器又要通过Authenticator认证器进行认证,通过Authorizer授权器进行授权,通过SessionManag会话管理器进行会话管理,有没有发现他就相当于一个中介,他来接收这些事情,而干这些事情的不是他来做的。
Authenticator:认证器,Realm从数据库中去获取到用户信息,然后认证器来做身份认证来进行身份认证。
Authorizer:授权器,通过认证器认证权限之后,得通过授权器来判断这个用户身份有什么权限,他可以访问那些资源
Realm:相当于数据源,从Realm中获取到用户的数据,比如用户的数据在MYSQL数据库,那么Realm就需要从MYSQL数据库中去获取到用户的信息,然后来做身份认证。
在Realm中也有一些认证授权相关的操作。
SessionManager:会话管理器,不依赖web容器的session,所以shiro可以使用在非web 应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO:会话,比如要将Session存储到数据库,那么可以通过jdbc来存储到数据库。
一、引入依赖
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.5.3</version>
</dependency>
二、shiro配置文件shiro.ini(用户名或者密码)
[users]
relaysec=123456
三、测试代码
public class ShiroDemo{public static void main(String[] args){//1.创建安全管理器对象DefaultSecurityManager securityManager = new DefaultSecurityManager();//2.给安全管理器设置realmsecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));//3.SecurityUtils给全局安全工具类设置安全管理器SecurityUtils.setSecurityManager(securityManager);//4.关键对象subject主体Subject subject = SecurityUtils.getSubject();//5.创建令牌UsernamePasswordToken token = new UsernamePasswordToken("relaysec","123456");try{subject.login(token);//用户认证System.out.println("登录成功");}catch(UnknownAccountException e){e.printStackTrace();System.out.println("认证失败: 用户名不存在~");}catch(IncorrectCredentialsException e){e.printStackTrace();System.out.println("认证失败: 密码错误~");}}
}
SpringBoot整合shrio
一、创建一个war项目
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-juli</artifactId><version>8.5.23</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--引入JSP解析依赖--><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId></dependency><dependency><groupId>jstl</groupId><artifactId>jstl</artifactId><version>1.2</version></dependency><!--引入shiro整合Springboot依赖shiro-spring-boot-web-starter--><!--CVE-2020-1957 Shiro <= 1.5.1--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.4.2</version></dependency>
<!-- <dependency>-->
<!-- <groupId>org.apache.shiro</groupId>-->
<!-- <artifactId>shiro-spring</artifactId>-->
<!-- <version>1.4.2</version>-->
<!-- </dependency>--><!--CVE-2020-11989 shiro < 1.5.3-->
<!-- <dependency>-->
<!-- <groupId>org.apache.shiro</groupId>-->
<!-- <artifactId>shiro-web</artifactId>-->
<!-- <version>1.4.2</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.apache.shiro</groupId>-->
<!-- <artifactId>shiro-spring</artifactId>-->
<!-- <version>1.4.2</version>-->
<!-- </dependency>--><!-- <dependency>-->
<!-- <groupId>org.apache.shiro</groupId>-->
<!-- <artifactId>shiro-spring</artifactId>-->
<!-- <version>1.5.3</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.apache.shiro</groupId>-->
<!-- <artifactId>shiro-web</artifactId>-->
<!-- <version>1.5.3</version>-->
<!-- </dependency>--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.7.0</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.7.0</version></dependency>
二、创建ShiroConfig(@Configuration修饰)
配置3个bean,ShiroFilterFactory、DefaultWebSecurityManager、Realm
@Configuration
public class ShiroConfig implements EnvironmentAware{private Environment env;
}
①、创建ShiroFilter
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean setSecrityManager(defaultWebSecurityManager);//配置系统受限资源和系统公共资源
/**map的key值代表的是我们的资源,map的value值代表的是我们的权限authc代表我们是需要认证和授权的,anon代表我们不需要认证和授权其实代码审计去审的就是shiroConfig文件,看他的jar包,以及ShiroConfig配置文件
*/
Map<String,String> map = new HashMap<>();
//authc 请求这个资源需要认证和授权
map.put("/admin/**","anon");
map.put("/admin/users","authc");
map.put("/demo/**","anon");
map.put("/index.jsp","authc");
map.put("/hello/*", "authc");
map.put("/toJsonList/*","authc");shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
还有一种方式:
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager sessionManager) {
//构建ShiroFilterFactoryBean对象,负责创建过滤器工厂ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置登录路径shiroFilterFactoryBean.setLoginUrl("/login");
//注意:必须设置SecuritManager
shiroFilterFactoryBean.setSecurityManager(sessionManager);
//设置访问未授权的需要跳转到的路径shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//设置登录成功访问路径shiroFilterFactoryBean.setSuccessUrl("/");
//自定义的过滤设置注入到shiroFilter中shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter());shiroFilterFactoryBean.getFilters().put("csrf", new CsrfFilter());shiroFilterFactoryBean.getFilters().put("user", new UserAuthcFilter());
//定义map指定请求过滤规则Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();ShiroUtils.loadBaseFilterChain(filterChainDefinitionMap);ShiroUtils.ignoreCsrfFilter(filterChainDefinitionMap);filterChainDefinitionMap.put("/**", "apikey, csrf, authc");return shiroFilterFactoryBean;
}
Shiro有两种方式可进行精度控制,一种是过滤器方式,根据访问的URL进行控制,该种方式允许使用*匹配URL,可以进行粗粒度控制;另一种是注解的方式,实现细粒度控制,但只能是在方法上控制,无法控制类级别访问。
过滤器的类型有很多,本文代码只用到anon和authc两种类型
定义一个Map类型的filterChainDefinitionMap,使用ShiroFilterChainDefinition来控制请求路径的鉴权与授权。
创建ShiroUtils类,自定义静态方法loadBaseFilterChain()和ignoreCsrfFilter()方法,判断哪些请求路径需要用户登录才能访问,哪些不需要登录就能访问,实现粗粒度控制。
②、创建安全管理器
将用户认证信息源设置到安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(realm);return defaultWebSecurityManager;
}
还有一种方式:
管理内部组件实例,并通过它来提供安全管理的各种服务。
modularRealmAuthenticator是shiro提供的realm管理器,用来设置realm生效, 通过setAuthenticationStrategy来设置多个realm存在时的生效规则
@Bean(name="securityManager")
public DefaultWebSecurityManager securityManager(SessionManager sessionManager, MemoryConstrainedCacheManager memoryConstrainedCacheManager){DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();dwsm.setSessionManager(sessionManager);dwsm.setCacheManager(memoryConstrainedCacheManager);dwsm.setAuthenticator(modularRealmAuthenticator());return dwsm;
}
重写ModularRealmAuthenticator,只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());return modularRealmAuthenticator;
}
securityManager不用直接注入Realm可能导致事务失效
可以定义一个handleContextRefresh方法,利用监听去初始化,等到ApplicationContext加载完成之后,完成shiroReaml
@EventListener
public void handleContextRefresh(ContextRefreshedEvent event){ApplicationContext context = event.getApplicationContext();List<Realm> realmList = new ArrayList<>();LocalRealm localRealm = context.getBean(LocalRealm.class);LdapRealm ldapRealm = context.getBean(LdapRealm.class);realmList.add(LocalRealm);realmList.add(ldapRealm);context.getBean(DefaultWebSecurityManager.class).setRealms(realmList);
}
③、自定义Realm
@Bean
public Realm getRealm(){CustomerRealm customerRealm = new CustomerRealm();return customerRealm;
}/**自定义Realm一般继承AuthorizingRealm,然后实现getAuthenticationInfo()和getAuthorizationInfo()方法,来完成身份认证和权限获取。
*/
public class CustomerRealm extends AuthorizingRealm{/**用于授权PrincipalCollection 是一个身份集合首先通过getPrimaryPrincipal()得到传入的用户名,然后调用getAuthorizationInfo()方法。再根据用户名调用 UserService接口获取角色及权限信息,并将得到的用户roles放到authorizationInfo中,并返回。*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){String userId = (String) principals.getPrimaryPrincipal();return getAuthorizationInfo(userId,usserService);}public static AuthorizationInfo getAuthorizationInfo(String userId,UserService userService){SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();UserDTO userDTO = userService.getUserDTO(userId);Set<String> roles = userDTO.getRoles().stream().map(Role::getId).collect(Collectors.toSet());authorizationInfo.setRoles(roles);return authorizationInfo();}//用于验证账户和密码@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {/**//直接从参数中获取用户名和密码String username = token.getUsername();String password = String.valueOf(token.getPassword())*/System.out.println("=============");//从传过来的token获取用户名String principal = (String) token.getPrincipal();System.out.println("用户名"+principal);//假设从数据库中获得用户名,密码String password_db="123";String username_db="zhangsan";if (username_db.equals(principal)){
// SimpleAuthenticationInfo simpleAuthenticationInfo =return new SimpleAuthenticationInfo(principal,"123", this.getName());}return null;}
}
还有方式:
展示一个LdapReam Bean,注解@DependsOn表示组件依赖,下图中表示依赖lifecycleBeanPostProcessor
LifecycleBeanPostProcessor用来管理shiro Bean的生命周期,在LdapReam创建之前先创建lifecycleBeanPostProcessor
@Bean
@DependsOn(lifecycleBeanPostProcessor)
public LdapRealm ldapRealm(){return new LdapRealm();
}@Bean(name="lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){return new LifecycleBeanPostProcessor();
}
三、Controller进行访问,登录成功之后转发到index.jsp,否则直接转发到login.jsp文件。
@RequestMapping("login")
public String login(String username,String password){Subject subject = Security.getSubject();try{//认证成功UsernamePasswordToken token = new UsernamePasswordToken(uername,password);subject.login(token);return "redirect:/index.jsp";}catch(UnknownAccountException e){e.printStackTrace();System.out.println("用户名错误");}catch(IncorrectCredentialsException e){e.printStackTrace();System.out.println("密码错误");}catch(Exception e){e.printStackTrace();System.out.println(e.getMessage());}return "redirect:/login.jsp";
}
漏洞复现
Shiro层面绕过之后,SpringBoot也需要解析路径的,所以如果Springboot版本过高的话,可能是复现不成功的。并且不能使用Springboot集成的shiro吗,那样子也有可能导致复现不成功
<groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.5.0</version>
</dependency>
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.5.0</version>
</dependency>
ShiroConfig配置
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();map.put("/login","anon");//anon 设置为公共资源 放行资源放在下面
// map.put("/user/register","anon");//anon 设置为公共资源 放行资源放在下面
// map.put("/register.jsp","anon");//anon 设置为公共资源 放行资源放在下面
// map.put("/user/getImage","anon");map.put("/doLogin", "anon");map.put("/demo/**","anon");map.put("/unauth", "user");map.put("/admin/*","authc");//默认认证界面路径---当认证不通过时跳转shiroFilterFactoryBean.setLoginUrl("/login.jsp");shiroFilterFactoryBean.setFilterChainDefinitionMap(map);return shiroFilterFactoryBean;
Controller:
绕过方式: /demo/…;/admin/index
漏洞分析:
定位到PathMatchingFilterChainResolver类的getchain方法,这个方法是处理过滤的
首先调用getPathWithinApplication方法获取路径,跟进去。
来到getPathWithinApplication方法,继续跟进WebUtils的getPathWithinApplication方法
首先getContextPath方法获取工程路径,调用getRequestUri获取访问路径,跟进去getRequestUri方法
来到getRequestUri方法,首先从域中获取,获取不到的话,调用getRequestURI方法获取路径,获取的就是我们访问的//demo/…;/admin/users 这个路径,然后调用decodeAndCleanUriString方法进行处理。
来到decodeAndCleanUriString方法,通过indexOf方法,因为我们的路径中存在分号,所以他获取到的位置是第9个,
然后判断如果不等于-1的话,调用substring方法进行字符串截取,从0到9 包前不包后 ,也就是说分号不需要截取,截取出来的字符串就是//demo/…。然后返回上一个方法。
来到normalize方法,这里进行了字符的替换,
替换反斜线
替换 // 为 /
替换 /./ 为 /
替换 /…/ 为 /
然后返回。
回到getChain方法,首先判断如果url不等于null并且他的最后一位是 / 的话,进行字符串截取然后赋值,我们拿到的字符串路径是/demo/… 所以往下走。
然后循环遍历我们的map中的内容,就是我们在Shiroconfig中写的那些过滤的内容,然后进行一一匹配,最后匹配到/demo/**的时候,然后调用proxy方法,我们跟进去。
来到proxy方法,首先调用getChain方法获取到请求路径对应的过滤器,然后调用过滤器的proxy方法,来到proxy方法
来到proxy方法,首先创建了一个ProxiedFilterChain对象,这个对象是一个代理对象。
基本上到这里我们的原始请求就会进入到 springboot中. springboot对于每一个进入的request请求也会有自己的处理方式,找到自己所对应的controller。
我们定位到Spring处理请求的地方。我们跟进去getPathWithinApplication方法
到getPathWithinApplication方法,调用getContextPath方法获取到工程路径,调用getRequestUri获取访问路径,我们跟进getRequestUri方法
来到getRequestUri方法,首先从域中获取,获取不到的话然后通过getRequestURI方法获取到url,然后调用decodeAndCleanUriString方法,我们跟进去。
来到decodeAndCleanUriString方法,跟进removeSemicolonContent方法。
首先获取到分号的位置,然后while循环如果不等于-1的话,然后进行字符串截取,将我们的分号截取掉 然后返回的路径就是//demo…
回到decodeAndCleanUriString方法,调用decodeRequestString进行decode解码,然后调用getSanitizedPath方法进行过滤 //
然后返回。
回到getPathWithinApplication方法,可以发现我们的分号已经被去掉了。
到这里基本上的流程就结束了,可以发现在Spring中会过滤分号,而在Shiro中不会。导致权限绕过。
===================================================
应用案例登录认证
- 客户端提交用户账号和密码,在Controller中拿到账号和密码封装到token对象.
- 然后借助subject的login方法,把数据提交给SecurityManager
- 使用Authenticator处理token,Authenticator从Realm列表中获取LdapRealm
- LdapRealm从token中获取数据,交给authenticate进行比对,对比通过返回AuthenticationInfo
一、创建maven工程,并导入相关依赖
shiro-core commons-logging
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.2.5</version>
</dependency>
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>1.2.5</version>
</dependency>
<dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>1.2.1</version>
</dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-quartz</artifactId></dependency>
登录控制器Controller
@PostMapping(value="/signin")
public ResultHolder login(@RequestBody LoginRequest request){SessionUser sessionUser = SessionUtils.getUser();if(sessionUser!=null){if(!StringUtils.equals(sessionUser.getId(), request.getUsername())){return ResultHolder.error(Translator.get("please_logout_current_user"));}SecurityUtils.getSubject().getSession().setAttribute("authenticate", UserSource.LOCAL.name());}return userService.login(request);
}
在login方法中,把用户名和密码封装为UsernamePasswordToken对象token,然后通过SecurityUtils.getSubject()获取Subject对象,并将前面获取token对象作为参数。若调用subject.login(token)时不抛出任何异常,说明认证通过,调用subject.isAuthenticated()返回true表示当前的用户已经登录。后续可以根据subject实例获取用户信息。
public ResultHolder login(LoginRequest request) {String login = (String) SecurityUtils.getSubject().getSession().getAttribute("authenticate");String username = StringUtils.trim(request.getUsername());String password = "";if (!StringUtils.equals(login, UserSource.LDAP.name())) {password = StringUtils.trim(request.getPassword());……}UsernamePasswordToken token = new UsernamePasswordToken (username, password, login);Subject subject = SecurityUtils.getSubject();try {subject.login(token);if (subject.isAuthenticated()) {UserDTO user = (UserDTO) subject.getSession().getAttribute(ATTR_USER);……return ResultHolder.success(subject.getSession().getAttribute("user"));
} else {return ResultHolder.error(Translator.get("login_fail"));}
} catch (ExcessiveAttemptsException e) {throw new ExcessiveAttemptsException(Translator.get("excessive_attempts"));
}
……
}
=============================================================
案例二:
@RestController
@CrossOrigin
@RequestMapping("/")
public class LoginController{private static final Logger logger = LoggerFactory.getLogger(LoginController.class);@Reference //Dubbo远程调用的服务private UserService userService;@RequestMapping(value="/login",method=RequestMethod.POST)public ResponseEntity login(){//获取存储在系统的用户ShiroUser user = (ShiroUser)SecurityUtils.getSubject().getPrincipal();//为获取的用户添加tokenuser.setToken(SecurityUtils.getSubpect().getSession().getId().toString());return ResponseEntity.ok(user);}/**获取当前登陆人的信息(包括角色权限)*/@GetMapping("/logininfo")public ResponsseEntity loginInfo(){ShiroUser shiroUser = (ShiroUser) SecurityUtils.getSubject().getPrincipal();Map<String,Object> map = new HashMap<>();Set<String> permissions = Sets.newHashSet();//将获取的角色和权限存入指定的mapUser user = userService.getById(shiroUser.getUserId().intValue());map.put("roleList",user.getRoles());map.put("permissionList",permissions);map.put("userId",shiroUser.getUserId());map.put("username",shiroUser.getLoginName());return ResponseEntity.ok(map);}
}
======================================================