spring boot 集成 shiro权限控制框架

本文介绍如何使用Spring Boot集成Shiro实现权限管理和用户认证,包括配置文件、配置类、自定义Token等,并提供示例代码及使用步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要是使用spring boot 集成shiro 实现 shiro-spring-boot-starter

1、首先创建相关配置文件有两个 ShiroProperties,JwtProperties

package com.shiro.sdk.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "shiro")
public class ShiroProperties {

    /**
     * 登录路径
     */
    private String loginUrl;

    /**
     * 登出路径
     */
    private String logoutUrl;

    /**
     * 首页路径
     */
    private String indexUrl;

    /**
     * 未授权跳转路径
     */
    private String unauthorizedUrl;

    /**
     * 要忽略的url
     */
    private String ignoresUrlList;

    /**
     * 是否启用ShiroJwt true 启用  false 禁用 ,  默认启用
     */
    private boolean enable = true;

    /**
     * 是否支持跨域访问 默认为 true
     */
    private boolean corsEnable = true;

    /**
     * 是否启用 token 自动刷新
     */
    private boolean tokenRefreshEnable = true;

    /**
     * token放在 http HEADER 里面的 key名称,默认为 Authorization
     */
    private String tokenHeaderKey = "Authorization";



    /**
     * 是否开启缓存 默认缓存组件MemoryConstrainedCacheManager,  默认不开启
     */
    private boolean enableCacheMemory = false;


    public String getLoginUrl() {
        return loginUrl;
    }

    public void setLoginUrl(String loginUrl) {
        this.loginUrl = loginUrl;
    }

    public String getLogoutUrl() {
        return logoutUrl;
    }

    public void setLogoutUrl(String logoutUrl) {
        this.logoutUrl = logoutUrl;
    }

    public String getIgnoresUrlList() {
        return ignoresUrlList;
    }

    public void setIgnoresUrlList(String ignoresUrlList) {
        this.ignoresUrlList = ignoresUrlList;
    }

    public boolean isEnable() {
        return enable;
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }

    public boolean isCorsEnable() {
        return corsEnable;
    }

    public void setCorsEnable(boolean corsEnable) {
        this.corsEnable = corsEnable;
    }

    public boolean isTokenRefreshEnable() {
        return tokenRefreshEnable;
    }

    public void setTokenRefreshEnable(boolean tokenRefreshEnable) {
        this.tokenRefreshEnable = tokenRefreshEnable;
    }

    public String getTokenHeaderKey() {
        return tokenHeaderKey;
    }

    public void setTokenHeaderKey(String tokenHeaderKey) {
        this.tokenHeaderKey = tokenHeaderKey;
    }

    public boolean isEnableCacheMemory() {
        return enableCacheMemory;
    }

    public void setEnableCacheMemory(boolean enableCacheMemory) {
        this.enableCacheMemory = enableCacheMemory;
    }

    public String getIndexUrl() {
        return indexUrl;
    }

    public void setIndexUrl(String indexUrl) {
        this.indexUrl = indexUrl;
    }

    public String getUnauthorizedUrl() {
        return unauthorizedUrl;
    }

    public void setUnauthorizedUrl(String unauthorizedUrl) {
        this.unauthorizedUrl = unauthorizedUrl;
    }
}
package com.shiro.sdk.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "shiro.jwt")
public class JwtProperties {

    /**
     * jwt 的签发者 默认为 trj
     */
    private String jwtIssuer = "trj";

    /**
     * jwt 的主题 默认为 jwtAuthToken
     */
    private String jwtSubject = "jwtAuthToken";


    /**
     * jwt token 过期秒数
     */
    private Integer tokenExpireSeconds;

    /**
     * jwt token 刷新秒数,表示 是否已经发布了 tokenRefreshSeconds 秒,如果超过就刷新一个新的token给前端
     */
    private Integer tokenRefreshSeconds;

    /**
     * 签名秘钥
     */
    private String secretKey;

    public String getJwtIssuer() {
        return jwtIssuer;
    }

    public void setJwtIssuer(String jwtIssuer) {
        this.jwtIssuer = jwtIssuer;
    }

    public String getJwtSubject() {
        return jwtSubject;
    }

    public void setJwtSubject(String jwtSubject) {
        this.jwtSubject = jwtSubject;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    public Integer getTokenExpireSeconds() {
        return tokenExpireSeconds;
    }

    public void setTokenExpireSeconds(Integer tokenExpireSeconds) {
        this.tokenExpireSeconds = tokenExpireSeconds;
    }

    public Integer getTokenRefreshSeconds() {
        return tokenRefreshSeconds;
    }

    public void setTokenRefreshSeconds(Integer tokenRefreshSeconds) {
        this.tokenRefreshSeconds = tokenRefreshSeconds;
    }
}

2、创建shiro配置类 ShiroJwtAutoConfig

package com.shiro.sdk.config;

import com.shiro.sdk.filter.JwtAuthorizationFilter;
import com.shiro.sdk.properties.JwtProperties;
import com.shiro.sdk.properties.ShiroProperties;
import com.shiro.sdk.realm.JwtAuthorizingRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Configuration
@EnableConfigurationProperties({JwtProperties.class, ShiroProperties.class})
@ConditionalOnProperty(prefix = "shiro", value = "enable", havingValue = "true", matchIfMissing = true)
public class ShiroJwtAutoConfig implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Resource
    private ShiroProperties shiroProperties;

    @Resource
    private JwtProperties jwtProperties;

    /*private JwtAuthorizingRealm jwtAuthorizingRealm;

    @Bean
    @ConditionalOnMissingBean
    public JwtAuthorizingRealm jwtAuthorizingRealm() {
        log.debug("Shiro Bean JwtAuthorizingRealm 初始化 ...");

        Map<String, JwtAuthorizingRealm> beans = applicationContext.getBeansOfType(JwtAuthorizingRealm.class);

        if (beans == null || beans.size() == 0) {
           throw new RuntimeException("请继承 JwtAuthorizingRealm 并实现它的方法");
        }
        for(JwtAuthorizingRealm jwtAuthorizingRealm : beans.values()){
            jwtAuthorizingRealm = jwtAuthorizingRealm;
            break;
        }

        jwtAuthorizingRealm.setCredentialsMatcher(new JwtCredentialsMatcher(jwtProperties));

        if (shiroProperties.isEnableCacheMemory()) {

            // 使用默认提供的 MemoryConstrainedCacheManager
            jwtAuthorizingRealm.setCacheManager(new MemoryConstrainedCacheManager());

            // 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
            jwtAuthorizingRealm.setAuthenticationCachingEnabled(true);

            // 启用授权缓存,即缓存AuthorizationInfo信息,默认false,一旦配置了缓存管理器,授权缓存默认开启
            jwtAuthorizingRealm.setAuthorizationCachingEnabled(true);
        }

        return jwtAuthorizingRealm;
    }*/

    @Bean
    @ConditionalOnMissingBean
    public DefaultWebSecurityManager securityManager() {
        log.info("Shiro Bean SecurityManager 初始化 ...");

        // 交由DefaultWebSecurityManager管理
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();

        Map<String, JwtAuthorizingRealm> beans = applicationContext.getBeansOfType(JwtAuthorizingRealm.class);

        manager.setRealms(new ArrayList<>(beans.values()));

        /*
         * 使用JWT方式进行访问控制,因此关闭 shiro 自带的 session管理,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        // 禁用remberMe功能
        manager.setRememberMeManager(null);

        return manager;
    }

    @Bean
    @ConditionalOnMissingBean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        log.info("Shiro Bean ShiroFilterFactoryBean 初始化 ...");

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl());

        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl(shiroProperties.getIndexUrl());

        // 未授权跳转页面
        shiroFilterFactoryBean.setUnauthorizedUrl(shiroProperties.getUnauthorizedUrl());

        //配置需要忽略的路径
        LinkedHashMap<String, String> ignoresUrlMap = getIgnoresUrlMap(shiroProperties.getIgnoresUrlList());
        shiroFilterFactoryBean.setFilterChainDefinitionMap(ignoresUrlMap);

        //配置相关过滤器
        LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>(3);
        filterMap.put("jwtAuthorizationFilter", new JwtAuthorizationFilter(shiroProperties));
        /*filterMap.put("jwtPermissionFilter", new JwtPermissionFilter());
        filterMap.put("jwtRoleFilter", new JwtRoleFilter());*/
        shiroFilterFactoryBean.setFilters(filterMap);
        Map<String, String> filterRuleMap = new HashMap<>();


        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwtAuthorizationFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }

    private LinkedHashMap<String, String> getIgnoresUrlMap(String ignoresUrlList) {
        if(StringUtils.isBlank(ignoresUrlList)){
            return new LinkedHashMap();
        }
        String[] urls = StringUtils.split(ignoresUrlList, ",");
        if(urls == null || urls.length <= 0){
           return new LinkedHashMap();
        }

        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(urls.length);
        Arrays.stream(urls).map(url -> { map.put(url, "anon");return map; }).collect(Collectors.toList());
        return map;
    }

    /**
     * 开启shiro注解支持,例如 @RequiresPermissions
     * @param securityManager DefaultWebSecurityManager
     * @return AuthorizationAttributeSourceAdvisor
     */
    @Bean
    @ConditionalOnMissingBean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

3、JwtAuthenticationToken implements AuthenticationToken 

Object getPrincipal() 与Object getCredentials() 在这里不实现,交由业务方去实现
package com.shiro.sdk.token;

import org.apache.shiro.authc.AuthenticationToken;

public class JwtAuthenticationToken implements AuthenticationToken {

    private String JwtAuthenticationToken;

    public JwtAuthenticationToken(String JwtAuthenticationToken){
        this.JwtAuthenticationToken = JwtAuthenticationToken;
    }

    @Override
    public Object getPrincipal() {
        return JwtAuthenticationToken;
    }

    @Override
    public Object getCredentials() {
        return JwtAuthenticationToken;
    }
}

4、JwtAuthorizingRealm extends AuthorizingRealm

package com.shiro.sdk.realm;

import com.shiro.sdk.token.JwtAuthenticationToken;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.realm.AuthorizingRealm;


public abstract class JwtAuthorizingRealm extends AuthorizingRealm{

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtAuthenticationToken;
    }
}

5、在项目resources下建 META-INF 文件夹并建 spring.factories 文件,里面加入如下内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.shiro.sdk.config.ShiroJwtAutoConfig

6、打包生成 相关jar

7、业务方使用时的步骤:

因为shiro是一个集合了认证授权的框架,因此既可以用来做登陆认证,也可以用来做用户授权,本次已经将shiro结合spring boot 做成了一个可配置的sdk,方便以后其他项目的使用。

使用时只需配置4步即可完成授权认证的相关功能



1、pom.xml 文件引入相关sdk



<dependency>
<groupId>com.shiro.authorization</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.0.1-SNAPSHOT</version>
</dependency>


2、用户登陆时调用shiro的用户认证api,用例如下


@RequestMapping(value = "login", method = RequestMethod.POST)
public Response<String> login(String username, String password){
    String token = new JwtUtil(jwtProperties).sign(username, password);
    JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token);
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(jwtAuthenticationToken);
        return new Response<String>().success(token);
    } catch (Exception e) {
        e.printStackTrace();
        log.error("登录失败,用户名[{}]", username, e);
        return new Response<String>().fail("登录失败", e.getMessage());
    }
}

具体与shiro有交互的代码有四行

//使用jwtUtil生成一个token(这里可以是jwt也可以是其他的)
String token = new JwtUtil(jwtProperties).sign(username, password);

//将生成的token封装为一个JwtAuthenticationToken参数供shiro在认证时使用
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token);

//调用 SecurityUtils 获取一个subject(可以理解为与shiro交互的api接口类,与我们的service类似)
Subject subject = SecurityUtils.getSubject();

//调用 subject.login;进行认证

subject.login(jwtAuthenticationToken)


3、具体的用户认证实现与相关权限实现


/**
 * Shiro-权限相关的业务处理
 */
@Slf4j
@Service
public class ShiroServiceImpl extends JwtAuthorizingRealm implements ShiroService {

    @Resource
    private JwtProperties jwtProperties;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        if (log.isDebugEnabled()) {
            log.debug("进入授权 doGetAuthorizationInfo");
            log.debug("the toke is  {}", principals.toString());
            log.debug("realmName = {}", getName());
        }

        // 该用户具有哪些权限,这里需要从数据库查数据,为了测试方便造了条数据
        String username = new JwtUtil(jwtProperties).getUsername(principals.toString());
        //HashSet<String> permissionsSet = permissionService.get(username)

        HashSet<String> permissionsSet = new HashSet();
        permissionsSet.add("edit");
        authorizationInfo.addStringPermissions(permissionsSet);

        // 该用户具有哪些角色,这里需要从数据库查数据,为了测试方便造了条数据
        //HashSet<String> roleSet = roleService.get(username)
        HashSet<String> roleSet = new HashSet();
        roleSet.add("admin");
        authorizationInfo.addRoles(roleSet);

        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = new JwtUtil(jwtProperties).getUsername(token);

        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        //这里需要从数据库查出用户,为了测试方便所以造了条数据
        //User userBean = userService.getUser(username);
        User userBean = new User();
        userBean.setUserName("zhangsan");
        userBean.setPassword("123456");
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }
        boolean checkTokenResult = false;
        try {
            checkTokenResult = new JwtUtil(jwtProperties).verify(token, username, userBean.getPassword());
        }catch (TokenExpiredException e){
            throw new UnauthenticatedException("token 已失效");
        }catch (Exception e){
            throw e;
        }
        if (! checkTokenResult) {
            throw new AuthenticationException("Username or password error");
        }
        return new SimpleAuthenticationInfo(token, token, "jwtAuthorizingRealm");
    }
}

首先需要定义一个bean 实现 JwtAuthorizingRealm 这个类中的 doGetAuthorizationInfo,doGetAuthenticationInfo 这两个方法

doGetAuthorizationInfo 这个方法是用来加载当前用户所拥有的权限(通常是需要根据当前用户从数据库中查出来)

doGetAuthenticationInfo 这个方法是用来做具体的用户登录认证

最后需要将 定义好的bean加入spring容器,这里使用的是 spring @Service 注解,其他只要能加载到spring中的方式也都可以


4、业务方法中如果有相关接口需要做权限控制,直接使用shiro相关权限控制注解,用例如下


@Slf4j
@RestController
@RequestMapping("/")
public class IndexController {
    @Resource
    private JwtProperties jwtProperties;

    @RequestMapping(value = "login", method = RequestMethod.POST)
    public Response<String> login(String username, String password){
        String token = new JwtUtil(jwtProperties).sign(username, password);
        JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(jwtAuthenticationToken);
            return new Response<String>().success(token);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("登录失败,用户名[{}]", username, e);
            return new Response<String>().fail("登录失败", e.getMessage());
        }
    }

    /**
     * 无需做权限控制的接口
     * */
    @RequestMapping(value = "anon", method = RequestMethod.POST)
    public Response<String> anon(){
        return new Response<String>().success(null);
    }

    /**
     * 需要登录才能访问的接口
     * */
    @RequiresAuthentication
    @RequestMapping(value = "/require", method = RequestMethod.POST)
    public Response<String> require(){
        return new Response<String>().success(null);
    }

    /**
     * 需要相关角色才能访问的接口
     * */
    @RequiresRoles(value = {"user"})
    @RequestMapping(value = "/role", method = RequestMethod.POST)
    public Response<String> role(){
        return new Response<String>().success(null);
    }

    /**
     * 需要相关权限才能访问的接口
     * */
    @RequiresPermissions(value = {"edit"})
    @RequestMapping(value = "/permission", method = RequestMethod.POST)
    public Response<String> permission(){
        return new Response<String>().success(null);
    }
}


@RequiresAuthentication 此注解表示此接口只有登录用户才能访问

@RequiresRoles(value = {"user"}) 此注解表示此接口只有拥有user角色的用户才能访问 value是一个数组说明可以设置多个角色,他们之间默认是且的关系

@RequiresPermissions(value = {"edit"}) 此注解表示只有拥有edit权限的用户才能访问 value是一个数组说明可以设置多个权限,他们之间默认是且的关系

shiro-spring-boot-starter 已提交到码云

shiro-spring-boot-test  已提交到码云

有问题可加微信

补充一句,加微信别老是您您您的,都是打工人,不必这么客气,我也才18啊哈哈

请大家关注下博客谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值