springboot项目整合shiro实现权限管理

准备工作

需要一个整合了mybatis的springboot项目;
这部分可以参考我之前的文章。
使用idea创建springboot项目并整合mybatis

整合shiro

shiro是一个开源的权限管理框架,其功能非常丰富,这里我只实现它最基本的登录认证和权限验证功能,我使用的开发环境是:
idea 2018.1
springboot 2.1.12
shiro-spring 1.3.2
mysql 8.0.19
jdk 1.8
首先在pom.xml中加入shiro-spring依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>

配置shiro

使用shiro之前,需要对shiro进行一些自定义配置,

  1. 创建shiroConfig类
package com.lg.shirodemo.common;

import com.lg.shirodemo.filter.ShiroLoginFilter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {
    private final static Logger log = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 创建ShrioFilterFactoryBean,配置filter规则
     * @Shiro内置过滤器
     * anon         org.apache.shiro.web.filter.authc.AnonymousFilter
     * authc        org.apache.shiro.web.filter.authc.FormAuthenticationFilter
     * authcBasic   org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
     * perms        org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
     * port         org.apache.shiro.web.filter.authz.PortFilter
     * rest         org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
     * roles        org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
     * ssl          org.apache.shiro.web.filter.authz.SslFilter
     * user         org.apache.shiro.web.filter.authc.UserFilter
     * @return
     */
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 添加自定义filters
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        //添加未通过认证时不跳转页面,而是返回json
        filters.put("authc", new ShiroLoginFilter());

        /*
         * 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
         * Shiro内置过滤器,可以实现权限相关的拦截器,常用的有:
         *   anon:无需认证(登录)即可访问
         *   authc:必须认证才可以访问
         *   user:如果使用rememberme的功能可以直接访问
         *   perms:该资源必须得到资源权限才能访问
         *   role:该资源必须得到角色资源才能访问
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        //放过登录请求
        filterMap.put("/login", "anon");
        filterMap.put("/addUser", "anon");

        filterMap.put("/**",  "authc");//其他资源全部拦截
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        
        //如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        // 这里可以配置默认登录地址
        //shiroFilterFactoryBean.setLoginUrl("/unauth");
        return shiroFilterFactoryBean;
    }

    /**
     * 创建DefaultWebSecurityManager
     * SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
     * @return
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //注入自定义cookie管理
        //securityManager.setRememberMeManager(cookieRememberMeManager());
        //注入自定义session管理
        //securityManager.setSessionManager(sessionManager());
        //注入自定义cache管理
        //securityManager.setCacheManager(redisCacheManager());
        //注入自定义realm
        securityManager.setRealm(userRealm());
        return securityManager;
    }

    /**
     * 创建Realm
     * @return
     */
    @Bean(name = "userRealm")
    public UserRealm userRealm(){
        UserRealm userRealm = new UserRealm();
        //设置解密规则
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        //不使用默认的缓存
        //userRealm.setCachingEnabled(false);
        return userRealm;
    }

    /**
     * 设置shiro验证密码时的解密方式
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //使用MD5散列算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //散列1次
        hashedCredentialsMatcher.setHashIterations(1);
        //storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

}

这个配置文件里可以对shiro进行多种自定义配置,比如实现自己的session管理,实现redis缓存,实现cookie管理等等,这些可以查看官方文档进行设置。
在设置过滤器的时候,我自己定义了一个无权限过滤器,是为了防止shiro在登录失败的时候默认跳转/login.jsp,实现前后端分离,直接返回json数据

package com.lg.shirodemo.filter;

import com.lg.shirodemo.util.JsonUtil;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

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

/**
 * 重写authc过滤器
 * shiro框架默认跳转到/login.jsp
 * 前后端分离模式不需要跳转,返回json数据就行
 * @author lg
 *
 */
public class ShiroLoginFilter extends FormAuthenticationFilter {

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        //这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
        httpServletResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        Map<String, Object> resultData = new HashMap<>();
        resultData.put("msg", "登录认证失败,请重新登录!");
        resultData.put("status", 403);
        httpServletResponse.getWriter().write(JsonUtil.toJson(resultData));
        return false;
    }
}
  1. 配置userReaml
    userReaml是shiro中非常重要的一个类,它决定了shiro如何去进行登录认证和权限鉴权。
package com.lg.shirodemo.common;

import com.lg.shirodemo.dto.LoginInfo;
import com.lg.shirodemo.mapper.UserExtendMapper;
import com.lg.shirodemo.table.entity.Menu;
import com.lg.shirodemo.table.entity.Role;
import com.lg.shirodemo.table.entity.User;
import com.lg.shirodemo.table.entity.UserExample;
import com.lg.shirodemo.table.mapper.UserMapper;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

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

/**
 * 自定义shiro的realm,用来认证和鉴权
 */
public class UserRealm extends AuthorizingRealm {
    private final static Logger log = LoggerFactory.getLogger(UserRealm.class);

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserExtendMapper userExtendMapper;

    /**
     * 认证(登陆时候调用)
     * @author lg
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("自定义认证(登陆时候调用)开始---------");
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String loginName = token.getUsername();
        // 从数据库获取该用户信息
        UserExample ue = new UserExample();
        ue.createCriteria().andLoginNameEqualTo(loginName);
        List<User> users = userMapper.selectByExample(ue);
        if (null == users || users.size() < 0){
            //没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
            return null;
        }
        User user = users.get(0);
        LoginInfo loginInfo = new LoginInfo();
        loginInfo.setUser(user);
        List<Role> roles = userExtendMapper.getRolesByUserId(user.getId());
        loginInfo.setRoleList(roles);
        if(roles != null && roles.size() > 0){
            Role role = roles.get(0);
            List<Menu> menus = userExtendMapper.getMenusByRoleId(role.getId());
            loginInfo.setMenuList(menus);
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                //这里的第一个参数,可以是查询到的用户实体,也可以是用户名。主要是为方便后期Subject的getPrincipal()方法取值。放进去是什么,getPrincipal()取到的就是什么。
                loginInfo,
                //这里的密码,一定是查询到的实体密码,不是参数传递的密码
                user.getPassword(),
                //加盐salt=userName+salt
                ByteSource.Util.bytes(user.getLoginName()+"salt"),
                //?
                getName()
        );
        return authenticationInfo;
    }

    /**
     * 授权(验证权限时候调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) throws UnauthorizedException {
        log.info("自定义授权(验证权限时候调用)开始------------");
        LoginInfo loginInfo = (LoginInfo) principalCollection.getPrimaryPrincipal();
        Set<String> roleSet = new HashSet<>();
        Set<String> permissionSet = new HashSet<>();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        List<Role> roles = loginInfo.getRoleList();
        if (roles != null && roles.size() > 0) {
            // 为了简单,一个用户设计只有一个角色
            Role role = roles.get(0);
            roleSet.add(role.getName());
            simpleAuthorizationInfo.addRoles(roleSet);
            List<Menu> menus = loginInfo.getMenuList();
            //将该角色拥有的权限添加到shiro鉴权器中
            for (Menu menu : menus) {
                permissionSet.add(menu.getName());
                simpleAuthorizationInfo.addStringPermissions(permissionSet);
            }
        }
        return simpleAuthorizationInfo;
    }

}

实现权限管理

现在数据库中创建经典的用户-角色-权限模型

CREATE TABLE `sys_user` (
  `id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
  `login_name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  `user_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `password` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `tel` varchar(16) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `enable` varchar(1) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `sys_role` (
  `id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
  `name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `sys_user_role` (
  `id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
  `user_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `role_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `sys_menu` (
  `id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
  `name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `parent_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `url` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `sort_no` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `sys_role_menu` (
  `id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
  `role_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `menu_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

创建表后,处理好mybatis的查询(此处不详细说明了),创建登录的controller

	@RequestMapping("/login")
    public Map<String, Object> login(User user){
        Map<String, Object> res = new HashMap<>();
        // 登录失败从request中获取shiro处理的异常信息。
        UsernamePasswordToken token = new UsernamePasswordToken(user.getLoginName(), user.getPassword());
        Subject subject = SecurityUtils.getSubject();
        try{
            subject.login(token);
            //从UserRealm里返回的SimpleAuthenticationInfo获取到认证成功的用户名,
            //subject.getPrincipal()获取的是SimpleAuthenticationInfo设置的第一个参数
            LoginInfo loginInfo = (LoginInfo) subject.getPrincipal();
            Session session = subject.getSession();
            session.setAttribute("loginUser", loginInfo);
            loginInfo.getUser().setPassword(null);
            res.put("data", loginInfo);
            res.put("token", subject.getSession().getId());
            res.put("status", 0);
        }catch (IncorrectCredentialsException e){
            res.put("status", -1);
            res.put("msg", "密码错误");
        }catch (LockedAccountException e){
            res.put("status", -1);
            res.put("msg", "账号被冻结");
        }catch (AuthenticationException e) {
            res.put("status", -1);
            res.put("msg", "账号不存在");
        }catch (Exception e) {
            log.error(e.getMessage());
        }
        return res;
    }

因为/login这个地址在之前的shiroConfig配置中设为了不拦截,所以可以直接访问,但是访问其他路径时
1
而登录/login成功后,返回用户的基本信息。
2

那么登录验证实现了,如何实现鉴权呢?方案之一就是使用俩个注解添加在controller方法上:

注意!!!这些注解是基于aop实现的,所以必须要确认你的springboot是否添加了aop依赖

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

@RequiresRoles("admin") //需要用户是拥有admin角色

@RequiresPermissions("updateUser") //需要用户有updateUser权限

测试结果,如果没有该角色或该权限,会抛出异常,这里可以对所有controller做一个全局异常处理。
3
至此,springboot整合shiro权限管理基本功能已经实现完毕。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值