SpringBoot+Shiro实现权限控制

情到浓时终转凉″ 提交于 2020-08-04 09:16:30

一、Shiro简介

二、项目实现

2.1  数据库结构

2.2 SQL

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
  `menu_id` int(32) NOT NULL AUTO_INCREMENT,
  `menu_ name` varchar(200) NOT NULL,
  `parent_id` int(32) NOT NULL DEFAULT '0',
  `url` varchar(250) NOT NULL DEFAULT '#',
  `menu_type` int(3) NOT NULL,
  `perms` varchar(250) NOT NULL,
  PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES ('1', '用户管理', '0', '#', '1', '');
INSERT INTO `menu` VALUES ('2', '添加用户', '1', '/user/add', '2', 'user:add');
INSERT INTO `menu` VALUES ('3', '分页查询用户', '1', '/user/page', '2', 'user:page');
INSERT INTO `menu` VALUES ('4', '修改用户', '1', '/user/update', '2', 'user:update');
INSERT INTO `menu` VALUES ('5', '删除用户', '1', '/user/delete', '2', 'user:delete');

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `role_id` int(32) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(200) DEFAULT NULL,
  `remark` varchar(255) DEFAULT NULL,
  `delete_flag` int(3) DEFAULT NULL,
  PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'admin', '系统管理员', '2');
INSERT INTO `role` VALUES ('2', 'biz', '业务用户', '2');

-- ----------------------------
-- Table structure for role_menu
-- ----------------------------
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
  `role_menu_id` int(32) NOT NULL AUTO_INCREMENT,
  `role_id` int(32) NOT NULL,
  `menu_id` int(32) NOT NULL,
  PRIMARY KEY (`role_menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role_menu
-- ----------------------------
INSERT INTO `role_menu` VALUES ('1', '1', '1');
INSERT INTO `role_menu` VALUES ('2', '1', '2');
INSERT INTO `role_menu` VALUES ('3', '1', '3');
INSERT INTO `role_menu` VALUES ('4', '1', '4');
INSERT INTO `role_menu` VALUES ('5', '1', '5');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `user_id` bigint(32) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(50) NOT NULL,
  `password` varchar(50) NOT NULL,
  `salt` varchar(255) NOT NULL,
  `del_flag` int(3) NOT NULL DEFAULT '2',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '14e1b600b1fd579f47433b88e8d85291', '14e1b600b1fd579', '2');
INSERT INTO `user` VALUES ('2', 'biz', '14e1b600b1fd579f47433b88e8d85291', '579f47433b88e8d85291', '2');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `user_role_id` int(32) NOT NULL AUTO_INCREMENT,
  `user_id` int(32) NOT NULL,
  `role_id` int(32) NOT NULL,
  PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '2', '2');

2.3 项目结构

2.4 Maven依赖

<properties>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

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

        <!--Shiro Spring-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.5.3</version>
        </dependency>

        <!--MyBatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-typehandlers-jsr310</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

2.5 基础代码

    POJO、Mapper、Service等,不再这里赘述。

2.6 自定义Shiro Realm

/**
 * 自定义权限控制数据源
 */
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private IUserService userService;

    @Autowired
    private IRoleService roleService;

    @Autowired
    private IMenuService menuService;

    /**
     * 授权,通过服务加载用户角色和权限信息设置进去
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        if(null == principalCollection){
            throw new AuthorizationException("principalCollection should not be null.");
        }
        //记录用户的角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //获取登录的用户名
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //从数据库中,根据用户名查询用户信息
        User user = userService.getUserByUserName(userName);
        //用户所属角色集合
        List<Role> roleList = roleService.queryByUserId(user.getUserId());
        for (Role role : roleList) {
            //添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            List<Menu> menuList = menuService.queryByRoleId(role.getRoleId());
            //添加权限
            for (Menu menu : menuList) {
                simpleAuthorizationInfo.addStringPermission(menu.getPerms());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * 身份验证,通过服务加载用户信息并构造认证对象返回
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //从Token获取用户名,从主体认证信息中获取。Post请求时会先进入认证再到请求。
        Object principal = authenticationToken.getPrincipal();
        if(null == principal){
            return null;
        }
        //获取用户名
        String userName = String.valueOf(principal);
        //获取密码
        String password = new String((char[])authenticationToken.getCredentials());
        //根据用户名查询用户信息
        User user = userService.getUserByUserName(userName);
        if(null == user){
            throw new UnknownAccountException("用户名或密码错误!");
        }
        if(!user.getPassword().equals(MD5Util.md5Encrypt32Lower(password))){
            throw new IncorrectCredentialsException("用户名或密码错误!");
        }
        //验证AuthenticationToken和SimpleAuthenticationInfo的信息
        SimpleAuthenticationInfo simpleAuthenticationInfo
                = new SimpleAuthenticationInfo(userName,user.getPassword(),super.getName());
        return simpleAuthenticationInfo;
    }
}

2.7 自定义密码验证器

/**
 * 自定义密码验证器
 */
public class CustomMatcher extends SimpleCredentialsMatcher {

    /**
     * 重写密码校验逻辑
     * @param token
     * @param info
     * @return
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String tokenPassword = this.encrypt(String.valueOf(usernamePasswordToken.getPassword()));
        String password = (String)info.getCredentials();
        return super.equals(tokenPassword,password);
    }

    /**
     * 将传来的密码再次加密
     * 可根据实际需求修改成别的加密方式
     * @param str
     * @return
     */
    private String encrypt(String str){
        return MD5Util.md5Encrypt32Lower(str);
    }
}

2.8 自定义记住我过滤器

/**
 * 自定义“记住我”过滤器
 */
public class CustomRememberMeFilter extends FormAuthenticationFilter {

    /**
     * 登录条件过滤:要么通过权限认证登录成功,要么通过“记住我”登录成功。
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = super.getSubject(request, response);
        if(!subject.isAuthenticated() && !subject.isRemembered()){
            if(null == subject.getSession().getAttribute("user") && null == subject.getPrincipal()){
                subject.getSession().setAttribute("user",subject.getPrincipal());
            }
        }
        return subject.isAuthenticated() || subject.isRemembered();
    }
}

2.9 Shiro配置类

@Configuration
public class ShiroConfig {

    /**
     * 密码验证器
     * @return
     */
    @Bean
    public CredentialsMatcher credentialsMatcher(){
        return new CustomMatcher();
    }

    /**
     * 权限验证
     * @param credentialsMatcher
     * @return
     */
    @Bean
    public CustomRealm customRealm(@Qualifier("credentialsMatcher") CredentialsMatcher credentialsMatcher){
        CustomRealm customRealm = new CustomRealm();
        //给权限验证器注入自定义的密码验证器
        customRealm.setCredentialsMatcher(credentialsMatcher);
        return customRealm;
    }

    /**
     * 缓存管理
     * @return
     */
    @Bean
    public CacheManager cacheManager(){
        return new MemoryConstrainedCacheManager();
    }

    /**
     * “记住我”Cookie管理
     * @return
     */
    @Bean
    public CookieRememberMeManager cookieRememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        //手动设置对称加密密钥,避免出现 Unable to execute 'doFinal' with cipher instance 异常。原因在于:rememberMe的cookie在第二次打开页面后Shiro无法解密
        cookieRememberMeManager.setCipherKey(Base64.decode("6ZmI6I2j3Y+R1aSn5BOlAA=="));
        cookieRememberMeManager.setCookie(this.rememberMeCookie());
        return cookieRememberMeManager;
    }

    /**
     * “记住我”Cookie对象
     * @return
     */
    private SimpleCookie rememberMeCookie(){
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //cookie有效时间为30天
        simpleCookie.setMaxAge(2592000);
        return simpleCookie;
    }

    /**
     * 自定义“记住我”过滤器
     * @return
     */
    @Bean
    public CustomRememberMeFilter customRememberMeFilter(){
        return new CustomRememberMeFilter();
    }

    /**
     * Session会话管理
     * @return
     */
    @Bean
    public DefaultWebSessionManager sessionManager(){
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        Long timeout=60L*1000*60;
        defaultWebSessionManager.setGlobalSessionTimeout(timeout);
        return defaultWebSessionManager;
    }

    /**
     * SecurityManager安全管理器,起桥梁作用
     * @param customRealm
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("customRealm") CustomRealm customRealm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //自定义Realm
        defaultWebSecurityManager.setRealm(customRealm);
        //自定义CacheManager
        defaultWebSecurityManager.setCacheManager(this.cacheManager());
        //自定义“记住我”
        defaultWebSecurityManager.setRememberMeManager(this.cookieRememberMeManager());
        //自定义SessionManager
        defaultWebSecurityManager.setSessionManager(this.sessionManager());
        return defaultWebSecurityManager;
    }

    /**
     * 全局配置,Filter工厂。
     * 设置对应的过滤条件和跳转条件,有自定义的过滤器、Shiro认证成功、失败、退出登录等跳转的页面。
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login");

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("customRememberFilter",this.customRememberMeFilter());

        LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<>();

        linkedHashMap.put("/logout","logout");

        linkedHashMap.put("/error","anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(linkedHashMap);

        return shiroFilterFactoryBean;
    }

    /**
     * Shiro生命周期
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 配置Shiro注解是否生效
     * @RequiresRoles,@RequiresPermissions
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        sourceAdvisor.setSecurityManager(securityManager);
        return sourceAdvisor;
    }
}

2.10 登录Controller

@Controller
public class LoginController {

    @Autowired
    private IUserService userService;

    @PostMapping("/login")
    @ResponseBody
    public String login(@RequestBody LoginDto loginDto){
        UsernamePasswordToken uToken
                = new UsernamePasswordToken(loginDto.getUserName(), loginDto.getPassword());
        //记住我
        uToken.setRememberMe(true);

        Subject subject = SecurityUtils.getSubject();

        try{
            subject.login(uToken);
        }catch (UnknownAccountException e){
            return e.getMessage();
        }catch (IncorrectCredentialsException e){
            return e.getMessage();
        }

        //这里可将一些用户信息保存在Session中
        User user = userService.getUserByUserName(loginDto.getUserName());
        subject.getSession().setAttribute("user",user);
        return "登录成功!";
    }

}

2.11 用户模块Controller

此处只演示用法,不赘述业务逻辑。具体业务逻辑可自行定义。

@RestController
@RequestMapping("/user")
public class UserController {

    @RequiresPermissions("user:page")
    @PostMapping("/page")
    public String page(){
        return "分页查询";
    }

    @RequiresPermissions("user:add")
    @PostMapping("/add")
    public String add(){
        return "添加用户";
    }

    @RequiresPermissions("user:update")
    @PostMapping("/update")
    public String update(){
        return "修改用户";
    }

    @RequiresPermissions("user:delete")
    @PostMapping("/delete")
    public String delete(){
        return "删除用户";
    }
}

2.12 Shiro异常处理

@ControllerAdvice
public class HandlerException {

    @ResponseBody
    @ExceptionHandler(UnauthorizedException.class)
    public String handlerShiroException(Exception e){
        return "无权限操作!";
    }

    public String authorizationException(Exception e){
        return "权限验证失败!";
    }
}

三、演示

    本案例的数据表中,设置了两个用户,两个角色,五个菜单。
用户名 角色 可操作菜单
admin admin(系统管理员) 用户管理:分页查询、添加、编辑、删除
biz biz(业务用户) 其他业务模块(不包括用户管理模块)
    验证规则:admin登录,可以调用page、add、update、delete接口;biz登录, 调用page、add、update、delete接口提示“无操作权限”。

3.1 admin登录

登录接口:

3.2 biz登录

登录接口:

四、笔记

    关于CustomRealm里定义的两个方法,
    doGetAuthorization方法用于授权,查询用户角色和权限信息,设置进AuthorizationInfo中;
    doGetAuthentication方法,用于身份验证,查询用户信息,构造一个认证对象。
    当我们调用“/login”接口,即调用subject.login(uToken)方法时,会自动进入CustomRealm中指定 doGetAuthentication(认证)方法,认证交由SimpleAuthenticationInfo来完成,它会完成身份密码的比较等功能。但是登录接口不走 doGetAuthorization(授权)方法。
    当访问带有@RequiresPermissions或@RequiresRoles注解的接口时,才会去调用 doGetAuthorization(授权)方法。

五、参考链接

https://blog.csdn.net/jiankang66/article/details/90473517

https://www.cnblogs.com/Slags/p/12403448.html

https://www.jianshu.com/p/7f724bec3dc3

https://blog.csdn.net/lidai352710967/article/details/83654132

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!