shiro前后端分离配置写法,返回json数据格式,不跳转页面

前段时间公司需求,写了一个RABC模式的权限管理,因为项目是前后端分离的,shiro默认是不支持前后端分离的写法的,它是跳转到配置的页面,看了很多人的写法都是比较复杂的,我特地的简化了,写了一个比较好用的前后端分离的写法。

简介

项目:springboot 项目 + mybatis-plus + redis
shrio相关的jar:
pom文件:

 <!-- https://mvnrepository.com/artifact/org.crazycake/shiro-redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.0</version>
        </dependency>

(一)shiro的配置(ShiroConfig)


import com.xxx.shiro.MySessionManager;
import com.xxx.shiro.MyShiroRealm;
import com.xxx.shiro.RedisSessionDao;
import org.apache.shiro.mgt.SecurityManager;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {

    private long expireTime = 3600;

    /**
     * 配置核心安全管理器
     *
     * @return
     */
    @Bean
    public SecurityManager securityManager(MyShiroRealm customizeRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customizeRealm);
        // 取消Cookie中的RememberMe参数
        securityManager.setRememberMeManager(null);
        // 配置自定义Session管理器
        securityManager.setSessionManager(mySessionManager());
        return securityManager;
    }


    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登录,无权限是跳转的路径
//        shiroFilterFactoryBean.setLoginUrl("/");
        // 登录成功后跳转的路径
//        shiroFilterFactoryBean.setSuccessUrl("/");
        // 错误页面,认证不通过跳转
//        shiroFilterFactoryBean.setUnauthorizedUrl("/");
        // 配置拦截规则
        Map<String, String> filterChainMap = new LinkedHashMap<>();

        // 登录页面和登录请求路径需要放行
        filterChainMap.put("/admin/login", "anon");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        return shiroFilterFactoryBean;
    }


    //注入权限管理
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 自定义Session管理器
     */
    @Bean
    public MySessionManager mySessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        // 配置自定义SessionDao
        mySessionManager.setSessionDAO(redisSessionDao());
        mySessionManager.setGlobalSessionTimeout(expireTime * 1000);
        return mySessionManager;
    }

    @Bean
    public RedisSessionDao redisSessionDao() {
        return new RedisSessionDao(expireTime);
    }
}

  1. 这个类是shiro的主配置类,我添加了自定义Session管理器,用户登录的时候,会自动把用户的sessionId存到redis里面,在授权的时候会用到。
  2. 在 shiroFilter的这个方法里面,这几个路径的跳转不需要去设置,后面通过全局异常去捕获处理就行了。
    // 登录,无权限是跳转的路径
    shiroFilterFactoryBean.setLoginUrl("/");
    // 登录成功后跳转的路径
    shiroFilterFactoryBean.setSuccessUrl("/");
    // 错误页面,认证不通过跳转
    shiroFilterFactoryBean.setUnauthorizedUrl("/");

(二)重写shiro的AuthorizingRealm抽象类

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.hx.xxx.dto.MenuDTO;
import com.hx.xxx.dto.RoleMenuDTO;
import com.hx.xxx.dto.UserDTO;
import com.hx.xxx.dto.UserRoleDTO;
import com.xxx.service.*;
import com.xxx.utils.Misc;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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;

import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;


@Component
public class MyShiroRealm extends AuthorizingRealm {

    private final UserService userService;
    private final RoleService roleService;
    private final UserRoleService userRoleService;
    private final RoleMenuService roleMenuService;
    private final MenuService menuService;

    @Autowired
    public MyShiroRealm(UserService userService, RoleService roleService, UserRoleService userRoleService, RoleMenuService roleMenuService, MenuService menuService) {
        this.userService = userService;
        this.roleService = roleService;
        this.userRoleService = userRoleService;
        this.roleMenuService = roleMenuService;
        this.menuService = menuService;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)

        Set<String> permsSet = new HashSet<>();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //获取登录用户名
        String name = (String) principalCollection.getPrimaryPrincipal();
        //查询用户名称
        UserDTO userDTO = userService.get(Wrappers.<UserDTO>lambdaQuery().eq(UserDTO::getUsername, name));
        if (Misc.isNotNull(userDTO)) {
            //查询用户对应的角色
            List<UserRoleDTO> userRoleList = userRoleService.list(Wrappers.<UserRoleDTO>lambdaQuery()
                    .eq(UserRoleDTO::getUserId, userDTO.getId()));
            for (UserRoleDTO userRoleDTO : userRoleList) {
                if (Misc.isNotNull(userRoleDTO)) {
                    //查询角色下对应的菜单
                    List<RoleMenuDTO> roleMenuDTOList = roleMenuService.list(Wrappers.<RoleMenuDTO>lambdaQuery().eq(RoleMenuDTO::getRoleId, userRoleDTO.getRoleId()));
                    Set<Long> menuIdSet = new HashSet<>();
                    menuIdSet = roleMenuDTOList.stream().map(RoleMenuDTO::getMenuId).collect(Collectors.toSet());
                    //获取菜单对应的操作权限
                    if (Misc.isNotNull(menuIdSet)) {
                        List<MenuDTO> menuDTOList = menuService.list(menuIdSet);
                        permsSet = menuDTOList.stream().map(MenuDTO::getPerms).collect(Collectors.toSet());
                    }
                }
            }
        }
        info.setStringPermissions(permsSet);
        // 可以直接放入角色
        // info.setRoles();
        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户的输入的账号.
        // 1.从主体传过来的认证信息中,获得用户名
        String username = (String) token.getPrincipal();
        // 2.通过用户名到数据库中获取凭证
        UserDTO userDTO = userService.get(Wrappers.<UserDTO>lambdaQuery().eq(UserDTO::getUsername, username));
        userDTO.setSessionId(SecurityUtils.getSubject().getSession().getId().toString());
        // 设置最后登录时间
        userDTO.setUpdatedTime(new Date());
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                username, userDTO.getPassword(), getName()
        );
        return authenticationInfo;
    }
}

  1. 继承AuthorizingRealm类,需要 重写两个方法,一个是授权,一个是认证。
    doGetAuthenticationInfo:中文意思表示认证,当用户在登录的时候,通过
    subject.login(usernamePasswordToken)调用,然后会进到这个方法。见下图:
    shiro前后端分离配置写法,返回json数据格式,不跳转页面_第1张图片
    当登录信息不正确,是会抛出不同的异常,这边进行try catch捕获就行了,直接抛出异常,不经过shiro自带的页面跳转。
    用户的实体最好有一个sessionId字段作为一个用户的外部标识,跟前端请求时的header里面的token是一样的(我这边是叫sessionId,看习惯,叫token也是可以的),shiro在登录成功的时候,会在redis里面去存用户的信息,redis的key就是通过
    SecurityUtils.getSubject().getSession().getId().toString();
    生成的,在用户下次请求的时候,header里面带上token,就会自动去找redis里面的用户信息。
  2. doGetAuthorizationInfo :中文意思是授权的意思,用户在登录成功后,请求其余接口,通过你在hearder里面的token,他会自动去找到redis里面存的用户名,然后你直接通过查询用户名将用户的角色和权限查询出来,因为我就用到了权限,所以就省略了将角色放进去的代码。

(三)重写RedisSessionDao继承AbstractSessionDAO

package xxx.shiro;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.TimeUnit;

/**
 * RedisSessionDao,以Redis持久化方式做Session共享,无需配置即可支持集群

 */
    public class RedisSessionDao extends AbstractSessionDAO {

    /**
     * Session超时时间(秒)
     */
    private long expireTime;

    public RedisSessionDao(long expireTime) {
        this.expireTime = expireTime;
    }

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        return sessionId == null ? null : (Session) redisTemplate.opsForValue().get(sessionId);
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        if (session != null && session.getId() != null) {
            session.setTimeout(expireTime * 1000);
            redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
        }
    }

    @Override
    public void delete(Session session) {
        if (session != null && session.getId() != null) {
            redisTemplate.opsForValue().getOperations().delete(session.getId());
        }
    }

    @Override
    public Collection<Session> getActiveSessions() {
        return redisTemplate.keys("*");
    }

}
  1. 这个直接照抄就行,不需要做改动。

(四)MySessionManager

package xxx.shiro;

import com.xxx.utils.IPUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * 自定义Session管理器,以Token做会话保持,同时兼容Cookie方式
 * 默认从请求头中获取Token,获取不到会继续读取Cookie
 */
public class MySessionManager extends DefaultWebSessionManager {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    /**
     * 前端ajax请求headers中须传入Authorization的值,也能兼容Cookie方式
     */
    private static final String AUTHORIZATION = "token";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        String ipAddr = IPUtils.getIpAddr(httpServletRequest);
        String requestUri = httpServletRequest.getRequestURI();
        log.debug(">>>>>>>>>>>>>>>>>>>>> MySessionManager.getSessionId(), IP: {}, URI: {}", ipAddr, requestUri);
        // 先从请求头中获取 Authorization
        Serializable sessionId = httpServletRequest.getHeader(AUTHORIZATION);
        // 如果请求头中有 Authorization 则其值为sessionId
        if (sessionId != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            log.debug(">>>>>>>>>> MySessionManager.getSessionId(), 从Header中获取sessionId: " + sessionId);
            return sessionId;
        }
        // 否则按默认规则从 cookie 取sessionId
        else {
            sessionId = super.getSessionId(request, response);
            log.debug(">>>>>>>>>> MySessionManager.getSessionId(), 使用默认模式从cookie获取sessionID为: " + sessionId);
            return sessionId;
        }
    }

}

  1. 注意常量 AUTHORIZATION 定义的值,因为我前端传过来的是token,你们使用的时候要看自己项目传用户标识定的是什么值就行了,只要前后端定义好就行。

(五)使用

/**
     * 菜单列表
     * @return
     */
    @RequiresPermissions("menu:list")
    @GetMapping("/list")
    public Result<Page<MenuDTO>> menuList(PageBO pageBO) {
        return Result.ok();
    }
  1. 我这边主要针对的是每个接口,权限用的比较细,没有用到角色这一层,直接用 @RequiresPermissions(“menu:list”)就可以了,里面的 menu:list是自定义的,你可以自定义成任意的标识,尽量做到唯一性,最好是按照功能来定义,这样不至于混淆。
  2. 请求到controller层的时候,是一定先执行授权后,再到controller的。
  3. 在用户无权限的时候,shiro默认是跳转到一个jsp页面的,所以这边需要去写一个捕获的异常类就行了。
@RestControllerAdvice
public class GlobalDefaultExceptionHandler {

    /**
     * AuthorizationException 类捕获
     */
    @ExceptionHandler(value = AuthorizationException.class)
    public Result handlerAuthorizationException() {
        return Result.failed(ErrMsgConsts.USER_UNPOWER);
    }

}

直接捕获 AuthorizationException异常,这个异常就表示用户无权限,你可以在这里直接返回json格式的异常就ok了。
总结: 我找了很多人写的教程里面,大部分人都是写了一个baseController基础类,然后全部的controller 去继承,我是觉得很繁琐,所以直接全局的异常类去直接捕获,然后在抛出。
上面的一些写法,我也是看了好几个gitlab上面的项目去实现的,很多东西都是雷同的,我写的比较简单,感兴趣的可以去尝试一下shiro的自定义过滤器去实现返回json格式。

你可能感兴趣的:(shiro,java,spring,boot,数据库,cookie)