第四章:配置Spring Security數據庫用戶登錄,並創建RBAC模型設計

如果不是很瞭解他,可以參考下面的文檔,對Security進行一次全面的瞭解

https://www.springcloud.cc/spring-security-zhcn.html

權限的話,我們就做成顆粒化,也就是控制到按鈕的級別。

現在我們先來加上Spring Security的jar吧

集成

<!-- SpringBoot 集成 Spring Security -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

加上jar後我們馬上啓動程序,會發現,訪問頁面後變了,提示需要登錄,而且這個登錄頁面好像跟我們沒啥關係,我們沒有寫的。

密碼隱藏在我們啓動的控制檯中:

我先按照控制檯的密碼登錄一下,還是提示用戶名密碼錯誤,這裏需要注意,頁面上的admin是我本地保存的密碼,自動填充上去的,真的默認的用戶名是user。

登錄成功後,我們訪問我們的TestController裏面的test方法:

因爲我們使用了Spring boot,Spring boot可以零配置啓動程序,但不是沒有配置,而且他幫我們加上了一些必要的默認配置。所以這裏集成了過後上面的內容都是默認的。

在繼續之前,我們先把數據庫豐富一下,在之前我們就只有一張用戶表,如果只使用用戶表來進行權限控制,顯然達不到我們要控制到按鈕的要求。

RBAC權限模型數據庫權限設計

首先要加上的肯定是跑不了一張角色的表。然後在是我們菜單表(資源)。在是菜單下面的按鈕表,在這裏我們可以將菜單跟按鈕都放在同一張表,用一個類型來進行菜單跟按鈕的區分。這樣也是能達到我們的目的的。在然後就是角色跟用戶的關係表了,最後就是我們角色跟菜單(資源)表的關係表了。這樣我們只需要在新增角色的時候,給角色配置對應的菜單(資源)。在新增用戶的時候給用戶設置對應的角色就能夠通過這個關係查詢到當前用戶擁有哪些菜單權限了。

# 修改用戶表
alter table sys_user 
	add head_img varchar(128) comment '用戶頭像',
	add is_deleted tinyint(1) default 0 comment '刪除標識:1已刪除;0未刪除',
	add create_user_id varchar(64) comment '創建人',
	add modify_time datetime comment '修改時間',
	add modify_user_id varchar(64) comment '修改人',
	add remark varchar(128) comment '備註',
    add unique UK_USER_ACCOUNT (real_name),
	add unique UK_USER_PHONE (user_phone),
	add unique UK_USER_EMAIL (user_email);

# 創建角色表
create table sys_role(
	role_id varchar(64) comment '角色編號',
	role_name varchar(30) comment '角色名稱',
	role_code varchar(30) comment '角色編碼',
	parent_role_id varchar(64) comment '上級角色編號',
	is_deleted tinyint(1) default 0 comment '刪除標識:1已刪除;0未刪除',
	create_time datetime comment '創建時間',
	create_user_id varchar(64) comment '創建人',
	modify_time datetime comment '修改時間',
	modify_user_id varchar(64) comment '修改人',
	remark varchar(128) comment '備註',
	PRIMARY KEY (role_id),
	UNIQUE KEY UK_ROLE_CODE (role_code)
) comment '角色表';

# 創建角色用戶關係表
create table sys_role_user(
	role_user_id varchar(64) comment '角色用戶關係編號',
	role_id varchar(64) comment '角色編號',
	user_id varchar(64) comment '用戶編號',
	create_time datetime comment '創建時間',
	create_user_id varchar(64) comment '創建人',
   PRIMARY KEY (role_user_id),
	UNIQUE KEY UK_ROLE_ID_USER_ID (role_id,user_id)
) comment '角色用戶關係表';

# 創建菜單(資源)表
create table sys_menu(
	menu_id varchar(64) comment '菜單編號',
	menu_name varchar(50) comment '菜單名稱',
	menu_code varchar(50) comment '菜單編碼',
	parent_menu_id varchar(64) comment '上級菜單編號',
	menu_class varchar(125) comment '菜單css樣式',
	menu_icon varchar(125) comment '菜單圖標',
	menu_type tinyint(1) comment '菜單類型:1目錄;2導航菜單;3按鈕',
	menu_url varchar(255) comment '菜單url',
	routing_json varchar(400) comment '路由json字符串信息',
	sort_id int comment '排序', 
	is_deleted tinyint(1) default 0 comment '刪除標識:1已刪除;0未刪除',
	is_enabled tinyint(1) default 1 comment '是否禁用:1未禁用;0已禁用',
	create_time datetime comment '創建時間',
	create_user_id varchar(64) comment '創建人',
	modify_time datetime comment '修改時間',
	modify_user_id varchar(64) comment '修改人',
	remark varchar(128) comment '備註',
	PRIMARY KEY (menu_id),
	UNIQUE KEY UK_MENU_CODE (menu_code)
) comment '菜單(資源)表';

# 創建角色菜單關係表
create table sys_role_menu(
	role_menu_id varchar(64) comment '角色菜單關係編號',
	role_id varchar(64) comment '角色編號',
	menu_id varchar(64) comment '菜單編號',
	create_time datetime comment '創建時間',
	create_user_id varchar(64) comment '創建人',
   PRIMARY KEY (role_menu_id),
	UNIQUE KEY UK_ROLE_ID_MENU_ID (role_id,menu_id)
) comment '角色菜單關係表';

登錄

現在我們開始使用數據庫用戶來做我們的Spring Security登錄用戶。首先我們要寫一個根據用戶賬號查詢用戶信息的一個接口,這裏比較簡單就不寫出來了。

要使用要使用數據庫用戶登錄,我們先了解一下流程:

1.首先我們要自定義實現一下UserDetails,這裏的UserDetails是Spring Security用來存儲用戶信息的接口。裏面肯定不可能有我們需要的所有字段。所有我們要實現他並擴展一些我們自己需要的屬性。

UserDetails的源碼如下:

public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

方法含義如下:

    • getAuthorites:獲取用戶權限,本質上是用戶的角色信息。
    • getPassword: 獲取密碼。
    • getUserName: 獲取用戶名。
    • isAccountNonExpired: 賬戶是否過期。
    • isAccountNonLocked: 賬戶是否被鎖定。
    • isCredentialsNonExpired: 密碼是否過期。
    • isEnabled: 賬戶是否可用。

2.然後自定義實現一下UserDetailsService接口,主要是去重寫裏面的loadUserByUsername登錄方法。

3.然後配置指定userDetailsService使用我們實現後的類,在指定一下密碼的加密方式。

有了上面的三步,我們就可以實現數據庫用戶登錄了。

CustomUserDetails

然後實現UserDetails,也就是Security默認的用戶信息類:

import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Date;

/**
 * 自定義Spring Security用戶詳情類
 */
@Data
public class CustomUserDetails implements UserDetails {

    /**
     * 用戶編號
     */
    @TableId
    private String userId;

    /**
     * 真實姓名
     */
    private String realName;

    /**
     * 密碼
     */
    private String userPass;

    /**
     * 登錄賬號
     */
    private String userName;

    /**
     * 手機號碼
     */
    private String userPhone;

    /**
     * 郵箱
     */
    private String userEmail;

    /**
     * 數據狀態:1啓用;2禁用
     */
    private Integer userStatus;

    /**
     * 創建時間
     */
    private Date createTime;

    /**
     * 用戶頭像
     */
    private String headImg;

    /**
     * 創建人
     */
    private String createUserId;

    /**
     * 修改時間
     */
    private Date modifyTime;

    /**
     * 修改人
     */
    private String modifyUserId;

    /**
     * 備註
     */
    private String remark;

    /**
     * 用戶權限
     */
   private Collection<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public String getPassword() {
        return this.userPass;
    }

    @Override
    public String getUsername() {
        return this.userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return userStatus == 1;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return userStatus == 1;
    }

}

CustomUserDetailsService

自定義一個CustomUserDetailsService來實現UserDetailsService接口,並實現loadUserByUsername(String login);方法。我們在實現loadUserByUsername方法的時候,就可以通過查詢數據庫來獲取用戶信息,然後設置一下權限信息並返回,但是現在我們如果沒有查詢角色,可以不設置或者給一個空的。

import com.hzw.code.fast.security.model.CustomUserDetails;
import com.hzw.code.fast.service.sys.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;

/**
 * 自定義UserDetailsService
 */
@Service
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {

    private final SysUserService userService;
    public CustomUserDetailsService(SysUserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        CustomUserDetails details = userService.getUserByUsername(username);
        if(details == null){
            String errorMsg = "賬號 " + username + "不存在";
            log.error(errorMsg);
            throw new UsernameNotFoundException(errorMsg);
        }
        // 設置權限
        Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        details.setAuthorities(grantedAuthorities);
        return details;
    }
}

SecurityConfiguration

這裏開始配置我們自定義的service爲userDetailsService,並指定加密方式。

import com.hzw.code.fast.security.service.CustomUserDetailsService;
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.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;

/**
 * spring security 配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final CustomUserDetailsService userDetailsService;
    public SecurityConfiguration(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 設置自定義的userDetailsService
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

BCryptPasswordEncoder

這裏指定了加密方式是BCryptPasswordEncoder,那麼在新增用戶的時候,用戶的密碼就需要用他來進行加密。

void passTest() {
   String pass = "123456";
   BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
   String hashPass = bcryptPasswordEncoder.encode(pass);
   System.out.println(hashPass);
   boolean flag = bcryptPasswordEncoder.matches("123456",hashPass);
   System.out.println(flag);
}

輸出的結果爲:$2a$10$DtALZDK/.ihjMyJT97QqFuVsRaiBIuyo6PL7jUpjR6gWtKKjEtMrW跟true

這裏我們可以多試幾次,會發現每一次的hashPass的結果都不一樣,但是最終的flag都爲 true。原理呢是每次密碼的隨機鹽,都保存在hashPass中。在進行matchs進行比較時,兩個參數即"123456"和hashPass。比較的時候就hashPass裏面的隨機鹽,而不生成新的隨機鹽,從而保證我們的兩次加密的密碼鹽都是一致的。

好了,我們將這個密碼插入到數據庫中,然後開始測試:

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 測試控制器
 */
@RestController
@RequestMapping("test")
public class TestController {

    @GetMapping("/{test}")
    public Object test(@PathVariable String test){
        return test + " 你好!";
    }

    /**
     * 獲得當前用戶信息
     * @return
     */
    @GetMapping("/user")
    public Object getUser(){
        return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}

這裏登錄成功後,我們調用一下/test/user方法獲取用戶信息:

ok,成功的返回了我們存儲的user信息。

這裏在說說Authentication,Authentication中存儲着用戶的認證信息,裏面包含了用戶信息跟權限信息。

源碼如下:

public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	Object getCredentials();
	Object getDetails();
	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

其中方法含義如下:

    • getAuthorities() 認證權限(角色)信息
    • getCredentials() 密碼信息
    • getDetails() 用戶信息
    • getPrincipal() 用戶名
    • isAuthenticated() 獲取當前用戶是否已認證
    • setAuthenticated(boolean var1) 設置當前用戶是否已認證

 

小結

好了,本章主要使用了數據庫用戶登錄,下一章節我們開始給我們的方法加上權限認證,並介紹一下Spring Security的工作原理。

 

----------------------------------------------------------

項目的源碼地址:https://gitee.com/gzsjd/fast

----------------------------------------------------------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章