springboot整合shiro,完

一、简介

shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。

二 、架构思路

shiro架构图
从上到下依次说明
1.subject :主体,主体可以是很多,用户或者程序,但是要访问系统必须进行验证和授权。
2.securityManager:安全管理器,里面包括授权和认证的。
3.authenticator:认证器,主体进行认证最终通过authenticator进行的。
4.authorizer:授权器,主体进行授权最终通过authorizer进行的。
5.sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。可以实现单点登录。
6.SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
7.cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
8.realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。(它的主要目的是与数据库打交道,查询数据库中的认证的信息(比如用户名和密码),查询授权的信息(比如权限的code等,所以这里可以理解为调用数据库查询一系列的信息,一般情况下在项目中采用自定义的realm,因为不同的业务需求不一样))

三、上手

shiro官网,个版本依赖都要自己选择
1.项目依赖,其他例如mysql的依赖就没有贴

		<!-- springBoot的依赖,直接全家桶简单 -->
		<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.4.0</version>
        </dependency>
        /**
        *	这个依赖主要包括的jar包如下,如果不用springboot,自己用spring
		*	org.apache.shiro:shiro-core:1.4.0
		*	org.apache.shiro:shiro-lang:1.4.0
		*	org.apache.shiro:shiro-cache:1.4.0
		*	org.apache.shiro:shiro-crypto-hash:1.4.0
		*	org.apache.shiro:shiro-crypto-cipher:1.4.0
		*	org.apache.shiro:shiro-config-core:1.4.0
		*	org.apache.shiro:shiro-config-ogdl:1.4.0
		*	org.apache.shiro:shiro-event:1.4.0
		*	org.apache.shiro:shiro-web:1.4.0
        */
        
		<!-- spirng的依赖,boot里面也可以这么用 -->
        <dependency>
	        <groupId>org.apache.shiro</groupId>
	        <artifactId>shiro-spring</artifactId>
	        <version>1.4.0</version>
        </dependency>
        
        <dependency>
	        <groupId>org.apache.shiro</groupId>
	        <artifactId>shiro-core</artifactId>
	        <version>1.4.0</version>
        </dependency>
        <dependency>
	        <groupId>org.apache.shiro</groupId>
	        <artifactId>shiro-web</artifactId>
	        <version>1.4.0</version>
        </dependency>
        <!-- shiro和redis集群的依赖,主要是用来做缓存的,不用缓存可不加 -->
        <dependency>
	        <groupId>org.crazycake</groupId>
	        <artifactId>shiro-redis</artifactId>
	        <version>3.1.0</version>
        </dependency>
        

2.bean的注册

@Configuration
public class ShiroConfig {

    /**
     * ShiroFilterFactoryBean 这个主要是配置ShiroFilter(通过此filter的配置实现对请求资源的过滤,哪些请求要放行,哪些要认证),用的工厂模式
     * 需要有安全认证 securityManager下面配置
     */
     //动态开关
     @Value("${com.git.open-shiro}")
    private boolean openShiro=true;
    @Bean
    public ShiroFilterFactoryBean ceateShirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
       shiroFilterFactoryBean.setSecurityManager(securityManager);
        //设定的登录的路径
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登陆成功要跳转的页面,楼主用的前端控制配不配之无所谓
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 没有权限直接跳到登录页,按需求配置
        shiroFilterFactoryBean.setUnauthorizedUrl("/login");
        // 拦截器拦截的路径
        Map<String, String> map = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        //map .put("/static/**", "anon");//楼主直接配static发现失效,没办法才将下面的目录全部在配一遍
        map .put("/css/**", "anon");//允许访问就是anon
        map .put("/editor/**", "anon");
        map .put("/file/**", "anon");
        map .put("/img/**", "anon");
        map .put("/js/**", "anon");
        map .put("/openUi/**", "anon");
        map .put("/Public/**", "anon");
        map .put("/roboto/**", "anon");
		map .put("/login", "anon");
        map .put("/login/login", "anon");//这个一定要记得配置,这就是登录的方法,不配置无限重定向
        // 退出,不用自己实现
        map .put("/logout", "logout");

        //配置需要权限的路径
        if(openShiro){
        	map .put("/**", "authc"); 
        }else{
        map .put("/**", "anon"); 
        }
		  //需要权限就是authc
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map );
        return shiroFilterFactoryBean;
    }

    /**
     * 配置shiro的SecurityManager对象,返回的是DefaultWebSecurityManager,这里面需要realm,
     * realm用的是自己的
     */
    @Bean
    public SessionsSecurityManager createSecurityManager(
            AuthorizingRealm realm){
        DefaultWebSecurityManager sManager =
                new DefaultWebSecurityManager();
        //通过realm访问数据库
        sManager.setRealm(realm);
        return sManager;
    }

    //shiro声明周期,注意注意,如果去前面用了@value注解读取yml里面的配置,就得把这个方法设置为static
    @Bean("lifecycleBeanPostProcessor")
    public static LifecycleBeanPostProcessor newLifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }
    @DependsOn(value="lifecycleBeanPostProcessor")
    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。这个bug是参考谋篇博客的,忘记作者了,以前
         * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }

     // 启用shiro注解
    @Bean
    public AuthorizationAttributeSourceAdvisor newAuthorizationAttributeSourceAdvisor(
            SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor bean = new AuthorizationAttributeSourceAdvisor();
        bean.setSecurityManager(securityManager);
        return bean;
    }
    //加密。这里就不配了

}

3.自定义的realm

@Service
public class ShiroUserRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(ShiroUserRealm.class);
    @Autowired
    private UserService userService;
    @Autowired
    private MenuService menuService;

    /**授权方法,因为没有配置缓存,所以每次请求都会执行该方法*/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("开始授权");
        User user = (User) principals.getPrimaryPrincipal();
        //获得权限的方法,说一下思路user-》userRole-》role-》roleMenu-》menu等下上这几个表的思路,可以直接一步怼sql也可以单表慢慢查,方法未贴出来
        List<Menu> menuList =menuService.getUserAllMenu(user);
        if(menuList==null||menuList.size()<=0)return null;
        //获得权限的set集合,这里提前排除,不然会报下面的异常
      Set<String> set = menuList.stream().map(Menu::getPermisson).filter(StringUtils::isNotBlank).collect(Collectors.toSet());
        SimpleAuthorizationInfo info=  new SimpleAuthorizationInfo();
        info.setStringPermissions(set);
        return info;
        /**java.lang.IllegalArgumentException: Wildcard string cannot be null or empty. Make sure permission strings are properly formatted.
	*	at org.apache.shiro.authz.permission.WildcardPermission.setParts(WildcardPermission.java:155),set有空值*/
    }
    /**认证方法,登录的时候执行的*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("开始认证");
        //1.获取用户身份对象(例如用户名)
        String username =(String) token.getPrincipal();
        //2.基于用户名从数据库查询记录,基于mybatisPlus
        User user=userService.getOne(new QueryWrapper<User>().eq("username",username));
        //3.对查询结果进行验证,用户不存在则抛出异常
        if(user==null) throw new AuthenticationException("用户不存在");
        if(user.getStatus()==0)throw new AuthenticationException("用户已被禁用");
        //4.对数据库查询出的相关信息进行封装
        ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
        SimpleAuthenticationInfo info=
                new SimpleAuthenticationInfo(
                        user,//principal (身份对象)
                        user.getPassword(), //hashedCredentials(已加密的密码)
                        //credentialsSalt,//credentialsSalt (盐),这里暂时不想用盐所以就传null了
                        null,
                        this.getName());//realm name
        //5.返回封装结果(传递给认证管理器)
        return info;
    }
    @Override
    public void setCredentialsMatcher(
            CredentialsMatcher credentialsMatcher) {
        HashedCredentialsMatcher cMatcher=
                new HashedCredentialsMatcher();
        cMatcher.setHashAlgorithmName("MD5");
        //设置加密的次数(这个次数应该与保存密码时那个加密次数一致),这里设置一下这样就不用自己传密码的时候加密了
        cMatcher.setHashIterations(1);
        super.setCredentialsMatcher(cMatcher);
    }
}

4.登录方法

@PostMapping("/login")
    @ResponseBody
    public ApiResult login(String username, String password, String code, Boolean rememberMe) {
        Subject subject= SecurityUtils.getSubject();
        //2.提交用户信息
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        subject.login(token);
        return ResultUtils.buildSucessObject(null);
    }

5.加权限注解

 @RequiresPermissions(value="user:read")//楼主这次在controller里面加的

四、演示

//控制台日志
2019-07-05 11:56:12,663 INFO (ShiroUserRealm.java:56)- 开始认证
username=张三
。。。
2019-07-05 11:56:14,639 INFO (ShiroUserRealm.java:40)- 开始授权
没权限的时候直接报错

在这里插入图片描述
没有权限后台的异常
java.lang.IllegalArgumentException: Wildcard string cannot be null or empty. Make sure permission strings are properly formatted.

加了权限,正常显示
拥有权限

5.数据表结构

	//用户表
	@ApiModelProperty(value="用户ID")
   @TableId(value="user_id", type= IdType.AUTO)
   private Long userId;
    /**
     * 用户名
     */
   @ApiModelProperty(value="用户名")
   @TableId(value="username")
   private String username;


   //用户角色表
    @ApiModelProperty(value="用户ID")
   @TableField("user_id")
   private Long userId;
    /**
     * 角色ID
     */
   @ApiModelProperty(value="角色ID")
   @TableField("role_id")
   private Long roleId;
//角色表
 @ApiModelProperty(value="角色ID")
   @TableId(value="role_id", type= IdType.AUTO)
   private Long roleId;
    /**
     * 角色名称
     */
   @ApiModelProperty(value="角色名称")
   @TableField("role_name")
   private String roleName;
//角色菜单表
 @ApiModelProperty(value="角色ID")
   @TableField("role_id")
   private Long roleId;
    /**
     * 菜单/按钮ID
     */
   @ApiModelProperty(value="菜单/按钮ID")
   @TableField("menu_id")
   private Long menuId;
//菜单表
 @ApiModelProperty(value="菜单/按钮ID")
    @TableId(value="menu_id", type= IdType.AUTO)
    private Long menuId;
    /**
     * 上级菜单ID
     */
    @ApiModelProperty(value="上级菜单ID")
    @TableField("parent_id")
    private Long parentId;
    /**
     * 菜单/按钮名称
     */
    @ApiModelProperty(value="菜单/按钮名称")
    @TableField("menu_name")
    private String menuName;
    /**
     * 菜单URL
     */
    @ApiModelProperty(value="菜单URL")
    @TableField("url")
    private String url;
    /**
     * 权限标识
     */
    @ApiModelProperty(value="权限标识")
    @TableField("permisson")
    private String permisson;

    /**
     * 类型 0菜单 1按钮
     */
    @ApiModelProperty(value="类型 0菜单 1按钮")
    @TableField("type")
    private String type;
    /**
     * 排序
     */
    @ApiModelProperty(value="排序")
    @TableField("sort")
    private Long sort;

6.总结

shiro的实现原理其实很简单,我这用着更简单,主要是配置一些bean,和自己的realm,其原理过滤器+拦截器+aop,下个版本可能会自己模拟实现授权和认证的代码(基于全注解模式)。
未实现配置:
1.缓存,下次楼主在会在使用redis实现shiro的缓存功能
2.web页面,楼主这次用的是 thymeleaf 模板,空也会在页面使用一下shiro的标签。
楼主邮箱 [email protected]
qq 2519946973
有疑问直接邮箱,原创文章,转发请注明出处。

7.后续

本文参考过以下文章 :
1.shiro官网 各版本依赖
2.小虾米的java梦,参考内容 原理解释部门
3.解决controller层的注解路径不拦截问题。这篇文章忘记连接了,抱歉!

发布了42 篇原创文章 · 获赞 13 · 访问量 8319

猜你喜欢

转载自blog.csdn.net/weixin_43328357/article/details/94721315