精华内容
下载资源
问答
  • SpringSecurity-Shiro对比学习
    2021-10-21 09:06:50

    一、安全简介

    在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。

    市面上存在比较有名的:Shiro,Spring Security

    这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?

    Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。

    Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求

    从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。

    怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。

    Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

    对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

    二、SpringSecurity

    2.1 项目搭建

    2.1.1 引入maven依赖

    <!--导入thymeleaf依赖-->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
        <version>3.0.11.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-java8time</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    
    <!--spring security-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!--spring web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    

    2.1.2 静态资源准备

    index.html
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <title>首页</title>
        <!--semantic-ui-->
        <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
        <link th:href="@{/style/css/qinstyle.css}" rel="stylesheet">
    </head>
    <body>
    
    <!--主容器-->
    <div class="ui container">
    
        <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
            <div class="ui secondary menu">
                <a class="item"  th:href="@{/index}">首页</a>
    
                <!--登录注销-->
                <div class="right menu">
                    <!--未登录-->
                    <a class="item" th:href="@{/toLogin}">
                        <i class="address card icon"></i> 登录
                    </a>
                    <!--注销-->
                    <a class="item" th:href="@{/logout}">
                        <i class="sign-out icon"></i> 注销
                    </a>
                    <!--已登录
                    <a th:href="@{/usr/toUserCenter}">
                        <i class="address card icon"></i> admin
                    </a>
                    -->
                </div>
            </div>
        </div>
    
        <div class="ui segment" style="text-align: center">
            <h3>Spring Security Study by yeyoo</h3>
        </div>
    
        <div>
            <br>
            <div class="ui three column stackable grid">
                <div class="column">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 1</h5>
                                <hr>
                                <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                                <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                                <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                            </div>
                        </div>
                    </div>
                </div>
    
                <div class="column">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 2</h5>
                                <hr>
                                <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                                <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                                <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                            </div>
                        </div>
                    </div>
                </div>
    
                <div class="column">
                    <div class="ui raised segment">
                        <div class="ui">
                            <div class="content">
                                <h5 class="content">Level 3</h5>
                                <hr>
                                <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                                <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                                <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                            </div>
                        </div>
                    </div>
                </div>
    
            </div>
        </div>
        
    </div>
    
    
    <script th:src="@{/style/js/jquery-3.1.1.min.js}"></script>
    <script th:src="@{/style/js/semantic.min.js}"></script>
    
    </body>
    </html>
    
    login.html
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <title>登录</title>
        <!--semantic-ui-->
        <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    </head>
    <body>
    
    <!--主容器-->
    <div class="ui container">
    
        <div class="ui segment">
    
            <div style="text-align: center">
                <h1 class="header">登录</h1>
            </div>
    
            <div class="ui placeholder segment">
                <div class="ui column very relaxed stackable grid">
                    <div class="column">
                        <div class="ui form">
                            <form th:action="@{/usr/login}" method="post">
                                <div class="field">
                                    <label>Username</label>
                                    <div class="ui left icon input">
                                        <input type="text" placeholder="Username" name="username">
                                        <i class="user icon"></i>
                                    </div>
                                </div>
                                <div class="field">
                                    <label>Password</label>
                                    <div class="ui left icon input">
                                        <input type="password" name="password">
                                        <i class="lock icon"></i>
                                    </div>
                                </div>
                                <input type="submit" class="ui blue submit button"/>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
    
            <div style="text-align: center">
                <div class="ui label">
                    </i>注册
                </div>
                <br><br>
                <small>blog.yeyoo.com</small>
            </div>
            <div class="ui segment" style="text-align: center">
                <h3>Spring Security Study by yeyoo</h3>
            </div>
        </div>
    
    
    </div>
    
    <script th:src="@{/style/js/jquery-3.1.1.min.js}"></script>
    <script th:src="@{/style/js/semantic.min.js}"></script>
    
    </body>
    </html>
    
    level.html
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <title>首页</title>
        <!--semantic-ui-->
        <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
        <link th:href="@{/style/css/qinstyle.css}" rel="stylesheet">
    </head>
    <body>
    
    <!--主容器-->
    <div class="ui container">
    
        <div th:replace="~{index::nav-menu}"></div>
    
        <div class="ui segment" style="text-align: center">
            <h3>Level-1-1</h3>
        </div>
    
    </div>
    
    
    <script th:src="@{/style/js/jquery-3.1.1.min.js}"></script>
    <script th:src="@{/style/js/semantic.min.js}"></script>
    
    </body>
    </html>
    

    复制八份用来测试权限
    在这里插入图片描述

    2.1.3 权限配置

    我们需要 首页所有人可以访问,功能页只有对应权限的人才能访问
    测试用例
    拥有v1的权限可以访问level1下的文件
    拥有v2的权限可以访问level2下的文件
    拥有v3的权限可以访问level3下的文件

    SecurityConfig
    
    package com.SpringSecurity.config;
    
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    @EnableWebSecurity
    //授权配置
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //    @Autowired
    //    DataSource dataSource;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //首页所有人可以访问,功能页只有对应权限的人才能访问
            //请求授权的规则
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("v1")
                    .antMatchers("/level2/**").hasRole("v2")
                    .antMatchers("/level3/**").hasRole("v3");
    
            //没有权限默认会跳到登录页面,需要开启登录的页面
            http.formLogin();
            //防止网站攻击:get;post
            http.csrf().disable();//关闭csrf(跨站请求伪造)功能,登出失败可能产生的原因
            //开启注销功能
            http.logout().logoutSuccessUrl("/");
        }
    
        //认证配置
        //认证,springboot 2.1.x可以直接使用,其他版本会报错(或者采用下面的密码编码解决)
        //密码编码:PasswordEncoder
        //在spring security 5.0+新增了很多的加密方法
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //数据库认证
    //        auth.jdbcAuthentication().dataSource(dataSource).withDefaultSchema()
    //                .passwordEncoder(new BCryptPasswordEncoder())
    //                .withUser(User.withUsername("user").password("password").roles("roles"));
    
            //内存认证
            auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                    .withUser("yeyoo").password(new BCryptPasswordEncoder().encode("123456")).roles("v1", "v2")
                    .and()
                    .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("v1", "v2","v3");
        }
    }
    

    2.1.4 业务编写

    RouterController 
    
    @Controller
    public class RouterController {
        //使得访问/,/index,/index.html都能跳到主页
        @RequestMapping({"/","/index","/index.html"})
        public String index(){
            return "index";
        }
        @RequestMapping("/toLogin")
        public String toLogin(){
            return "views/login";
        }
        //实现对level的三个页面的跳转,下面也是如此
        @RequestMapping("/level1/{id}")
        public String level1(@PathVariable("id") int id){
            return "views/level1/"+id;
        }
        @RequestMapping("/level2/{id}")
        public String level2(@PathVariable("id") int id){
            return "views/level2/"+id;
        }
        @RequestMapping("/level3/{id}")
        public String level3(@PathVariable("id") int id){
            return "views/level3/"+id;
        }
    }
    

    2.2 知识点整理

    2.2.1 SpringSecurity

    Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理

    记住几个类:

    • WebSecurityConfigurerAdapter:自定义Security策略

    • AuthenticationManagerBuilder:自定义认证策略

    • @EnableWebSecurity:开启WebSecurity模式

    Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

    “认证”(Authentication)

    身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

    身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

    “授权” (Authorization)

    授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

    这个概念是通用的,而不是只在Spring Security 中存在。

    2.2.2 流程总结

    1. 引入 Spring Security 模块
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    1. 编写 Spring Security 配置类
      参考官网:https://spring.io/projects/spring-security

    查看我们自己项目中的版本,找到对应的帮助文档:

    https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5 #servlet-applications 8.16.4

    1. 编写基础配置类 SecurityConfig

    @EnableWebSecurity // 开启WebSecurity模式

    1. 定制请求的授权规则

    三、Shiro

    3.1 Shiro架构设计

    在这里插入图片描述
    为了简化配置并启用灵活配置/可插性,Shiro的实现都是高度模块化设计——由于如此的模块化,SecurityManager实现(以及它的类层次结构)并没有做很多事情。相反,SecurityManager 实现主要是作为一个轻量级的“容器”组件,委托计划所有的行为到嵌套/包裹的组件。
    在这里插入图片描述
    Shiro 核心三大组件

    • Subject
    • SecurityManager
    • Realm

    Subject

    Subject实质上是一个当前执行用户的特定的安全“视图”。鉴于"User"一词通常意味着一个人,而一个Subject可以是一个人,但它还可以代表第三方服务,daemon account,cron job,或其他类似的任何东西——基本上是当前正与软件进行交互的任何东西。

    所有Subject实例都被绑定到(且这是必须的)一个SecurityManager上。当你与一个Subject交互时,那些交互作用转化为与SecurityManager交互的特定subject的交互作用。

    SecurityManager

    SecurityManager是Shiro架构的心脏,并作为一种“保护伞”对象来协调内部的安全组件共同构成一个对象图。然而,一旦SecurityManager和它的内置对象图已经配置给一个应用程序,那么它单独留下来,且应用程序开发人员几乎使用他们所有的时间来处理Subject API。

    重要的是要认识到,当你正与一个Subject进行交互时,实质上是幕后的 SecurityManager处理所有繁重的Subject安全操作。这反映在上面的基本流程图。

    简单的来说SecurityManager就是控制 Subject可以访问哪些资源。

    Realms

    Realms担当Shiro和你的应用程序的安全数据之间的“桥梁”或“连接器”。当它实际上与安全相关的数据如用来执行身份验证(登录)及授权(访问控制)的用户帐户交互时,Shiro 从一个或多个为应用程序配置的Realm中寻找许多这样的东西。

    在这个意义上说,Realm本质上是一个特定安全的DAO:它封装了数据源的连接详细信息,使Shiro所需的相关的数据可用。当配置Shiro时,你必须指定至少一个Realm用来进行身份验证和/或授权。SecurityManager可能配置多个Realms,但至少有一个是必须的。

    Shiro提供了立即可用的Realms来连接一些安全数据源(即目录),如LDAP,关系数据库(JDBC),文本配置源,像 INI 及属性文件,以及更多。你可以插入你自己的Realm 实现来代表自定义的数据源,如果默认地Realm不符合你的需求。

    像其他内置组件一样,Shiro SecurityManager控制 Realms是如何被用来获取安全和身份数据来代表 Subject 实例的。

    3.2 整合SpringBoot设计思路

    在这里插入图片描述

    3.3 项目搭建

    3.3.1 核心pom依赖

    <!--shiro-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.2</version>
    </dependency>
    <!--shiro-thymeleaf整合-->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
    

    3.3.2 Shiro 配置

    UserRealm

    public class UserRealm extends AuthorizingRealm {
        @Autowired
        private IUserService userService;
    
        //授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("执行了 => 授权");
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            info.addStringPermission("user:add");
            Subject subject = SecurityUtils.getSubject();
            //拿到user
            User currentUser =(User)subject.getPrincipal();
            //设置权限 数据库拿
            info.addStringPermissions(Arrays.asList(currentUser.getPerms().split(",")));
            return info;
        }
    
        //认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            System.out.println("执行了 => 认证");
            UsernamePasswordToken userToken = (UsernamePasswordToken) token;
            //用户名,密码去数据库取
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
            wrapper = wrapper.eq(User::getName, userToken.getUsername());
            User user = userService.getOne(wrapper);
            if (user == null) {  //没有这个人
                return null;  //其实就是抛出UnknownAccountException异常
            }
            ByteSource passwordSalt = ByteSource.Util.bytes(userToken.getUsername());//这里的参数要给个唯一的;
            //之后密码认证,shiro   它自己会做
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPwd(), passwordSalt,"");
            Subject currentSubject = SecurityUtils.getSubject();
            Session session = currentSubject.getSession();
            session.setAttribute("loginUser", user);
            return info;
        }
    }
    

    ShiroConfig

    • 创建reaml 对象 需要自定义 第一步
    @Bean(name = "userRealm")
    public UserRealm userRealm() {
        return new UserRealm();
    }
    
    • DefaultWebSecurityManner 第二步
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //关联UserRealm
        manager.setRealm(userRealm);
        return manager;
    }
    
    • ShiroFilterFactoryBean 第三步
      Shiro内置过滤器

         anon:无需认证就可访问
         authc: 必须认证才可访问
         user: 必须拥有记住我才可访问
         perms: 拥有对某个资源权限才能访问
         role:拥有某个角色权限才能访问
      
        @Bean
        public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
            ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
            //设置安全管理器
            bean.setSecurityManager(defaultWebSecurityManager);
            //添加shiro内置过滤器
            //拦截
            HashMap<String, String> map = new LinkedHashMap<>();
    
            //授权
            map.put("/user/add","perms[user:add]");
            map.put("/user/update","perms[user:update]");
    //        map.put("/user/logout","logout");
            map.put("/user/*","authc");
            bean.setFilterChainDefinitionMap(map);
            //设置登录请求
            bean.setLoginUrl("/toLoginShiro");
            //设置未授权页面
            bean.setUnauthorizedUrl("/unauthorized");
            return bean;
        }
    

    3.3.3 业务编写

    用户登录页面-不同的用户对应不同的权限-不同的权限展示不同的按钮
    未登录之前点击任何按钮调转登录
    显示的按钮 若当前用户无权访问 跳转无权访问页面
    在这里插入图片描述

    如上图 路径跳转以及权限配置在 config中标明

    @Controller
    public class UserController {
    
        @RequestMapping("/shiroIndex")
        public String shiroIndex(Model model) {
            model.addAttribute("msg", "shiro test");
            return "shiro/shiroIndex";
        }
    
        @RequestMapping("/user/add")
        public String shiroAdd() {
            return "shiro/shiroAdd";
        }
    
        @RequestMapping("/user/update")
        public String shiroUpdate() {
            return "shiro/shiroUpdate";
        }
    
        @RequestMapping("/toLoginShiro")
        public String toLogin() {
            return "shiro/shiroLogin";
        }
    
        @RequestMapping("/login")
        public String login(String username, String password, Model model) {
            //获取当前用户
            Subject currentSubject = SecurityUtils.getSubject();
            //封装用户的登录数据
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            //登录
            try {
                currentSubject.login(token);
                return "shiro/shiroIndex";
            } catch (UnknownAccountException e) {
                model.addAttribute("msg", "用户名不存在");
                return "shiro/shiroLogin";
            } catch (IncorrectCredentialsException e) {
                model.addAttribute("msg", "密码错误");
                return "shiro/shiroLogin";
            }
        }
    
        @RequestMapping("/unauthorized")
        @ResponseBody
        public String unauthorized(){
            return "无权访问";
        }
    
        @RequestMapping("/user/logout")
        public String logout(Model model){
    
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
            model.addAttribute("msg","安全退出!");
            return "shiro/shiroIndex";
        }
    }
    
    更多相关内容
  • Spring SecurityShiro 该如何选择?

    千次阅读 2021-12-15 00:56:48
    Elastic-Job 源码解析 分布式事务中间件 TCC-Transaction 源码解析 Eureka 和 Hystrix 源码解析 Java 并发源码 Shiro 执行流程 特点 Spring Security 执行流程 特点 两者对比 我的看法 大家好,我是艿艿。...

    点击上方“Java基基”,选择“设为星标”

    做积极的人,而不是积极废人!

    每天 14:00 更新文章,每天掉亿点点头发...

    源码精品专栏

     

    71cb92c64295944aba4384b0e0958a2d.png


    大家好,我是艿艿。

    在开始本文之前,我们做个问卷调查,看看胖友都在使用哪个?

    嘿嘿,不管胖友怎么选,艿艿各给他们肝了一篇实战文章:

    • Spring Security:http://www.iocoder.cn/Spring-Boot/Spring-Security/?nb

    • Shiro :http://www.iocoder.cn/Spring-Boot/Shiro/?nb

    OK,不哔哔了,开始~

    要知道Shiro和Spring Security该如何选择,首先要看看两者的区别和对比

    Shiro

    Apache Shiro是一个强大且易用的Java安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

    执行流程

    7113c0e7b0e0e000b11a8c63f03d50be.png

    特点

    1. 易于理解的 Java Security API;

    2. 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);

    3. 对角色的简单的签权(访问控制),支持细粒度的签权;

    4. 支持一级缓存,以提升应用程序的性能;

    5. 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;

    6. 异构客户端会话访问;

    7. 非常简单的加密 API;

    8. 不跟任何的框架或者容器捆绑,可以独立运行。

    推荐下自己做的 Spring Boot 的实战项目:

    https://github.com/YunaiV/ruoyi-vue-pro

    Spring Security

    Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。它是一个轻量级的安全框架,它确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC有很好地集成 ,并配备了流行的安全算法实现捆绑在一起。

    执行流程

    cb15e7ff6edea85ebae3d467f4bde1b8.png

    1. 客户端发起一个请求,进入 Security 过滤器链。

    2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。

    3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。

    4. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。

    特点

    shiro能实现的,Spring Security 基本都能实现,依赖于Spring体系,但是好处是Spring全家桶的亲儿子,集成上更加契合,在使用上,比shiro略负责。

    推荐下自己做的 Spring Cloud 的实战项目:

    https://github.com/YunaiV/onemall

    两者对比

    Shiro比Spring Security更容易使用,也就是实现上简单一些,同时基本的授权认证Shiro也基本够用

    Spring Security社区支持度更高,Spring社区的亲儿子,支持力度和更新维护上有优势,同时和Spring这一套的结合较好。

    Shiro 功能强大、且 简单、灵活。是Apache 下的项目比较可靠,且不跟任何的框架或者容器绑定,可以独立运行。

    我的看法

    如果开发的项目是Spring这一套,用Spring Security我觉得更合适一些,他们本身就是一套东西,顺畅,可能略微复杂一些,但是学会了就是自己的。如果开发项目比较紧张,Shiro可能更合适,容易上手,也足够用,Spring Security中有的,Shiro也基本都有,没有的部分网上也有大批的解决方案。

    如果项目没有使用Spring这一套,不用考虑,直接Shiro。

    同时要考虑团队成员的技术栈,更加熟悉使用哪个,在选型上,也要尽量避免给同行增加不必要的学习成本!

    - END -

    欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

    0447ebc3187cd844ad274de5a51a94c8.png

    已在知识星球更新源码解析如下:

    d8705210c79ada379746da84dc6eb4cd.png

    55daa4bbecfe1922c37a9eaf024e306b.png

    b8d30b9d419fb5bdbfdbf948a06438d1.png

    fe7a4e3ad8340dda3d135db6ecb7804d.png

    最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

    提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。

    获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

    文章有帮助的话,在看,转发吧。
    谢谢支持哟 (*^__^*)
    展开全文
  • 松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot+Vue+微人事视频教程和大家分享一个松哥原创的 Shiro 教程吧,还没写完,...

    松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot+Vue+微人事视频教程


    和大家分享一个松哥原创的 Shiro 教程吧,还没写完,先整一部分,剩下的敬请期待。

    1.Shiro简介

    Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架具有直观、易用等特性,同时也能提供健壮的安全性,虽然它的功能不如SpringSecurity那么强大,但是在普通的项目中也够用了。

    1.1 由来

    Shiro的前身是JSecurity,2004年,Les Hazlewood和Jeremy Haile创办了Jsecurity。当时他们找不到适用于应用程序级别的合适Java安全框架,同时又对JAAS非常失望。2004年到2008年期间,JSecurity托管在SourceForge上,贡献者包括Peter Ledbrook、Alan Ditzel和Tim Veil。2008年,JSecurity项目贡献给了Apache软件基金会(ASF),并被接纳成为Apache Incubator项目,由导师管理,目标是成为一个顶级Apache项目。期间,Jsecurity曾短暂更名为Ki,随后因商标问题被社区更名为“Shiro”。随后项目持续在Apache Incubator中孵化,并增加了贡献者Kalle Korhonen。2010年7月,Shiro社区发布了1.0版,随后社区创建了其项目管理委员会,并选举Les Hazlewood为主席。2010年9月22日,Shrio成为Apache软件基金会的顶级项目(TLP)。

    1.2 有哪些功能

    Apache Shiro是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,授权,企业会话管理和加密。Apache Shiro的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架应该尽可能掩盖复杂的地方,露出一个干净而直观的API,来简化开发人员在应用程序安全上所花费的时间。

    以下是你可以用Apache Shiro 所做的事情:

    1. 验证用户来核实他们的身份

    2. 对用户执行访问控制,如:判断用户是否被分配了一个确定的安全角色;判断用户是否被允许做某事

    3. 在任何环境下使用Session API,即使没有Web容器

    4. 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应

    5. 聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”

    6. 单点登录(SSO)功能

    7. 为没有关联到登录的用户启用"Remember Me"服务

      等等

    Apache Shiro是一个拥有许多功能的综合性的程序安全框架。下面的图表展示了Shiro的重点:

    359e6461930ec19a33bdc7a1669b3539.png

    Shiro中有四大基石——身份验证,授权,会话管理和加密。

    1. Authentication:有时也简称为“登录”,这是一个证明用户是谁的行为。

    2. Authorization:访问控制的过程,也就是决定“谁”去访问“什么”。

    3. Session Management:管理用户特定的会话,即使在非Web 或EJB 应用程序。

    4. Cryptography:通过使用加密算法保持数据安全同时易于使用。

    除此之外,Shiro也提供了额外的功能来解决在不同环境下所面临的安全问题,尤其是以下这些:

    1. Web Support:Shiro的web支持的API能够轻松地帮助保护Web应用程序。

    2. Caching:缓存是Apache Shiro中的第一层公民,来确保安全操作快速而又高效。

    3. Concurrency:Apache Shiro利用它的并发特性来支持多线程应用程序。

    4. Testing:测试支持的存在来帮助你编写单元测试和集成测试。

    5. "Run As":一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。

    6. "Remember Me":在会话中记住用户的身份,这样用户只需要在强制登录时候登录。

    2.从一个简单的案例开始身份认证

    2.1 shiro下载

    要学习shiro,我们首先需求去shiro官网下载shiro,官网地址地址https://shiro.apache.org/,截至本文写作时,shiro的最新稳定版本为1.7.1(Shiro 在 2017-2019 曾经停更了两年,我一度以为以为这个项目 gg 了),本文将采用这个版本。当然,shiro我们也可以从github上下载到源码。两个源码下载地址如下:

    1.apache shiro2.github-shiro

    上面我主要是和小伙伴们介绍下源码的下载,并没有涉及到jar包的下载,jar包我们到时候直接使用maven即可。

    2.2 创建演示工程

    这里我们先不急着写代码,我们先打开刚刚下载到的源码,源码中有一个samples目录,如下:

    b7db880b91b7d18ea723761bd26102c6.png

    这个samples目录是官方给我们的一些演示案例,其中有一个quickstart项目,这个项目是一个maven项目,参考这个quickstart,我们来创建一个自己的演示工程。

    1.首先使用maven创建一个JavaSE工程 工程创建成功后在pom文件中添加如下依赖:

    <dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-all</artifactId>
     <version>RELEASE</version>
    </dependency>

    2.配置用户

    参考quickstart项目中的shiro.ini文件,我们来配置一个用户,配置方式如下:首先在resources目录下创建一个shiro.ini文件,文件内容如下:

    [users]
    sang=123,admin
    [roles]
    admin=*

    以上配置表示我们创建了一个名为sang的用户,该用户的密码是123,该用户的角色是admin,而admin具有操作所有资源的权限。

    3.执行登录

    OK,做完上面几步之后,我们就可以来看看如何实现一次简单的登录操作了。这个登录操作我们依然是参考quickstart项目中的类来实现,首先我们要通过shiro.ini创建一个SecurityManager,再将这个SecurityManager设置为单例模式,如下:

    Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
    org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);

    如此之后,我们就配置好了一个基本的Shiro环境,注意此时的用户和角色信息我们配置在shiro.ini这个配置文件中,接下来我们就可以获取一个Subject了,这个Subject就是我们当前的用户对象,获取方式如下:

    Subject currentUser = SecurityUtils.getSubject();

    拿到这个用户对象之后,接下来我们可以获取一个session了,这个session和我们web中的HttpSession的操作基本上是一致的,不同的是,这个session不依赖任何容器,可以随时随地获取,获取和操作方式如下:

    //获取session
    Session session = currentUser.getSession();
    //给session设置属性值
    session.setAttribute("someKey", "aValue");
    //获取session中的属性值
    String value = (String) session.getAttribute("someKey");

    说了这么多,我们的用户到现在还没有登录呢,Subject中有一个isAuthenticated方法用来判断当前用户是否已经登录,如果isAuthenticated方法返回一个false,则表示当前用户未登录,那我们就可以执行登陆,登录方式如下:

    if (!currentUser.isAuthenticated()) {
        UsernamePasswordToken token = new UsernamePasswordToken("sang", "123");
        try {
            currentUser.login(token);
        } catch (UnknownAccountException uae) {
            log.info("There is no user with username of " + token.getPrincipal());
        } catch (IncorrectCredentialsException ice) {
            log.info("Password for account " + token.getPrincipal() + " was incorrect!");
        } catch (LockedAccountException lae) {
            log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                    "Please contact your administrator to unlock it.");
        }
        catch (AuthenticationException ae) {
        }
    }

    首先构造UsernamePasswordToken,两个参数就是我们的用户名和密码,然后调用Subject中的login方法执行登录,当用户名输错,密码输错、或者账户锁定等问题出现时,系统会通过抛异常告知调用者这些问题。

    当登录成功之后,我们可以通过如下方式获取当前登陆用户的用户名:

    log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

    我们也可以通过调用Subject中的hasRole和isPermitted方法来判断当前用户是否具备某种角色或者某种权限,如下:

    if (currentUser.hasRole("admin")) {
        log.info("May the Schwartz be with you!");
    } else {
        log.info("Hello, mere mortal.");
    }
    if (currentUser.isPermitted("lightsaber:wield")) {
        log.info("You may use a lightsaber ring.  Use it wisely.");
    } else {
        log.info("Sorry, lightsaber rings are for schwartz masters only.");
    }

    最后,我们可以通过logout方法注销本次登录,如下:

    currentUser.logout();

    OK,至此,我们通过官方案例给小伙伴们简单介绍了Shiro中的登录操作,完整案例大家可以参考官方的demo。

    3. 聊一聊Shiro中的Realm

    3.1 登录流程是什么样的

    首先我们来看shiro官方文档中这样一张登录流程图:

    a46e9617c36a5a088944bdf516793816.png

    参照此图,我们的登录一共要经过如下几个步骤:

    1. 应用程序代码调用Subject.login方法,传递创建好的包含终端用户的Principals(身份)和Credentials(凭证)的AuthenticationToken实例(即上文例子中的UsernamePasswordToken)。

    2. Subject实例,通常是DelegatingSubject(或子类)委托应用程序的SecurityManager通过调用securityManager.login(token)开始真正的验证工作(在DelegatingSubject类的login方法中打断点即可看到)。

    3. SubjectManager作为一个基本的“保护伞”的组成部分,接收token以及简单地委托给内部的Authenticator实例通过调用authenticator.authenticate(token)。这通常是一个ModularRealmAuthenticator实例,支持在身份验证中协调一个或多个Realm实例。ModularRealmAuthenticator本质上为Apache Shiro 提供了PAM-style 范式(其中在PAM 术语中每个Realm 都是一个'module')。

    4. 如果应用程序中配置了一个以上的Realm,ModularRealmAuthenticator实例将利用配置好的AuthenticationStrategy来启动Multi-Realm认证尝试。在Realms 被身份验证调用之前,期间和以后,AuthenticationStrategy被调用使其能够对每个Realm的结果作出反应。如果只有一个单一的Realm 被配置,它将被直接调用,因为没有必要为一个单一Realm的应用使用AuthenticationStrategy。

    5. 每个配置的Realm用来帮助看它是否支持提交的AuthenticationToken。如果支持,那么支持Realm的getAuthenticationInfo方法将会伴随着提交的token被调用。

    OK,通过上面的介绍,相信小伙伴们对整个登录流程都有一定的理解了,小伙伴可以通过打断点来验证我们上文所说的五个步骤。那么在上面的五个步骤中,小伙伴们看到了有一个Realm承担了很重要的一部分工作,那么这个Realm到底是个什么东西,接下来我们就来仔细看一看。

    3.2 什么是Realm

    根据Realm文档上的解释,Realms担当Shiro和你的应用程序的安全数据之间的“桥梁”或“连接器”。当它实际上与安全相关的数据如用来执行身份验证(登录)及授权(访问控制)的用户帐户交互时,Shiro从一个或多个为应用程序配置的Realm 中寻找许多这样的东西。在这个意义上说,Realm 本质上是一个特定安全的DAO:它封装了数据源的连接详细信息,使Shiro 所需的相关的数据可用。当配置Shiro 时,你必须指定至少一个Realm 用来进行身份验证和/或授权。SecurityManager可能配置多个Realms,但至少有一个是必须的。Shiro 提供了立即可用的Realms 来连接一些安全数据源(即目录),如LDAP,关系数据库(JDBC),文本配置源,像INI 及属性文件,以及更多。你可以插入你自己的Realm 实现来代表自定义的数据源,如果默认地Realm不符合你的需求。

    看了上面这一段解释,可能还有小伙伴云里雾里,那么接下来我们来通过一个简单的案例来看看Realm到底扮演了一个什么样的作用,注意,本文的案例在上文案例的基础上完成。首先自定义一个MyRealm,内容如下:

    public class MyRealm implements Realm {
        public String getName() {
            return "MyRealm";
        }
        public boolean supports(AuthenticationToken token) {
            return token instanceof UsernamePasswordToken;
        }
        public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String password = new String(((char[]) token.getCredentials()));
            String username = token.getPrincipal().toString();
            if (!"sang".equals(username)) {
                throw new UnknownAccountException("用户不存在");
            }
            if (!"123".equals(password)) {
                throw new IncorrectCredentialsException("密码不正确");
            }
            return new SimpleAuthenticationInfo(username, password, getName());
        }
    }

    自定义Realm实现Realm接口,该接口中有三个方法,第一个getName方法用来获取当前Realm的名字,第二个supports方法用来判断这个realm所支持的token,这里我假设值只支持UsernamePasswordToken类型的token,第三个getAuthenticationInfo方法则进行了登陆逻辑判断,从token中取出用户的用户名密码等,进行判断,当然,我这里省略掉了数据库操作,当登录验证出现问题时,抛异常即可,这里抛出的异常,将在执行登录那里捕获到(注意,由于我这里定义的MyRealm是实现了Realm接口,所以这里的用户名和密码都需要我手动判断是否正确,后面的文章我会介绍其他写法)。

    OK,创建好了MyRealm之后还不够,我们还需要做一个简单配置,让MyRealm生效,将shiro.ini文件中的所有东西都注释掉,添加如下两行:

    MyRealm= org.sang.MyRealm
    securityManager.realms=$MyRealm

    第一行表示定义了一个realm,第二行将这个定义好的交给securityManger,这里实际上会调用到RealmSecurityManager类的setRealms方法。OK,做好这些之后,小伙伴们可以在MyRealm类中的一些关键节点打上断点,再次执行main方法,看看整个的登录流程。

    4. 再来聊一聊Shiro中的Realm

    4.1 Realm的继承关系

    通过查看类的继承关系,我们发现Realm的子类实际上有很多种,这里我们就来看看有代表性的几种:

    1. IniRealm

    可能我们并不知道,实际上这个类在我们第二篇文章中就已经用过了。这个类一开始就有如下两行定义:

    public static final String USERS_SECTION_NAME = "users";
    public static final String ROLES_SECTION_NAME = "roles";

    这两行配置表示shiro.ini文件中,[users]下面的表示表用户名密码还有角色,[roles]下面的则是角色和权限的对应关系。

    1. PropertiesRealm

    PropertiesRealm则规定了另外一种用户、角色定义方式,如下:

    user.user1=password,role1 role.role1=permission1

    1. JdbcRealm

    这个顾名思义,就是从数据库中查询用户的角色、权限等信息。打开JdbcRealm类,我们看到源码中有如下几行:

    protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
    protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
    protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
    protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";

    根据这几行预设的SQL我们就可以大致推断出数据库中表的名称以及字段了,当然,我们也可以自定义SQL。JdbcRealm实际上是AuthenticatingRealm的子类,关于AuthenticatingRealm我们在后面还会详细说到,这里先不展开。接下来我们就来详细说说这个JdbcRealm。

    4.2 JdbcRealm

    1. 准备工作

    使用JdbcRealm,涉及到数据库操作,要用到数据库连接池,这里我使用Druid数据库连接池,因此首先添加如下依赖:

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>RELEASE</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.27</version>
    </dependency>
    1. 数据库创建

    想要使用JdbcRealm,那我首先要创建数据库,根据JdbcRealm中预设的SQL,我定义的数据库表结构如下:

    35633711043c987fe6ecff873cb662e9.png

    这里为了大家能够直观的看到表的关系,我使用了外键,实际工作中,视情况而定。然后向表中添加几条测试数据。数据库脚本小伙伴可以在github上下载到(https://github.com/lenve/shiroSamples/blob/v4/shiroDemo.sql)。

    1. 配置文件处理

    然后将shiro.ini中的所有配置注释掉,添加如下配置:

    jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
    dataSource=com.alibaba.druid.pool.DruidDataSource
    dataSource.driverClassName=com.mysql.jdbc.Driver
    dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
    dataSource.username=root
    dataSource.password=123
    jdbcRealm.dataSource=$dataSource
    jdbcRealm.permissionsLookupEnabled=true
    securityManager.realms=$jdbcRealm

    这里的配置文件都很简单,不做过多赘述,小伙伴唯一需要注意的是permissionsLookupEnabled需要设置为true,否则一会JdbcRealm就不会去查询权限用户权限。

    1. 测试

    OK,做完上面几步就可以测试了,测试方式和第二篇文章中一样,我们可以测试下用户登录,用户角色和用户权限。

    1. 自定义查询SQL

    小伙伴们看懂了上文,对于自定义查询SQL就没什么问题了。我这里举一个简单的例子,比如我要自定义authenticationQuery对对应的SQL,查看JdbcRealm源码,我们发现authenticationQuery对应的SQL本来是select password from users where username = ?,如果需要修改的话,比如说我的表名不是users而是employee,那么在shiro.ini中添加如下配置即可:

    jdbcRealm.authenticationQuery=select password from employee where username = ?

    OK,这个小伙伴下来自己做尝试,我这里就不演示了。

    5. Shiro中多Realm的认证策略问题

    5.1 多Realm认证策略

    不知道小伙伴们是否还记得这张登录流程图:

    937eb8e73d14804d27eb39c2d2b6b8e3.png

    从这张图中我们可以清晰看到Realm是可以有多个的,不过到目前为止,我们所有的案例都还是单Realm,那么我们先来看一个简单的多Realm情况。

    前面的文章我们自己创建了一个MyRealm,也用过JdbcRealm,但都是单独使用的,现在我想将两个一起使用,只需要修改shiro.ini配置即可,如下:

    MyRealm= org.sang.MyRealm
    
    jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
    dataSource=com.alibaba.druid.pool.DruidDataSource
    dataSource.driverClassName=com.mysql.jdbc.Driver
    dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
    dataSource.username=root
    dataSource.password=123
    jdbcRealm.dataSource=$dataSource
    jdbcRealm.permissionsLookupEnabled=true
    securityManager.realms=$jdbcRealm,$MyRealm

    但是此时我数据库中用户的信息是sang/123,MyRealm中配置的信息也是sang/123,我把MyRealm中的用户信息修改为江南一点雨/456,此时,我的MyRealm的getAuthenticationInfo方法如下:

    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String password = new String(((char[]) token.getCredentials()));
        String username = token.getPrincipal().toString();
        if (!"江南一点雨".equals(username)) {
            throw new UnknownAccountException("用户不存在");
        }
        if (!"456".equals(password)) {
            throw new IncorrectCredentialsException("密码不正确");
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }

    这个时候我们就配置了两个Realm,还是使用我们一开始的测试代码进行登录测试,这个时候我们发现我既可以使用江南一点雨/456进行登录,也可以使用sang/123进行登录,用sang/123登录成功之后用户的角色信息和之前是一样的,而用江南一点雨/456登录成功之后用户没有角色,这个也很好理解,因为我们在MyRealm中没有给用户配置任何权限。总而言之,就是当我有了两个Realm之后,现在只需要这两个Realm中的任意一个认证成功,就算我当前用户认证成功。

    5.2 原理追踪

    好了,有了上面的问题后,接下来我们在Subject的login方法上打断点,跟随程序的执行步骤,我们来到了ModularRealmAuthenticator类的doMultiRealmAuthentication方法中,如下:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms();
        return realms.size() == 1?this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken):this.doMultiRealmAuthentication(realms, authenticationToken);
    }

    在这个方法中,首先会获取当前一共有多少个realm,如果只有一个则执行doSingleRealmAuthentication方法进行处理,如果有多个realm,则执行doMultiRealmAuthentication方法进行处理。doSingleRealmAuthentication方法部分源码如下:

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        ...
        ...
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if(info == null) {
            String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "].";
            throw new UnknownAccountException(msg);
        } else {
            return info;
        }
    }

    小伙伴们看到这里就明白了,这里调用了realm的getAuthenticationInfo方法,这个方法实际上就是我们自己实现的MyRealm中的getAuthenticationInfo方法。

    那如果有多个Realm呢?我们来看看doMultiRealmAuthentication方法的实现,部分源码如下:

    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
        AuthenticationStrategy strategy = this.getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        Iterator var5 = realms.iterator();
        while(var5.hasNext()) {
            Realm realm = (Realm)var5.next();
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
            if(realm.supports(token)) {
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable var11) {
                }
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
            } else {
                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
            }
        }
        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }

    我这里主要来说下这个方法的实现思路:

    1. 首先获取多Realm认证策略

    2. 构建一个AuthenticationInfo用来存放一会认证成功之后返回的信息

    3. 遍历Realm,调用每个Realm中的getAuthenticationInfo方法,看是否能够认证成功

    4. 每次获取到AuthenticationInfo之后,都调用afterAttempt方法进行结果合并

    5. 遍历完所有的Realm之后,调用afterAllAttempts进行结果合并,这里主要判断下是否一个都没匹配上

    5.3 自由配置认证策略

    OK,经过上面的简单解析,小伙伴们对认证策略应该有一个大致的认识了,那么在Shiro中,一共支持三种不同的认证策略,如下:

    1. AllSuccessfulStrategy,这个表示所有的Realm都认证成功才算认证成功

    2. AtLeastOneSuccessfulStrategy,这个表示只要有一个Realm认证成功就算认证成功,默认即此策略

    3. FirstSuccessfulStrategy,这个表示只要第一个Realm认证成功,就算认证成功

    配置方式也很简单,在shiro.ini中进行配置,在上面配置的基础上,增加如下配置:

    authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
    securityManager.authenticator=$authenticator
    allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
    securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

    此时,我们再进行登录测试,则会要求每个Realm都认证通过才算认证通过。

    6. Shiro中密码加密

    6.1 密码为什么要加密

    2011年12月21日,有人在网络上公开了一个包含600万个CSDN用户资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后CSDN在微博、官方网站等渠道发出了声明,解释说此数据库系2009年备份所用,因不明原因泄露,已经向警方报案。后又在官网网站发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于CSDN把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄露就会造成很大的安全隐患。由于有了这么多前车之鉴,我们现在做系统时,密码都要加密处理。

    密码加密我们一般会用到散列函数,又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。我们常用的散列函数有如下几种:

    1. MD5消息摘要算法

    MD5消息摘要算法是一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值,用于确保信息传输完整一致。MD5由美国密码学家罗纳德·李维斯特设计,于1992年公开,用以取代MD4算法。这套算法的程序在 RFC 1321中被加以规范。将数据(如一段文字)运算变为另一固定长度值,是散列算法的基础原理。1996年后被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如SHA-2。2004年,证实MD5算法无法防止碰撞,因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。

    1. 安全散列算法

    安全散列算法(Secure Hash Algorithm)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。且若输入的消息不同,它们对应到不同字符串的机率很高。SHA家族的算法,由美国国家安全局所设计,并由美国国家标准与技术研究院发布,是美国的政府标准,其分别是:SHA-0:1993年发布,是SHA-1的前身;SHA-1:1995年发布,SHA-1在许多安全协议中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5的后继者。但SHA-1的安全性在2000年以后已经不被大多数的加密场景所接受。2017年荷兰密码学研究小组CWI和Google正式宣布攻破了SHA-1;SHA-2:2001年发布,包括SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。虽然至今尚未出现对SHA-2有效的攻击,它的算法跟SHA-1基本上仍然相似;因此有些人开始发展其他替代的散列算法;SHA-3:2015年正式发布,SHA-3并不是要取代SHA-2,因为SHA-2目前并没有出现明显的弱点。由于对MD5出现成功的破解,以及对SHA-0和SHA-1出现理论上破解的方法,NIST感觉需要一个与之前算法不同的,可替换的加密散列算法,也就是现在的SHA-3。

    6.2 Shiro中如何加密

    Shiro中对以上两种散列算法都提供了支持,对于MD5,Shiro中生成消息摘要的方式如下:

    Md5Hash md5Hash = new Md5Hash("123", null, 1024);

    第一个参数是要生成密码的明文,第二个参数密码的盐值,第三个参数是生成消息摘要的迭代次数。

    Shiro中对于安全散列算法的支持如下(支持多种算法,这里我举一个例子):

    Sha512Hash sha512Hash = new Sha512Hash("123", null, 1024);

    这里三个参数含义与上文基本一致,不再赘述。shiro中也提供了通用的算法,如下:

    SimpleHash md5 = new SimpleHash("md5", "123", null, 1024);
    SimpleHash sha512 = new SimpleHash("sha-512", "123", null, 1024);

    当用户注册时,我们可以通过上面的方式对密码进行加密,将加密后的字符串存入数据库中。我这里为了简单,就不写注册功能了,就把昨天数据库中用户的密码123改成sha512所对应的字符串,如下:

    cb5143cfcf5791478e057be9689d2360005b3aac951f947af1e6e71e3661bf95a7d14183dadfb0967bd6338eb4eb2689e9c227761e1640e6a033b8725fabc783

    同时,为了避免其他Realm的干扰,数据库中我只配置一个JdbcRealm。

    此时如果我不做其他修改的话,登录必然会失败,原因很简单:我登录时输入的密码是123,但是数据库中的密码是一个很长的字符串,所以登录肯定不会成功。通过打断点,我们发现最终的密码比对是在SimpleCredentialsMatcher类中的doCredentialsMatch方法中进行密码比对的,比对的方式也很简单,直接使用了对用户输入的密码和数据库中的密码生成byte数组然后进行比较,最终的比较在MessageDigest类的isEqual方法中。部分逻辑如下:

    protected boolean equals(Object tokenCredentials, Object accountCredentials) {
            ...
            ...
            //获取用户输入密码的byte数组
            byte[] tokenBytes = this.toBytes(tokenCredentials);
            //获取数据库中密码的byte数组
            byte[] accountBytes = this.toBytes(accountCredentials);
            return MessageDigest.isEqual(tokenBytes, accountBytes);
            ...
    }

    MessageDigest的isEqual方法如下:

    public static boolean isEqual(byte[] digesta, byte[] digestb) {
        if (digesta == digestb) return true;
        if (digesta == null || digestb == null) {
            return false;
        }
        if (digesta.length != digestb.length) {
            return false;
        }
    
        int result = 0;
        // time-constant comparison
        for (int i = 0; i < digesta.length; i++) {
            result |= digesta[i] ^ digestb[i];
        }
        return result == 0;
    }

    都是很容易理解的比较代码,这里不赘述。我们现在之所以登录失败是因为没有对用户输入的密码进行加密,通过对源代码的分析,我们发现是因为在AuthenticatingRealm类的assertCredentialsMatch方法中获取了一个名为SimpleCredentialsMatcher的密码比对器,这个密码比对器中比对的方法就是简单的比较,因此如果我们能够将这个密码比对器换掉就好了。我们来看一下CredentialsMatcher的继承关系:

    d8726bea06d57308a6a8f400e2a9fa01.png

    我们发现这个刚好有一个Sha512CredentialsMatcher比对器,这个比对器的doCredentialsMatch方法在它的父类HashedCredentialsMatcher,方法内容如下:

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = hashProvidedCredentials(token, info);
        Object accountCredentials = getCredentials(info);
        return equals(tokenHashedCredentials, accountCredentials);
    }

    这时我们发现获取tokenHashedCredentials的方式不像以前那样简单粗暴了,而是调用了hashProvidedCredentials方法,而hashProvidedCredentials方法最终会来到下面这个重载方法中:

    protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
        String hashAlgorithmName = assertHashAlgorithmName();
        return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
    }

    这几行代码似曾相识,很明显,是系统帮我们对用户输入的密码进行了转换。了解了这些之后,那我只需要将shiro.ini修改成如下样子即可实现登录了:

    sha512=org.apache.shiro.authc.credential.Sha512CredentialsMatcher
    # 迭代次数
    sha512.hashIterations=1024
    jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
    dataSource=com.alibaba.druid.pool.DruidDataSource
    dataSource.driverClassName=com.mysql.jdbc.Driver
    dataSource.url=jdbc:mysql://localhost:3306/shiroDemo
    dataSource.username=root
    dataSource.password=123
    jdbcRealm.dataSource=$dataSource
    jdbcRealm.permissionsLookupEnabled=true
    # 修改JdbcRealm中的credentialsMatcher属性
    jdbcRealm.credentialsMatcher=$sha512
    securityManager.realms=$jdbcRealm

    如此之后,我们再进行登录测试,就可以登录成功了。

    本小节案例下载:https://github.com/lenve/shiroSamples/archive/refs/tags/v6.zip

    7. Shiro中密码加盐

    7.1 密码为什么要加盐

    不管是消息摘要算法还是安全散列算法,如果原文一样,生成密文也是一样的,这样的话,如果两个用户的密码原文一样,存到数据库中密文也就一样了,还是不安全,我们需要做进一步处理,常见解决方案就是加盐。盐从那里来呢?我们可以使用用户id(因为一般情况下,用户id是唯一的),也可以使用一个随机字符,我这里采用第一种方案。

    7.2 Shiro中如何实现加盐

    shiro中加盐的方式很简单,在用户注册时生成密码密文时,就要加入盐,如下几种方式:

    Md5Hash md5Hash = new Md5Hash("123", "sang", 1024);
    Sha512Hash sha512Hash = new Sha512Hash("123", "sang", 1024);
    SimpleHash md5 = new SimpleHash("md5", "123", "sang", 1024);
    SimpleHash sha512 = new SimpleHash("sha-512", "123", "sang", 1024)

    然后我们首先将sha512生成的字符串放入数据库中,接下来我要配置一下我的jdbcRealm,因为我要指定我的盐是什么。在这里我的盐就是我的用户名,每个用户的用户名是不一样的,因此这里没法写死,在JdbcRealm中,系统提供了四种不同的SaltStyle,如下:

    SaltStyle含义
    NO_SALT默认,密码不加盐
    CRYPT密码是以Unix加密方式储存的
    COLUMNsalt是单独的一列储存在数据库中
    EXTERNALsalt没有储存在数据库中,需要通过JdbcRealm.getSaltForUser(String)函数获取

    四种不同的SaltStyle对应了四种不同的密码处理方式,部分源码如下:

    switch (saltStyle) {
    case NO_SALT:
        password = getPasswordForUser(conn, username)[0];
        break;
    case CRYPT:
        // TODO: separate password and hash from getPasswordForUser[0]
        throw new ConfigurationException("Not implemented yet");
        //break;
    case COLUMN:
        String[] queryResults = getPasswordForUser(conn, username);
        password = queryResults[0];
        salt = queryResults[1];
        break;
    case EXTERNAL:
        password = getPasswordForUser(conn, username)[0];
        salt = getSaltForUser(username);
    }

    在COLUMN这种情况下,SQL查询结果应该包含两列,第一列是密码,第二列是盐,这里默认执行的SQL在JdbcRealm一开头就定义好了,如下:

    protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";

    即系统默认的盐是数据表中的password_salt提供的,但是我这里是username字段提供的,所以这里我一会要自定义这条SQL。自定义方式很简单,修改shiro.ini文件,添加如下两行:

    jdbcRealm.saltStyle=COLUMN
    jdbcRealm.authenticationQuery=select password,username from users where username=?

    首先设置saltStyle为COLUMN,然后重新定义authenticationQuery对应的SQL。注意返回列的顺序很重要,不能随意调整。如此之后,系统就会自动把username字段作为盐了。

    不过,由于ini文件中不支持枚举,saltStyle的值实际上是一个枚举类型,所以我们在测试的时候,需要增加一个枚举转换器在我们的main方法中,如下:

    BeanUtilsBean.getInstance().getConvertUtils().register(new AbstractConverter() {
        @Override
        protected String convertToString(Object value) throws Throwable {
            return ((Enum) value).name();
        }
    
        @Override
        protected Object convertToType(Class type, Object value) throws Throwable {
            return Enum.valueOf(type, value.toString());
        }
    
        @Override
        protected Class getDefaultType() {
            return null;
        }
    }, JdbcRealm.SaltStyle.class);

    当然,以后当我们将shiro和web项目整合之后,就不需要这个转换器了。

    如此之后,我们就可以再次进行登录测试了,会发现没什么问题了。

    7.3 非JdbcRealm如何配置盐

    OK,刚刚是在JdbcRealm中配置了盐,如果没用JdbcRealm,而是自己定义的普通Realm,要怎么解决配置盐的问题?

    首先要说明一点是,我们前面的文章在自定义Realm时都是通过实现Realm接口实现的,这种方式有一个缺陷,就是密码比对需要我们自己完成,一般在项目中,我们自定义Realm都是通过继承AuthenticatingRealm或者AuthorizingRealm,因为这两个方法中都重写了getAuthenticationInfo方法,而在getAuthenticationInfo方法中,调用doGetAuthenticationInfo方法获取登录用户,获取到之后,会调用assertCredentialsMatch方法进行密码比对,而我们直接实现Realm接口则没有这一步,部分源码如下:

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //调用doGetAuthenticationInfo获取info,这个doGetAuthenticationInfo是我们在自定义Realm中自己实现的
            info = doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }
        if (info != null) {
            //获取到info之后,进行密码比对
            assertCredentialsMatch(token, info);
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }
    
        return info;
    }

    基于上面所述的原因,这里我先继承AuthenticatingRealm,如下:

    public class MyRealm extends AuthenticatingRealm {
        public String getName() {
            return "MyRealm";
        }
        public boolean supports(AuthenticationToken token) {
            return token instanceof UsernamePasswordToken;
        }
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String username = token.getPrincipal().toString();
            if (!"sang".equals(username)) {
                throw new UnknownAccountException("用户不存在");
            }
            String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58";
            return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
        }
    }

    关于这个类,我说如下几点:

    1. 用户名我这里还是手动判断了下,实际上这个地方要从数据库查询用户信息,如果查不到用户信息,则直接抛UnknownAccountException

    2. 返回的SimpleAuthenticationInfo中,第二个参数是密码,正常情况下,这个密码是从数据库中查询出来的,我这里直接写死了

    3. 第三个参数是盐值,这样构造好SimpleAuthenticationInfo之后返回,shiro会去判断用户输入的密码是否正确

    上面的核心步骤是第三步,系统去自动比较密码输入是否正确,在比对的过程中,需要首先对用户输入的密码进行加盐加密,既然加盐加密,就会涉及到credentialsMatcher,这里我们要用的credentialsMatcher实际上和在JdbcRealm中用的credentialsMatcher一样,只需要在配置文件中增加如下一行即可:

    MyRealm.credentialsMatcher=$sha512

    sha512和我们上文定义的一致,这里就不再重复说了。

    本小节案例下载:https://github.com/lenve/shiroSamples/archive/refs/tags/v7.zip

    8. Shiro中自定义带角色和权限的Realm

    密码加密加盐小伙伴们应该没有问题了,但是前面几篇文章又给我们带来了一个新的问题:我们前面IniRealm、JdbcRealm以及自定义的MyRealm,其中前两个我们都能实现用户认证以及授权,即既能管理用户登录,又能管理用户角色,而我们自定义的MyRealm,目前还只能实现登录,不能实现授权,本文我们就来看看自定义Realm如何实现授权。

    8.1 问题追踪

    上篇文章我们没有实现自定义Realm的授权操作,但是这个并不影响我们调用hasRole方法去获取用户的权限,我在上文测试代码上的currentUser.hasRole上面打断点,通过层层追踪,我们发现最终来到了ModularRealmAuthorizer类的hasRole方法中,部分源码如下:

    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        assertRealmsConfigured();
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)) continue;
            if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
                return true;
            }
        }
        return false;
    }

    我们看到在这里会遍历所有的realm,如果这个realm是Authorizer的实例,则会进行进一步的授权操作,如果不是Authorizer的实例,则直接跳过,而我们只有一个自定义的MyRealm继承自AuthenticatingRealm,很明显不是Authorizer的实例,所以这里必然返回false,授权失败,所以要解决授权问题,第一步,得先让我们的MyRealm成为Authorizer的实例。

    8.2 解决方案

    如下图是Authorizer的继承关系:

    1cb65472792e0e76379027ed099a1dac.png

    小伙伴们看到,在Authorizer的实现类中有一个AuthorizingRealm,打开这个类,我们发现它的继承关系如下:

    public abstract class AuthorizingRealm extends AuthenticatingRealm
            implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
                ...
            }

    我们发现,这个AuthorizingRealm不仅是Authorizer的实现类,同时也是我们上文所用的AuthenticatingRealm的实现类,既然AuthorizingRealm同时是这两个类的实现类,那么我把MyRealm的继承关系由AuthenticatingRealm改为AuthorizingRealm,肯定不会影响我上文的功能,修改之后的MyRealm如下(部分关键代码):

    public class MyRealm extends AuthorizingRealm {
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String username = token.getPrincipal().toString();
            if (!"sang".equals(username)) {
                throw new UnknownAccountException("用户不存在");
            }
            String dbPassword = "a593ccad1351a26cf6d91d5f0f24234c6a4da5cb63208fae56fda809732dcd519129acd74046a1f9c5992db8903f50ebf3c1091b3aaf67a05c82b7ee470d9e58";
            return new SimpleAuthenticationInfo(username, dbPassword, ByteSource.Util.bytes(username), getName());
        }
    
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            Set<String> roles = new HashSet<String>();
            if ("sang".equals(principals.getPrimaryPrincipal().toString())) {
                roles.add("普通用户");
            }
            return new SimpleAuthorizationInfo(roles);
        }
    }

    继承了AuthorizingRealm之后,需要我们实现doGetAuthorizationInfo方法。在这个方法中,我们配置用户的权限。这里我为了方便,直接添加了普通用户这个权限,实际上,这里应该根据用户名去数据库里查询权限,查询方式不赘述。

    通过源码追踪,我们发现最终授权会来到AuthorizingRealm类的如下两个方法中:

    public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
        AuthorizationInfo info = getAuthorizationInfo(principal);
        return hasRole(roleIdentifier, info);
    }
    
    protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
        return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
    }

    这两个方法的逻辑很简单,第一个方法中调用的getAuthorizationInfo方法会最终调用到我们自定义的doGetAuthorizationInfo方法,第二个hasRole方法接收的两个参数,第一个是用户申请的角色,第二个是用户具备的角色集,一个简单的contains函数就判断出用户是否具备某个角色了。

    但是这个时候,用户只有角色,没有权限,我们可以对doGetAuthorizationInfo方法做进一步的完善,如下:

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Set<String> roles = new HashSet<String>();
        Set<String> permiss = new HashSet<String>();
        if ("sang".equals(principals.getPrimaryPrincipal().toString())) {
            roles.add("普通用户");
            permiss.add("book:update");
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        info.setStringPermissions(permiss);
        return info;
    }

    当然,正常情况下,权限也应当是从数据库中查询得到的,我这里简化下。

    那么这个角色是怎么验证的呢?追踪源码我们来到了AuthorizingRealm类的如下两个方法中:

    public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        AuthorizationInfo info = getAuthorizationInfo(principals);
        return isPermitted(permission, info);
    }
    
    //visibility changed from private to protected per SHIRO-332
    protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
        Collection<Permission> perms = getPermissions(info);
        if (perms != null && !perms.isEmpty()) {
            for (Permission perm : perms) {
                if (perm.implies(permission)) {
                    return true;
                }
            }
        }
        return false;
    }

    第一个isPermitted方法中调用了getAuthorizationInfo方法,而getAuthorizationInfo方法最终会调用到我们自己定义的doGetAuthorizationInfo方法,即获取到用户的角色权限信息,然后在第二个方法中进行遍历判断,查看是否具备相应的权限,第二个isPermitted方法的第一个参数就是用户要申请的权限。

    本小节案例下载:https://github.com/lenve/shiroSamples/archive/refs/tags/v8.zip

    9. Shiro整合Spring

    9.1 Spring&SpringMVC环境搭建

    Spring和SpringMVC环境的搭建,整体上来说,还是比较容易的,因为这个不是本文的重点,因此这里我不做详细介绍,小伙伴可以在文末下载源码查看Spring+SpringMVC环境的搭建。同时,由于MyBatis的整合相对要容易很多,这里为了降低项目复杂度,我也就先不引入MyBatis。

    对于项目依赖,除了Spring、SpringMVC、Shiro相关的依赖,还需要加入Shiro和Spring整合的jar,如下:

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>RELEASE</version>
    </dependency>

    9.2 整合Shiro

    搭建好Spring+SpringMVC环境之后,整合Shiro我们主要配置两个地方:

    1. web.xml中配置代理过滤器,如下:

    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    这样之后,当DelegatingFilterProxy拦截到所有请求之后,都会委托给shiroFilter来处理,shiroFilter是我们第二步在Spring容器中配置的一个实例。

    1. 配置Spring容器

    在Spring容器中至少有两个Bean需要我们配置,一个就是第一步中的shiroFilter,还有一个就是SecurityManager,完整配置如下:

    <bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
    </bean>
    <bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"></property>
        <property name="successUrl" value="/success.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        <property name="filterChainDefinitions">
            <value>
                /**=authc
            </value>
        </property>
    </bean>

    这是一个非常简单的配置,我们在以后的文章中还会继续完善它,关于这个配置我说如下几点:

    1. 首先我们需要配置一个securityManager,到时候我们的realm要配置在这里。

    2. 还要配置一个名为shiroFilter的bean,这个名字要和web.xml中代理过滤器的名字一致。

    3. shiroFilter中,loginUrl表示登录页面地址。

    4. successUrl表示登录成功地址。

    5. unauthorizedUrl表示授权失败地址。

    6. filterChainDefinitions中配置的/**=authc表示所有的页面都需要认证(登录)之后才能访问。

    7. authc实际上是一个过滤器,这个我们在后文还会再详细说到。

    8. 匹配符遵循Ant风格路径表达式,这里可以配置多个,匹配顺序从上往下匹配到了就不再匹配了。比如下面这个写法:

    /a/b/*=anon
    /a/**=authc

    假设我的路径是/a/b/c那么就会匹配到第一个过滤器anon,而不会匹配到authc,所以这里的顺序很重要。

    OK,这些配置写完后,在webpap目录下创建对应的jsp文件,如下:

    8f5df2c0f47c238c446f37ca3c752433.png

    此时,启动项目去浏览器中访问,无论我们访问什么地址,最后都会回到login.jsp页面,因为所有的页面(即使不存在的地址)都需要认证后才可以访问。

    本小节案例:https://github.com/lenve/shiroSamples/archive/refs/tags/v9.zip

    10. Shiro处理登录的三种方式

    10.1 准备工作

    很明显,不管是那种登录,都离不开数据库,这里数据库我采用我们前面的数据库,这里不做赘述(文末可以下载数据库脚本),但是我这里需要首先配置JdbcRealm,在applicationContext.xml中首先配置数据源,如下:

    <context:property-placeholder location="classpath:db.properties"/>
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
        <property name="username" value="${db.username}"/>
        <property name="password" value="${db.password}"/>
        <property name="url" value="${db.url}"/>
    </bean>

    有了数据源之后,接下来配置JdbcRealm,如下:

    <bean class="org.apache.shiro.realm.jdbc.JdbcRealm" id="jdbcRealm">
        <property name="dataSource" ref="dataSource"/>
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="sha-512"/>
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
        <property name="saltStyle" value="COLUMN"/>
        <property name="authenticationQuery" value="select password, username from users where username = ?"/>
    </bean>

    JdbcRealm中这几个属性和我们本系列第七篇文章基本是一致的,首先我们配置了密码比对器为HashedCredentialsMatcher,相应的算法为sha512,密码加密迭代次数为1024次,然后我们配置了密码的盐从数据表的列中来,username列就是我们的盐,这些配置和前文都是一致的,不清楚的小伙伴可以参考我们本系列第七篇文章。

    10.2 自定义登录逻辑

    自定义登录逻辑比较简单,首先我们把login.jsp页面进行简单改造:

    <form action="/login" method="post">
        <table>
            <tr>
                <td>用户名:</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td colspan="2"><input type="submit" value="登录"></td>
            </tr>
        </table>
    </form>

    然后创建我们的登录处理Controller,如下:

    @PostMapping("/login")
    public String login(String username, String password) {
        Subject currentUser = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            currentUser.login(token);
            return "success";
        } catch (AuthenticationException e) {
        }
        return "login";
    }

    登录成功我们就去success页面,登录失败就回到登录页面。做完这两步之后,我们还要修改shiroFilter中的filterChainDefinitions属性,要设置/login接口可以匿名访问,如下:

    <bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"></property>
        <property name="successUrl" value="/success.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        <property name="filterChainDefinitions">
            <value>
                /login=anon
                /**=authc
            </value>
        </property>
    </bean>

    做完这些之后,就可以去login.jsp页面测试登录了。

    上面中方式是我们自己写登录逻辑,shiro也给我们提供了两种不用自己写登录逻辑的登录方式,请继续往下看。

    10.3 基于HTTP的认证

    shiro中也提供了基于http协议的认证,当然,这种认证也得有数据库的辅助,数据配置和前文一样,我们只需要修改一个配置即可,如下:

    <bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
        <property name="securityManager" ref="securityManager"/>
        <property name="filterChainDefinitions">
            <value>
                /**=authcBasic
            </value>
        </property>
    </bean>

    这个表示所有的页面都要经过基于http的认证。此时我们打开任意一个页面,认证方式如下:

    000029667b4afad2dc57c64707f77e9c.png

    10.4 表单登录

    表单登录和基于HTTP的登录类似,都是不需要我们自己写登录逻辑的登录,但是出错的逻辑还是要稍微处理下,首先修改shiroFilter:

    <bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login"/>
        <property name="successUrl" value="/success.jsp"/>
        <property name="filterChainDefinitions">
            <value>
                /**=authc
            </value>
        </property>
    </bean>

    配置登录页面,也配置登录成功后的跳转页面,同时设置所有页面都要登录后才能访问。

    配置登录页面请求,如下:

    @RequestMapping("/login")
    public String login(HttpServletRequest req, Model model) {
        String shiroLoginFailure = (String) req.getAttribute("shiroLoginFailure");
        if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
            model.addAttribute("error", "账户不存在!");
        }
        if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
            model.addAttribute("error", "密码不正确!");
        }
        return "login";
    }

    如果登录失败,那么在request中会有一个shiroLoginFailure的属性中保存了登录失败的异常类名,通过判断这个类名,我们就可以知道是什么原因导致了登录失败。

    OK,配置好这两步之后,就可以去登录页面测试了。

    10.5 注销登录

    注销登录比较简单,就一个过滤器,按如下方式配置:

    <property name="filterChainDefinitions">
        <value>
            /logout=logout
            /**=authc
        </value>
    </property>

    通过get请求访问/logout即可注销登录。

    本小节有三个案例,下载地址如下:

    • https://github.com/lenve/shiroSamples/archive/refs/tags/v10.1.zip

    • https://github.com/lenve/shiroSamples/archive/refs/tags/v10.2.zip

    • https://github.com/lenve/shiroSamples/archive/refs/tags/v10.3.zip

    11. Shiro中的授权问题

    11.1 配置角色

    本文的案例在上文的基础上完成,因此Realm这一块我依然采用JdbcRealm,相关的授权就不必配置了。但是这里的数据库脚本有更新,小伙伴需要下载重新执行(https://github.com/lenve/shiroSamples/blob/v11/shiroDemo.sql)。

    先来介绍下目前数据库中用户的情况,数据库中有两个用户,sang具有admin的角色,同时具有book:*author:create两个权限,lisi具有user的角色,同时具有user:infouser:delete两个权限。修改shiroFilter,如下:

    <bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login"/>
        <property name="successUrl" value="/success.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        <property name="filterChainDefinitions">
            <value>
                /admin.jsp=authc,roles[admin]
                /user.jsp=authc,roles[user]
                /logout=logout
                /**=authc
            </value>
        </property>
    </bean>

    关于这里的配置,我说如下几点:

    1. unauthorizedUrl表示授权失败时展示的页面

    2. filterChainDefinitions中我们配置了admin.jsp页面必须登录后才能访问,同时登录的用户必须具有admin角色,user.jsp也是必须登录后才能访问,同时登录的用户必须具有user角色

    11.2 测试

    测试时我们分别用sang/123和lisi/123进行登录,登录成功后分别访问user.jsp和admin.jsp就能看到效果。

    11.3 配置权限

    上面的方式是配置角色,但是还没有配置权限,要配置权限,首先要在jdbcRealm中添加允许权限信息的查询:

    <property name="permissionsLookupEnabled" value="true"/>

    然后配置下shiroFilter:

    <property name="filterChainDefinitions">
        <value>
            /admin.jsp=authc,roles[admin]
            /user.jsp=authc,roles[user]
            /userinfo.jsp=authc,perms[user:info]
            /bookinfo.jsp=authc,perms[book:info]
            /logout=logout
            /**=authc
        </value>
    </property>

    这里假设访问userinfo.jsp需要user:info权限,访问bookinfo.jsp需要book:info权限。

    OK,做完这些之后就可以测试了,分别用sang/123和lisi/123进行登录,登录成功后分别访问bookinfo.jsp和userinfo.jsp就可以看到不同效果了。

    本小节案例下载:https://github.com/lenve/shiroSamples/archive/refs/tags/v11.zip

    12. Shiro中的JSP标签

    12.1 缘起

    上篇文章中,我们在success.jsp中写了很多像下面这种超链接:

    <h1>登录成功!</h1>
    <h3><a href="/logout">注销</a></h3>
    <h3><a href="/admin.jsp">admin.jsp</a></h3>
    <h3><a href="/user.jsp">user.jsp</a></h3>
    <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
    <h3><a href="/userinfo.jsp">userinfo.jsp</a></h3>

    但是对于不同身份的用户,并不是每一个链接都是有效的,点击无效的链接会进入到未授权的页面,这样用户体验并不好,最好能够把不可达的链接自动隐藏起来,同时,我也希望能够方便获取当前登录用户的信息等,考虑到这些需求,我们来聊聊shiro中的jsp标签。

    12.2 标签介绍

    shiro中的标签并不多,主要有如下几种:

    1. shiro:guest

    shiro:guest标签只有在当前未登录时显示里边的内容,如下:

    <shiro:guest>
        欢迎【游客】访问!
    </shiro:guest>
    1. shiro:user

    shiro:user是在用户登录之后显示该标签中的内容,无论是通过正常的登录还是通过Remember Me登录,如下:

    <shiro:user>
        欢迎【<shiro:principal/>】访问!
    </shiro:user>
    1. shiro:principal

    shiro:principal用来获取当前登录用户的信息,显示效果如下:

    7c4d6d7b1c57478768e6402800609059.png

    4.shiro:authenticated

    和shiro:user相比,shiro:authenticated的范围变小,当用户认证成功且不是通过Remember Me认证成功,这个标签中的内容才会显示出来:

    <shiro:authenticated>
        用户【<shiro:principal/>】身份认证通过,不是通过Remember Me认证!
    </shiro:authenticated>
    1. shiro:notAuthenticated

    shiro:notAuthenticated也是在用户未认证的情况下显示内容,和shiro:guest不同的是,对于通过Remember Me方式进行的认证,shiro:guest不会显示内容,而shiro:notAuthenticated会显示内容(因为此时并不是游客,但是又确实未认证),如下:

    <shiro:notAuthenticated>
        用户未进行身份认证
    </shiro:notAuthenticated>
    1. shiro:lacksRole

    当用户不具备某个角色时候,显示内容,如下:

    <shiro:lacksRole name="admin">
        用户不具备admin角色
    </shiro:lacksRole>
    1. shiro:lacksPermission

    当用户不具备某个权限时显示内容:

    <shiro:lacksPermission name="book:info">
        用户不具备book:info权限
    </shiro:lacksPermission>
    1. shiro:hasRole

    当用户具备某个角色时显示的内容:

    <shiro:hasRole name="admin">
        <h3><a href="/admin.jsp">admin.jsp</a></h3>
    </shiro:hasRole>
    1. shiro:hasAnyRoles

    当用户具备多个角色中的某一个时显示的内容:

    <shiro:hasAnyRoles name="user,aaa">
        <h3><a href="/user.jsp">user.jsp</a></h3>
    </shiro:hasAnyRoles>
    1. shiro:hasPermission

    当用户具备某一个权限时显示的内容:

    <shiro:hasPermission name="book:info">
        <h3><a href="/bookinfo.jsp">bookinfo.jsp</a></h3>
    </shiro:hasPermission>

    本小节案例下载:https://github.com/lenve/shiroSamples/archive/refs/tags/v12.zip

    13.Shiro 中的缓存机制

    13.1 添加依赖

    使用缓存,首先需要添加相关依赖,如下:

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.4.0</version>
    </dependency>

    13.2 添加配置文件

    ehcache的配置文件主要参考官方的配置,在resources目录下创建ehcache.xml文件,内容如下:

    <ehcache>
        <diskStore path="java.io.tmpdir/shiro-spring-sample"/>
        <defaultCache
                maxElementsInMemory="10000"
                eternal="false"
                timeToIdleSeconds="120"
                timeToLiveSeconds="120"
                overflowToDisk="false"
                diskPersistent="false"
                diskExpiryThreadIntervalSeconds="120"
        />
        <cache name="shiro-activeSessionCache"
               maxElementsInMemory="10000"
               eternal="true"
               overflowToDisk="true"
               diskPersistent="true"
               diskExpiryThreadIntervalSeconds="600"/>
        <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization"
               maxElementsInMemory="100"
               eternal="false"
               timeToLiveSeconds="600"
               overflowToDisk="false"/>
    </ehcache>

    这些都是ehcache缓存中常规的配置,含义我就不一一解释了,文末下载源码有注释。

    13.3 缓存配置

    接下来我们只需要在applicationContext中简单配置下缓存即可,配置方式如下:

    <bean class="org.apache.shiro.cache.ehcache.EhCacheManager" id="cacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>
    <bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" id="securityManager">
        <property name="realm" ref="jdbcRealm"/>
        <property name="cacheManager" ref="cacheManager"/>
    </bean>

    首先配置EhCacheManager类,指定缓存位置,然后在DefaultWebSecurityManager中引入cacheManager即可,如此之后,我们的缓存就应用上了。

    13.4 测试

    由于我这里使用了JdbcRealm,如果使用了自定义Realm那么可以通过打日志看是否使用了缓存,使用了JdbcRealm之后,我们可以通过打断点来查看是否应用了缓存,比如我执行如下代码:

    subject.checkRole("admin");
    subject.checkPermission("book:info");

    通过断点跟踪,发现最终会来到AuthorizingRealm的getAuthorizationInfo方法中,在该方法中,首先会去缓存中检查数据,如果缓存中有数据,则不会执行doGetAuthorizationInfo方法(数据库操作就在doGetAuthorizationInfo方法中进行),如果缓存中没有数据,则会执行doGetAuthorizationInfo方法,并且在执行成功后将数据保存到缓存中(前提是配置了缓存,cache不为null),此时我们通过断点,发现执行了缓存而没有查询数据库中的数据,部分源码如下:

    protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
        AuthorizationInfo info = null;
        Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
        if (cache != null) {
            Object key = getAuthorizationCacheKey(principals);
            info = cache.get(key);
        }
        if (info == null) {
            info = doGetAuthorizationInfo(principals);
            if (info != null && cache != null) {
                Object key = getAuthorizationCacheKey(principals);
                cache.put(key, info);
            }
        }
        return info;
    }

    OK,整体来说shiro中的缓存配置还是非常简单的。

    That's all.

    本小节案例下载地址:https://github.com/lenve/shiroSamples/archive/v13.zip

    推荐阅读:

    一周学完MyBatis源码,万字总结

    Redis为什么变慢,一文讲透如何排查Redis性能问题

    7张图揭晓RocketMQ存储设计的精髓

    腾讯二面:Redis 事务支持 ACID 么?

    聊聊分布式锁——Redis和Redisson的方式

    欢迎关注微信公众号:互联网全栈架构,收取更多有价值的信息。

    735ffcf18f6087acba2854625dcfeee2.png

    展开全文
  • Spring Security ​ Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP(面向切...

    Spring Security

    ​ Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

    官网:https://spring.io/projects/spring-security

    在springboot中使用Spring Security

    用户认证和授权

    搭建项目,引入依赖

    image-20201011161410930

    编写配置类

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
       //授权
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            //配置首页所有人都可以访问,功能只有对应角色的才能访问
            http.authorizeRequests()
                    .antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level3/**").hasRole("vip3");
    
            //没有权限会到登陆页面
            http.formLogin();
              //关闭csrf功能,保证正常注销
            http.csrf().disable();
             //开启注销功能
            http.logout().logoutSuccessUrl("/");
        }
    
        //配置用户角色(认证)
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             //数据来源于数据库,但在springboot 2.1.x可以这样使用
            //密码需要加密,spring security5.0+有很多加密方法
            auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                    .withUser("huang").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
                    .and()
                    .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2", "vip3")
                    .and()
                    .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
        }
    
    
    }
    

    level相当于菜单,目前还没有权限控制,只是把权限控制放在了菜单下的资源下

    image-20201011180831078

    Spring Security和thymeleaf整合

    ​ Spring Security在后台进行安全控制,当然权限的控制离不开前端也页面的一些操作,因此需要Spring Security和thymeleaf整合,使得thymeleaf可以使用Security的一些东西进行逻辑判断。

    首先引入依赖(只在springboot2.1.0以下版本可用)

    <!--security-thymeleaf整合-->
    <!--可以在thymeleaf中写一些security的东西-->
      <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    

    控制用户名和注销按钮的权限控制

    <!--未登录,则不显示-->
    <div sec:authorize="!isAuthenticated()" >
        <a class="item" th:href="@{/toLogin}">
            <i class="address card icon"></i> 登录
        </a>
    </div>
    <!--登录,显示用户名-->
    <div sec:authorize="isAuthenticated()" >
        <a class="item" >
            用户 <span sec:authentication="name"></span>
           <!-- 角色 <span sec:authentication=""></span>-->
        </a>
    </div>
    <!--登录,显示注销按钮-->
    <div sec:authorize="isAuthenticated()" >
    <a class="item" th:href="@{/logout}">
        <i class="address card icon"></i> 注销
    </a>
    </div>
    

    image-20201011185542238菜单的权限控制

    image-20201011190539309

    guest用户只有vip2角色,所以只能看到自己的菜单

    image-20201011190755700

    shiro

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

    image-20201011221956017

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

    Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。

    Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。

    SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

    Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

    springboot集成Shiro

    环境搭建

    首先导入依赖

    <!--shiro-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.6.0</version>
    

    创建对应配置类和页面

    image-20201011232110278

    @Configuration
    public class ShiroConfig  {
    
    
    
        //ShiroFilterFactoryBean
        @Bean
        public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            //设置安全管理器
            shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
            return shiroFilterFactoryBean;
        }
    
    
    
        //DefaultWebSecurityManager
        @Bean(name = "securityManager")
        public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm")UserRealm userRealm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(userRealm);
            return securityManager;
        }
    
        //创建Realm对象,需要自定义类
        @Bean
        public UserRealm userRealm(){
            return  new UserRealm();
        }
    }
    
    public class UserRealm  extends AuthorizingRealm {
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
            System.out.println("授权======================");
            return null;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("认证======================");
            return null;
        }
    }
    
    @Controller
    public class ShiroController {
    
    
        @RequestMapping({"/","/index"})
        public String  toIndex(Model model){
            model.addAttribute("msg","hello shiro");
            return "index";
        }
        @RequestMapping("user/add")
        public String  add(Model model){
            return "user/add";
        }
        @RequestMapping("user/update")
        public String  update(Model model){
            return "user/update";
        }
    
    }
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <p >add</p>
    </body>
    </html>
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <p >update</p>
    </body>
    </html>
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    
       <title>shiro</title>
    </head>
    <body>
    
    <p>测试shiro</p>
    <P  th:text="${msg}"></P>
    
    <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
    </body>
    </html>
    

    结果:点击连接可以跳转到对应页面

    image-20201011232406839

    配置过滤功能

    此时添加过滤器,只有具有authc的/user/*才会放行,否则报错。

    //ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
    
        //添加shiro的内置过滤器
        /**
         * onon:无需认证就可以访问
         * authc:必须认证了才能访问
         * user:必须拥有记住我功能才能使用
         * perms:拥有对某个资源的权限才能访问
         *role:拥有某个角色权限才能访问
         */
        LinkedHashMap<String, String> fiterMap = new LinkedHashMap<>();
        //fiterMap.put("/user/add" ,"authc");
        //fiterMap.put("/user/update" ,"authc");
        fiterMap.put("/user/*" ,"authc");
    
        bean.setFilterChainDefinitionMap(fiterMap);
        //设置登陆跳转
        bean.setLoginUrl("/toLogin");
    
        return bean;
    }
    

    添加登录页以及授权和认证功能

    用户名和密码校验(认证)

    登录页面

    <!DOCTYPE html>
    <html lang="en"xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <p>登录</p>
    <p th:text="${msg}" style="color: red"></p>
    <form th:action="@{/login}">
    <p>用户名<input type="text" name="username"></p>
    <p>密码<input type="text" name="password"></p>
    <p><input type="submit" ></p>
    </form>
    </body>
    </html>
    
    @RequestMapping("/login")
    public String  login(String username, String password,Model model){
    
        //获取当前用户
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            return "index";
        }catch (UnknownAccountException e){
            model.addAttribute("msg","用户名错误");
            return "login";
        }catch (IncorrectCredentialsException e ){
            model.addAttribute("msg","密码错误");
            return "login";
        }
    
    }
    
    public class UserRealm  extends AuthorizingRealm {
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
            System.out.println("授权======================");
            return null;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("认证======================");
    
            String name = "root";
            String password = "123456";
    
            UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
    
            if(!userToken.getUsername().equals(name)){
                return null;//抛出异常 UnknownAccountException
            }
            //密码认证,shiro做
            return new SimpleAuthenticationInfo("",zhpassword,"");
        }
    }
    

    ​ 这里自定义了用户名和密码,模拟数据库数据,当前端用这个账号登录时,会进行校验,是在realm里面做的,controller里面只需要获取当前Subject,调用登录方法即可,这两部分看起来并没有什么关联。

     ##### 用户授权
    

    image-20201012113104461

    image-20201012114223848

    image-20201012114252010

    shiro整合thymeleaf

    ​ 前面的权限都是后端控制的,在前端页面如果只有某个功能权限,那么也只能看到自己的功能菜单,因此前端页面需要做一些逻辑控制。

    首先导入依赖

    <!--shiro-thymeleaf-->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
    

    注册ShiroDialect

    @Bean
    public  ShiroDialect  getShiroDialect(){
        return new ShiroDialect();
    }
    

    image-20201012130554657

    用户进来只能看到自己的功能菜单

    image-20201012131726187

    Swagger

    ​ swagger是一个专门用于管理后端接口的web服务,最大的优点是能实时同步api与文档,方便前后端联调,也可以给接口添加注释信息,可以在线测试。

    image-20201012150054918

    springboot集成swagger

    导入依赖

    <!--swagger-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    

    配置类

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
        
       @Bean
        public Docket docket() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo())
                    .select()
                    //指定包扫描
                    .apis(RequestHandlerSelectors.basePackage("com.huang.controller.ShiroController"))
                    .build();
        }
    
    
        private ApiInfo apiInfo() {
            Contact contact = new Contact("test", "https://www.baidu.com/?tn=02003390_10_hao_pg", "1531756119@qq.com");
            return new ApiInfo(
                    "测试swagger",
                    "好好学习,天天向上",
                    "v1.0",
                    "https://www.baidu.com/?tn=02003390_10_hao_pg",
                    contact,
                    "Apache 2.0",
                    "https://www.baidu.com/?tn=02003390_10_hao_pg",
                    new ArrayList()
            );
        }
    }
    

    image-20201012153802170

    测试

    image-20201012154452938

    配置多个组和注释

    image-20201012165422048

    image-20201012165441304

    image-20201012165350163

    展开全文
  • 主要介绍了Spring Security 和Apache Shiro你需要具备哪些条件,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
  • Shiro+Spring Security学习文档,Spring+Security-3.0.1中文官方文档.pdf,跟我学Shiro教程.pdf。。。
  • Java企业级权限系统,可供学习Spring Security权限框架、 Apache Shiro权限框架、Spring MVC、 RBAC模型、模块开发等等,内有详细视频教程,由于上传大小限制,可联系免费获取!
  • SpringSecurityShiro学习笔记 使用与SpringSecurityShiro的初学者。 文章目录SpringSecurityShiro学习笔记一、安全简介二、SpringSecurity1、SpringSecurity简介2、认识SpringSecurity3、实战测试3.1、实验...
  • 在微服务项目中,Spring SecurityShiro 强在哪? 虽然目前 Spring Security 一片火热,但是 Shiro 的市场依然存在,今天我就来稍微的说一说这两个框架的,方便大家在实际项目中选择适合自己的安全管理框架。首先...
  • 点击上方好好java,选择星标公众号 重磅资讯,干货,第一时间送达 今日推荐:推荐19个github超牛逼项目!个人原创100W +访问量博客:点击前往,查看更多 要知道Shi...
  • 要知道Shiro和Spring Security该如何选择,首先要看看两者的区别和对比 Shiro Apache Shiro是一个强大且易用的Java安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。使用Shiro的易于理解的API,您...
  • spring securityshiro简单使用

    千次阅读 2022-01-13 17:32:36
    spring security 引入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> 编写配置...
  • SpringBoot整合SpringSecurityShiro

    千次阅读 2020-08-29 14:47:18
    Shiro,SpringSecurity
  • 点击上方“Java精选”,选择“设为星标”别问别人为什么,多问自己凭什么!下方有惊喜,留言必回,有问必答!每天08:15更新文章,每天进步一点点...要知道Shiro和Spring S...
  • 目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。对于它俩到底...
  • 要知道Shiro和Spring Security该如何选择,首先要看看两者的区别和对比ShiroApache Shiro是一个强大且易用的Java安全框架,能够非常清晰的处理认证、授权、管...
  • Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。 Shiro架构...
  • shiro shiro 是一种功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理,此为四大核心功能,另有缓存支持,rememberMe功能,可用于保护任何应用程序的安全-从命令行应用程序,移动应用程序到...
  • Shiro 的基本使用笔记 学习链接 目录结构 划红线的是Shiro 需要用到的类 shiro的简易工作流程 user(用户) –> Token(UsernamePasswordToken,令牌,Shiro 用来封装用户登录信息,使用用户的登陆信息来创建...
  • Spring Security 介绍 Spring Security是一个功能强大、高度可定制性的身份验证和访问控制的框架。它能够保护基于Spring框架的应用程序。 Spring Security是一个专注于为 Java 应用程序提供身份验证和授权的框架。与...
  • 虽然目前 Spring Security 一片火热,但是 Shiro 的市场依然存在,今天我就来稍微的说一说这两个框架的,方便大家在实际项目中选择适合自己的安全管理框架。 首先我要声明一点,框架无所谓好坏,关键是适合当前项目...
  • SpringSecurity 安全简介 Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。 Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权...
  • Java Shiro 和 SpringSecurity 框架的区别

    千次阅读 2021-11-20 11:22:59
    1. Shiro简介 Apache Shiro是一个强大且易用的Java安全框架。可以实现身份验证、授权、密码和会话管理。使用Shiro的易于理解的API。您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业...
  • ACL、RBAC、spring SecurityShiro的比较一、ACL二、RBAC三、spring Security四、Shiro五、Shiro和Spring Security比较 一、ACL ACL: 访问控制列表( Access Control List) 以前盛行的一种权限设计,它的核心在于...
  • Shiro Or Spring Security

    2018-11-26 16:17:21
    前言:公司想要把之前的框架重新弄一下,然后这边让我和另外一位同事负责搭建框架部分和基础数据部分,这边按照要求对两大权限框架啊做一个...以下是Shiro官方对于Shiro大概功能的介绍: 这边我们结合Spring Se...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,417
精华内容 2,566
关键字:

先学security还是shiro