Spring Boot Security5 動態(tài)用戶角色資源的權(quán)限管理(6)

前言

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

本篇文章,可以說比較核心、實用的功能,動態(tài)用戶角色資源管理(RBAC),可能篇幅會比較長,廢話不多說,馬上進入正題

簡單介紹

相信每個正規(guī)的系統(tǒng),都會對系統(tǒng)安全訪問權(quán)限有嚴格的控制。簡單的一句話總結(jié),就是對的人訪問對的資源,這里可能會比較抽象,小編給大家舉個例子就懂了:

現(xiàn)在假設有個系統(tǒng),里面有菜單A、菜單B和菜單C
客戶有這么個需求,就是對于管理員來說,可以訪問所有資源菜單,對于普通用戶來說,只能訪問菜單A和菜單B,如圖:

image.png

相信這個也是廣大系統(tǒng)都有的最基礎的需求,那么在系統(tǒng)中的表現(xiàn),就是用戶登錄了系統(tǒng)后,如果是普通用戶的話,前端只顯示菜單A和菜單B,其他途徑訪問(直接輸入URL)菜單C會被提示無權(quán)限,而管理員則顯示所有菜單

那么怎么實現(xiàn)呢,小編這里就是基于RABC模型去實現(xiàn)的,簡單來說就是:


image.png

舉個例子:

  • 用戶就是登陸系統(tǒng)的用戶,像張三、李四、小王這樣的具體登陸用戶
  • 角色就是假如張三是教師、李四是學生,那么教師和學生角色,也可能可以分得更細,這個根據(jù)需求來定義
  • 資源就是訪問系統(tǒng)的資源,如查詢學生信息、編輯學生信息等等之類

用戶和資源是沒有直接關聯(lián)的,用戶是通過關聯(lián)角色,角色再關聯(lián)資源這種間接的方式去判斷自己的資源權(quán)限。這樣做的好處就是可以更簡單直觀的去管理用戶資源間的關聯(lián),不需要說每創(chuàng)建一個用戶,就去再重新分配資源這么繁瑣,減少數(shù)據(jù)庫冗余設計

數(shù)據(jù)庫設計

數(shù)據(jù)庫表的設計如圖:


20191213203541355.png

這里有幾點要說明下:

  • 一般 用戶 與 角色 是一對一或者一對多的關系,我這里為了方便所以選擇一對一的關系
  • 角色 與 資源 是多對多的關系,所以需要中間表 sys_role_resource 存儲中間的聯(lián)系

實體代碼如下:

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()方法返回的當前用戶對象擁有的權(quán)限,簡單的說就是該用戶的角色信息,所以這里我用角色標識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方法...
}

相信這些代碼大家都看得明白,下面開始進入核心部分

實現(xiàn)

在這里,小編介紹下怎么在Spring Security中實現(xiàn)資源管理功能,也就是針對不同的用戶角色,動態(tài)的判斷是否能訪問相應的資源菜單

先看看項目結(jié)構(gòu)圖:


20191213203541355.png

首先,我們需要在自定義登錄認證那里,設置權(quán)限信息:

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("該賬戶已被禁用,請聯(lián)系管理員");
            
        } else if (user.isAccountNonLocked()) {
            throw new LockedException("該賬號已被鎖定");

        } else if (user.isAccountNonExpired()) {
            throw new AccountExpiredException("該賬號已過期,請聯(lián)系管理員");

        } else if (user.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException("該賬戶的登錄憑證已過期,請重新登錄");
        }

        //驗證密碼
        if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
            throw new BadCredentialsException("輸入密碼錯誤!");
        }

        //設置角色權(quán)限信息
        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能轉(zhuǎn)成該類
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

這里要注意的是,我們把resource實體的resourceKey作為資源的權(quán)限標識,設置進grantedAuthorities集合里面,以便spring security根據(jù)注解@PreAuthorize自動權(quán)限判斷

由于我們設計的用戶與角色是一對一關聯(lián),所以我們這里GrantedAuthority集合就只有一條角色信息數(shù)據(jù)

然后就是自定義權(quán)限不足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 {

    /**
     * 數(shù)據(jù)源
     */
    @Resource
    private DataSource dataSource;

    /**
     * 用戶業(yè)務層
     */
    @Resource
    private UserService userService;

    /**
     * 自定義認證
     */
    @Resource
    private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;

    /**
     * 登錄成功handler
     */
    @Resource
    private LoginSuccessHandler loginSuccessHandler;

    /**
     * 登錄失敗handler
     */
    @Resource
    private LoginFailureHandler loginFailureHandler;

    /**
     * 權(quán)限不足handler
     */
    @Resource
    private PerAccessDeniedHandler perAccessDeniedHandler;


    /**
     * 權(quán)限核心配置
     * @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")//成功登錄跳轉(zhuǎn)
                .successHandler(loginSuccessHandler)//成功登錄處理器
                .failureHandler(loginFailureHandler)//失敗登錄處理器
                .permitAll()//登錄成功后有權(quán)限訪問所有頁面
            .and()
                .exceptionHandling().accessDeniedHandler(perAccessDeniedHandler)//設置權(quán)限不足handler
            .and()
                .rememberMe()//記住我功能
                .userDetailsService(userService)//設置用戶業(yè)務層
                .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();
        //數(shù)據(jù)源設置
        tokenRepository.setDataSource(dataSource);
        //啟動的時候創(chuàng)建表,這里只執(zhí)行一次,第二次就注釋掉,否則每次啟動都重新創(chuàng)建表
        //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 {

    /**
     * 登錄頁面跳轉(zhuǎn)
     * @return
     */
    @GetMapping("login")
    public String login() {
        return "login.html";
    }

    /**
     * index頁跳轉(zhuǎn)
     * @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";
    }
    
}

這里要注意的是,每個需要權(quán)限判斷的方法中,都需要增加@PreAuthorize("hasAuthority('key')")注解,否則權(quán)限判斷不生效,key對應數(shù)據(jù)庫資源表中的資源標識字段

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>

這里簡單的說說數(shù)據(jù)庫的數(shù)據(jù)

用戶表:admin、teacher1和student1
角色表:管理員、教師和學生
資源表:menu1、menu2、menu3

對應權(quán)限:
管理員:menu1、menu2、menu3
教師:menu1、menu2
學生:meun1

下面我們看看效果,登錄頁

image.png

index頁:

image.png

這里我們先用admin管理員角色登錄,然后點擊所有菜單

image.png

可以看到數(shù)據(jù)正常,并且已經(jīng)訪問到了所有資源菜單

然后我們用 teacher1教師角色 登錄,也是點擊所有菜單

image.png

會發(fā)現(xiàn),在點擊第三個菜單的時候,會返回沒有權(quán)限訪問

我們再用 student1學生角色 登錄,也是點擊所有菜單

image.png

這里說明我們的動態(tài)權(quán)限資源管理都生效了

那么文章就介紹到這里,在這里留了個坑,一般系統(tǒng)是不會讓用戶去點擊了菜單才發(fā)現(xiàn)沒有權(quán)限訪問,而是針對不同的用戶,動態(tài)顯示不同的菜單,這個內(nèi)容小編下篇文章就會講解

demo也已經(jīng)放到github,獲取方式在文章的Spring Boot2 + Spring Security5 系列搭建教程開頭篇(1) 結(jié)尾處

如果小伙伴遇到什么問題,或者哪里不明白歡迎評論或私信,也可以在公眾號里面私信問都可以,謝謝大家~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容