SpringBoot整合Shiro

Apache Shiro

在 Java 领域一般有 Spring Security、 Apache Shiro 等安全框架,但是由于 Spring Security 过于庞大和复杂,大多数公司会选择 Apache Shiro 来使用。

Shiro 能做什么呢?

  • 验证用户身份
  • 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
  • 在非 Web 或 EJB 容器的环境下可以任意使用 Session API
  • 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
  • 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
  • 支持单点登录(SSO)功能
  • 支持提供“Remember Me”服务,获取用户关联信息而无需登录

关于Shiro更详细的介绍:Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理

这里不再赘述关于Shiro的基本知识,只针对几个重点说明一下。

RBAC 基于角色的访问控制

主要涉及三个实体类:

  • 用户信息
    • 除了基本的用户名与密码外,增加一个用于加密的盐。
    • 用@ManyToMany与角色信息表联结。
  • 角色信息
    • 角色唯一标识(String类型)
    • 用@ManyToMany与权限信息表联结。
  • 权限信息
    • 权限唯一标识(String类型)
    • 当前权限可访问的URL(不是必要的)

Shiro配置

@Configuration
public class ShiroConfig {

	/**
	 * Shiro过滤器
	 * @param securityManager
	 * @return
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
		System.out.println("ShiroConfiguration.shirFilter()");

		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		shiroFilterFactoryBean.setSecurityManager(securityManager);

		/*
		 * 配置URL
		 */
		shiroFilterFactoryBean.setLoginUrl("/login"); // 登录界面
		shiroFilterFactoryBean.setSuccessUrl("/index"); // 登录成功跳转页面
		shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // 权限不足跳转页面

		/*
		 * 拦截器(从上往下顺序执行)
		 * anon:所有URL可匿名访问
		 * authc:需要认证才可访问
		 * user:配置记住我或认证通过可访问
		 */
		Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
		filterChainDefinitionMap.put("/static/**", "anon");
		filterChainDefinitionMap.put("/logout", "logout"); //退出过滤器,Shiro封装好了退出业务
		filterChainDefinitionMap.put("/**", "authc"); //其他资源全部需要认证,该过滤器放在最后,不会覆盖之前的过滤器

		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		return shiroFilterFactoryBean;
	}

	/**
	 * 凭证匹配器 加密配置
	 * 由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
	 * @return
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcher(){
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法:这里使用MD5算法;
		hashedCredentialsMatcher.setHashIterations(2); // 散列的次数,比如散列两次,相当于 md5(md5(""));
		hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); //true时用Hex编码,false时用Base64编码
		return hashedCredentialsMatcher;
	}

	/**
	 * 告诉realm,使用credentialsMatcher加密算法类来验证密文
	 * @return
	 */
	@Bean
	public MyShiroRealm myShiroRealm(){
		MyShiroRealm myShiroRealm = new MyShiroRealm();
		myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return myShiroRealm;
	}


	@Bean
	public SecurityManager securityManager(){
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(myShiroRealm());
		return securityManager;
	}

	/**
	 *  开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
	 *  需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
	 *  使用代理方式;所以需要开启代码支持;
	 * @param securityManager
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}

	@Bean(name="simpleMappingExceptionResolver")
	public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
		SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
		Properties mappings = new Properties();
		mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
		mappings.setProperty("UnauthorizedException","403");
		r.setExceptionMappings(mappings);  // None by default
		r.setDefaultErrorView("error");    // No default
		r.setExceptionAttribute("ex");     // Default is "exception"
		//r.setWarnLogCategory("example.MvcLogger");     // No default
		return r;
	}
}

Shiro登录认证实现

doGetAuthorizationInfo如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。

doGetAuthenticationInfo所做的就是根据用户输入的用户名,去数据库查出该用户的加密密码与盐,再将用户输入的密码按照同样方法加密,与正确的加密密码比较是否相等。

public class MyShiroRealm extends AuthorizingRealm {

    // 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据
    @Resource
    private UserInfoService userInfoService;

    /**
     * 权限认证
     * 提供用户信息返回权限信息
     * @param principals 用户信息
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        UserInfo userInfo  = (UserInfo)principals.getPrimaryPrincipal();

        for(SysRole role : userInfo.getRoleList()){
            authorizationInfo.addRole(role.getRole()); // 将角色名称提供给info
            for(SysPermission p : role.getPermissions()){
                authorizationInfo.addStringPermission(p.getPermission()); // 将权限名称提供给info
            }
        }
        return authorizationInfo;
    }

    /**
     * 身份认证
     * 提供账户信息返回认证信息
     * @param token 令牌
     * @return
     * @throws AuthenticationException 权限异常,交给Controller处理
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");

        //获取用户的输入的账号
        String username = (String)token.getPrincipal();
        System.out.println(token.getCredentials());

        //通过username从数据库中查找 User对象
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        UserInfo userInfo = userInfoService.findByUsername(username);
        System.out.println("----->>userInfo="+userInfo);
        if (userInfo == null) {
            return null;
        }
        if (userInfo.getState() == UserInfo.StateEnum.LOCKED) {
            throw new LockedAccountException(); //用户被锁定抛出异常
        } else if (userInfo.getState() == UserInfo.StateEnum.UNACTIVATED) {
            throw new DisabledAccountException(); //用户未激活异常
        }

        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userInfo, //用户名
                userInfo.getPassword(), //密码
                ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
                getName()  //realUsernamePasswordToken m name
        );
        return authenticationInfo;
    }
    
}

散列算法与加密算法

md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密本质上都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值