zoukankan      html  css  js  c++  java
  • Spring Boot 整合 Shiro 实现登录认证与权限控制

    用户角色权限数据库设计

    数据库这里以 MySQL 为例

    创建数据库

    所需表如下:

    • user:用户表
    • role:角色表
    • perm:权限菜单表
    • user_role:用户与角色关联的中间表
    • role_prem:角色与权限菜单关联的中间表

    执行数据库脚本

    /*
     Navicat Premium Data Transfer
    
     Source Server         : 127.0.0.1
     Source Server Type    : MySQL
     Source Server Version : 50718
     Source Host           : 127.0.0.1:3306
     Source Schema         : shiro
    
     Target Server Type    : MySQL
     Target Server Version : 50718
     File Encoding         : 65001
    */
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for perm
    -- ----------------------------
    DROP TABLE IF EXISTS `perm`;
    CREATE TABLE `perm`  (
      `perm_id` int(32) NOT NULL COMMENT '权限主键',
      `perm_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限url',
      `perm_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限描述',
      PRIMARY KEY (`perm_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of perm
    -- ----------------------------
    INSERT INTO `perm` VALUES (1, '/user/*', '拥有对用户的所有操作权限');
    
    -- ----------------------------
    -- Table structure for role
    -- ----------------------------
    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role`  (
      `role_id` int(32) NOT NULL COMMENT '角色主键',
      `role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名',
      `role_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色描述',
      PRIMARY KEY (`role_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of role
    -- ----------------------------
    INSERT INTO `role` VALUES (1, '超级管理员', '超级管理员');
    
    -- ----------------------------
    -- Table structure for role_perm
    -- ----------------------------
    DROP TABLE IF EXISTS `role_perm`;
    CREATE TABLE `role_perm`  (
      `role_id` int(32) NOT NULL COMMENT '角色主键',
      `perm_id` int(32) DEFAULT NULL COMMENT '权限主键'
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of role_perm
    -- ----------------------------
    INSERT INTO `role_perm` VALUES (1, 1);
    
    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user`  (
      `user_id` int(32) NOT NULL COMMENT '用户主键',
      `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '用户名',
      `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '密码(存储加密后的密码)',
      PRIMARY KEY (`user_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of user
    -- ----------------------------
    INSERT INTO `user` VALUES (1, 'root', '5dbc683c53b7f317fa45c05bf9499fdd');
    
    -- ----------------------------
    -- Table structure for user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role`  (
      `user_id` int(32) NOT NULL COMMENT '用户主键',
      `role_id` int(32) NOT NULL COMMENT '角色主键'
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of user_role
    -- ----------------------------
    INSERT INTO `user_role` VALUES (1, 1);
    
    SET FOREIGN_KEY_CHECKS = 1;
    

    数据库设计完成以后,将相对应的实体类和 mapper 文件加入到项目当中

    业务代码

    这里我们需要定义一个业务接口查询用户的相关信息(包括用户关联的角色与权限)

    这里不阐述具体的 SQL 语句

    UserService

    public interface UserService {
    	
       /**
         * 根据用户名查询用户信息(包含角色及权限信息)
         * @param username 用户名
         * @return User
         */
        User selectByUsername(String username);
    }
    

    UserServiceImpl

    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public User selectByUsername(String username) {
    
            return userMapper.selectByUsername(username);
        }
    }
    

    引入依赖

    pox.xml 中添加 org.apache.shiro:shiro-springcom.github.theborakompanioni:thymeleaf-extras-shiro 依赖

    <properties>
        <thymeleaf-extras-shiro.version>2.0.0</thymeleaf-extras-shiro.version>
        <shiro.version>1.4.0</shiro.version>
    </properties>
    
    <dependencies>
        <!-- Shiro核心依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>
    
        <!-- Thymeleaf对Shiro的支持 -->
        <dependency>
          <groupId>com.github.theborakompanioni</groupId>
          <artifactId>thymeleaf-extras-shiro</artifactId>
          <version>${thymeleaf-extras-shiro.version}</version>
        </dependency>
    </dependencies>
    

    自定义认证和授权

    创建 MyRealm 类实现认证与授权

    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.*;
    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.apache.shiro.subject.Subject;
    import org.apache.shiro.util.ByteSource;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.List;
    
    /**
     * 自定义Realm,实现授权与认证
     */
    public class MyRealm extends AuthorizingRealm {
    
        @Autowired
        private UserService userService;
    
        /**
         * 用户认证
         **/
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
            User user = userService.selectByUsername(token.getUsername());
            if (user == null) {
                throw new UnknownAccountException();
            }
            return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        }
    
        /**
         * 用户授权
         **/
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
            Subject subject = SecurityUtils.getSubject();
            User user = (User) subject.getPrincipal();
            if (user != null) {
                SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
                List<String> roles = new LinkedList<>();
                List<String> perms = new LinkedList<>();
                for (Role role : user.getRoleList()) {
                    roles.add(role.getRoleName());
                }
                for (Perm perm : user.getPermList()) {
                    perms.add(perm.getPermUrl());
                }
                simpleAuthorizationInfo.addRoles(roles);
                simpleAuthorizationInfo.addStringPermissions(perms);
                return simpleAuthorizationInfo;
            }
            return null;
        }
    }
    

    Shiro 配置类

    创建 ShiroConfig 配置类

    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    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.session.mgt.DefaultWebSessionManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.Properties;
    
    @Configuration
    public class ShiroConfig {
    
        /**
         * 配置密码加密
         */
        @Bean("hashedCredentialsMatcher")
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
    
            HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
            // 散列算法(加密)
            credentialsMatcher.setHashAlgorithmName("MD5");
            // 散列次数(加密次数)
            credentialsMatcher.setHashIterations(1);
            // storedCredentialsHexEncoded 默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
            credentialsMatcher.setStoredCredentialsHexEncoded(true);
            return credentialsMatcher;
        }
    
        /**
         * 注入自定义的 Realm
         */
        @Bean("MyRealm")
        public MyRealm MyRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
    
            MyRealm MyRealm = new MyRealm();
            MyRealm.setCredentialsMatcher(matcher);
            return MyRealm;
        }
    
        /**
         * 配置自定义权限过滤规则
         */
        @Bean
        public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
    
            ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
            bean.setSecurityManager(securityManager);
            bean.setSuccessUrl("/index.html");
            bean.setLoginUrl("/login.html");
            bean.setUnauthorizedUrl("/unauthorized.html");
    
            /**
             * anon:匿名用户可访问
             * authc:认证用户可访问
             * user:使用rememberMe可访问
             * perms:对应权限可访问
             * role:对应角色权限可访问
             **/
            Map<String, String> filterMap = new LinkedHashMap<>();
            /**
             * 允许匿名访问静态资源
             */
            filterMap.put("/image/**", "anon");
            filterMap.put("/css/**", "anon");
            filterMap.put("/js/**", "anon");
            filterMap.put("/plugin/**", "anon");
            /**
             * 允许匿名访问登录页面和登录操作
             */
            filterMap.put("/login.html", "anon");
            filterMap.put("/login.do", "anon");
            /**
             * 其它所有请求需要登录认证后才能访问
             */
            filterMap.put("/**", "authc");
            bean.setFilterChainDefinitionMap(filterMap);
            return bean;
        }
    
        /**
         * 注入 securityManager
         */
        @Bean(name = "securityManager")
        public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher, @Qualifier("sessionManager") DefaultWebSessionManager defaultWebSessionManager) {
    
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(MyRealm(hashedCredentialsMatcher));
            securityManager.setSessionManager(defaultWebSessionManager);
            return securityManager;
        }
    
        /**
         * 开启权限注解
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
    
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
        @Bean
        @ConditionalOnMissingBean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    
            DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
            return defaultAdvisorAutoProxyCreator;
        }
    
        /**
         * 配置异常跳转页面
         */
        @Bean
        public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
    
            SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
            Properties properties = new Properties();
            // 未认证跳转页面(跳转路径为项目里的页面相对路径,并非 URL)
            properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "login");
            // 权限不足跳转页面
            properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "unauthorized");
            resolver.setExceptionMappings(properties);
            return resolver;
        }
    
        /**
         * 会话管理器
         */
        @Bean("sessionManager")
        public DefaultWebSessionManager defaultWebSessionManager() {
    
            DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
            // 设置用户登录信息失效时间为一天(单位:ms)
            defaultWebSessionManager.setGlobalSessionTimeout(1000L * 60L * 60L * 24L);
            return defaultWebSessionManager;
        }
    
        /**
         * 重置 ShiroDialect,省略此步将不能在 Thymeleaf 页面使用 Shiro 标签
         */
        @Bean(name = "shiroDialect")
        public ShiroDialect shiroDialect(){
            return new ShiroDialect();
        }
    }
    

    Controller

    @Controller
    public class IndexController {
    
        @Autowired
        private UserService userService;
    
        @RequestMapping(value = "login.html")
        public String loginView() {
    
            // 判断当前用户是否通过认证
            if (SecurityUtils.getSubject().isAuthenticated()) {
                // 认证通过,重定向到首页
                return "redirect:index.html";
            } else {
                // 未认证或认证失败,转发到登录页
                return "login";
            }
        }
    
        @RequestMapping(value = "login.do")
        @ResponseBody
        public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
            try {
                // 执行认证
                subject.login(usernamePasswordToken);
            } catch (UnknownAccountException e) {
                return AppReturn.defeated("账号不存在");
            } catch (IncorrectCredentialsException e) {
                return AppReturn.defeated("密码错误");
            }
            return AppReturn.succeed("登录成功");
        }
    
        @RequestMapping(value = "index.html")
        public String indexView() {
            return "index";
        }
    
        @RequestMapping(value = "logout.do")
        public String logoutDo() {
    
            if (SecurityUtils.getSubject().isAuthenticated()) {
                // 退出
                SecurityUtils.getSubject().logout();
            }
            return "redirect:login.html";
        }
    
        @RequestMapping(value = "unauthorized.html")
        public String unauthorizedView() {
    
            return "unauthorized";
        }
    }
    @Controller
    public class IndexController {
    
        @Autowired
        private UserService userService;
    
        @RequestMapping(value = "login.html")
        public String loginView() {
    
            // 判断当前用户是否通过认证
            if (SecurityUtils.getSubject().isAuthenticated()) {
                // 认证通过,重定向到首页
                return "redirect:index.html";
            } else {
                // 未认证或认证失败,转发到登录页
                return "login";
            }
        }
    
        @RequestMapping(value = "login.do")
        @ResponseBody
        public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
            return userService.loginDo(username, password);
        }
    
        @RequestMapping(value = "index.html")
        public String indexView() {
            return "index";
        }
    
        @RequestMapping(value = "logout.do")
        public String logoutDo() {
    
            if (SecurityUtils.getSubject().isAuthenticated()) {
                // 退出
                SecurityUtils.getSubject().logout();
            }
            return "redirect:login.html";
        }
    
        @RequestMapping(value = "unauthorized.html")
        public String unauthorizedView() {
    
            return "unauthorized";
        }
    }
    

    Web 页面

    引入 jquery.js

    login.html

    <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
        <div>
            用户名:<input id="username" name="username" type="text" /><br/>
            密码:<input id="password" name="password" type="password"><br/>
            <span id="tip" class="tip"></span><br/>
            <button onclick="login()">点击登录</button>
        </div>
    </body>
    
    <script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
    <script type="text/javascript">
        function login() {
            var username = $('#username').val()
            var password = $('#password').val()
            $.ajax({
                url: '/login.do'
                , data: {
                    username: username
                    , password: password
                }
                , type: 'post'
                , dataType: 'json'
                , success: function(res) {
                    if (res.code == 200) {
                        // 登录成功,跳转到 index.html
                        window.location.href = '/index.html'
                    } else {
                        // 登录失败,提示登录错误信息
                        $("#tip").text(res.msg)
                    }
                }
                , error: function() {
                    $("#tip").text('服务器响应失败')
                }
            })
        }
    </script>
    </html>
    

    index.html

    <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        Hello Shiro
        <a href="/logout.do">退出</a>
    </body>
    </html>
    

    unauthorized.html

    <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>无权访问</title>
    </head>
    <body>
        权限不足
    </body>
    </html>
    

    Java 中使用 Shiro 权限注解

    除了在 ShiroConfig 配置类中自定义权限过滤规则,还可以使用 Shiro 提供的注解实现权限过滤,在 Controller 中的每个请求方法上可以添加以下注解实现权限控制:

    @RequiresAuthentication: 只有认证通过的用户才能访问

    @RequiresRoles(value = {“root”}, logical = Logical.OR)

    • value:指定拥有 root 角色才能访问,角色可以是多个,以逗号隔开
    • logical:该属性有两个值,Logical.OR(只要拥有其中一个角色就能访问),Logical.AND(需要拥有指定的全部角色才能访问,否则会抛出权限不足异常)

    @RequiresPermissions(value = {“/user/delete”}, logical = Logical.OR)

    • value:指定拥有 /user/delete 权限才能访问,权限可以是多个,以逗号隔开
    • logical:有两个值,Logical.OR(只要拥有其中一个权限就访问),Logical.AND(需要拥有指定的全部权限才能访问,否则会抛出权限不足异常)

    Thymeleaf 模板中使用 Shiro 权限标签

    修改 thymeleaf 模板的 html 标签,加入 xmlns:shiro=”http://www.pollix.at/thymeleaf/shiro 命名空间:

    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org"
          xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
    

    常用的 Shiro 标签有以下:

    登录

  • 相关阅读:
    springboot 集成RabbitMQ
    服务接口API限流 Rate Limit 续
    服务接口API限流 Rate Limit
    聊下并发和Tomcat线程数
    java 线程池 异步任务
    Tomcat中更改网站根目录和默认页的配置方法
    QPS从0到4000请求每秒,谈达达后台架构演化之路
    分布式与集群是什么 ? 区别是什么?
    大型网站技术架构演变总结
    提升高并发量服务器性能解决思路
  • 原文地址:https://www.cnblogs.com/antoniopeng/p/12687281.html
Copyright © 2011-2022 走看看