首页 > 技术文章 > SpringBoot+Mybatis+PostMan(九):用户角色权限访问控制二(加入资源表和资源角色对应表)

yeyuting 2021-02-02 11:29 原文

Springboot+Mybatis+redis+postman项目实战总目录*

第二篇章:用户角色权限访问控制

SpringBoot+Mybatis+PostMan(十):用户角色权限访问控制三(禁用session、启用token并集成redis)

 

在实现用户角色控制后,接下来我们将资源整理放入数据库,然后对数据库资源分别进行角色控制,这个部分主要用到技术是springboot security ,接下来一步一步讲解。

项目代码下载地址:https://github.com/yeyuting-1314/testdemo_roleControll-1.5.git

一、准备工作

角色控制开始之前,先进行一些准备工作

1. 数据库创建资源表和资源角色对应表,如下:

         

CREATE TABLE testdemo.`sys_privilege` (
    `id` bigint NOT NULL AUTO_INCREMENT comment '权限ID ',
    `privilege_name` varchar(50) comment '权限名',
    `privilege_url` varchar(50) comment '资源访问路径',
    PRIMARY KEY (`id`)
);

CREATE TABLE testdemo.`sys_role_privilege` (
    `role_id` bigint default null comment '角色id ',
    `privilege_id` bigint default null comment '权限id'
);

 2. pom文件引入依赖:

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

3. Privilege实体类实现:

/**
 * @author yeyuting
 * @create 2021/1/28
 */
public class Privilege {
    int id ;
    String privilegeName ;
    String privilegeUrl ;

    private List<Role> roleList ;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getPrivilegeName() {
        return privilegeName;
    }

    public void setPrivilegeName(String privilegeName) {
        this.privilegeName = privilegeName;
    }

    public String getPrivilegeUrl() {
        return privilegeUrl;
    }

    public void setPrivilegeUrl(String privilegeUrl) {
        this.privilegeUrl = privilegeUrl;
    }

    public List<Role> getRoleList() {
        return roleList;
    }

    public void setRoleList(List<Role> roleList) {
        this.roleList = roleList;
    }
}

4. PrivilegeMapper类实现:

/**
 * @author yeyuting
 * @create 2021/1/28
 */
@Repository
@Mapper
public interface PrivilegeMapper {
    List<Privilege>  getAllPrivileges()  ;

}

5. privilegeMapper.xml实现:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="select.system.dao.PrivilegeMapper">
    <resultMap id="privilegeMap"  type="select.system.dto.Privilege">
        <result property="id" column="id"/>
        <result property="privilegeName" column="privilege_name"/>
        <result property="privilegeUrl" column="privilege_url"/>
    </resultMap>

    <resultMap id="privilegeMap1"  extends="privilegeMap"
               type="select.system.dto.Privilege">
        <collection property="roleList" ofType="select.system.dto.Role">
            <result property="id" column="id"/>
            <result property="roleName" column="role_name"/>
            <result property="role" column="role"/>
        </collection>

    </resultMap>


    <select id="getAllPrivileges"  resultMap="privilegeMap1">
        select sp.id ,
        sp.privilege_name ,
        sp.privilege_url  ,
        sr.id  ,
        sr.role  ,
        sr.role_name
        from sys_privilege sp
        LEFT JOIN sys_role_privilege srp  ON sp.id  = srp.privilege_id
        LEFT JOIN sys_role sr  ON sr.id = srp.role_id
    </select>



</mapper>

 

二、Springboot Security部署

 (1)WebSecurityConfig配置文件实现用户身份验证,验证通过后进行的操作(验证通过后查看权限,如果没有权限再执行无权限逻辑),没验证通过的话进行说明操作,以及对页面进行重定向等等,代码如下:

/**
 * @author yeyuting
 * @create 2021/1/28
 */
//将自定义FilterInvocationSecurityMetadataSource和自定义AccessDecisionManager配置到Spring Security的配置类中
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserServiceImpl userDetailsService ;
    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler customAuthenticationSuccessHandler ;
    @Autowired
    AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;
    @Autowired
    SimpleAuthenticationEntryPoint simpleAuthenticationEntryPoint;
    /**
     * 配置角色继承关系
     *
     * @return
     */
    @Bean
    RoleHierarchy roleHierarchy(){
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl() ;
        String hierarchy = "ROLE_SUPERADMIN > ROLE_ADMIN > ROLE_USER" ;
        roleHierarchy.setHierarchy(hierarchy) ;
        return roleHierarchy ;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()) ;

    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //引入数据库对资源访问操作
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(cfisms());
                        object.setAccessDecisionManager(cadm());
                        return object ;
                    }
                })
                .antMatchers("/userLogin").permitAll()
                // 所有访问该应用的http请求都要通过身份认证才可以访问
                .anyRequest().authenticated()
                .and().httpBasic()
                .and()
                .csrf().disable()
                // 指定登陆URL
                .formLogin()
               //页面默认访问页面重命名
                .loginProcessingUrl("/userLogin")
                //如果身份验证通过并且也具有相关权限时执行此操作
                .successHandler(customAuthenticationSuccessHandler)
                //如果身份验证失败则执行此代码逻辑,提示用户先进行登陆操作,用于 用户身份验证
                .failureHandler(customAuthenticationFailureHandler)
                .and()
                //用户访问通过但是不具备相关权限时执行此代码逻辑
                .exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler)
                //用户未进行身份验证就访问系统页面时候会提示用户先进行登录操作,才能顺利进行后续操作,这是一个大入口
                .authenticationEntryPoint(simpleAuthenticationEntryPoint) ;
                //这里暂时通过session对应的cookie判断用户身份是否进行了验证  在后面完善中加入redis集成后再将瑟斯哦那禁用
                // 不需要session
                //.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }


    /**
     * 自定义的FilterInvocationSecurityMetadataSource
     * 将访问当前资源的URL与数据库中访问该资源的URL进行匹配
     *
     * @return
     */
    @Bean
    FilterInvocationSecurityMetadataSource cfisms() {
        return new FilterInvocationSecurityMetadataSource();
    }

    /**
     * 自定义的AccessDecisionManager
     * 判断登录用户是否具备访问当前URL所需要的角色
     *
     * @return
     */
    @Bean
    AccessDecisionManager cadm() {
        return new AccessDecisionManager();
    }


}

(2)AuthenticationAccessDeniedHandler类实现:

/**
 * 无权限返回
 * @author yeyuting
 * @create 2021/1/29
 */
@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        resp.setCharacterEncoding("UTF-8");
        PrintWriter out = resp.getWriter();
        Result result = Results.failure("403","没有权限");
        resp.setContentType("application/json;charset=UTF-8");
        out.println(JSONObject.toJSONString(result));
        out.flush();
        out.close();
    }
}

(3)CustomAuthenticationFailureHandler类实现:

/**
 * 登陆失败
 */
@Component("customAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
                                        HttpServletResponse httpServletResponse,
                                        AuthenticationException e) throws IOException, ServletException {
        Result result = Results.failure("403","请登陆111");
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(JSONObject.toJSONString(result));
    }
}

(4)CustomAuthenticationSuccessHandler类实现:

/**
 * 登陆成功
 * @author yeyuting
 * @create 2021/1/29
 */
@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication auth) throws IOException, ServletException {
        Object principal = auth.getPrincipal() ;
        //生成token,存到redis里面去

        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        response.setStatus(200);
        Map<String , Object > map = new HashMap<>(16) ;
        map.put("status", 200);
        map.put("msg", principal);



        map.put("token", "ddddddd");
        ObjectMapper om = new ObjectMapper();
        out.write(om.writeValueAsString(map));
        out.flush();
        out.close();
    }
}

(5)SimpleAuthenticationEntryPoint类未登陆返回实现:

/**
 * 未登陆返回
 * @author yeyuting
 * @create 2021/1/29
 */
@Component
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        Result result = Results.failure("401","请登陆");
        response.setContentType("application/json;charset=UTF-8");
        out.println(JSONObject.toJSONString(result));
        out.flush();
        out.close();
    }
}

这样一来,spring Security就配置好了,接下来就是实现业务逻辑了。

三、业务逻辑角色权限控制实现

1. 实现AccessDecisionManager类,获取当前登录用户的角色信息和FilterInvocationSecurityMetadataSource类返回的资源访问的相关信息进行比对,判断该用户是否具有访问当前URL的角色

/**
 * @author yeyuting
 * @create 2021/1/28
 */
@Component
//获取当前登录用户的角色信息和FilterInvocationSecurityMetadataSource类返回的资源访问的相关信息进行比对,判断该用户是否具有访问当前URL的角色
public class AccessDecisionManager implements org.springframework.security.access.AccessDecisionManager {

    /**
    *2021/1/28 10:51
    *该方法用于判断当前登录的用户是否具备当前请求的URL所需要的角色
    * * @param auth 包含用户登陆的信息
     * @param o FilterInvocation 对象
     * @param ca CustomFilterInvocationSecurityMetadataSource 中getAttributes()方法的返回值
    * * @return : void
    */
    @Override
    public void decide(Authentication auth, Object o, Collection<ConfigAttribute> ca) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> auths = auth.getAuthorities() ;
        for(ConfigAttribute configAttribute : ca) {
            if("ROLE_USER".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken){
                return;
            }
            for(GrantedAuthority authority : auths){
                if(configAttribute.getAttribute().equals(authority.getAuthority())){
                    return;
                }
            }
            throw new AccessDeniedException("权限不足") ;
        }
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

2. 实现FilterInvocationSecurityMetadataSource类,用来匹配当前用户访问资源的URL和数据库中该资源对应的URL,和获取数据库中该资源对应的角色。

/**
 * @author yeyuting
 * @create 2021/1/28
 */
//用来匹配当前用户访问资源的URL和数据库中该资源对应的URL,和获取数据库中该资源对应的角色
public class FilterInvocationSecurityMetadataSource implements org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource {
    /**
     * 用于实现ant风格的Url匹配
     */
    AntPathMatcher antPathMatcher = new AntPathMatcher() ;

    @Autowired
    PrivilegeMapper privilegeMapper ;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        /**
         * 从参数中获取当前请求的URL
         */
        String requestUrl = ((FilterInvocation) o).getRequestUrl() ;
        int len = requestUrl.indexOf("?");
        if(len != -1){
            requestUrl = requestUrl.substring(0,len);
        }
        List<Privilege> privilegeList = privilegeMapper.getAllPrivileges() ;
        for(Privilege privilege : privilegeList) {
            //将数据库中访问资源的URL与当前访问的URL进行ant风格匹配
            privilege.getPrivilegeUrl() ;
            if (antPathMatcher.match(privilege.getPrivilegeUrl() , requestUrl)) {
                //获取访问一个资源需要的角色
                List<Role> roleList = privilege.getRoleList() ;
                String[] roleArr = new String[roleList.size()] ;
                for(int i = 0 ; i < roleArr.length ; i++){
                    roleArr[i] = roleList.get(i).getRole() ;
                }
                return SecurityConfig.createList(roleArr) ;
            }
        }
        throw new AccessDeniedException("权限不足");
        //return null ;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {

        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {

        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

这样一来,业务代码逻辑也就完成了,接下来postman前端模拟实现信息传递

四、前端模拟

1. 初始页面访问:

 

 2. 访问页面updateById

 

 这里没有提示请登陆是因为之前测试此接口的时候已经生成过cookie,所以身份验证通过,但是在数据库中没有给此接口对应的权限,所以提示没有权限。

3. 访问一个数据库中赋予了权限的资源selectStartIndexAndPageSize,访问接口后发现可以顺利访问,如下所示:

 

这样一来,用户角色权限控制也就实现了,后续我们继续实现redis集成,禁用session,实现用户角色权限控制。

pl:大致捋一下代码逻辑:

1. 默认页登陆:
进入UserServiceImpl的loadUserByUsername方法中,拿到数据库中用户对应的角色,以及对用户登陆名和密码进行校验,校验通过才会往后执行。
紧接着进入customAuthenticationSuccessHandler中,显示登陆成功信息,至此,结束。

2. updateById接口访问
进入JwtAuthenticationFilter,没有Authorization信息,放行,进入FilterInvocationSecurityMetadataSource类,通过当前要访问的资源url和数据库资源对比,如果对比成功,接着就找到对应角色,这里没找到,接着进入SimpleAuthenticationEntryPoint类,提示用户登陆。

2. updateById接口访问 , 将cookie给他
进入JwtAuthenticationFilter,没有Authorization信息,放行,进入FilterInvocationSecurityMetadataSource类,通过当前要访问的资源url和数据库资源对比,如果对比成功,接着就找到对应角色,这里没找到,说明当前资源url在数据库中没有对应的角色与之对应,无法正常访问。接着进入AuthenticationAccessDeniedHandler,提示没有对应权限(这里为什么进入了AuthenticationAccessDeniedHandler,而不是进入SimpleAuthenticationEntryPoint中的原因,是WebSecurityConfig中的session起到了作用,我们在UpdateById中赋予的cookie会跟着进入配置中,作为一个判断的条件,如果cookie存在且正确时就跳转到权限不足代码逻辑段,否则就是请登陆逻辑段)

3. updateById接口访问,将cookie给他 同时数据库进行设置
进入JwtAuthenticationFilter,没有Authorization信息,放行,进入FilterInvocationSecurityMetadataSource类,通过当前要访问的资源url和数据库资源对比,如果对比成功,接着就找到对应角色,这里找到了对应的角色ROLE_USER ,接着进入AccessDecisionManager类,判断当前登录的用户是否具备当前请求的URL所需要的角色,这里auth和ca进行对比,对比成功,接下来就执行控制层updateById方法,正常执行,过程结束。

 这里通过debug的形式可以看到程序走向,在debug期间注意不能断开,如果断开的话cookie将变成无效,就达不到一次性走完全过程的效果。

至此,结束。

推荐阅读