• spring boot:spring security用mysql数据库实现RBAC权限管理(spring boot 2.3.1)


    一,用数据库实现权限管理要注意哪些环节?

    1,需要生成spring security中user类的派生类,用来保存用户id和昵称等信息,

        避免页面上显示用户昵称时需要查数据库

    2,如果需要在页面上显示用户的登录信息,

       需要自定义一个interceptor,

       把用户的昵称等信息添加到 modelandview

    3,普通用户的角色,即默认的权限,因为每个用户都具有,

       就不要写入到数据表中,

       避免数据量大时查询缓慢

    说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

             对应的源码可以访问这里获取: https://github.com/liuhongdi/

    说明:作者:刘宏缔 邮箱: 371125307@qq.com

    二,演示项目的相关信息

    1,项目地址:

    https://github.com/liuhongdi/securitylogin

    2,项目功能说明

            演示了使用数据库实现的用户RBAC权限管理 

           三种页面:

            无权限限制页面:任何人都可访问

            需登录页面:修改密码等:登录才可以访问

           有权限限制页面:必须授予相应的角色后才能访问

    3,项目结构:如图:

    三,配置文件说明

    1,pom.xml

            <!--security begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <!--thymeleaf begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <!--validation begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
            <!--mysql mybatis begin-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.3</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <!-- JSON解析fastjson begin-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.72</version>
            </dependency>

    2,application.properties

    #thymeleaf
    spring.thymeleaf.cache=false
    spring.thymeleaf.encoding=UTF-8
    spring.thymeleaf.mode=HTML
    spring.thymeleaf.prefix=classpath:/templates/
    spring.thymeleaf.suffix=.html
    
    #mysql
    spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=lhddemo
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    #mybatis
    mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
    mybatis.type-aliases-package=com.example.demo.mapper
    
    #error
    server.error.include-stacktrace=always
    #log
    logging.level.org.springframework.web=trace

    3,数据库:

     表结构:

    CREATE TABLE `sys_user` (
     `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
     `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用户名',
     `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码',
     `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称',
     PRIMARY KEY (`userId`),
     UNIQUE KEY `userName` (`userName`)
    ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'

    添加数据 :

    INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES
    (1, 'lhd', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '老刘'),
    (2, 'admin', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '管理员'),
    (3, 'merchant', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '商户老张');

    说明:3个密码都是111111,仅供演示使用,大家在生产环境中一定不要这样设置

    CREATE TABLE `sys_user_role` (
     `urId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
     `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
     `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色id',
     PRIMARY KEY (`urId`),
     UNIQUE KEY `userId` (`userId`,`roleName`)
    ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表'

    插入数据:

    INSERT INTO `sys_user_role` (`urId`, `userId`, `roleName`) VALUES
    (1, 2, 'ADMIN'),
    (2, 3, 'MERCHANT');

    四,java代码说明

    1,WebSecurityConfig.java

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        private final static BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder();
        @Resource
        private UserLoginFailureHandler userLoginFailureHandler;//登录失败的处理类
        @Resource
        private UserLoginSuccessHandler userLoginSuccessHandler;//登录成功的处理类
        @Resource
        private UserLogoutSuccessHandler userLogoutSuccessHandler;//退出成功的处理类
        @Resource
        private UserAccessDeniedHandler userAccessDeniedHandler;//无权访问的处理类
        @Resource
        private SecUserDetailService secUserDetailService;     //用户信息类,用来得到UserDetails
    
        //指定加密的方式,避免出现:There is no PasswordEncoder mapped for the id "null"
        @Bean
        public PasswordEncoder passwordEncoder(){//密码加密类
            return  new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //static
            http.authorizeRequests()
                    .antMatchers("/css/**","/js/**","/img/**")//静态资源等不需要验证
                    .permitAll();
            //permitall
            http.authorizeRequests()
                    .antMatchers("/home/**")//permitall
                    .permitAll();
            //login
            http.formLogin()
                    .loginPage("/login/login")
                    .loginProcessingUrl("/login/logined")//发送Ajax请求的路径
                    .usernameParameter("username")//请求验证参数
                    .passwordParameter("password")//请求验证参数
                    .failureHandler(userLoginFailureHandler)//验证失败处理
                    .successHandler(userLoginSuccessHandler)//验证成功处理
                    .permitAll(); //登录页面用户任意访问
            //logout
            http.logout()
                    .logoutUrl("/login/logout")
                    .logoutSuccessUrl("/login/logout")
                    .logoutSuccessHandler(userLogoutSuccessHandler)//登出处理
                    .deleteCookies("JSESSIONID")
                    .clearAuthentication(true)
                    .invalidateHttpSession(true)
                    .permitAll();
             //有角色的用户才能访问
             http.authorizeRequests()
                     .antMatchers("/admin/**").hasRole("ADMIN")
                     .antMatchers("/merchant/**").hasAnyRole("MERCHANT","ADMIN");
    
            //其他任何请求,登录后可以访问
            http.authorizeRequests().anyRequest().authenticated();
            //accessdenied
            http.exceptionHandling().accessDeniedHandler(userAccessDeniedHandler);//无权限时的处理
            //user detail
            http.userDetailsService(secUserDetailService);
            //rememberme
            //图形验证码
            //http.csrf().disable();
      }
    
        @Resource
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(secUserDetailService).passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence charSequence) {
                    return ENCODER.encode(charSequence);
                }
                //密码匹配,看输入的密码经过加密与数据库中存放的是否一样
                @Override
                public boolean matches(CharSequence charSequence, String s) {
                    return ENCODER.matches(charSequence,s);
                }
            });
        }
    }

    2,SecUser.java

    public class SecUser extends User {
        //用户id
        private int userid;
        //用户昵称
        private String nickname;
    
        public SecUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, authorities);
        }
    
        public SecUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        }
    
        public String getNickname() {
            return nickname;
        }
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
    
        public int getUserid() {
            return userid;
        }
        public void setUserid(int userid) {
            this.userid = userid;
        }
    }

    spring security中User类的子类,增加了用户id和昵称,

    需要保存到session中的信息,在这里扩展

    目的是避免在每个页面上显示用户信息需要查数据库

    3,SecUserDetailService.java

    /**
     * Created by liuhongdi on 2020/07/09.
    */
    @Component("SecUserDetailService")
    public class SecUserDetailService implements UserDetailsService{
        @Resource
        private SysUserService sysUserService;
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            //查库
            SysUser oneUser = sysUserService.getOneUserByUsername(s);//数据库查询 看用户是否存在
            String encodedPassword = oneUser.getPassword();
            Collection<GrantedAuthority> collection = new ArrayList<>();//权限集合
            //用户权限:需要加 ROLE_
            List<String> roles = oneUser.getRoles();
            //System.out.println(roles);
            for (String roleone : roles) {
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone);
                collection.add(grantedAuthority);
           }
            //增加用户的userid,nickname
            SecUser user = new SecUser(s,encodedPassword,collection);
            user.setUserid(oneUser.getUserId());
            user.setNickname(oneUser.getNickName());
            return user;
        }
    }

    4,UserAccessDeniedHandler.java

    @Component("UserAccessDeniedHandler")
    public class UserAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                           AccessDeniedException e) throws IOException, ServletException {
            boolean isAjax = ServletUtil.isAjax();
            //System.out.println("isajax:"+isAjax);
            if (isAjax == true) {
    ServletUtil.printRestResult(RestResult.error(ResponseCode.ACCESS_DENIED));
            } else {
    ServletUtil.printString(ResponseCode.ACCESS_DENIED.getMsg());
            }
        }
    }

    5,UserLoginFailureHandler.java

    @Component("UserLoginFailureHandler")
    public class UserLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                            AuthenticationException exception) throws IOException, ServletException {
            //System.out.println("UserLoginFailureHandler");
            ServletUtil.printRestResult(RestResult.error(ResponseCode.LOGIN_FAIL));
        }
    }

    6,UserLoginSuccessHandler.java

    @Component("UserLoginSuccessHandler")
    public class UserLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            //System.out.println("UserLoginSuccessHandler");
            ServletUtil.printRestResult(RestResult.success(0,"登录成功"));
        }
    }

    7,UserLogoutSuccessHandler.java

    @Component("UserLogoutSuccessHandler")
    public class UserLogoutSuccessHandler implements LogoutSuccessHandler{
        @Override
        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            httpServletRequest.getSession().invalidate();
            ServletUtil.printRestResult(RestResult.success(0,"退出成功"));
        }
    }

    8,WebInterceptor.java

    @Component
    public class WebInterceptor extends HandlerInterceptorAdapter {
        //如果view不为空,把登录信息传递给模板
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
            if (modelAndView != null) {
                ModelMap modelMap = modelAndView.getModelMap();
                SecUser currentUser = SessionUtil.getCurrentUser();
                if (currentUser != null) {
                    modelMap.addAttribute("is_login","1");
                    modelMap.addAttribute("login_username",currentUser.getNickname());
                } else {
                    modelMap.addAttribute("is_login","0");
                    modelMap.addAttribute("login_username","");
                }
            }
        }
    }

    负责把传递页面公共部分显示的数据到模板

    9,login.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta content="text/html;charset=UTF-8"/>
        <title>登录页面</title>
        <script type="text/javascript" language="JavaScript" src="/js/jquery-1.6.2.min.js"></script>
        <style type="text/css">
            body {
                padding-top: 50px;
            }
            .starter-template {
                padding: 40px 15px;
                text-align: center;
            }
        </style>
        <!-- CSRF -->
        <meta name="_csrf" th:content="${_csrf.token}"/>
        <!-- default header name is X-CSRF-TOKEN -->
        <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    </head>
    <body>
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div id="navbar" class="collapse navbar-collapse">
                <ul class="nav navbar-nav">
                    <li><a href="/home/home"> 首页 </a></li>
                </ul>
            </div><!--/.nav-collapse -->
        </div>
    </nav>
    <div class="container">
        <div class="starter-template">
            <h2>使用账号密码登录</h2>
                <div class="form-group">
                    <label for="username">账号</label>
                    <input type="text" class="form-control" id="username" name="username" value="" placeholder="账号" />
                </div>
                <div class="form-group">
                    <label for="password">密码</label>
                    <input type="password" class="form-control" id="password" name="password" placeholder="密码" />
                </div>
                <button name="formsubmit" value="登录" onclick="go_login()" >登录</button>
        </div>
    </div>
    <script>
        function go_login(){
            if ($("#username").val() == "") {
                alert('用户名不可为空');
                $("#username").focus();
                return false;
            }
            if ($("#password").val() == "") {
                alert('密码不可为空');
                $("#password").focus();
                return false;
            }
            var postdata = {
                username:$("#username").val(),
                password:$("#password").val(),
            }
            var csrfToken = $("meta[name='_csrf']").attr("content");
            var csrfHeader = $("meta[name='_csrf_header']").attr("content");
            $.ajax({
                type:"POST",
                //type:"GET",
                url:"/login/logined",
                data:postdata,
                //返回数据的格式
                datatype: "json",//"xml", "html", "script", "json", "jsonp", "text".
                beforeSend: function(request) {
                    request.setRequestHeader(csrfHeader, csrfToken); // 添加  CSRF Token
                },
                success:function(data){
                    if (data.code == 0) {
                        alert('login success:'+data.msg);
                        window.location.href="/home/home";
                    } else {
                        alert("failed:"+data.msg);
                    }
                },
                //调用执行后调用的函数
                complete: function(XMLHttpRequest, textStatus){
                },
                //调用出错执行的函数
                error: function(){
                    //请求出错处理
                    alert('error');
                }
            });
        }
    </script>
    </body>
    </html>

    10,页面上用到的其他代码,可以移步github.com上查看

    五,测试效果

    1,访问首页:

    http://127.0.0.1:8080/home/home

    未登录时:

    2,以普通用户lhd登录:

    访问:管理员首页/商户首页,都会得到提示

    无权访问

    访问修改密码 页面,可以访问

    3,以merchant用户登录:

       role是MERCHANT

    访问:管理员首页,提示:

    无权访问

    访问商户首页:可以访问

    访问修改密码 页面,可以访问

    4,以admin用户登录:

    role是ADMIN

    访问管理员首页:可以访问

    访问商户首页:可以访问

    访问修改密码 页面,可以访问

    六,查看spring boot版本:

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.3.1.RELEASE)
  • 相关阅读:
    某些输入文件使用或覆盖了已过时的 API
    laravel 重写以及500错误
    Ubuntu镜像使用帮助
    E: Sub-process /usr/bin/dpkg returned an error code (1) 解决方案
    python请求java Selenium Webdriver
    Selenium Grid 简易安装
    selenium + python 添加等待时间
    selenium帮助手册以及 webdriver的各种driver
    thinkphp结合layui上传图片
    thinkphp----替换写标签的方法
  • 原文地址:https://www.cnblogs.com/architectforest/p/13576701.html
Copyright © 2020-2023  润新知