SpringBoot2.1版本的個人應用開發框架 - 使用SpringSecurity管理我們的訪問許可權2

人生長恨水發表於2019-04-11

本篇作為SpringBoot2.1版本的個人開發框架 子章節,請先閱讀SpringBoot2.1版本的個人開發框架再次閱讀本篇文章

專案地址:SpringBoot2.1版本的個人應用開發框架

參考:

在上一篇文章我們對spring security有了初步認識以後,我們這篇主要實現 從資料庫查詢使用者來進行使用者是否具有登陸的認證。

資料庫表的設計

參考:

  • RBAC許可權管理 這篇文章講的非常詳細,只不過有點久遠,12年我還在上高一。。。

我們在做使用者登陸之前,我們先要設計資料庫表,RBAC(Role-Based Access Control,基於角色的訪問控制),就是使用者通過角色與許可權進行關聯。簡單地說,一個使用者擁有若干角色,每一個角色擁有若干許可權。這樣,就構造成“使用者-角色-許可權”的授權模型。在這種模型中,使用者與角色之間,角色與許可權之間,一般者是多對多的關係。

在這裡插入圖片描述
按照我參考的部落格中的設計好了五張基礎表,欄位的話可以根據自己的需求變化,生成的sql檔案我放到我專案中了。

表結構確定好以後,我們在專案中把系統使用者的entity、dao、service、以及dao對應的xml都設定好,這裡我就不一一展示程式碼了,可以拿之前的MybatisPlus的自動生成,正好自己又可以複習一遍之前的,新增好以後可以現在測試類中確認一下,沒有錯誤再進行下一步。

security核心類

在上一篇筆記中我們知道security的核心類之一的WebSecurityConfigurerAdapter,我們可以在這個配置類中建立自己的使用者,放行哪個請求,認證什麼請求,在這篇筆記我們要實現從資料庫中查詢使用者,所以我們要對以下核心類做實現。

  • UserDetails介面
  • UserDetailsService介面

UserDetails介面

UserDetails介面是security為我們提供的可擴充套件的使用者資訊介面,儲存一些非敏感類的資訊,在security模組下entity包中實現此介面類,其實就是一個實體類。

package com.ywh.security.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

/**
 * CreateTime: 2019-01-24 16:10
 * ClassName: SecurityUserDetails
 * Package: com.ywh.security.entity
 * Describe:
 * security的使用者詳情類
 *
 * @author YWH
 */
public class SecurityUserDetails implements UserDetails {


    /**
     * 使用者的密碼
     */
    private String password;

    /**
     * 使用者的名字
     */
    private String username;

    /**
     * 使用者狀態,1 表示有效使用者, 0表示無效使用者
     */
    private Integer state;

    /**
     * 使用者的許可權,可以把使用者的角色資訊的集合先放進來,角色代表著許可權
     */
    private Collection<? extends GrantedAuthority> authorties;


    public SecurityUserDetails(String password, String username, Integer state, Collection<? extends GrantedAuthority> authorties) {
        this.password = password;
        this.username = username;
        this.state = state;
        this.authorties = authorties;
    }

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

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 指示使用者的帳戶是否已過期。過期的帳戶無法通過身份驗證。
     * @return true如果使用者的帳戶有效(即未過期), false如果不再有效(即已過期)
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指示使用者是鎖定還是解鎖。鎖定的使用者無法進行身份驗證。
     * @return true是未鎖定,false是已鎖定
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示使用者的憑據(密碼)是否已過期。過期的憑據會阻止身份驗證
     * @return true如果使用者的憑證有效(即未過期), false如果不再有效(即已過期)
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 指示使用者是啟用還是禁用。禁用的使用者無法進行身份驗證。
     * @return true使用者已啟用,false使用者已經禁用
     */
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return state == 1;
    }
}

複製程式碼

UserDetailsService介面

此介面主要重寫一個方法就好了loadUserByUsername,這個方法主要作用就是通過使用者名稱查詢使用者的詳細資訊,然後放到我們的UserDetails 實現的子類中,儲存在security的SecurityContextHolder中,上一篇我們通過這個來獲取使用者資訊的。

在security模組中的service/impl中實現此介面,selectByUserName方法就是一個普通的dao介面,在mapper.xml檔案中定義好sql語句,由於很簡單我就不貼了

package com.ywh.security.service.impl;

/**
 * CreateTime: 2019-01-25 16:39
 * ClassName: SecurityUserDetailsServiceImpl
 * Package: com.ywh.security.service.impl
 * Describe:
 * UserDetailService的實現類
 * 這個@Primary表示這個類所繼承的介面有多個實現類,當不知道引入哪個的時候,優先使用@Primary所註解的類
 * @author YWH
 */
@Primary
@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {

	// 使用者查詢介面
    private SysUserDao sysUserDao;

    @Autowired
    public SecurityUserDetailsServiceImpl(SysUserDao sysUserDao) {
        this.sysUserDao = sysUserDao;
    }

	@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUserEntity sysUserEntity = sysUserDao.selectByUserName(username);
        if(sysUserEntity != null){
            // stream java8的新特性,Stream 使用一種類似用 SQL 語句從資料庫查詢資料的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。
            // 參考http://www.runoob.com/java/java8-streams.html
            List<SimpleGrantedAuthority> collect = sysUserEntity.getRoles().stream().map(SysRoleEntity::getSysRoleName)
                    .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
            return new SecurityUserDetails(sysUserEntity.getSysUserPassword(),sysUserEntity.getSysUserName(),sysUserEntity.getSysUserState(),collect);
        }
        throw MyExceptionUtil.mxe(String.format("'%s'.這個使用者不存在", username));
    }
}

複製程式碼

修改WebSecurityConfigurerAdapter實現類

在上一篇中我們對此類進行了重寫,我們知道使用者配置是重寫configure(AuthenticationManagerBuilder auth)方法,我們是通過記憶體管理器來建立使用者的,這回我們實現了UserDetailsService介面,我們就可以通過這個來查詢使用者並使用。 具體修改如下:

package com.ywh.security.config;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {

	private UserDetailsService userDetailsService;


    @Autowired
    public SecurityConfigurer(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    
    /**
     * 使用者資訊配置
     * @param auth 使用者資訊管理器
     * @throws Exception 異常資訊
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//修改以前
//        auth
//                .inMemoryAuthentication()
//                .withUser("root")
//                .password("root")
//                .roles("user")
//                .and()
//                .passwordEncoder(CharEncoder.getINSTANCE());

//修改以後
        auth
                .userDetailsService(this.userDetailsService)
                .passwordEncoder(passwordEncoder());

    }
    
	/**
     * 密碼加密
     * @return 返回加密後的密碼
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
複製程式碼

重啟專案後我們可以看到效果如下,ywh這個使用者是資料庫中的,這個使用者的密碼是我手動呼叫PasswordEncoder 加密過後放到資料庫中的,密碼都是123。

@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void getOneTest(){
    System.out.println("password:" + passwordEncoder.encode("123"));
}
複製程式碼

在這裡插入圖片描述
如果輸入一個資料庫中沒有的使用者,則會提示你查無此人
在這裡插入圖片描述

自定義登陸

參考:Spring Security自定義使用者登陸認證

到此步之後我們完成了從資料庫中查詢使用者登陸的操作,但是登陸頁面是security給我們預設的頁面。

想使用我們自定義的頁面,security繼續替我們認證,在core模組下的resource檔案下建立public目錄後建立我們自定義的login.html登陸頁面

<!DOCTYPE HTML>
<html>
<head>
    <title>登陸頁面</title>
</head>

<body>
<h2>表單登入</h2>
<form action="/core/login" method="post">
    <table>
        <tr>
            <td>使用者名稱:</td>
            <td>
                <input type="text" placeholder="使用者名稱" name="username" required="required" />
            </td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td>
                <input type="password" placeholder="密碼" name="password" required="required" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登入</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>
複製程式碼

要注意form表單中的action屬性,這裡的值要與後面在security配置類中的loginProcessingUrl("/login")相同,我寫成/core/login是因為我配置了context-path,如果你沒配置不用寫/core字首

訪問的index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>測試</title>
</head>
<body>

登陸了!!!

</body>
</html>
複製程式碼

修改security配置類的configure(HttpSecurity httpSecurity)方法

	/**
     * 配置如何通過攔截器保護我們的請求,哪些能通過哪些不能通過,允許對特定的http請求基於安全考慮進行配置
     * @param httpSecurity http
     * @throws Exception 異常
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 暫時禁用csrc否則無法提交
                .csrf().disable()
                // 設定最多一個使用者登入,如果第二個使用者登陸則第一使用者被踢出,並跳轉到登陸頁面
                .sessionManagement().maximumSessions(1).expiredUrl("/login.html");
        httpSecurity
                // 開始認證
                .authorizeRequests()
                // 對靜態檔案和登陸頁面放行
                .antMatchers("/static/**").permitAll()
                .antMatchers("/auth/**").permitAll()
                .antMatchers("/login.html").permitAll()
                // 其他請求需要認證登陸
                .anyRequest().authenticated();
        httpSecurity
                // 表單登陸
                .formLogin()
                // 設定跳轉的登陸頁面
                .loginPage("/login.html")
                // .failureUrl("/auth/login?error") 設定如果出錯跳轉到哪個頁面
                // security預設使用的就是login路徑認證,如果想使用自定義自行修改就可以了
                .loginProcessingUrl("/login")
                // 如果直接訪問登入頁面,則登入成功後重定向到這個頁面,否則跳轉到之前想要訪問的頁面
                .defaultSuccessUrl("/index.html");
    }
複製程式碼

loginPage()中也可以填寫介面路徑,通過介面來返回登陸頁面;通過以上程式碼實現了我們自己定義的登陸頁面,security會為我們攔截並認證的,只是建立了兩個頁面和增加了兩個屬性。

想要自定義實現登陸成功的邏輯,可以配置successHandler()方法,要實現的介面為AuthenticationSuccessHandler,重寫其中的方法為onAuthenticationSuccess()。

自定義退出

想要實現自定義退出功能的邏輯,需要實現AuthenticationFailureHandler 介面,重寫其中的onAuthenticationFailure方法。或者在配置中的方法中寫匿名內部類。

而關於security中的退出,也有給我們預設實現,通過“/logout”就可以實現退出功能了,在index.html介面新增一個 a 標籤即可。

<a href="/core/logout">退出</a>
複製程式碼

再次提示我這裡寫/core是因為我配置了context-path,如果你沒有配置直接寫/logout即可,預設security機制會進行如下操作:

  • 使HttpSession無效
  • 清理記住密碼
  • 清理SecurityContextHolder
  • 重定向/login?logout

當然我們也可以自定義退出,在configure(HttpSecurity httpSecurity)方法中新增以下內容,這裡我只實現了最簡單的操作

httpSecurity
                // 登出
                .logout()
                // 登出處理,使用security預設的logout,也可以自定義路徑,實現即可
                .logoutUrl("/logout")
                // 登出成功後跳轉到哪個頁面
                .logoutSuccessUrl("/login.html")
                .logoutSuccessHandler((request, response, authentication) -> {
                    //登出成功處理函式
                    System.out.println("logout success");
                    response.sendRedirect("/core/login.html");
                })
                .addLogoutHandler((request, response, authentication) ->{
                    //登出處理函式
                    System.out.println("logout------");
                })
                // 清理Session
                .invalidateHttpSession(true);
複製程式碼

關於security上面我們實現了最基本的功能,它還可以做更多的事情,比如記住我的密碼,第三方登入等等。

相關文章