记一次项目中使用到的shiro:

1、什么是shiro:

Shiro是Apache下的一个开源项目。shiro属于轻量级框架,相对于SpringSecurity简单的多,也没有SpringSecurity那么复杂。以下是我自己学习之后的记录。

官方架构图如下:

springboot 简单权限系统 springboot权限认证_springboot 简单权限系统

2.主要功能

shiro主要有三大功能模块:

  1. Subject:主体,一般指用户。
  2. SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet)
  3. Realms:用于进行权限信息的验证,一般需要自己实现。
    3.细分功能
  4. Authentication:身份认证/登录(账号密码验证)。
  5. Authorization:授权,即角色或者权限验证。
  6. Session Manager:会话管理,用户登录后的session相关管理。
  7. Cryptography:加密,密码加密等。
  8. Web Support:Web支持,集成Web环境。
  9. Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
  10. Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。
  11. Testing:测试支持;
  12. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
  13. Remember Me:记住我,登录后,下次再来的话不用登录了。
    (更多关于shiro是什么的文字请自行去搜索引擎找,本文主要记录springboot与shiro的集成)
    首先先创建springboot项目,此处不过多描述。

3、上代码

1)引入jar包

<!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>

2)实现我们自己的Realms:
新建类:UserRealm ,继承AuthorizingRealm,重写其认证和授权这两个主要方法:

package com.tzwy.mcsp.base.shiro;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import com.tzwy.mcsp.entity.sys.SysMenuEntity;
import com.tzwy.mcsp.entity.sys.UserAccount;
import com.tzwy.mcsp.mapper.sys.UserAccountDao;
import com.tzwy.mcsp.mapper.sys.UserDao;
import com.tzwy.mcsp.sys.service.CommonService;
import com.tzwy.mcsp.sys.service.SysMenuService;
import com.tzwy.mcsp.sys.service.UserAccountService;
import com.tzwy.mcsp.sys.service.UserService;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.stream.Collectors;

/**
 * shiro用于认证用户~授权
 **/
@Component
public class UserRealm extends AuthorizingRealm{

    private static final Logger log= LoggerFactory.getLogger(UserRealm.class);

    @Autowired
    private UserDao userMapper;
    @Autowired
    private UserService userService;

    @Autowired
    private UserAccountDao userAccountMapper;
    @Autowired
    private UserAccountService userAccountService;

    @Autowired
    private CommonService commonService;

    @Autowired
    private SysMenuService sysMenuService;


    /**
     * 资源-权限分配 ~ 授权 ~ 需要将分配给当前用户的权限列表塞给shiro的权限字段中去
     * @param principalCollection
     * @return
     * PrincipalCollection 是主体的集合
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取当前登录用户身份信息,此处得到的身份信息是账户表
        UserAccount userAccount= (UserAccount) principalCollection.getPrimaryPrincipal();
        Integer userId= userAccount.getUserId();
        List<String> perms= new LinkedList<>();

        //系统超级管理员拥有最高的权限,不需要发出sql的查询,直接拥有所有权限;否则,则需要根据当前用户id去查询权限列表
        boolean isAdmin = commonService.isSuperadmin();//判断当前用户是否是超级管理员
        if (isAdmin){
            List<SysMenuEntity> list=sysMenuService.list();
            if (list!=null && !list.isEmpty()){
                perms=list.stream().map(SysMenuEntity::getPerms).collect(Collectors.toList());
            }
        }else{
            perms=userMapper.queryAllPerms(userId);
        }

        //对于每一个授权编码进行 , 的解析拆分
        Set<String> stringPermissions= new HashSet<>();
        if (perms!=null && !perms.isEmpty()){
            for (String p:perms){
                if (StringUtils.isNotBlank(p)){
                    stringPermissions.addAll(Arrays.asList(StringUtils.split(p.trim(),",")));
                }
            }
        }

        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
        info.setStringPermissions(stringPermissions);
        return info;
    }

    /**
     * 用户认证 ~ 登录认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken;
        final String userName=token.getUsername();
        final String password=String.valueOf(token.getPassword());

        log.info("用户名: {} 密码:{}",userName,password);

        //SysUserEntity entity=sysUserDao.selectOne(new QueryWrapper<SysUserEntity>().eq("username",userName));
        //根据username与password获取用户信息
        UserAccount userAccount=userAccountMapper.selectOne(new QueryWrapper<UserAccount>().eq("account_code",
                userName));
        //账户不存在
        if (userAccount==null){
            throw new UnknownAccountException("账户不存在!");
        }
        //账户被禁用
        if (0 == userAccount.getStatus()){
            throw new DisabledAccountException("账户已被禁用,请联系管理员!");
        }

        //第一种 : 明文匹配
        //账户名密码不匹配
        /*if (!entity.getPassword().equals(password)){
            throw new IncorrectCredentialsException("账户密码不匹配!");
        }
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(entity,password,getName());*/

        //第三种验证逻辑
        /*String realPassword=ShiroUtil.sha256(password,entity.getSalt());
        if (StringUtils.isBlank(realPassword) || !realPassword.equals(entity.getPassword())){
            throw new IncorrectCredentialsException("账户密码不匹配!");
        }
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(entity,password,getName());*/

        //第二种验证逻辑-交给shiro的密钥匹配器去实现
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userAccount, userAccount.getPassword(), ByteSource.Util
                .bytes(userAccount.getSalt()), getName());
        return info;
    }

    /**
     * 密码验证器~匹配逻辑 ~ 第二种验证逻辑
     * @param credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
        shaCredentialsMatcher.setHashAlgorithmName(ShiroUtil.hashAlgorithmName);
        shaCredentialsMatcher.setHashIterations(ShiroUtil.hashIterations);
        super.setCredentialsMatcher(shaCredentialsMatcher);
    }
}

解释: 可以看到下面的方法doGetAuthenticationInfo是认证,上面的方法doGetAuthorizationInfo是授权方法;
在认证方法里面有三种方式,可以根据自己喜好使用,当前采用shiro自带的校验方式;

3)把CustomRealm和SecurityManager等加入到spring容器:
ShiroConfig.java

package com.tzwy.mcsp.base.config;


import com.tzwy.mcsp.base.shiro.UserRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    //安全器管理-管理所有的subject
    @Bean
    public SecurityManager securityManager(UserRealm userRealm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

    //过滤链配置
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //设定用户没有登录认证时的跳转链接、没有授权时的跳转链接
        shiroFilter.setLoginUrl("/login.html");
        shiroFilter.setUnauthorizedUrl("/");

        //过滤器链配置
        Map<String, String> filterMap = new LinkedHashMap();
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/swagger-resources/**", "anon");

        filterMap.put("/statics/**", "anon");
        filterMap.put("/login.html", "anon");
        filterMap.put("/sys/login", "anon");
//        filterMap.put("/sys/test/testMsg", "anon");
        filterMap.put("/favicon.ico", "anon");
        filterMap.put("/captcha.jpg", "anon");


        filterMap.put("/**","authc");

        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }


    //关于Shiro的Bean生命周期的管理  下面两个方法配套使用
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
//    @Bean(name = "sessionManager")
//    public DefaultWebSessionManager sessionManager() {
//        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//        // 设置session过期时间
//        sessionManager.setGlobalSessionTimeout(0L);
//        return sessionManager;
//    }

}

3)自己又加了一个关于shiro的通用工具:
ShiroUtil.java

package com.tzwy.mcsp.base.shiro;



import com.tzwy.mcsp.entity.sys.User;
import com.tzwy.mcsp.exception.CommonException;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;

//Shiro工具类
public class ShiroUtil {

	//加密算法
	public final static String hashAlgorithmName = "SHA-256";

	//循环次数
	public final static int hashIterations = 16;

	public static String sha256(String password, String salt) {
		return new SimpleHash(hashAlgorithmName, password, salt, hashIterations).toString();
	}

	//获取Shiro Session
	public static Session getSession() {
		return SecurityUtils.getSubject().getSession();
	}

	//获取Shiro Subject
	public static Subject getSubject() {
		return SecurityUtils.getSubject();
	}

	//获取Shiro中的真正主体
	public static User getUserEntity() {
		return (User)SecurityUtils.getSubject().getPrincipal();
	}

	public static Integer getUserId() {
		return getUserEntity().getId();
	}
	
	public static void setSessionAttribute(Object key, Object value) {
		getSession().setAttribute(key, value);
	}

	public static Object getSessionAttribute(Object key) {
		return getSession().getAttribute(key);
	}

	public static boolean isLogin() {
		return SecurityUtils.getSubject().getPrincipal() != null;
	}

	public static void logout() {
		SecurityUtils.getSubject().logout();
	}

	/**
	 * 获取验证码
	 * @param key
	 * @return
	 */
	public static String getKaptcha(String key) {
		Object object=getSessionAttribute(key);
		if (object==null){
			throw new CommonException("验证码已失效!");
		}
		String newCode=object.toString();
		getSession().removeAttribute(key);
		System.out.println("新的验证码:"+newCode);

		return newCode;
	}

	public static void main(String[] args) {
		String password="123456";
		System.out.println(ShiroUtil.sha256(password, "YzcmCZNvbXocrsz9dm8e"));
	}

}

4)添加freemark校验
因为项目中前端使用Freemarker,在配置时指定shiro校验:
引入jar包:

<!--freemarker-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

FreemarkerConfig.java

import com.tzwy.mcsp.base.shiro.ShiroVariable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

//Freemarker配置
@Configuration
public class FreemarkerConfig {

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer(ShiroVariable shiroVariable){
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");

        Map<String, Object> variables = new HashMap<>(1);
        variables.put("shiro", shiroVariable);
        configurer.setFreemarkerVariables(variables);

        Properties settings = new Properties();
        settings.setProperty("default_encoding", "utf-8");
        settings.setProperty("number_format", "0.##");
        configurer.setFreemarkerSettings(settings);
        return configurer;
    }

}

ShiroVariable.java

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;

/**
 * 推送给前端使用的shiro对象变量
 **/
@Component
public class ShiroVariable {

    /**
     * 判断当前登录用户(主体)是否有 指定的权限
     * @param permission 指定的权限
     * @return
     */
    public Boolean hasPermission(String permission){
        Subject subject=SecurityUtils.getSubject();
        /*if (subject!=null && subject.isPermitted(permission)){
            return true;
        }
        return false;*/

        return (subject!=null && subject.isPermitted(permission))? true : false;
    }

}

这样在前端页面就可以具体控制哪些按钮可以显示可以不显示:

<#if shiro.hasPermission("sys:user:save")>
			<a class="btn btn-primary" @click="add"><i class="fa fa-plus"></i> 新增</a>
		    </#if>
			<#if shiro.hasPermission("sys:user:update")>
			<a class="btn btn-primary" @click="update"><i class="fa fa-pencil-square-o"></i> 修改</a>
			</#if>
			<#if shiro.hasPermission("sys:user:delete")>
			<a class="btn btn-primary" @click="del"><i class="fa fa-trash-o"></i> 删除</a>
			</#if>

解释: “sys:user:save”是我们自定义的权限code,使用字符串;

完结;