【SpringBoot框架篇】15.使用shiro对web应用进行权限认证

文章目录

  • 简介
  • 核心组件
  • 使用版本
  • shiro实战应用
    • 目录结构
    • 引入依赖
    • 实体类
      • User类
      • Role类
      • Permission类
    • 权限认证管理类
    • Shiro配置类
    • 密码加盐加密工具类
    • 持久化操作类
    • 登录接口
    • 自定义全局异常处理器
    • 资源认证拦截注解
      • @RequiresRoles (角色认证注解)
      • @RequiresPermissions(权限认证注解)
    • 测试
      • user角色测试
        • 测试跨角色访问
        • 测试跨权限访问
      • admin角色访问

简介

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
shiro官网

核心组件

三个核心组件:Subject, SecurityManager 和 Realms.

  • Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
      Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
  • SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
  • Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

使用版本

  • springboot版本是: 2.3.0.RELEASE
  • shiro-spring-boot-web-starter 版本是: 1.5.3

shiro实战应用

目录结构

【SpringBoot框架篇】15.使用shiro对web应用进行权限认证_第1张图片

引入依赖

        
            org.apache.shiro
            shiro-spring-boot-web-starter
            1.5.3
        

实体类

User类

存放用户的信息

public class User implements Serializable {
    private Integer id;
    private String username;
    private String password;
    /**
     * 密码加密用的盐
     */
    private String salt;
    /**
     * 用户拥有的角色
     */
    private Set<Role> roles;
    //省略...  get set方法 
}    

Role类

存放角色的信息

public class Role {
    public Role(String roleName, Set<Permission> permissions) {
        this.roleName=roleName;
        this.permissions=permissions;
    }
    private Integer id;
    private String roleName;
    /**
     * 角色拥有的权限
     */
    private Set<Permission> permissions;
     //省略...  get set方法 
}

存放权限信息

Permission类

public class Permission {
    public Permission(String permissionName){
        this.permissionName=permissionName;
    }
    private Integer  id;
    private String  permissionName;
    //省略...  get set方法 
  }
    

权限认证管理类

public class CustomRealm extends AuthorizingRealm {

   private final Logger logger= LoggerFactory.getLogger(this.getClass());

    @Autowired
    private UserRepository userRepository;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set<String> permissionList = new HashSet<>();
        Set<String> roleNameList = new HashSet<>();
        for(Role role: user.getRoles()){
            for(Permission permission : role.getPermissions()){
                //添加权限
                permissionList.add(permission.getPermissionName());
            }
            //添加角色
            roleNameList.add(role.getRoleName());
        }
        info.setStringPermissions(permissionList);
        info.setRoles(roleNameList);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken;
        User user=userRepository.findByUserName(upt.getUsername());
        if(user==null){
            //帐号不存在
            logger.info("account does not exist!");
            throw  new UnknownAccountException();
        }
        logger.info("account exist ,Certification...!");
        //把数据库的密码和(当前登录输入的密码+盐 后加密的密码比对)
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName());
        return authenticationInfo;
    }

}

Shiro配置类

  • userRealm() 配置自定义权限认证管理
  • shiroFilterFactoryBean() 非常重要,配置资源过滤和跳转路径
  • credentialsMatcher()设置密码匹配规则,本处使用的Sha256Hash算法
    完整代码如下
@Configuration
public class ShiroConfig {

    /**
     * 配置自定义Realm
     */
    @Bean
    public CustomRealm userRealm() {
        CustomRealm userRealm = new CustomRealm();
        //配置使用哈希密码匹配
        userRealm.setCredentialsMatcher(credentialsMatcher());
        return userRealm;
    }

    /**
     * 设置对应的过滤条件和跳转条件
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        //没有登录的用户请求需要登录的页面时自动跳转到登录页面。
        shiroFilterFactoryBean.setLoginUrl("/login");
        //filterChainDefinitionMap 配置过滤规则,从上到下的顺序匹配。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //authc是需要认证后访问的接口,anon是放行的接口
        filterChainDefinitionMap.put("/user/**", "authc");
        filterChainDefinitionMap.put("/admin/**", "authc");
        //除了user和admin路径开头的接口,其它资源都开放
        filterChainDefinitionMap.put("/**", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面。
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        return shiroFilterFactoryBean;
    }


    /**
     * 设置用于匹配密码的CredentialsMatcher
     */
    @Bean
    public HashedCredentialsMatcher credentialsMatcher() {
        // 散列算法,这里使用更安全的sha256算法
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        // 数据库存储的密码字段使用HEX还是BASE64方式加密
        credentialsMatcher.setStoredCredentialsHexEncoded(false);
        // 散列迭代次数
        credentialsMatcher.setHashIterations(1024);
        return credentialsMatcher;
    }

    /**
     * 配置security并设置userReaml,避免xxxx required a bean named 'authorizer' that could not be found.的报错
     */
    @Bean
    public SessionsSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }



    @Bean
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
        app.setProxyTargetClass(true);
        return app;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager());
        return advisor;
    }

}

密码加盐加密工具类

public class PasswordHelper {

	private static RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
	//加密算法
	private static String algorithmName = Sha256Hash.ALGORITHM_NAME;
	//迭代次数
	private static int hashIterations = 1024;
	//生成随机的盐
	public static String randomSalt(){
		return randomNumberGenerator.nextBytes().toBase64();
	}
	
	public static String encryptPassword(String username, String password,String salt) {
		String encryPassword= new SimpleHash(algorithmName, password,
				ByteSource.Util.bytes(salt), hashIterations).toBase64();
		System.out.printf("username=%s ,password=%s , salt=%s , encryPassword=%s ",username,password,salt,encryPassword);
		return encryPassword;
	}

	public static void main(String[] args) {
		String username="admin";
		//盐前面必须拼接用户名,不然密码认证的时候会失败 (盐需要存在数据库)
		String salt=username+randomSalt();
		String newpassword=encryptPassword(username,"123",salt);
	}
}

持久化操作类

为方便测试,此处制造假数据.实际场景需要重数据库获取

/**
 * @author Dominick Li
 * @description 初始化用户数据,模拟数据库
 **/
@Component
public class UserRepository {

    HashMap<String, User> users = new HashMap();
    {
        //初始化权限
        Set<Permission> permissions = new HashSet<>();
        permissions.add(new Permission("save"));
        permissions.add(new Permission("delete"));
        permissions.add(new Permission("select"));
        Set<Role> roles = new HashSet<>();
        //初始化角色
        roles.add(new Role("admin",permissions));
        //初始化管理员
        User user = new User();
        user.setUsername("admin");
        String userPassword="123";
        //盐前面必须拼接用户名,不然密码认证的时候会失败 (盐需要存在数据库)
        String salt=user.getUsername()+PasswordHelper.randomSalt();
        user.setPassword(PasswordHelper.encryptPassword(user.getUsername(),userPassword,salt));
        user.setSalt(salt);
        user.setRoles(roles);

        //初始化普通用户信息
        Set<Permission> permissions2 = new HashSet<>();
        permissions.add(new Permission("select"));
        Set<Role> roles2 = new HashSet<>();
        roles2.add(new Role("user",permissions2));
        User user2 = new User();
        user2.setUsername("test");
        String user2Password="123";
        String salt2=user2.getUsername()+PasswordHelper.randomSalt();
        user2.setPassword(PasswordHelper.encryptPassword(user2.getUsername(),user2Password,salt2));
        user2.setSalt(salt2);
        user2.setRoles(roles2);

        users.put("admin",user);
        users.put("test",user2);
    }

    /**
     * 模拟重数据库获取用户信息
     */
    public User findByUserName(String username) {
        return users.get(username);
    }

    public List<User> findAll() {
        List<User> userList = new ArrayList<>();
        for (User user : users.values()) {
            userList.add(user);
        }
        return userList;
    }

    public void deleteByUserName(String username) {
        users.remove(username);
    }

}

登录接口

ResponseResult 是我自定义返回数据的map类,大家可以换成自己的返回类型

    @PostMapping("/login.do")
    @ResponseBody
    public ResponseResult login(User user, HttpSession session) {
        try {
            //进行验证,这里可以捕获异常,然后返回对应信息
            UsernamePasswordToken upt = new UsernamePasswordToken(user.getUsername(), user.getPassword());
            SecurityUtils.getSubject().login(upt);
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return new ResponseResult(false, "账号或密码错误!");
        } catch (AuthorizationException e) {
            e.printStackTrace();
            return new ResponseResult(false, "没有权限!");
        }
        return new ResponseResult(true,"登录成功!");
    }

自定义全局异常处理器

@ControllerAdvice
public class GlobalExceptionController {

    private final Logger logger= LoggerFactory.getLogger(this.getClass());
    private static final String ERROR_MESSAGE = "系统内部错误,请联系管理员!";
    @ExceptionHandler(value = Exception.class)
    public ModelAndView ExceptionHndler(Exception e) {
        ModelAndView mv=new ModelAndView("error");
        String msg=ERROR_MESSAGE;
         if(e instanceof UnauthorizedException){
            msg=e.getMessage();
        }
        mv.addObject("error",msg);
        logger.error("global error:{}", e);
        return  mv;
    }
}

资源认证拦截注解

@RequiresRoles (角色认证注解)

  • value属性可以支持多个值,多个值写在{}里,单个值直接value = "角色名"即可
  • logical属性 默认是 Logical.AND,需要多个角色都拥有才能访问,Logical.OR 表示只要拥有其中一个角色即可访问
    @RequiresRoles( value = {"admin", "user"}, logical = Logical.OR)
    @GetMapping("/user/index")
    public String user(Model model)
    {
        List<User> users=userRepository.findAll();
        model.addAttribute("userList",users);
        return "user/index";
    }

@RequiresPermissions(权限认证注解)

  • value属性可以支持多个值,多个值写在{}里,单个值直接value = "权限名"即可
  • logical属性 默认是 Logical.AND,需要多个权限都拥有才能访问,Logical.OR 表示只要拥有其中一个权限即可访问
 @RequiresPermissions(value = {"delete","*"},logical = Logical.OR)
    @GetMapping("/user/delete/{username}")
    public String delete(@PathVariable String username){
        userRepository.deleteByUserName(username);
        return "forward:/user/index";
    }

测试

教程配套代码已上传到github,https://github.com/Dominick-Li/springboot-master

user角色测试

使用普通用户登录,测试跨权限访问资源
在这里插入图片描述
帐号: test 密码: 123

测试跨角色访问

点击导航到管理员页面按钮,跳转管理员页面
在这里插入图片描述
因为管理员页面设置了只有拥有admin角色才能访问,test用户只有user角色,所以不能访问,重定向到了错误页
在这里插入图片描述
在这里插入图片描述

测试跨权限访问

点击删除按钮,删除用户
在这里插入图片描述
因为test用户没有delete或者*权限,所有无法执行删除用户操作
在这里插入图片描述

在这里插入图片描述

admin角色访问

在这里插入图片描述
帐号: admin 密码: 123
跳转管理员页面和删除用户操作都可以执行的。
在这里插入图片描述
在这里插入图片描述

你可能感兴趣的:(springBoot)