zoukankan      html  css  js  c++  java
  • SpringBoot集成Apache Shiro

    笔者因为项目转型的原因,对Apache Shiro安全框架做了一点研究工作,故想写点东西以便将来查阅。之所以选择Shiro也是看了很多人的推荐,号称功能丰富强大,而且易于使用。实践下来的确如大多数人所说简约优美,小巧精悍。

    介绍demo项目前,简单说明一下Shiro框架的特性。

    1.  Apache Shiro Features

     

     

    从上图可以看出Shiro具备应用程序安全框架的四大基石”:身份验证、授权、会话管理和密码。

    Authentication有时被称为‘登录’,这是需要明确用户是谁

    Authorization访问控制,即确定‘谁’对‘什么’有访问权限。

    Session Management管理特定用户的会话,即使在非web或EJB应用程序中也是如此。

    Cryptography使用加密算法保持数据安全,但易于使用。

    在不同的应用程序环境中,还有更多的特性来支持和增强这些关注点,特别是:

    Web SupportShiro的Web支持API帮助轻松地保护web应用程序。

    Caching缓存是ApacheShiro的API中的第一等公民,以确保安全操作同时保持快速和高效。

    ConcurrencyApacheShiro支持具有并发特性的多线程应用程序。

    Testing提供测试支持,以帮助编写单元和集成测试,并确保代码如预期的安全。

    Run as允许用户假定另一个用户的身份(如果允许的话)的特性,有时在管理场景中很有用。

    Remember Me记住用户在会话中的身份,这样他们就只需要在强制的情况下输入口令登录。

    2. High-Level Overview

    Shiro的体系结构有三个主要概念:Subject、SecurityManager和Realms。下图展现了它的运行原理,

    主题:主题本质上是当前正在执行的用户。虽然“用户”这个词通常意味着一个人,一个主题可以是一个人,但它也可以代表一个第三方服务、守护进程帐户、cron作业或任何类似的东西-基本上是任何当前与软件交互的东西。Subject实例都绑定到(并且需要)一个SecurityManager。当与主题交互时,这些交互转化为与SecurityManager的特定主题交互。

    SecurityManagerSecurityManager是Shiro体系结构的核心,它将其内部安全组件协调在一起形成一个对象图。然而,一旦为应用程序配置了SecurityManager及其内部对象图,它通常会被单独使用,应用程序开发人员将几乎所有的时间都花在Subject API上。当与一个主题交互时,实际上是幕后的SecurityManager为任何主题安全操作做了所有繁重的工作。

    领域:领域充当Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当涉及到实际与用户帐户等安全相关的数据交互以执行身份验证(登录)和授权(访问控制)时,Shiro从一个或多个为应用程序配置的领域中查找数据。从这个意义上说,领域本质上是一个特定于安全的DAO:它封装数据源的连接细节,并根据需要将相关数据提供给Shiro。配置Shiro时,必须指定至少一个用于身份验证和/或授权的域。SecurityManager可以配置多个Realm,但至少需要一个。Shiro提供了开箱即用的领域,以连接到许多安全数据源(也称为目录),如LDAP、关系数据库(JDBC)、INI和属性文件等文本配置源。

    3. Detailed Architecture

    4. 过滤器

    当 Shiro 被运用到 web 项目时,Shiro 会自动创建一些默认的过滤器对客户端请求进行过滤。以下是 Shiro 内置过滤器:

    过滤器简称

    对应的 Java 类

    anon

    org.apache.shiro.web.filter.authc.AnonymousFilter

    authc

    org.apache.shiro.web.filter.authc.FormAuthenticationFilter

    authcBasic

    org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

    perms

    org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

    port

    org.apache.shiro.web.filter.authz.PortFilter

    rest

    org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

    roles

    org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

    ssl

    org.apache.shiro.web.filter.authz.SslFilter

    user

    org.apache.shiro.web.filter.authc.UserFilter

    logout

    org.apache.shiro.web.filter.authc.LogoutFilter


    项目中常用的解释一下, 

    /test/**=anon,  所有url 可以匿名访问

    /test/**=authc,  url需要认证才能访问

    /test/**=perms[user:add],  url需要认证用户拥有 user:add 权限才能访问

    /test/**=roles[admin],  url需要认证用户拥有 admin 角色才能访问

    /test/**=user,  url 需要认证或通过记住我认证才能访问

    5. DEMO

    开发工具为Eclipse+Maven,新建Springboot项目,版本号为1.5.14

    模板引擎使用Thymeleaf

    为了突出shiro,数据访问层略过,服务层模拟查询数据,Realm里面硬编码权限和角色信息简化代码。

    整个系统的核心在于两个class(MyShiroRealm + ShiroConfiguration), 项目结构如下,

    5.1 在pom.xml里面添加好shiro-core, shiro-spring

    <dependency>
    
             <groupId>org.apache.shiro</groupId>
    
             <artifactId>shiro-core</artifactId>
    
             <version>1.4.0</version>
    
    </dependency>
    
    <!-- shiro权限控制框架 -->
    
    <dependency>
    
             <groupId>org.apache.shiro</groupId>
    
             <artifactId>shiro-spring</artifactId>
    
             <version>1.4.0</version>
    
    </dependency>

    5.2 显示层

    模板引擎Thymeleaf,故application配置文件如下:

    1 spring.thymeleaf.cache=true
    2 spring.thymeleaf.prefix=classpath:/templates/
    3 spring.thymeleaf.suffix=.html
    4 spring.thymeleaf.mode=HTML5
    5 spring.thymeleaf.encoding=UTF-8
    6 spring.thymeleaf.content-type=text/html

    5.3 显示层静态页面如下:

    403.html

    add.html

    delete.html

    details.html

    edit.html

    index.html

    login.html

    logout.html

    5.4 几乎所有静态页面就是一个空壳,大体如add.html

     1 <!DOCTYPE html>
     2 <html>
     3 <head>
     4 <meta charset="UTF-8"></meta>
     5 <title>Add Page</title>
     6 </head>
     7 <body>
     8     <h1>Add Page</h1>
     9 </body>
    10 </html>
    add.html
     1 <!DOCTYPE html>
     2 <html>
     3 <head>
     4 <meta charset="UTF-8"></meta>
     5 <title>Login Page</title>
     6 <style type="text/css">
     7 table {
     8      360px;
     9     min-height: 25px;
    10     line-height: 25px;
    11     text-align: center;
    12     border-color: #b6ff00;
    13     border-collapse: collapse;
    14     
    15 }
    16 </style>
    17 </head>
    18 <body>
    19     <div>
    20         <form action="/home/check" method="post">
    21             <table border="1">
    22                 <tr>
    23                     <th>User Name</th>
    24                     <th><input type="text" name="name" /></th>
    25                 </tr>
    26                 <tr>
    27                     <td>Password</td>
    28                     <td><input type="password" name="password" /></td>
    29                 </tr>
    30                 <tr>
    31                     <td><input type="submit" value="Submit" /></td>
    32                     <td></td>
    33                 </tr>
    34             </table>
    35         </form>
    36     </div>
    37 </body>
    38 </html>
    login.html
     1 <!DOCTYPE html>
     2 <html>
     3 <head>
     4 <meta charset="UTF-8"></meta>
     5 <title>Index Page</title>
     6 <style type="text/css">
     7 p {
     8     font-family: Times, TimesNR, 'New Century Schoolbook', Georgia,
     9         'New York', serif;
    10     font-size: 20px;
    11 }
    12 </style>
    13 
    14 </head>
    15 <body>
    16     <h1>This is index page.</h1>
    17     <p>
    18         Customer Name: <span th:text="${name}"></span> --- Role: <span
    19             th:text="${role}"></span>
    20     </p>
    21     <table border="1">
    22         <tr>
    23             <th>角色</th>
    24             <th>权限</th>
    25         </tr>
    26         <tr>
    27             <td>admin</td>
    28             <td>增加,删除,编辑,查看</td>
    29         </tr>
    30         <tr>
    31             <td>operator</td>
    32             <td>编辑,查看</td>
    33         </tr>
    34         <tr>
    35             <td>viewer</td>
    36             <td>查看</td>
    37         </tr>
    38     </table>
    39     <ul>
    40         <li><a th:href="@{/customer/index}">Index</a></li>
    41         <li><a th:href="@{/customer/details}">Details</a></li>
    42         <li><a th:href="@{/customer/add}">Add</a></li>
    43         <li><a th:href="@{/customer/edit}">Edit</a></li>
    44         <li><a th:href="@{/customer/delete}">Delete</a></li>
    45     </ul>
    46 </body>
    47 </html>
    index.html

    5.5 Model

    public class Customer implements Serializable {
        private static final long serialVersionUID = 7429292944316962328L;
        private String name;
        private String password;
        private String role;
        
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getPassword() {
            return password;
        }
        public void setPassword(String password) {
            this.password = password;
        }
        
        public String getRole() {
            return role;
        }
        public void setRole(String role) {
            this.role = role;
        }
        
        public Customer(String name, String password, String role) {
            super();
            this.name = name;
            this.password = password;
            this.role = role;
        }
        
        @Override
        public String toString() {
            return "Name : --- " + name + ", Password : --- " + password + ", Role : *** " + role;
        }
        
    }
    Customer.java

    5.6 Service

     1 @Service
     2 public class CustomerService {
     3     public Customer findByName(String name) {
     4         // 模拟查询数据库
     5         // tom is admin, alice is operator, lucy is viewer
     6         if (name.equals("alice")) {
     7             return new Customer(name, "123", "operator");
     8         }
     9         return null;
    10     }
    11 }
    CustomerService.java

    5.7 Controller

    @Controller
    @RequestMapping("customer")
    public class CustomerController {    
        @RequestMapping("/index")
        @RequiresPermissions("customer:index")//权限管理; 
        public String index(Model model) {    
            Subject subject = SecurityUtils.getSubject();
            Customer customer = (Customer)subject.getPrincipal();
            if(customer != null) {
                model.addAttribute("name", customer.getName());
                model.addAttribute("role", customer.getRole());
            }
            return "index";
        }
        
        @RequestMapping("/details")
        @RequiresPermissions("customer:details")//权限管理; 
        public String details() {    
            return "details";
        }
        
        @RequestMapping("/add")    
        @RequiresRoles("admin")
        public String add() {    
            return "add";
        }
        
        @RequestMapping("/edit")
        @RequiresPermissions("customer:edit")//权限管理; 
        public String edit() {    
            return "edit";
        }
        
        @RequestMapping("/delete")
        @RequiresPermissions("customer:delete")//权限管理;
        public String delete() {    
            return "delete";
        }    
    }
    CustomerController.java
    @Controller
    @RequestMapping("home")
    public class HomeController {
        @RequestMapping("/login")
        public String login() {
            return "login";
        }
    
        @RequestMapping("/check")
        public String check(HttpServletRequest request) throws Exception {
            System.out.println("HomeController.check()");
    
            String name = request.getParameter("name");
            String password = request.getParameter("password");
            UsernamePasswordToken token = new UsernamePasswordToken(name, password);
            Subject subject = SecurityUtils.getSubject();
    
            try {
                subject.login(token);
            } catch (Exception ex) {
                System.out.println(ex.getMessage());
                System.out.println(ex.getStackTrace());
                return "login";
            }
    
            return "redirect:/customer/index";
        }
    }
    HomeController.java

    5.8 最重要的两个类如下:

     1 package com.example.demo.config;
     2 
     3 import java.util.LinkedHashMap;
     4 import java.util.Map;
     5 
     6 import org.apache.shiro.mgt.SecurityManager;
     7 import org.apache.shiro.spring.LifecycleBeanPostProcessor;
     8 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
     9 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    10 import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    11 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    12 import org.springframework.context.annotation.Bean;
    13 import org.springframework.context.annotation.Configuration;
    14 
    15 @Configuration
    16 public class ShiroConfiguration {
    17 
    18     @Bean
    19     public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
    20         System.out.println("ShiroConfiguration.shirFilter()");
    21         ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    22         shiroFilterFactoryBean.setSecurityManager(securityManager);
    23         // 过滤器.
    24         Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    25         // 配置不会被拦截的链接 顺序判断
    26         filterChainDefinitionMap.put("/static/**", "anon");
    27         filterChainDefinitionMap.put("/home/**", "anon");
    28         filterChainDefinitionMap.put("/test/**", "anon");
    29         filterChainDefinitionMap.put("/customer/**", "authc");
    30         shiroFilterFactoryBean.setLoginUrl("/home/login");
    31         // 登录成功后要跳转的链接
    32         shiroFilterFactoryBean.setSuccessUrl("/customer/index");
    33         
    34         shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    35         return shiroFilterFactoryBean;
    36     }
    37 
    38     @Bean
    39     public MyShiroRealm myShiroRealm() {
    40         MyShiroRealm myShiroRealm = new MyShiroRealm();
    41         return myShiroRealm;
    42     }
    43 
    44     @Bean
    45     public SecurityManager securityManager() {
    46         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    47         securityManager.setRealm(myShiroRealm());
    48         return securityManager;
    49     }
    50 
    51     // 开启Shiro AOP注解支持.
    52     @Bean
    53     public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    54         System.out.println("OPNE AOP......");
    55         AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    56         authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    57         return authorizationAttributeSourceAdvisor;
    58     }
    59     
    60     @Bean
    61     public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
    62         return new DefaultAdvisorAutoProxyCreator();
    63     }
    64 
    65     // 管理shiro生命周期
    66     @Bean
    67     public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
    68         return new LifecycleBeanPostProcessor();
    69     }
    70 }
    ShiroConfiguration.java

    因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO.

    Shiro的认证过程最终会交由Realm执行,这时会调用Realm的getAuthenticationInfo(token)方法。

    该方法主要执行以下操作:

    1)         根据口令信息检查标识主体(帐户标识信息)

    2)         在数据源中查找相应的帐户信息

    3)         确保令牌提供的凭据与存储在数据存储中的凭据匹配

    4)         如果凭证匹配,则返回一个AuthenticationInfo实例,该实例将帐户数据封装为Shiro理解的格式

    5)         如果凭证不匹配,则引发身份验证异常

    在应用程序中需要自定义一个Realm类,继承AuthorizingRealm抽象类,覆盖doGetAuthenticationInfo(),重写获取用户信息的方法。

    shiro的权限授权是通过继承AuthorizingRealm抽象类,覆盖doGetAuthorizationInfo()。当访问到页面的时候,URL配置了相应的权限或者shiro标签才会执行此方法否则不会执行,所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。

    在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。SecurityManager将权限或角色检查的任务委托给Authorizer,默认为ModularRealmAuthorizer。

    应用程序则可以通过角色或者权限进行访问控制。

     1 package com.example.demo.config;
     2 
     3 import java.util.HashSet;
     4 import java.util.Set;
     5 
     6 import org.apache.shiro.authc.AuthenticationException;
     7 import org.apache.shiro.authc.AuthenticationInfo;
     8 import org.apache.shiro.authc.AuthenticationToken;
     9 import org.apache.shiro.authc.SimpleAuthenticationInfo;
    10 import org.apache.shiro.authz.AuthorizationInfo;
    11 import org.apache.shiro.authz.SimpleAuthorizationInfo;
    12 import org.apache.shiro.realm.AuthorizingRealm;
    13 import org.apache.shiro.subject.PrincipalCollection;
    14 import org.springframework.beans.factory.annotation.Autowired;
    15 
    16 import com.example.demo.model.Customer;
    17 import com.example.demo.service.CustomerService;
    18 
    19 public class MyShiroRealm extends AuthorizingRealm {
    20 
    21     @Autowired
    22     private CustomerService customerService;
    23     
    24     @Override
    25     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    26         System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
    27 
    28         // 获取用户的输入的账号.
    29         String name = (String) token.getPrincipal();
    30         System.out.println(token.getCredentials());
    31         Customer c = customerService.findByName(name);
    32         System.out.println("Customer info : " + c);
    33         if (c == null) {
    34             return null;
    35         }
    36         SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(c, // 用户名
    37                 c.getPassword(), // 密码
    38                 getName() // realm name
    39         );
    40 
    41         return authenticationInfo;
    42     }
    43 
    44     @Override
    45     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    46 
    47         System.out.println("权限管理-->MyShiroRealm.doGetAuthorizationInfo()");
    48         SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    49         Customer customer = (Customer) principals.getPrimaryPrincipal();
    50         System.out.println("Customer is : " + customer);
    51         // 权限单个添加;
    52         // 添加一个角色,不是配置意义上的添加,而是证明该用户拥有admin角色
    53         // 模拟查询数据库,得到用户角色为admin或者operator或者viewer
    54         // admin有所有权限,operator有查看和编辑权限,没有添加和删除权限
    55         // viewer只有查看权限
    56         authorizationInfo.addRole("operator");
    57         // 添加权限
    58         Set<String> permissionSet = new HashSet<String>();
    59         permissionSet.add("customer:details");
    60         permissionSet.add("customer:index");
    61         permissionSet.add("customer:edit");
    62         //permissionSet.add("customer:add");
    63         //permissionSet.add("customer:delete");
    64         
    65         authorizationInfo.setStringPermissions(permissionSet);
    66         return authorizationInfo;
    67     }
    68 }
    MyShiroRealm

    6. 参考资料

    http://shiro.apache.org/introduction.html

  • 相关阅读:
    前端整体流程
    django安装
    scrapy中出现[scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to 如何解决
    python测试当前代理IP是否有效
    grequests模块
    scrapy中发起post请求
    post请求中的payload解决办法
    SSM配置动态数据源
    前端(十):使用redux管理数据
    前端(九):react生命周期
  • 原文地址:https://www.cnblogs.com/sankt/p/9278886.html
Copyright © 2011-2022 走看看