SpringBoot-Shiro安全权限框架

Apache Shiro是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,授权,企业会话管理和加密。
官网:

http://shiro.apache.org/

源码:

https://github.com/apache/shiro
SpringBoot-Shiro安全权限框架_第1张图片

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来存储到数据库。
SpringBoot-Shiro安全权限框架_第2张图片

一、引入依赖


<dependency>
  <groupId>org.apache.shirogroupId>
  <artifactId>shiro-coreartifactId>
  <version>1.5.3version>
dependency>

二、shiro配置文件shiro.ini(用户名或者密码)

[users]
relaysec=123456

三、测试代码

public class ShiroDemo{
	
	public static void main(String[] args){
		
		//1.创建安全管理器对象
		DefaultSecurityManager securityManager = new DefaultSecurityManager();

		//2.给安全管理器设置realm
		securityManager.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.bootgroupId>
 <artifactId>spring-boot-starter-webartifactId>
dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>

        <dependency>
            <groupId>org.apache.tomcatgroupId>
            <artifactId>tomcat-juliartifactId>
            <version>8.5.23version>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

        
        <dependency>
            <groupId>org.apache.tomcat.embedgroupId>
            <artifactId>tomcat-embed-jasperartifactId>
        dependency>
        <dependency>
            <groupId>jstlgroupId>
            <artifactId>jstlartifactId>
            <version>1.2version>
        dependency>

        
        
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-webartifactId>
            <version>1.4.2version>
        dependency>





        






















        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-webartifactId>
            <version>1.7.0version>
        dependency>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-springartifactId>
            <version>1.7.0version>
        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中,并返回。
	*/
	@Override
	protected 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();
	}

	//用于验证账户和密码
	@Override
	protected 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.bootgroupId>
 <artifactId>spring-boot-starter-parentartifactId>
<version>2.2.6.RELEASEversion>

<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-webartifactId>
    <version>1.5.0version>
dependency>
<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-springartifactId>
    <version>1.5.0version>
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
SpringBoot-Shiro安全权限框架_第3张图片
SpringBoot-Shiro安全权限框架_第4张图片

漏洞分析:
定位到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中不会。导致权限绕过。

===================================================

应用案例登录认证

SpringBoot-Shiro安全权限框架_第5张图片

  1. 客户端提交用户账号和密码,在Controller中拿到账号和密码封装到token对象.
  2. 然后借助subject的login方法,把数据提交给SecurityManager
  3. 使用Authenticator处理token,Authenticator从Realm列表中获取LdapRealm
  4. LdapRealm从token中获取数据,交给authenticate进行比对,对比通过返回AuthenticationInfo

一、创建maven工程,并导入相关依赖

shiro-core commons-logging

<dependency>
  <groupId>org.apache.shirogroupId>
    <artifactId>shiro-springartifactId>
    <version>1.2.5version>
dependency>
<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-ehcacheartifactId>
    <version>1.2.5version>
dependency>
<dependency>
    <groupId>com.github.theborakompanionigroupId>
    <artifactId>thymeleaf-extras-shiroartifactId>
    <version>1.2.1version>
dependency>

 <dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-quartzartifactId>
 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();

		//为获取的用户添加token
		user.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();

		//将获取的角色和权限存入指定的map
		User 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);
	}
}

======================================================

你可能感兴趣的:(spring,boot,安全,后端,Shiro)