SpringBoot从零整合轻量安全框架Shiro
本文最后更新于 23 天前,如有失效请评论区留言。

简介

Apache Shiro是一个简单、灵活且强大的Java安全框架,用于提供身份验证、授权、加密和会话管理等安全功能。它的设计理念是无侵入性,使得它可以轻松地与任何Java应用程序集成。Shiro提供了一系列内建的安全组件,如Subject(表示当前用户)、SecurityManager(管理所有安全操作)、Realm(与安全数据交互)等。通过这些组件,开发者可以快速实现用户身份验证、授权和会话管理。Shiro还提供了灵活的插件机制和可扩展的API,支持自定义功能和数据源集成。总体而言,Apache Shiro为Java开发者提供了一个全面且易于使用的安全解决方案,适用于各种应用场景,从Web应用程序到企业级应用,都能轻松地添加安全保护。

框架对比

Shiro(Apache Shiro)和Spring Security都是流行的安全框架,它们都提供了一系列的安全功能来帮助开发者保护应用程序。选择使用哪一个框架通常取决于项目的需求和开发者的偏好。以下是Shiro和Spring Security之间的一些主要比较点:

Apache Shiro

  1. 简单性和灵活性:Shiro设计的目标之一是提供一个简单、直观且灵活的安全框架。它的API设计是直观的,使得开发者可以快速地集成和使用。
  2. 无侵入性:Shiro可以轻松地与任何Java应用程序集成,而不需要大量的配置。这使得它在微服务和其他轻量级应用程序中非常受欢迎。
  3. 内建功能:Shiro提供了许多内建的安全功能,如身份验证、授权、加密和会话管理等。
  4. 社区支持:虽然相对于Spring Security来说,Shiro的社区规模可能较小,但它仍然有一个活跃的社区,提供各种教程、文档和支持。

Spring Security

  1. 深度整合:Spring Security是Spring框架的一部分,因此它与Spring生态系统的其他组件(如Spring Boot、Spring MVC等)有深度的整合,提供了更加一致和完善的体验。
  2. 强大的功能:Spring Security提供了一套全面的安全功能,包括基于角色的授权、方法级别的安全、OAuth支持、单点登录(SSO)等。
  3. 大型社区和广泛支持:由于Spring的流行性,Spring Security有一个非常庞大和活跃的社区。这意味着你可以轻松地找到大量的资源、教程和第三方库。
  4. 复杂性和学习曲线:尽管Spring Security功能强大,但它的配置和使用可能比Shiro复杂一些,特别是对于新手来说。

总结

Shiro 适合那些需要快速集成和简单使用的项目,特别是轻量级应用或者希望避免复杂配置的项目。

Spring Security 适合大型、复杂的企业级应用,特别是那些已经使用或计划使用Spring框架的项目,因为它提供了深度的整合和更多的高级功能。

因此小鹿认为:对于初学者或者一些个人开发的项目来说,Shiro框架提供的安全性完全是足够用的。

开始整合

首先在项目中引入依赖

image-20240424172542704

这里需要注意的是:Shiro框架所需要的依赖原则上只有第一个,第二个依赖只是因为小鹿所写的代码恰好需要这个依赖而已。如果你接下来需要跟着小鹿一步一步实现的话,小鹿建议你和我一样将该依赖进行导入

除此以外还需要引入redis和JPA,这两者的作用主要是结合数据库查询用户和Token存储,另外所需要用到的业务插件我在这里也会提到。

<!--JPA-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--JDBC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--mysql-connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
            <scope>runtime</scope>
        </dependency>
        <!-- druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

        <!-- commons-lang -->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

                <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

JPA(Java Persistence API)和MyBatis都是Java中常用的持久化框架,但它们有着不同的设计理念和应用场景。JPA是Java的标准持久化API,提供了强大的对象关系映射(ORM)支持。它能够将Java对象映射到数据库表,自动生成查询语句,简化了数据持久化的操作。这为开发者提供了一个统一的接口和规范,有助于提高代码的可维护性和可读性。然而,由于其自动化特性,可能会产生效率不高的查询语句,需要手动进行优化。同时,JPA的学习曲线相对陡峭,对于初学者可能需要一些时间来掌握。

MyBatis提供了更灵活的SQL编写方式,允许开发者直接编写SQL语句,更容易进行性能优化,满足复杂的业务需求。它的配置相对简单,上手快速,特别适合熟悉SQL的开发者。然而,由于需要手动编写SQL,对于简单的CRUD操作可能会显得繁琐。此外,相比于JPA,MyBatis提供的ORM支持较弱,开发者可能需要手动管理对象和数据库之间的映射。

小鹿认为:选择JPA还是MyBatis取决于项目需求和开发团队的经验。JPA适用于快速开发和维护的项目,尤其是对于不熟悉SQL的开发者。而MyBatis更适用于需要高度优化和灵活性的项目,特别是对于需要复杂SQL查询和定制化操作的场景。

既然说了是搭建轻量安全框架,小鹿在这里就使用JPA来进行数据库操作,大家其实也可以使用Mybatis持久层框架,区别仅仅只是操作数据库的方式有点不同而已,并不影响框架的搭建。

需要注意的一点是:

image-20240425091746758

解决方案:提高依赖版本。实测可用

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>10.1.20</version>
</dependency>

image-20240425093351115

创建config

在引入了必要的依赖之后,就开始对ShiroConfig来进行必要的配置。准备好了吗?小鹿的朋友们!

11_8517031

package com.process.config;

import com.process.shiro.auth.AuthFilter;
import com.process.shiro.auth.AuthRealm;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author xiayexiaolu
 * @date 2024/4/24 17:01
 */
@Configuration
public class ShiroConfig {

    //做一个记住我选项

    /**
     * cookie对象;
     * rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie(){
        //System.out.println("ShiroConfiguration.rememberMeCookie()");
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");

        //setcookie()的第七个参数
        //设为true后,只能通过http访问,javascript无法访问
        //防止xss读取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * cookie管理对象;
     * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        //System.out.println("ShiroConfiguration.rememberMeManager()");
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAuuw=="));
        return cookieRememberMeManager;
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(AuthRealm authRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm);
        //用户授权/认证信息Cache, 采用EhCache缓存
        //securityManager.setCacheManager(getEhCacheManager());
        //注入记住我管理器
        securityManager.setRememberMeManager(rememberMeManager());
        //securityManager.setRememberMeManager(null);
        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //auth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("auth", new AuthFilter());
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = new LinkedHashMap<>();
        // anno匿名访问  auth验证
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/sys/login", "anon");      //登陆接口放开
        filterMap.put("/sys/register","anon");        //注册接口放开
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/doc.html", "anon");
        filterMap.put("/static/**","anon");          //静态资源访问打开
        filterMap.put("/file/**","anon");
        // 除了以上路径,其他都需要权限验证
        filterMap.put("/**", "auth");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     *
     * @param securityManager
     * @return
     * 开启shiro aop注解支持
     * 使用代理方式,所以需要代码支持
     */

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

}

这里值得注意的是,小鹿在这里实现了记住我功能,但是在实际的简单项目中,你可以选择忽略该功能,只给定Token过期时间即可。这里需要重点讲一下Shiro框架最重要的安全配置,也就是过滤规则:

image-20240425140146246

通常来说,正常的过滤规则你和小鹿保持一致即可,我在这里重点放开了登录和注册接口的权限检查,当然,你可以根据你的实际业务来规定你的权限验证规则。

第二个重点内容:authorizationAttributeSourceAdvisor方法的实现

在实际项目中,如果想要对特别的方法或者特别的接口开启权限校验,通过实现上面的方法创建一个AuthorizationAttributeSourceAdvisor实例,并将DefaultWebSecurityManager设置为其安全管理器。这样,就为应用程序启用了基于注解的权限控制支持。当你在应用程序中使用Shiro的注解(如@RequiresPermissions@RequiresRoles等)时,AuthorizationAttributeSourceAdvisor会自动进行权限检查,确保用户具有执行相应操作所需的权限或角色。通常小鹿会针对接口来进行权限的区分,例如在controller层通过@RequiresRoles(Base.ROLE_ADMIN)注解来标明,这个接口需要管理员权限才能调用。

所需要的相关类

1.AuthFilter

AuthFilter 是一个自定义的过滤器,继承自 Shiro 的 AuthenticatingFilter,它在 Shiro 的安全流程中扮演了一个关键的角色。AuthFilter 主要负责用户身份验证的逻辑,并在需要时进行拦截和处理。

package com.process.shiro.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.process.common.utils.HttpContextUtils;
import com.process.common.utils.TokenUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class AuthFilter extends AuthenticatingFilter {

    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 生成自定义token
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);

        return new AuthToken(token);
    }

    /**
     * 步骤1.所有请求全部拒绝访问
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }

    /**
     * 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
            httpResponse.setCharacterEncoding("UTF-8");
            Map<String, Object> result = new HashMap<>();
            result.put("status", 403);
            result.put("msg", "请先登录");
            String json = MAPPER.writeValueAsString(result);
            httpResponse.getWriter().print(json);
            return false;
        }
        return executeLogin(request, response);
    }

    /**
     * token失效时候调用
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Map<String, Object> result = new HashMap<>();
            result.put("status", 403);
            result.put("msg", "登录凭证已失效,请重新登录");
            String json = MAPPER.writeValueAsString(result);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }

}

2.AuthRealm

AuthRealm是Shiro中非常关键的组件之一。在Shiro中,Realm负责连接数据源,并负责获取身份验证和授权所需的信息。Realm是Shiro的核心安全组件,它提供了与应用程序的数据交互的桥梁,如数据库、LDAP、文件系统等。

package com.process.shiro.auth;

import com.process.common.constant.Base;
import com.process.shiro.dao.redis.IRedisService;
import com.process.shiro.entity.SysUserEntity;
import com.process.shiro.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Autowired
    IRedisService iRedisService;
    @Override
    /**
     * 授权 获取用户的角色和权限
     *@param  [principals]
     *@return org.apache.shiro.authz.AuthorizationInfo
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1. 从 PrincipalCollection 中来获取登录用户的信息
        SysUserEntity sysUserEntity = (SysUserEntity) principals.getPrimaryPrincipal();
        //2.添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        if(sysUserEntity.getAdmin()){
            simpleAuthorizationInfo.addRole(Base.ROLE_ADMIN);
        }else{
            simpleAuthorizationInfo.addRole(Base.CURRENT_USER);
        }
        return simpleAuthorizationInfo;
    }

    @Override
    /**
     * 认证 判断token的有效性
     *@param  [token]
     *@return org.apache.shiro.authc.AuthenticationInfo
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //获取token,既前端传入的token
        String accessToken = (String) token.getPrincipal();
        //1. 根据accessToken,查询用户信息
        //SysToken tokenEntity = shiroService.findByToken(accessToken);
        String  account = (String)iRedisService.get(accessToken);
        //2. token失效
//        if (tokenEntity == null || tokenEntity.getExpireTime().isBefore(LocalDateTime.now())) {
//            throw new IncorrectCredentialsException("token失效,请重新登录");
//        }
        if(account == null){
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }
        //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
        SysUserEntity sysUserEntity = userService.findByAccount(account);
        //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
        if (sysUserEntity == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        //5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(sysUserEntity, accessToken, this.getName());
        return info;
    }
}

其实到这里,Shiro所需要的关键文件就已经全部实现了,Shiro就是一个首先根据你的请求并且结合过滤规则拦截需要验证权限的请求,然后通过AuthFilter来执行登录的拦截操作。而AuthRealm是用来赋予角色的重要一环,便于后期接口可以通过角色来灵活使用,并且AuthRealm可以配置缓存管理器,以提高性能并减少对数据源的访问,很显然,在上面的代码中,小鹿就使用了redis。

让小鹿带大家总结一下吧:

Shiro的工作流程

  1. 拦截请求:Shiro首先会根据配置的过滤器规则拦截所有请求,识别哪些请求需要进行身份验证和授权。
  2. 执行身份验证:对于需要身份验证的请求,AuthFilter将负责执行身份验证逻辑,如获取并验证令牌、处理登录等。
  3. 角色和权限授权:在身份验证成功后,AuthRealm会负责授予用户角色和权限。这是Shiro授权过程的关键部分,它决定了用户可以执行哪些操作。

AuthFilter的作用

  1. 生成自定义TokenAuthFilter创建一个自定义的 AuthenticationToken,通常包含从请求中提取的令牌信息。
  2. 请求访问控制:它检查请求的HTTP方法,并根据需要进行访问控制。
  3. 处理访问拒绝:如果请求未通过访问控制或身份验证失败,AuthFilter将拦截请求并返回适当的错误响应。
  4. 执行登录:在身份验证通过后,AuthFilter会执行登录操作,允许用户访问受保护的资源。

AuthRealm的作用

  1. 身份验证AuthRealm负责验证用户的身份,从数据源中获取用户的凭证信息,并与输入的凭证进行比对。
  2. 角色和权限授权:验证成功后,AuthRealm获取用户的角色和权限信息,并将其授予用户。这些角色和权限信息用于后续的授权检查,决定用户是否有权访问某个资源。
  3. 缓存支持AuthRealm可以配置缓存管理器,以提高性能并减少对数据源的访问。

其实在这个实现中,还有很多很多需要注意的点,之后小鹿会带大家对于Shiro代码进行详细解释,其实吧,小鹿对于这个框架的了解也只是源于大学时期做的一个毕业设计,后来在工作之后开始对于这个框架渐渐的学习。当然,小鹿在这里也会给出完整的源代码,大家如果想要快速开发一些后台业务,也只需要创建对应业务即可,避免搭框架以及写权限的重复造轮子。

Gitee代码地址:https://gitee.com/xiayexiaolu/shiro-demo.git

希望大家能给小鹿点一个小小的Star呀~

0_b70e9626238df6b822c474c02ffbe9aa

版权声明:除特殊说明,博客文章均为夏夜小鹿原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。 由于可能会成为AI模型(如ChatGPT)的训练样本,本博客禁止将AI自动生成内容作为文章上传(特别声明时除外)。如果您有什么想对小鹿说的,可以到留言板 进行留言
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇