springboot整合shiro

shiro的简介

这是一款安全权限框架,进行角色、权限管理。
主要功能:

  1. Authentication:登录;
  2. Authorization:授权,返回用户所有的角色和权限;
  3. Session Manager:会话管理;
  4. Cryptography:加密。

主要的类:

  1. Subject:指当前用户,类似于Web里面的Session?
  2. SecurityManager:安全事务管理器,管理所有的subject,是shiro的核心,需要在内设置realm和rememberMeManager。
  3. principals:身份,即主体的标识属性,一般为唯一的用户名或id,一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
  4. realm:指域,需要在内设置登录验证和授权验证。授权代码一般需要自己编写。
  • 登录验证:从token中获取用户名,在数据库中查询是否有该用户,并将信息返回到authenticationInfo之中。
  • 授权认证:从principals获取用户信息,在数据库中查询该用户的角色集合和权限集合,并返回到authorizationInfo
  1. HashedCredentialsMatcher:哈希密码比较器,在realm中作为参数使用,需要设置参数:setHashAlgorithmNamesetHashIterations,分别为加密算法和散列次数。

建表:
主要有五个表:user,role,permission,user-role,role-permission。
主要字段

  1. user:id,用户名,密码,盐值
  2. role:id,角色名,描述,状态
  3. permission:id,权限名,描述,url,父节点,类型,状态

配置:
主要为ShiroConfiguration,UserRealm,CacheConfiguration (缓存)

ShiroConfiguration:

package com.example.shiro.config;

import org.apache.log4j.Logger;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;


@Configuration
public class ShiroConfiguration {
    private Logger logger=Logger.getLogger(ShiroConfiguration.class);
    @Bean
    public ShiroFilterFactoryBean shiroFilter (SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //登录页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //登录成功后的页面
        shiroFilterFactoryBean.setSuccessUrl("index");
        //权限不足返回的页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        //自定义过滤器
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        shiroFilterFactoryBean.setFilters(filterMap);

        //权限控制map
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/user:add","perms[user:add]");
        //配置不会被拦截的链接
        filterChainDefinitionMap.put("/static/**","anon");

        filterChainDefinitionMap.put("logout","logout");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
    /**
     * 核心的安全事务管理器
     * @return
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager ();
        //设置realm
        securityManager.setRealm(myShiroRealm());
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /**
     * 身份认证Realm,此处的注入不可以缺少。否则会在UserRealm中注入对象会报空指针.
     * @return
     */
    @Bean
    public UserRealm myShiroRealm(  ){
        UserRealm myShiroRealm = new UserRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }


    /**
     * 哈希密码比较器。在myShiroRealm中作用参数使用
     * 登陆时会比较用户输入的密码,跟数据库密码配合盐值salt解密后是否一致。
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用md5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5( md5(""));
        return hashedCredentialsMatcher;
    }

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

    /**
     *  shiro缓存管理器;
     * 需要注入对应的其它的实体类中: 安全管理器:securityManager
     * 可见securityManager是整个shiro的核心;
     * @return
     */
    @Bean
    public EhCacheManager ehCacheManager(){
        logger.info("------------->ShiroConfiguration.getEhCacheManager()执行");
        EhCacheManager cacheManager=new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
        return cacheManager;
    }

    /**
     * 记住我管理器
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //rememberMe cookie加密的密钥  默认AES算法
//         cookieRememberMeManager.setCipherKey();
        return  cookieRememberMeManager;
    }

    /**
     * cookie对象
     * @return
     */
    @Bean
    public Cookie rememberMeCookie() {
        SimpleCookie simpleCookie=new SimpleCookie("rememberMe");
        //记住我cookie生效时间,单位秒
        simpleCookie.setMaxAge(3600);
        return simpleCookie;
    }


    /**
     * Shiro生命周期处理器
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 自动创建代理
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
}

UserRealm:

package com.example.shiro.config;

import com.example.shiro.entity.User;
import com.example.shiro.entity.UserRole;
import com.example.shiro.service.*;
import com.example.shiro.utils.State;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.*;
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 javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;

public class UserRealm extends AuthorizingRealm {
    @Resource
    private UserService userService;
    @Resource
    private RoleService roleService;
    @Resource
    private UserRoleService userRoleService;
    @Resource
    private RolePermissionService rolePermissionService;
    @Resource
    private PermissionService permissionService;

    private Logger logger=Logger.getLogger(UserRealm.class);

    /**
     * 提供用户信息,返回权限信息
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("---------------------------->授权认证:");
        System.out.println("---------------------------->开始授权验证:");
        SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
        String userName=(String) principals.getPrimaryPrincipal();
        String userId=userService.findUserIdByUserName(userName);
        Set<UserRole> roleIdSet=userRoleService.findRoleIdByUserId(userId);
        Set<String> roleSet=new HashSet<>();
        Set<String>  pemissionIdSet=new HashSet<>();
        Set<String>  pemissionSet=new HashSet<>();
        for(UserRole roleInfo : roleIdSet) {
            String roleId=roleInfo.getRoleId();
            roleSet.add( roleService.findRoleNameByRoleId(roleId));
            //将拥有角色的所有权限放进Set里面,也就是求Set集合的并集
            //由于我这边的数据表设计得不太好,所以提取set集合比较麻烦
            pemissionIdSet.addAll( rolePermissionService.findPerIdByRoleId(roleId));
        }
        for(String permissionId : pemissionIdSet) {
            String permission= permissionService.findPermissionById(permissionId).getPerName() ;
            pemissionSet.add(  permission );
        }
        // 将角色名称组成的Set提供给授权info
        authorizationInfo.setRoles( roleSet );
        // 将权限名称组成的Set提供给info
        authorizationInfo.setStringPermissions(pemissionSet);
        System.out.println("角色名称集合:");
        for (String name:roleSet
             ) {
            System.out.println(name);
        }

        System.out.println("权限名称集合:");
        for (String name:pemissionSet
        ) {
            System.out.println(name);
        }
        System.out.println("---------------------------->结束授权验证:");
        return authorizationInfo;
    }

    /**
     * 提供帐户信息,返回认证信息
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        logger.info("---------------------------->登陆验证:");
        System.out.println("---------------------------->开始登陆验证:");
        String userName=(String)authenticationToken.getPrincipal();
        User user=userService.findUserByName(userName);
        if(user==null) {
            //用户不存在就抛出异常
            throw new UnknownAccountException();
        }
        if( State.LOCKED.equals(user.getUserStatu())) {
            //用户被锁定就抛异常
            throw new  LockedAccountException();
        }
        //密码可以通过SimpleHash加密,然后保存进数据库。
        //此处是获取数据库内的账号/用户名、密码、盐值,保存到登陆信息info中
        SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(user.getUserName(),
                user.getUserPassword(),
                ByteSource.Util.bytes(user.getUserSalt())   ,
                getName());                   //realm name
        System.out.println("用户名:"+user.getUserName());
        System.out.println("---------------------------->结束登陆验证:");
        return authenticationInfo;
    }
}

--------------------------分割线
以下部分为后续添加内容,标题为 springboot如何跳过shiro的登录要求进行测试
从上文得知,shiro的登录和授权逻辑主要是写在realm里,那测试的话只需要在测试代码中写一个简单的登录逻辑的realm,跳过数据库查询便可以完成登录状态。
具体代码如下:
ShiroRealm:

public class ShiroRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        UsernamePasswordToken token2 = (UsernamePasswordToken)token;

        SimpleAuthenticationInfo info =
            new SimpleAuthenticationInfo(token2.getUsername(), token2.getPassword(), this.getName());
        return info;
    }

}

Login方法:

private void login(String username, String password) {
        final UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        final Subject subject = SecurityUtils.getSubject();
        subject.login(token);
    }

装配过程:

@BeforeEach
    public void mock() {
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        securityManager.setRealm(new ShiroRealm());
        SecurityUtils.setSecurityManager(securityManager);
        login("admin", "password");
    }

然后将此realm装配到测试的DefaultSecurityManager中,再将DefaultSecurityManager装配到SecurityUtils中即可调用login方法(自己写的)模拟登录状态。

再说一点,写完realm后,理论来讲可以将它装配到spring容器中,再在后面使用@Autowired获取,但是我这里不知道为什么不行,就直接new来使用了。