美文网首页
Spring Boot Security5 动态用户角色资源的权

Spring Boot Security5 动态用户角色资源的权

作者: Ceruleano | 来源:发表于2019-08-15 21:22 被阅读0次

    前言

    上篇文章介绍了Spring Boot Security基于Redis的Spring Session管理

    本篇文章,可以说比较核心、实用的功能,动态用户角色资源管理(RBAC),可能篇幅会比较长,废话不多说,马上进入正题

    简单介绍

    相信每个正规的系统,都会对系统安全访问权限有严格的控制。简单的一句话总结,就是对的人访问对的资源,这里可能会比较抽象,小编给大家举个例子就懂了:

    现在假设有个系统,里面有菜单A、菜单B和菜单C
    客户有这么个需求,就是对于管理员来说,可以访问所有资源菜单,对于普通用户来说,只能访问菜单A和菜单B,如图:

    image.png

    相信这个也是广大系统都有的最基础的需求,那么在系统中的表现,就是用户登录了系统后,如果是普通用户的话,前端只显示菜单A和菜单B,其他途径访问(直接输入URL)菜单C会被提示无权限,而管理员则显示所有菜单

    那么怎么实现呢,小编这里就是基于RABC模型去实现的,简单来说就是:


    image.png

    举个例子:

    • 用户就是登陆系统的用户,像张三、李四、小王这样的具体登陆用户
    • 角色就是假如张三是教师、李四是学生,那么教师和学生角色,也可能可以分得更细,这个根据需求来定义
    • 资源就是访问系统的资源,如查询学生信息、编辑学生信息等等之类

    用户和资源是没有直接关联的,用户是通过关联角色,角色再关联资源这种间接的方式去判断自己的资源权限。这样做的好处就是可以更简单直观的去管理用户资源间的关联,不需要说每创建一个用户,就去再重新分配资源这么繁琐,减少数据库冗余设计

    数据库设计

    数据库表的设计如图:


    20191213203541355.png

    这里有几点要说明下:

    • 一般 用户 与 角色 是一对一或者一对多的关系,我这里为了方便所以选择一对一的关系
    • 角色 与 资源 是多对多的关系,所以需要中间表 sys_role_resource 存储中间的联系

    实体代码如下:

    Role.java

    package com.demo.ssdemo.sys.entity;
    
    import com.alibaba.fastjson.annotation.JSONField;
    import org.springframework.security.core.GrantedAuthority;
    
    import javax.persistence.*;
    import java.io.Serializable;
    import java.util.Set;
    
    @Entity
    @Table(name = "sys_role")
    public class Role implements GrantedAuthority {
    
        //id
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column
        protected Integer id;
    
        //角色标识
        @Column
        private String roleKey;
    
        //角色名称
        @Column
        private String roleName;
    
        //角色拥有的资源
        @ManyToMany(targetEntity = Resource.class, fetch = FetchType.EAGER)
        @JoinTable(
                name = "sys_role_resource",
                joinColumns = {
                        @JoinColumn(name = "role_id", referencedColumnName = "id", nullable = false)
                },
                inverseJoinColumns = {
                        @JoinColumn(name = "resource_id", referencedColumnName = "id", nullable = false)
                })
        private Set<Resource> resources;
    
        @Override
        public String getAuthority() {
            return roleKey;
        }
        
        ...get、set方法...
        
    }
    

    这里要说明下,GrantedAuthority 接口中的getAuthorities()方法返回的当前用户对象拥有的权限,简单的说就是该用户的角色信息,所以这里我用角色标识roleKey表示

    Resource.java

    package com.demo.ssdemo.sys.entity;
    
    import javax.persistence.*;
    import java.io.Serializable;
    
    @Entity
    @Table(name = "sys_resource")
    public class Resource  implements Serializable {
    
        //id
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column
        protected Integer id;
    
        //资源名称
        @Column(nullable = false)
        private String resourceName;
        
        //资源标识
        @Column(nullable = false)
        private String resourceKey;
    
        //资源url
        @Column(nullable = false)
        private String url;
    
        /**
         * 资源类型
         * 0:菜单
         * 1:按钮
         */
        @Column(nullable = false)
        private Integer type;
    
        ...get、set方法...
    }
    

    相信这些代码大家都看得明白,下面开始进入核心部分

    实现

    在这里,小编介绍下怎么在Spring Security中实现资源管理功能,也就是针对不同的用户角色,动态的判断是否能访问相应的资源菜单

    先看看项目结构图:


    20191213203541355.png

    首先,我们需要在自定义登录认证那里,设置权限信息:

    LoginValidateAuthenticationProvider.java

    package com.demo.ssdemo.core;
    
    import com.demo.ssdemo.sys.entity.User;
    import com.demo.ssdemo.sys.service.UserService;
    import org.springframework.security.authentication.*;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    import javax.annotation.Resource;
    import java.util.HashSet;
    import java.util.Set;
    
    /**
     * @Description 自定义登陆验证
     **/
    @Component
    public class LoginValidateAuthenticationProvider implements AuthenticationProvider {
    
        @Resource
        private UserService userService;
    
        @Resource
        private PasswordEncoder passwordEncoder;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            //获取输入phone
            String username = authentication.getName();
            String rawPassword = (String) authentication.getCredentials();
    
            //查询用户是否存在
            User user = (User) userService.loadUserByUsername(username);
    
            if (user.isEnabled()) {
                throw new DisabledException("该账户已被禁用,请联系管理员");
                
            } else if (user.isAccountNonLocked()) {
                throw new LockedException("该账号已被锁定");
    
            } else if (user.isAccountNonExpired()) {
                throw new AccountExpiredException("该账号已过期,请联系管理员");
    
            } else if (user.isCredentialsNonExpired()) {
                throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录");
            }
    
            //验证密码
            if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
                throw new BadCredentialsException("输入密码错误!");
            }
    
            //设置角色权限信息
            Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
            grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().getRoleKey()));
            user.setAuthorities(grantedAuthorities);
    
            return new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            //确保authentication能转成该类
            return authentication.equals(UsernamePasswordAuthenticationToken.class);
        }
    
    }
    
    

    这里要注意的是,我们把resource实体的resourceKey作为资源的权限标识,设置进grantedAuthorities集合里面,以便spring security根据注解@PreAuthorize自动权限判断

    由于我们设计的用户与角色是一对一关联,所以我们这里GrantedAuthority集合就只有一条角色信息数据

    然后就是自定义权限不足handler

    PerAccessDeniedHandler.java

    package com.demo.ssdemo.core.handler;
    
    import com.alibaba.fastjson.JSONObject;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class PerAccessDeniedHandler implements AccessDeniedHandler {
    
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            //登录成功返回
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("code", "503");
            paramMap.put("message", accessDeniedException.getMessage());
            //设置返回请求头
            response.setContentType("application/json;charset=utf-8");
            //写出流
            PrintWriter out = response.getWriter();
            out.write(JSONObject.toJSONString(paramMap));
            out.flush();
            out.close();
        }
    
    }
    
    

    最后我们看看Spring Security配置类的变化:

    SecurityConfig.java

    package com.demo.ssdemo.config;
    
    import com.demo.ssdemo.core.LoginValidateAuthenticationProvider;
    import com.demo.ssdemo.core.handler.LoginFailureHandler;
    import com.demo.ssdemo.core.handler.LoginSuccessHandler;
    import com.demo.ssdemo.core.handler.PerAccessDeniedHandler;
    import com.demo.ssdemo.sys.service.UserService;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    
    /**
     * @Author OZY
     * @Date 2019/08/08 13:59
     * @Description
     * @Version V1.0
     **/
    
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled=true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        /**
         * 数据源
         */
        @Resource
        private DataSource dataSource;
    
        /**
         * 用户业务层
         */
        @Resource
        private UserService userService;
    
        /**
         * 自定义认证
         */
        @Resource
        private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;
    
        /**
         * 登录成功handler
         */
        @Resource
        private LoginSuccessHandler loginSuccessHandler;
    
        /**
         * 登录失败handler
         */
        @Resource
        private LoginFailureHandler loginFailureHandler;
    
        /**
         * 权限不足handler
         */
        @Resource
        private PerAccessDeniedHandler perAccessDeniedHandler;
    
    
        /**
         * 权限核心配置
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //基础设置
            http.httpBasic()//配置HTTP基本身份验证
                .and()
                    .authorizeRequests()
                    .anyRequest().authenticated()//所有请求都需要认证
                .and()
                    .formLogin() //登录表单
                    .loginPage("/login")//登录页面url
                    .loginProcessingUrl("/login")//登录验证url
                    .defaultSuccessUrl("/index")//成功登录跳转
                    .successHandler(loginSuccessHandler)//成功登录处理器
                    .failureHandler(loginFailureHandler)//失败登录处理器
                    .permitAll()//登录成功后有权限访问所有页面
                .and()
                    .exceptionHandling().accessDeniedHandler(perAccessDeniedHandler)//设置权限不足handler
                .and()
                    .rememberMe()//记住我功能
                    .userDetailsService(userService)//设置用户业务层
                    .tokenRepository(persistentTokenRepository())//设置持久化token
                    .tokenValiditySeconds(24 * 60 * 60); //记住登录1天(24小时 * 60分钟 * 60秒)
    
            //关闭csrf跨域攻击防御
            http.csrf().disable();
    
        }
    
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //这里要设置自定义认证
            auth.authenticationProvider(loginValidateAuthenticationProvider);
        }
    
    
        /**
         * BCrypt加密方式
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 记住我功能,持久化的token服务
         * @return
         */
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            //数据源设置
            tokenRepository.setDataSource(dataSource);
            //启动的时候创建表,这里只执行一次,第二次就注释掉,否则每次启动都重新创建表
            //tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    
    }
    

    在Spring Security配置文件中,我们只需要设置PerAccessDeniedHandler 就可以了,还要记得在头部添加@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled=true)注解,以启动spring security注解生效

    接下来就是前端页面和控制层:

    package com.demo.ssdemo.sys.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @Controller
    @RequestMapping("/")
    public class UserController {
    
        /**
         * 登录页面跳转
         * @return
         */
        @GetMapping("login")
        public String login() {
            return "login.html";
        }
    
        /**
         * index页跳转
         * @return
         */
        @GetMapping("index")
        public String index() {
            return "index.html";
        }
    
        /**
         * menu1
         * @return
         */
        @PreAuthorize("hasAuthority('menu1')")
        @GetMapping("menu1")
        @ResponseBody
        public String menu1() {
            return "menu1";
        }
    
        /**
         * menu2
         * @return
         */
        @PreAuthorize("hasAuthority('menu2')")
        @GetMapping("menu2")
        @ResponseBody
        public String menu2() {
            return "menu2";
        }
    
        /**
         * menu3
         * @return
         */
        @PreAuthorize("hasAuthority('menu3')")
        @GetMapping("menu3")
        @ResponseBody
        public String menu3() {
            return "menu3";
        }
        
    }
    

    这里要注意的是,每个需要权限判断的方法中,都需要增加@PreAuthorize("hasAuthority('key')")注解,否则权限判断不生效,key对应数据库资源表中的资源标识字段

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>index页</title>
    </head>
    <body>
    index页<br/><br/>
    
    <button id="menu1Btn" type="button" onclick="sendAjax('/menu1')">菜单1</button>
    <button id="menu2Btn" type="button" onclick="sendAjax('/menu2')">菜单2</button>
    <button id="menu3Btn" type="button" onclick="sendAjax('/menu3')">菜单3</button>
    
    <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
    <script type="text/javascript">
    
        function sendAjax(url) {
    
            $.ajax({
                type: "GET",
                url: url,
                dataType: "text",
                success: function (data) {
                    console.log(data);
                }
            });
        }
    
    </script>
    </body>
    </html>
    

    这里简单的说说数据库的数据

    用户表:admin、teacher1和student1
    角色表:管理员、教师和学生
    资源表:menu1、menu2、menu3

    对应权限:
    管理员:menu1、menu2、menu3
    教师:menu1、menu2
    学生:meun1

    下面我们看看效果,登录页

    image.png

    index页:

    image.png

    这里我们先用admin管理员角色登录,然后点击所有菜单

    image.png

    可以看到数据正常,并且已经访问到了所有资源菜单

    然后我们用 teacher1教师角色 登录,也是点击所有菜单

    image.png

    会发现,在点击第三个菜单的时候,会返回没有权限访问

    我们再用 student1学生角色 登录,也是点击所有菜单

    image.png

    这里说明我们的动态权限资源管理都生效了

    那么文章就介绍到这里,在这里留了个坑,一般系统是不会让用户去点击了菜单才发现没有权限访问,而是针对不同的用户,动态显示不同的菜单,这个内容小编下篇文章就会讲解

    demo也已经放到github,获取方式在文章的Spring Boot2 + Spring Security5 系列搭建教程开头篇(1) 结尾处

    如果小伙伴遇到什么问题,或者哪里不明白欢迎评论或私信,也可以在公众号里面私信问都可以,谢谢大家~

    相关文章

      网友评论

          本文标题:Spring Boot Security5 动态用户角色资源的权

          本文链接:https://www.haomeiwen.com/subject/gzitsctx.html