浅玩springboot整合shiro框架

什么是shiro

Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。

基本功能点

Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support:Web 支持,可以非常容易的集成到 Web 环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;

Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。

接下来我们分别从外部和内部来看看 Shiro 的架构,对于一个好的框架,从外部来看应该具有非常简单易于使用的 API,且 API 契约明确;从内部来看的话,其应该有一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。

首先,我们从外部来看 Shiro 吧,即从应用程序角度的来观察如何使用 Shiro 完成工作。如下图:

在这里插入图片描述

可以看到:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject;其每个 API 的含义:

Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个 Shiro 应用:

  1. 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
  2. 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入。

看懂了吗

其实上面的概念我们大概知道就行了,没必要死磕到底,如果还是想了解多一点概念可以点击我。其实对于新手来说,更重要的是能自己实践出来即可。最近玩的项目用到了shiro框架身份认证和权限,所以总结了一下springboot整合shiro框架的应用。

代码实现
导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ao</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--shiro依赖包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.21</version>
        </dependency>

        <!--mybatis-plus依赖包-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

shiro配置

ShiroConfig

package com.ao.demo.config;

import com.ao.demo.shiro.AdminAuthorizingRealm;
import com.ao.demo.shiro.AdminWebSessionManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {


    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        /*anon: 无需认证即可访问
		authc: 需要认证才可访问
		user: 点击“记住我”功能可访问
		perms: 拥有权限才可以访问
		role: 拥有某个角色权限才能访问*/
        filterChainDefinitionMap.put("/admin/auth/login", "anon");
        filterChainDefinitionMap.put("/admin/**", "authc");
        /*没有登录的用户请求需要登录的页面时自动跳转到登录页面。*/
        shiroFilterFactoryBean.setLoginUrl("/admin/auth/401");
        /*登录成功默认跳转页面,不配置则跳转至”/”,可以不配置,直接通过代码进行处理。*/
        shiroFilterFactoryBean.setSuccessUrl("/admin/auth/success");
        /*没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面*/
        shiroFilterFactoryBean.setUnauthorizedUrl("/admin/auth/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    //将自己的验证方式加入容器
    @Bean
    public Realm realm() {
        return new AdminAuthorizingRealm();
    }

    //管理会话
    @Bean
    public SessionManager sessionManager() {
        return new AdminWebSessionManager();
    }

    //权限管理,配置主要是Realm的管理认证
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

}

配置自己的验证方式

AdminAuthorizingRealm

package com.ao.demo.shiro;


import com.ao.demo.pojo.Admin;
import com.ao.demo.service.IAdminService;
import com.ao.demo.service.IPermissionService;
import com.ao.demo.service.IRoleService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;

import java.util.List;
import java.util.Set;

@Slf4j
public class AdminAuthorizingRealm extends AuthorizingRealm {
    /*当调用判断权限的方法, 才会触发 doGetAuthorizationInfo() 方法
	subject.hasRole();
	subject.checkPermission();
	subject.isPermitted();
	....
*/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("----------doGetAuthorizationInfo方法被调用----------");
        if (principals == null) {
            throw new AuthorizationException("");
        }
        Admin admin = (Admin) getAvailablePrincipal(principals);
        String[] roleIds = admin.getRoleIds();
        Set<String> roles =  new HashSet<>();
        roles.add("role1");
        Set<String> permissions = new HashSet<>();
        permissions.add("order:list");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles);
        info.setStringPermissions(permissions);
        return info;
    }

//    3.在认证方法中的doGetAuthenticationInfo
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token; 
        String username = upToken.getUsername();  //获取到用户名
        String password = new String(upToken.getPassword());  //获取到密码

//        查询数据库是否有这个用户,此处是mybatisplus的写法
        List<Admin> adminList = adminService.list(new QueryWrapper<Admin>().lambda().eq(Admin::getUsername, username));
        Assert.state(adminList.size() < 2, "同一个用户名存在两个账户");
        if (adminList.size() == 0) {
            throw new UnknownAccountException("找不到用户(" + username + ")的帐号信息");
        }
        Admin admin = adminList.get(0);

        if (admin.getState() == 1){
            throw new LockedAccountException("帐号待审核");
        }

        if (admin.getState() == 2){
            throw new DisabledAccountException("帐号已禁用");
        }

        if (admin.getState() == 3){
            throw new ExcessiveAttemptsException("帐号已锁定");
        }
        /*.......一些业务逻辑*/

        //这里如果上面的都成立后会进行密码校验,第二个参数是用户数据库的密码
        return new SimpleAuthenticationInfo(admin, admin.getPassword(), getName());
    }
}

会话管理

AdminWebSessionManager

package com.ao.demo.shiro;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

public class AdminWebSessionManager extends DefaultWebSessionManager {

    public static final String LOGIN_TOKEN_KEY = "Shiro-Token";
    private static final String REFERENCED_SESSION_ID_SOURCE = "shiro request";

    /**
     * 获取session id
     * 从请求头中获取jsesssionid
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        // 从请求头中获取token
        String id = WebUtils.toHttp(request).getHeader(LOGIN_TOKEN_KEY);
        // 判断是否有值
        if (!StringUtils.isEmpty(id)) {
            // 设置当前session状态
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            // 若header获取不到token则尝试从cookie中获取
            return super.getSessionId(request, response);
        }
    }
}

controller

AdminAuthController

package com.ao.demo.controller;


import com.ao.demo.pojo.Admin;
import com.ao.demo.utils.JacksonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/admin/auth")
@Slf4j
public class AdminAuthController {

    @PostMapping("/login")
    public Object login(@RequestBody Admin loginAdmin) {
       // 1.SecurityUtils:是shiro的一个工具类。通过SecurityUtils获取Subject
        Subject currentUser = SecurityUtils.getSubject();
        try {
            /*UsernamePasswordToken是一个简单的包含username及password即用户名及密码的登录验证用token*/
            //2、new一个 UsernamePasswordToken,并传上用户名及密码。把返回值传给登入作为条件
            //3、当调用subject的登入方法时,会跳转到reaml认证的方法(doGetAuthenticationInfo)上。
         currentUser.login(new UsernamePasswordToken(loginAdmin.getUsername(), loginAdmin.getPassword()));
        } catch (UnknownAccountException uae) {
            return uae.getMessage();
        } catch (LockedAccountException lae) {
            return lae.getMessage();
        } catch (DisabledAccountException dae) {
            return dae.getMessage();
        }catch (AuthenticationException ae) {
            return "认证失败";
        }
 		//是否有role1这个角色
        if(currentUser.hasRole("role1")){
            log.info("有角色role1");
        }else{
            log.info("没有角色role1");
        }
        //查看是否有查看订单的权限
        if(currentUser.isPermitted("order:list")){
            log.info("拥有查看订单的权限");
        }else {
            log.info("没有查看订单的权限");
        }
        //4、在认证方法中subject已经把获取到了用户,所以我们用subject.getPrincipal 可以获取到登录的用户
        Admin admin = (Admin) SecurityUtils.getSubject().getPrincipal();
        return "登录成功,用户信息:"+admin+"token:" + currentUser.getSession().getId();
    }


    @PostMapping("/tt")
    public String tt(){
        return "访问成功";
    }

}

测试
表的数据

在这里插入图片描述

测试一波

可以看到doGetAuthorizationInfo被执行了两次,这是因为我在controller调用了hasRole和isPermitted。
在这里插入图片描述
登录成功,返回了token(网上说这个token的过期时间是30分钟,具体需要查证源码)
在这里插入图片描述
登录失败,执行到new SimpleAuthenticationInfo(admin, admin.getPassword(), getName())这里,密码错误抛出AuthenticationException,controller捕获到所以返回结果认证失败。
在这里插入图片描述

再测试一下会话管理

登录成功了,此时的token是:81973a5e-6e4c-4d7a-a6a6-7b212eea2b97

然后我们验证一下会话管理起不起作用
在这里插入图片描述

根据返回来的报文,没有登录的用户请求需要登录的页面时自动跳转到登录页面,这就是我们在shiroconfig设置的setLoginUrl,因为这个接口没写,所以404。接下来拿到登录用户的token,进行访问,结果是预期的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YzrQCrZk-1593503405792)(../source/images/springboot%E6%95%B4%E5%90%88shiro/image-20200628202059162.png)]

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值