51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Shiro关闭session,无状态接入Springboot

# Shiro关闭session配置 {#shiro关闭session配置}

Apache_Shiro_logo.svg

# 前言 {#前言}

本文基于token进行身份认证,由于接入cas会和shiro的session管理冲突,所以关闭shiro的session,进行无状态管理。

特此记录一下shiro如何进行无状态管理。

# 一、引入依赖 {#一、引入依赖}

此处引入的为 shiro-spring ,版本为 1.7.1

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

# 二、实现DefaultWebSubjectFactory {#二、实现defaultwebsubjectfactory}

实现 DefaultWebSubjectFactory 关闭session

package com.hcframe.base.module.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

/**
 * @author lhc
 * @version 1.0
 * @className StatelessDefaultSubjectFactory
 * @date 2021年04月19日 1:54 下午
 * @description 描述
 */
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        //不创建session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

# 三、实现AuthenticationToken {#三、实现authenticationtoken}

此处是为了将用户信息改为token传递,通过token方式进行验证

package com.hcframe.base.module.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author lhc
 * @version 1.0
 * @className AuthToken
 * @date 2021年04月19日 2:56 下午
 * @description 实现shiro AuthenticationToken
 */
public class AuthToken implements AuthenticationToken {

    private String token;

    public AuthToken(String token) {
        this.token = token;
    }

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

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

# 四、实现shiro的过滤器 {#四、实现shiro的过滤器}

此处为权限过滤器,具体内容参见注释

package com.hcframe.base.module.shiro;

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
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;

/**
 * @author lhc
 * @version 1.0
 * @className AuthFilter
 * @date 2021年04月19日 2:56 下午
 * @description 实现shiro 过滤器
 */
public class AuthFilter extends AuthenticatingFilter {

    /**
     * @author lhc
     * @description 创建token
     * @date 4:35 下午 2021/4/26
     * @params [request, response]
     * @return org.apache.shiro.authc.AuthenticationToken
     **/
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            // 增加跨域支持
            String myOrigin = httpServletRequest.getHeader("origin");
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with, X-Access-Token, datasource-Key");
            httpResponse.setHeader("Access-Control-Allow-Origin", myOrigin);
            httpResponse.setCharacterEncoding("UTF-8");
            // 返回错误状态信息
            Map<String, Object> result = new HashMap<>();
            result.put("code", 3);
            result.put("msg", "未登陆");
            String json = JSON.toJSONString(result);
            httpResponse.getWriter().print(json);
            return null;
        }
        return new AuthToken(token);
    }

    /**
     * @author lhc
     * @description 步骤1.所有请求全部拒绝访问
     * @date 4:37 下午 2021/4/26
     * @params [request, response, mappedValue]
     * @return boolean
     **/
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
    }

    /**
     * @author lhc
     * @description 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
     * @date 4:37 下午 2021/4/26
     * @params [request, response]
     * @return boolean
     **/
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            // 增加跨域支持
            String myOrigin = httpServletRequest.getHeader("origin");
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with, X-Access-Token, datasource-Key");
            httpResponse.setHeader("Access-Control-Allow-Origin", myOrigin);
            httpResponse.setCharacterEncoding("UTF-8");
            // 返回错误状态信息
            Map<String, Object> result = new HashMap<>();
            result.put("code", 3);
            result.put("msg", "未登陆");
            String json = JSON.toJSONString(result);
            httpResponse.getWriter().print(json);
            return false;
        }
        return executeLogin(request, response);
    }

    /**
     * @author lhc
     * @description 登陆失败时候调用
     * @date 4:38 下午 2021/4/26
     * @params [token, e, request, response]
     * @return boolean
     **/
    @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");
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String myOrigin = httpServletRequest.getHeader("origin");
        httpResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with, X-Access-Token, datasource-Key");
        httpResponse.setHeader("Access-Control-Allow-Origin", myOrigin);
        httpResponse.setCharacterEncoding("UTF-8");
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Map<String, Object> result = new HashMap<>();
            result.put("code", 3);
            result.put("msg", "未登陆");
            String json = JSON.toJSONString(result);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }

    /**
     * @author lhc
     * @description 获取请求的token
     * @date 4:38 下午 2021/4/26
     * @params [httpRequest]
     * @return java.lang.String
     **/
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        String token = httpRequest.getHeader("X-Access-Token");
        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
            if (StringUtils.isBlank(token)) {
                token = httpRequest.getParameter("token");
            }
        }
        return token;
    }
}

# 五、编写自定义的Realm {#五、编写自定义的realm}

编写自定义realm,此步骤是为了定义权限校验和用户信息验证。

package com.hcframe.base.module.shiro;


import com.hcframe.base.common.config.FrameConfig;
import com.hcframe.base.module.shiro.service.ShiroService;
import com.hcframe.base.module.shiro.service.SystemRealm;
import com.hcframe.redis.RedisUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.AssertionImpl;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Resource;
import java.util.Date;
import java.util.Map;


/**
 * @author lhc
 * @version 1.0
 * @className CustomRealm
 * @date 2021年04月19日 2:56 下午
 * @description 自定义Realm
 */
public class CustomRealm extends AuthorizingRealm {

    @Resource
    private RedisUtil redisUtil;

    @Autowired
    AuthService authService;

    @Resource
    private SystemRealm systemRealm;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        Object user = principalCollection.getPrimaryPrincipal();
        Map<String, Object> map = (Map<String, Object>) user;
        // 从数据库读取权限,注入到shiro中
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        Set<String> set = authService.getUserAuth(String.valueOf(map.get("ID")));
        for (String auth : set) {
            simpleAuthorizationInfo.addStringPermission(auth);
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        String accessToken = (String) token.getPrincipal();
        //1. 根据accessToken,查询用户信息
        FtToken tokenEntity = shiroService.findByToken(accessToken);
        userId = tokenEntity.getUserId();
        //2. token失效
        if (tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }
        
        //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
        Object user = shiroService.findByUserId(userId);
        //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        //5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
        return new SimpleAuthenticationInfo(user, accessToken, this.getName());
    }

    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof AuthToken;
    }
}

# 六、编写Shiro配置类 {#六、编写shiro配置类}

编写shiro配置类,将bean交给Spring管理

package com.hcframe.base.module.shiro;

import com.hcframe.base.module.shiro.service.SystemRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionManager;
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.apache.shiro.web.mgt.DefaultWebSubjectFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {

    /**
     * 不加这个注解不生效,具体不详
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * 将自己的验证方式加入容器
     */
    @Bean
    public CustomRealm myShiroRealm() {
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCachingEnabled(false);
        return customRealm;
    }

    /**
     * @return org.apache.shiro.web.mgt.DefaultWebSubjectFactory
     * @author lhc
     * @description // 自定义subject工厂
     * @date 4:50 下午 2021/4/19
     * @params []
     **/
    @Bean
    public DefaultWebSubjectFactory subjectFactory() {
        return new StatelessDefaultSubjectFactory();
    }

    /**
     * @return org.apache.shiro.session.mgt.SessionManager
     * @author lhc
     * @description // 自定义session管理器
     * @date 5:50 下午 2021/4/19
     * @params []
     **/
    @Bean
    public SessionManager sessionManager() {
        DefaultSessionManager shiroSessionManager = new DefaultSessionManager();
        // 关闭session校验轮询
        shiroSessionManager.setSessionValidationSchedulerEnabled(false);
        return shiroSessionManager;
    }

    /**
     * 权限管理,配置主要是Realm的管理认证
     */
    @Bean("securityManager")
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 禁用session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        // 设置自定义subject工厂
        securityManager.setSubjectFactory(subjectFactory());
        // 设置自定义session管理器
        securityManager.setSessionManager(sessionManager());
        // 设置自定义realm
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }


    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, Filter> filters = new HashMap<>(1);
        // 设置自定义过滤器
        filters.put("auth", new AuthFilter());
        shiroFilterFactoryBean.setFilters(filters);
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        // 用户登陆
        map.put("/ftUser/login", "anon");
        // Vue静态资源
        map.put("/img/**", "anon");
        map.put("/static/**", "anon");
        map.put("/tinymce/**", "anon");
        map.put("/favicon.ico", "anon");
        map.put("/manifest.json", "anon");
        map.put("/robots.txt", "anon");
        map.put("/precache*", "anon");
        map.put("/service-worker.js", "anon");
        // swagger UI 静态资源
        map.put("/swagger-ui.html","anon");
        map.put("/doc.html","anon");
        map.put("/swagger-resources/**","anon");
        map.put("/webjars/**","anon");
        map.put("/v2/api-docs","anon");
        map.put("/v2/api-docs-ext","anon");
        map.put("/swagger/**","anon");
        // druid 资源路径
        map.put("/druid/**","anon");
        // cas 接口
        map.put("/cas/valid","anon");
        map.put("/cas/logout","anon");
        // 其余路径均拦截
        map.put("/**", "auth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 加入注解的使用,不加入这个注解不生效
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

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

# 七、添加权限注解 {#七、添加权限注解}

此处只展示权限注解,其余注解请查询官方文档
注意

添加权限的注解必须被自定义拦截器拦截

否则会出现不调用自定义 CustomRealm中的doGetAuthorizationInf()方法的情况

代码示例:

    @GetMapping("/system/list")
    @RequiresPermissions(value = { "systemManage","system:list" },logical = Logical.OR)
    public ResultVO<Integer> resetPassword(String userId,@PathVariable Integer version) {
        return manageService.resetPassword(userId,version);
    }
  1. value :字符串数组,填写之前realm中权限注入的字符串即可
  2. logical:逻辑关系,数组中权限的逻辑关系,分为AND和OR两种,默认为AND
赞(1)
未经允许不得转载:工具盒子 » Shiro关闭session,无状态接入Springboot