国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

[SpringBoot]Spring Security框架

這篇具有很好參考價(jià)值的文章主要介紹了[SpringBoot]Spring Security框架。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

目錄

關(guān)于Spring Security框架

Spring Security框架的依賴項(xiàng)

Spring Security框架的典型特征

?關(guān)于Spring Security的配置

關(guān)于默認(rèn)的登錄頁

關(guān)于請(qǐng)求的授權(quán)訪問(訪問控制)

?使用自定義的賬號(hào)登錄

使用數(shù)據(jù)庫中的賬號(hào)登錄

關(guān)于密碼編碼器

使用BCrypt算法

關(guān)于偽造的跨域攻擊

使用前后端分離的登錄

關(guān)于認(rèn)證的標(biāo)準(zhǔn)

未通過認(rèn)證時(shí)拒絕訪問

識(shí)別當(dāng)事人(Principal)

實(shí)現(xiàn)根據(jù)權(quán)限限制訪問

補(bǔ)充解釋(關(guān)于使用resultMap標(biāo)簽):

基于方法的權(quán)限檢查

添加Token?

首先添加Token-JWT的依賴項(xiàng):

生成JWT:

?解析JWT

補(bǔ)充:

在項(xiàng)目中使用JWT識(shí)別用戶的身份

核心流程

驗(yàn)證登錄成功時(shí)響應(yīng)JWT

解析客戶端攜帶的JWT

?我們這次選擇去繼承Spring系列框架提供的OncePerRequestFilter這個(gè)類。

關(guān)于認(rèn)證信息中的當(dāng)事人

處理解析JWT時(shí)的異常

處理復(fù)雜請(qǐng)求的跨域問題

單點(diǎn)登錄


?文章來源地址http://www.zghlxwxcb.cn/news/detail-480051.html

關(guān)于Spring Security框架

Spring Security框架主要解決了認(rèn)證與授權(quán)相關(guān)的問題。 ?

認(rèn)證信息(Authentication):表示用戶的身份信息

認(rèn)證(Authenticate):識(shí)別用戶的身份信息的行為,例如:登錄

授權(quán)(Authorize):授予用戶權(quán)限,使之可以進(jìn)行某些訪問,反之,如果用戶沒有得到必要的授權(quán),將無法進(jìn)行訪問

Spring Security框架的依賴項(xiàng)

在Spring Boot中使用Spring Security時(shí)需要添加spring-boot-starter-security依賴。 ?

Spring Security框架的典型特征

?當(dāng)添加了spring-boot-starter-security依賴后,在啟動(dòng)項(xiàng)目時(shí)執(zhí)行一些自動(dòng)配置,具體表現(xiàn)有:

  • 所有請(qǐng)求(包括根本不存在的)都是必須要登錄才允許訪問的,如果未登錄,會(huì)自動(dòng)跳轉(zhuǎn)到框架自帶的登錄頁面(1.項(xiàng)目重啟之后需要重新登錄,2.原來想去的頁面會(huì)要求登錄,登錄完成之后回到原來的位置)

[SpringBoot]Spring Security框架

  • 當(dāng)嘗試登錄時(shí),如果在打開登錄頁面后重啟過服務(wù)器端,則第1次的輸入是無效的 ?
  • 默認(rèn)的用戶名是user,密碼是在啟動(dòng)項(xiàng)目是控制臺(tái)提示的一段UUID值,每次啟動(dòng)項(xiàng)目時(shí)都不同(同一時(shí)空的唯一性,即同一時(shí)間同一空間的值都不同)

    • UUID是通過128位算法(運(yùn)算結(jié)果是128個(gè)bit)運(yùn)算得到的,是一個(gè)隨機(jī)數(shù),在同一時(shí)空是唯一的,通常使用32個(gè)十六進(jìn)制數(shù)來表示,每種平臺(tái)生成UUID的API和表現(xiàn)可能不同,UUID值的種類有2的128次方個(gè),即:3.4028237e+38,也就是340282366920938463463374607431768211456

[SpringBoot]Spring Security框架

  • 當(dāng)?shù)卿洺晒?,?huì)自動(dòng)跳轉(zhuǎn)到此前嘗試訪問的URL

  • 當(dāng)?shù)卿洺晒?,可以通過 /logout 退出登錄

  • 默認(rèn)不接受普通POST請(qǐng)求,如果提交POST請(qǐng)求,將響應(yīng)403(Forbidden)

?關(guān)于Spring Security的配置

?在項(xiàng)目的根包下創(chuàng)建config.SecurityConfiguration類,作為Spring Security的配置類,此類需要繼承自WebSecurityConfigurerAdapter,并重寫void configure(HttpSecurity http)方法,例如:

@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http); // 不要保留調(diào)用父級(jí)同名方法的代碼,不要保留!不要保留!不要保留!
    }

}

做了配置后,此時(shí)重啟工程就不需要登陸了,就算訪問登錄頁面也沒有。

[SpringBoot]Spring Security框架

?

?

?寫此配置是為了調(diào)整Spring Security框架的特征的所有表現(xiàn)由自己來設(shè)置。

關(guān)于默認(rèn)的登錄頁

在自定義的配置類中的void configure(HttpSecurity http)方法中,調(diào)用參數(shù)對(duì)象的formLogin()方法即可開啟默認(rèn)的登錄表單,如果沒有調(diào)用此方法,則不會(huì)應(yīng)用默認(rèn)的登錄表單,例如:

@Slf4j
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http); // 不要保留調(diào)用父級(jí)同名方法的代碼,不要保留!不要保留!不要保留!

        // 如果調(diào)用以下方法,當(dāng)Security認(rèn)為需要通過認(rèn)證,但實(shí)際未通過認(rèn)證時(shí),就會(huì)跳轉(zhuǎn)到登錄頁面
        // 如果未調(diào)用以下方法,將會(huì)響應(yīng)403錯(cuò)誤
        http.formLogin();
    }

}

關(guān)于請(qǐng)求的授權(quán)訪問(訪問控制)

在剛剛添加spring-boot-starter-security時(shí),所有請(qǐng)求都是需要登錄后才允許訪問的,當(dāng)添加了自定義的配置類且沒有調(diào)用父級(jí)同名方法后,所有請(qǐng)求都是不需要登錄就可以訪問的!

為了實(shí)現(xiàn)一部分需要登錄,一部分不需要登錄就需要做配置類,不然如果是自己做了一個(gè)登錄頁面,訪問登錄頁面還需要登錄就不合適。?

在配置類中的void configure(HttpSecurity http)方法中,調(diào)用參數(shù)對(duì)象的authorizeRequests()方法開始配置授權(quán)訪問:

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 白名單
    // 使用1個(gè)星號(hào),可以通配此層級(jí)的任何資源,例如:/admin/*,可以匹配:/admin/add-new、/admin/list,但不可以匹配:/admin/password/change
    // 使用2個(gè)連續(xù)的星可以,可以通配若干層級(jí)的資源,例如:/admin/**,可以匹配:/admin/add-new、/admin/password/change
    String[] urls = {
            "/doc.html",
            "/**/*.css",
            "/**/*.js",
            "/swagger-resources",
            "/v2/api-docs",
    };

    // 配置授權(quán)訪問
    // 注意:以下授權(quán)訪問的配置,是遵循“第一匹配原則”的,即“以最先匹配到的規(guī)則為準(zhǔn)”
    // 例如:anyRequest()是匹配任何請(qǐng)求,通常,應(yīng)該配置在最后,表示“除了以上配置過的以外的所有請(qǐng)求”
    // 所以,在開發(fā)實(shí)踐中,應(yīng)該將更具體的請(qǐng)求配置在靠前的位置,將更籠統(tǒng)的請(qǐng)求配置在靠后的位置
    http.authorizeRequests() // 開始對(duì)請(qǐng)求進(jìn)行授權(quán)
            .mvcMatchers(urls) // 匹配某些請(qǐng)求
            .permitAll() // 許可,即不需要通過認(rèn)證就可以訪問
            .anyRequest() // 任何請(qǐng)求
            .authenticated() // 要求已經(jīng)完成認(rèn)證的
    ;
}

?http.authorizeRequests() // 開始對(duì)請(qǐng)求進(jìn)行授權(quán)?

表示開始對(duì)請(qǐng)求進(jìn)行授權(quán) 。

??.anyRequest() // 任何請(qǐng)求
??.authenticated() // 要求已經(jīng)完成認(rèn)證的

上面兩句需要連起來理解,表示任何請(qǐng)求都要求是已經(jīng)完成認(rèn)證的。加上這兩句就回到了最開始的樣子,所有的請(qǐng)求都需要登錄才能訪問,不登錄訪問不了。

?.mvcMatchers(urls) // 匹配某些請(qǐng)求
?.permitAll() // 許可,即不需要通過認(rèn)證就可以訪問?

這兩句話也是連起來理解的, 理解同上面一樣,上面是任何請(qǐng)求,這里是匹配某些請(qǐng)求,上面的行為是所有都要求認(rèn)證,這里的行為是許可訪問,不需要通過認(rèn)證。(因?yàn)樽裱暗谝黄ヅ湓瓌t”的,即“以最先匹配到的規(guī)則為準(zhǔn)”,所有這兩行代碼要放在最上面才有效)

這里的urls是怎么來的:
首先為了方便頁面正確顯示,勾上禁用緩存。? ?

[SpringBoot]Spring Security框架

看下面錯(cuò)誤提示,看到有一堆的200都是login的,為了更好的看到提示信息,把http.formLogin()關(guān)掉。

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

? ?可以看到大量的403

[SpringBoot]Spring Security框架

?從中我們對(duì)這些403進(jìn)行許可(案例訪問的API文檔,給文檔需要的資源進(jìn)行許可,就可以順利訪問API文檔了),urls就是這么來的。

[SpringBoot]Spring Security框架

注意:有的比如表面是說的api-docs這樣一個(gè)名字,實(shí)際在配白名單的時(shí)候,看它的url是在一個(gè)v2的文件夾里面,配置為?"/v2/api-docs"。

[SpringBoot]Spring Security框架

?[SpringBoot]Spring Security框架

?使用自定義的賬號(hào)登錄

在使用Spring Security框架時(shí),可以自定義組件類,實(shí)現(xiàn)UserDetailsService接口,則Spring Security就會(huì)基于此類的對(duì)象來處理認(rèn)證!

則在項(xiàng)目的根包下創(chuàng)建security.UserDetailsServiceImpl,在類上添加@Service注解使其成為組件類,實(shí)現(xiàn)UserDetailsService接口:

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return null;
    }
}

(通過loadUserByUsername(String s)這個(gè)方法的名字可以理解為通過用戶名加載用戶,參數(shù)s就是username用戶名,返回UserDetails用戶詳情)

在項(xiàng)目中存在UserDetailsService接口類型的組件對(duì)象時(shí),嘗試登錄時(shí),Spring Security就會(huì)自動(dòng)使用登錄表單中輸入的用戶名來調(diào)用以上方法, 把輸入的用戶名作為一個(gè)參數(shù),?并得到方法返回的UserDetails類型的結(jié)果,?此結(jié)果中應(yīng)該包含用戶的相關(guān)信息,例如密碼、賬號(hào)狀態(tài)、權(quán)限等等,接下來,Spring Security框架會(huì)自動(dòng)判斷賬號(hào)的狀態(tài)(例如是否啟用或禁用)、驗(yàn)證密碼(在UserDetails中的密碼與登錄表單中的密碼是否匹配)等,從而決定此次是否登錄成功!

所以,對(duì)于開發(fā)者而言,在以上方法中只需要完成“根據(jù)用戶名返回匹配的用戶詳情”即可!例如:?

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("用戶名:{}", s);
        // 假設(shè)正確的用戶名是root,匹配的密碼是1234
        if (!"root".equals(s)) {
            log.debug("此用戶名沒有匹配的用戶數(shù)據(jù),將返回null");
            return null;
        }

        log.debug("用戶名匹配成功!準(zhǔn)備返回此用戶名匹配的UserDetails類型的對(duì)象");
        UserDetails userDetails = User.builder()
                .username(s)
                .password("1234")
                .disabled(false) // 賬號(hào)狀態(tài)是否禁用
                .accountLocked(false) // 賬號(hào)狀態(tài)是否鎖定
                .accountExpired(false) // 賬號(hào)狀態(tài)是否過期
                .credentialsExpired(false) // 賬號(hào)的憑證是否過期
                .authorities("這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!") // 權(quán)限
                .build();
        log.debug("即將向Spring Security返回UserDetails類型的對(duì)象:{}", userDetails);
        return userDetails;
    }

}

?以上代碼中,用User.builder()開啟它的構(gòu)建者模式,?.build()表示構(gòu)建完了。這是一個(gè)鏈?zhǔn)綄懛?,先有個(gè)builder()在執(zhí)行?.build()就可以創(chuàng)建這個(gè)對(duì)象。創(chuàng)建的過程中就傳入例如密碼、賬號(hào)狀態(tài)、權(quán)限等相關(guān)信息。

當(dāng)項(xiàng)目中存在UserDetailsService類型的對(duì)象后,啟動(dòng)項(xiàng)目時(shí),控制臺(tái)不會(huì)再提示臨時(shí)使用的UUID密碼!并且,user賬號(hào)也不可用! 用的就是自己配的??.username(s)? .password("1234")這個(gè)。

另外,Spring Security框架認(rèn)為所有的密碼都是必須顯式的經(jīng)過某種算法處理過的,如果使用的密碼是明文(原始密碼例如1234這種),也必須明確的指出!例如,使用沒加密的原始密碼在Security的配置類中添加配置NoOpPasswordEncoder這種密碼編碼器告訴Security是沒有加密的,不然會(huì)報(bào)錯(cuò): ?

@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

?此時(shí)嘗試登錄,輸入用戶名root,密碼1234,登錄成功,輸入錯(cuò)誤的用戶名提示以下為null,這是因?yàn)檫@個(gè)null是在用戶名不對(duì)的時(shí)候我們給它的。

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?如果用戶名是輸入的root,密碼故意輸出會(huì)提示:

[SpringBoot]Spring Security框架

?如果把禁用打開,輸入正確的用戶名密碼也會(huì)顯示用戶已失效:

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

使用數(shù)據(jù)庫中的賬號(hào)登錄

需要將UserDetailsServiceImpl中的實(shí)現(xiàn)改為“根據(jù)用戶名查詢數(shù)據(jù)庫中的用戶信息”!需要執(zhí)行的SQL語句大致是:

select username, password, enable from ams_admin where username=?

?在pojo.vo.AdminLoginInfoVO類:

@Data
@Accessors(chain = true)
public class AdminLoginInfoVO implements Serializable {
    private String username;
    private String password;
    private Integer enable;
}

?在AdminMapper接口中添加抽象方法:

AdminLoginInfoVO getLoginInfoByUsername(String username);

?在AdminMapper.xml中配置SQL:

<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultType="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
    SELECT 
        username, password, enable 
    FROM 
         ams_admin 
    WHERE 
        username=#{username}
</select>

?在AdminMapperTests中編寫并執(zhí)行測試:

@Test
void getStandardById() {
    String username = "root";
    Object queryResult = mapper.getLoginInfoByUsername(username);
    System.out.println("根據(jù)【username=" + username + "】查詢數(shù)據(jù)完成,結(jié)果:" + queryResult);
}

?然后,在UserDetailsServiceImpl中調(diào)整原來的實(shí)現(xiàn),改成:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    log.debug("Spring Security框架自動(dòng)調(diào)用了UserDetailsServiceImpl.loadUserByUsername()方法,用戶名:{}", s);
    // 根據(jù)用戶名從數(shù)據(jù)庫中查詢匹配的用戶信息
    AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
    if (loginInfo == null) {
        log.debug("此用戶名沒有匹配的用戶數(shù)據(jù),將返回null");
        return null;
    }

    log.debug("用戶名匹配成功!準(zhǔn)備返回此用戶名匹配的UserDetails類型的對(duì)象");
    UserDetails userDetails = User.builder()
            .username(loginInfo.getUsername())
            .password(loginInfo.getPassword())
            .disabled(loginInfo.getEnable() == 0) // 賬號(hào)狀態(tài)是否禁用
            .accountLocked(false) // 賬號(hào)狀態(tài)是否鎖定
            .accountExpired(false) // 賬號(hào)狀態(tài)是否過期
            .credentialsExpired(false) // 賬號(hào)的憑證是否過期
            .authorities("這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!") // 權(quán)限
            .build();
    log.debug("即將向Spring Security返回UserDetails類型的對(duì)象:{}", userDetails);
    return userDetails;
}

為了得到較好的運(yùn)行效果,應(yīng)該在數(shù)據(jù)表中插入一些新的測試數(shù)據(jù),例如:

[SpringBoot]Spring Security框架

因?yàn)槟壳芭渲玫拿艽a編碼器是NoOpPasswordEncoder,所以,本次測試運(yùn)行時(shí),使用的賬號(hào)在數(shù)據(jù)庫的密碼應(yīng)該是明文密碼!

關(guān)于密碼編碼器

?Spring Security定義了PasswordEncoder接口,可以有多種不同的實(shí)現(xiàn),此接口中的抽象方法主要有:

// 對(duì)原密碼進(jìn)行編碼,返回編碼后的結(jié)果(密文)
String encode(String rawPassword);

// 驗(yàn)證密碼原文(第1個(gè)參數(shù))和密文(第2個(gè)參數(shù))是否匹配
boolean matches(String rawPassword, String encodedPassword);

常見的對(duì)密碼進(jìn)行編碼,實(shí)現(xiàn)“加密”效果所使用的算法主要有:

  • MD(Message Digest)系列:MD2 / MD4 / MD5

  • SHA(Secure Hash Algorithm)系列:SHA-1 / SHA-256 / SHA-384 / SHA-512

  • BCrypt

  • SCrypt

目前,推薦使用的算法是BCrypt算法!在Spring Security框架中,也提供了BCryptPasswordEncoder類,其基本使用: ?

public class BCryptTests {

    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Test
    void encode() {
        String rawPassword = "123456";
        System.out.println("原文:" + rawPassword);

        for (int i = 0; i < 5; i++) {
            String encodedPassword = passwordEncoder.encode(rawPassword);
            System.out.println("密文:" + encodedPassword);
        }
    }

    // 原文:123456
    // 密文:$2a$10$YOW67gn1jGQsNd1lWFOktuxGEK3Ai4obSCo6m0o0zP3YA4iTm0QoS
    // 密文:$2a$10$AoGlKthb1ZKzTAng5ssX6OUwN8.tC9junqbYhtF0POkr.XdFuoEWy
    // 密文:$2a$10$wgBhSmnoFQ.LdvFCLd8lyOSsHuGVIpVYKW8.bW4yt2kBMYqG1G.5u
    // 密文:$2a$10$OIiWGSjFH02Vr9khLEQnG.s2rGowkotMV14TThAgJK8KQm.WQq6pm
    // 密文:$2a$10$DluGioTO7Zcc0hmwDz8Ld.4Uyp2hIIZ/PcGhFCVd1P3FuSukqJN36
    
    @Test
    void matches() {
        String rawPassword = "123456";
        System.out.println("原文:" + rawPassword);

        String encodedPassword = "$2a$10$wgBhSmnoFQ.LdvFCLd8lyOSsHuGVIpVYKW8.bW4yt2kBMYqG1G.5u";
        System.out.println("密文:" + encodedPassword);

        boolean result = passwordEncoder.matches(rawPassword, encodedPassword);
        System.out.println("匹配結(jié)果:" + result);
    }

}

關(guān)于BCrypt算法,其典型特征有:

  • 使用同樣的原文,每次得到的密文都不相同

    • BCrypt算法在編碼過程中,使用了隨機(jī)的“鹽”(salt)值,所以,每次編碼結(jié)果都不同

    • 編碼結(jié)果中保存了這個(gè)隨機(jī)的鹽值,所以,并不影響驗(yàn)證是否匹配

  • 運(yùn)算效率極為低下,可以非常有效的避免暴力破解

    • 可以通過構(gòu)造方法傳入strength值,增加強(qiáng)度(默認(rèn)為10),表示運(yùn)算過程中執(zhí)行2的多少次方的哈希運(yùn)算

      [SpringBoot]Spring Security框架

    • 此特征是MD系列和SHA家庭的算法所不具備的特征

另外,SCrypt算法的安全性比BCrypt還要高,但是,執(zhí)行效率比BCrypt更低,通常,由于BCrypt算法已經(jīng)能夠提供足夠的安全強(qiáng)度,所以,目前,使用BCrypt是常見的選擇。

使用BCrypt算法

只需要在Security配置類中將密碼編碼器換成BCryptPasswordEncoder即可: ?

[SpringBoot]Spring Security框架

?接下來,便可以使用數(shù)據(jù)庫中那些密碼是密文的賬號(hào)測試登錄:

[SpringBoot]Spring Security框架

(注意:專業(yè)名詞上BCrypt算法以及上文提到的MD等都不是加密算法,加密算法是能加密還能解密的,而這些算法都是單向加密不可逆和還原的。登錄的時(shí)候只能用數(shù)據(jù)庫的密文和傳遞進(jìn)來的密文做匹配,是不能驗(yàn)證原密碼的。)

在以上案例中還不能使用post請(qǐng)求,需要以下:
Spring Security框架設(shè)計(jì)了“防止偽造的跨域攻擊”的防御機(jī)制,所以,默認(rèn)情況下,自定義的POST請(qǐng)求是不可用的,簡單的解決方案就是在Spring Security的配置類中禁用這個(gè)防御機(jī)制即可,例如:

[SpringBoot]Spring Security框架

?

?

關(guān)于偽造的跨域攻擊

?偽造的跨域攻擊:此類攻擊原理是利用服務(wù)器端對(duì)客戶端瀏覽器的“信任”來實(shí)現(xiàn)的!目前,主流的瀏覽器都是多選項(xiàng)卡模式的,假設(shè)在第1個(gè)選項(xiàng)卡中登錄了某個(gè)網(wǎng)站,在第2個(gè)選項(xiàng)卡也打開這個(gè)網(wǎng)站的頁面,就會(huì)被當(dāng)作是已經(jīng)登錄的狀態(tài)!基于這種特征,假設(shè)在第1個(gè)選項(xiàng)卡中登錄了某個(gè)網(wǎng)上銀行,在第2個(gè)選項(xiàng)卡中打開了某個(gè)壞人的網(wǎng)站(不是網(wǎng)上銀行的網(wǎng)站),但是,在這個(gè)壞人的網(wǎng)站的頁面中隱藏了一個(gè)使用網(wǎng)上銀行進(jìn)行轉(zhuǎn)賬的請(qǐng)求,這個(gè)請(qǐng)求在壞人的網(wǎng)站的頁面剛剛打開時(shí)就自動(dòng)發(fā)送出去了(自動(dòng)發(fā)送:方法很多,例如將URL設(shè)置為某個(gè)不顯示的<img>標(biāo)簽的src值),由于在第1個(gè)選項(xiàng)卡中已經(jīng)登錄了網(wǎng)上銀行,從第2個(gè)選項(xiàng)卡中發(fā)出的請(qǐng)求也會(huì)被視為已經(jīng)登錄網(wǎng)上銀行的狀態(tài),這就實(shí)現(xiàn)了一種攻擊行為!當(dāng)然,以上只是舉例,真正的銀行轉(zhuǎn)賬不會(huì)這么簡單,例如還需要輸入密碼、手機(jī)驗(yàn)證碼等等,但是,這種模式的攻擊行為是確實(shí)存在的,由于使用另一個(gè)網(wǎng)站(壞人的網(wǎng)站)偷偷的實(shí)現(xiàn)的攻擊,所以,稱之為“偽造的跨域攻擊”!

?

典型的防御手段:在Spring Security框架中,默認(rèn)就開啟了對(duì)于“偽造跨域攻擊”的防御機(jī)制,其做法是在所有POST表單中隱藏一個(gè)具有“唯一性”的“隨機(jī)值”,例如UUID值,當(dāng)客戶端提交請(qǐng)求時(shí),必須提交這個(gè)UUID值,如果未提交,則服務(wù)器端將其直接視為攻擊行為,將拒絕處理此請(qǐng)求!以Spring Security默認(rèn)的登錄表單為例:

[SpringBoot]Spring Security框架

?

當(dāng)把防御機(jī)制禁用后,這個(gè)數(shù)值也就沒有了。

?提示:此前“如果在打開登錄頁面后重啟過服務(wù)器端,則第1次的輸入是無效的”,也是因?yàn)檫@種防御機(jī)制,當(dāng)打開登錄頁,服務(wù)器端生成了此次使用的UUID,但重啟服務(wù)器后,服務(wù)器不再識(shí)別此前生成的UUID,所以,第1次的輸入是無效的!

?

目前以上已經(jīng)實(shí)現(xiàn)Spring Security框架它默認(rèn)帶來的效果,解決了認(rèn)證和授權(quán)的問題,最主要的用它來處理登錄。但目前還不夠,還需要實(shí)現(xiàn)前后端分離的登錄。

使用前后端分離的登錄

?Spring Security框架自帶了登錄頁面和退出登錄頁面,不是前后端分離的,則不可以與自行開發(fā)的前端項(xiàng)目中的登錄頁面進(jìn)行交互,如果要改為前后端分離的模式,需要:

?

  • 不再啟用服務(wù)器端Spring Security框架自帶的登錄頁面和退出登錄頁面

    • 在配置類中不再調(diào)用http.formLogin()即可

[SpringBoot]Spring Security框架

?

  • 使用控制器接收客戶端的登錄請(qǐng)求

    • 自定義Param類,封裝客戶端將提交的用戶名和密碼,在控制器類中添加接收登錄請(qǐng)求的方法

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

?

  • 注意:需要將此請(qǐng)求配置在“白名單”中(不能登錄之后在登錄)

[SpringBoot]Spring Security框架

?

使用Service處理登錄的業(yè)務(wù)

  • 在接口中聲明抽象方法,并在實(shí)現(xiàn)類中重寫此方法

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

?

  • 具體的驗(yàn)證登錄,仍可以由Spring Security框架來完成,調(diào)用AuthenticationManager(認(rèn)證管理器)對(duì)象的authenticate()方法即可,則Spring Security框架會(huì)自動(dòng)基于調(diào)用方法時(shí)傳入的用戶名來調(diào)用UserDetailsService接口對(duì)象的loadUserByUsername()方法,并得到返回的UserDetails對(duì)象,然后,自動(dòng)判斷賬號(hào)狀態(tài)、對(duì)比密碼等等

    • 可以在Spring Security的配置類中重寫authenticationManagerBean()方法,并在此方法上添加@Bean注解,則可以在任何所需要的位置自動(dòng)裝配AuthenticationManager類型的數(shù)據(jù),注意:不要使用authenticationManager()方法,此方法在某些場景(例如某些測試等)中可能導(dǎo)致死循環(huán),最終內(nèi)存溢出

?

[SpringBoot]Spring Security框架

?[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?調(diào)用AuthenticationManager(認(rèn)證管理器)對(duì)象的authenticate()方法,傳入authentication這個(gè)參數(shù)。

?[SpringBoot]Spring Security框架

點(diǎn)開Authentication?發(fā)現(xiàn),也是一個(gè)接口

?

[SpringBoot]Spring Security框架

?而Authentication實(shí)現(xiàn)類是

[SpringBoot]Spring Security框架

它需要傳入?yún)?shù)? ,?有兩套構(gòu)造方法,第一套第一個(gè)參數(shù)是用戶名,第二個(gè)是密碼。通過傳入的參數(shù)取出用戶名和密碼。

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?以上:根據(jù)取出的用戶名和密碼創(chuàng)建了用戶認(rèn)證對(duì)象authentication ,用于去調(diào)用認(rèn)證管理器AuthenticationManager的認(rèn)證方法authenticate()。最后驗(yàn)證登錄成功。

?

完成后,重啟項(xiàng)目,可以通過API文檔的調(diào)試功能來測試登錄,如果使用無法登錄的賬號(hào)信息,會(huì)在服務(wù)器端的控制臺(tái)看到對(duì)應(yīng)的異常: ?

  • 用戶名不存在

org.springframework.security.authentication.InternalAuthenticationServiceException: UserDetailsService returned null, which is an interface contract violation
  • 密碼錯(cuò)誤

org.springframework.security.authentication.BadCredentialsException: 用戶名或密碼錯(cuò)誤
  • 賬號(hào)被禁用

org.springframework.security.authentication.DisabledException: 用戶已失效

可以在全局異常處理器中添加處理以上異常的方法,通常,在處理時(shí),不會(huì)嚴(yán)格區(qū)分“用戶名不存在”和“密碼錯(cuò)誤”這2種錯(cuò)誤,也就是說,無論是這2種錯(cuò)誤中的哪一種,一般提示“用戶名或密碼錯(cuò)誤”即可,以進(jìn)一步保障賬號(hào)安全!

關(guān)于以上用戶名不存在、密碼錯(cuò)誤時(shí)對(duì)應(yīng)的異常,其繼承結(jié)構(gòu)是:

AuthenticationException
-- BadCredentialsException // 密碼錯(cuò)誤
-- AuthenticationServiceException
-- -- InternalAuthenticationServiceException // 用戶名不存在

則可以在處理異常的方法上,在@ExceptionHandler注解中指定需要處理的2種異常,并且,使用這2種異常公共的父類作為方法的參數(shù),(如果光使用父類作為參數(shù),父類下的其他異常也會(huì)被處理,所以要指定要處理的兩種異常)例如:

// 如果@ExceptionHandler沒有配置參數(shù),則以方法參數(shù)的異常為準(zhǔn),來處理異常
// 如果@ExceptionHandler配置了參數(shù),則只處理此處配置的異常
@ExceptionHandler({
        InternalAuthenticationServiceException.class,
        BadCredentialsException.class
})
public JsonResult handleAuthenticationException(AuthenticationException e) {
    // 暫不關(guān)心方法內(nèi)部的代碼
}

在實(shí)際處理時(shí),需要先在ServiceCode中添加新的枚舉值,以表示以上錯(cuò)誤的狀態(tài)碼:

[SpringBoot]Spring Security框架

?

然后,在全局異常處理器中添加處理異常的方法: ?

// 如果@ExceptionHandler沒有配置參數(shù),則以方法參數(shù)的異常為準(zhǔn),來處理異常
// 如果@ExceptionHandler配置了參數(shù),則只處理此處配置的異常
@ExceptionHandler({
        InternalAuthenticationServiceException.class,
        BadCredentialsException.class
})
public JsonResult handleAuthenticationException(AuthenticationException e) {
    log.warn("程序運(yùn)行過程中出現(xiàn)了AuthenticationException,將統(tǒng)一處理!");
    log.warn("異常:", e);
    String message = "登錄失敗,用戶名或密碼錯(cuò)誤!";
    return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
}

@ExceptionHandler
public JsonResult handleDisabledException(DisabledException e) {
    log.warn("程序運(yùn)行過程中出現(xiàn)了DisabledException,將統(tǒng)一處理!");
    log.warn("異常:", e);
    String message = "登錄失敗,賬號(hào)已經(jīng)被禁用!";
    return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLE, message);
}

?

以上只能算驗(yàn)證已經(jīng)完成了,還不能算登錄已經(jīng)成功,因?yàn)樵谂袛嘤脩裘兔艽a對(duì)了以后,還需要把相關(guān)的信息比如用戶,把它放進(jìn)例如session里面去,回頭判斷有沒有登錄的標(biāo)準(zhǔn),就是看session有沒有這個(gè)信息,有就是登錄了,沒有就是沒登錄。所以登錄不是判斷用戶名密碼就結(jié)束,還需要把信息留下來,下次在來訪問的時(shí)候才知道你是誰。不僅僅是驗(yàn)證的過程。

關(guān)于認(rèn)證的標(biāo)準(zhǔn)

Spring Security為每個(gè)客戶端分配了一個(gè)SecurityContext(可稱之為“Security上下文”),并且,會(huì)根據(jù)在SecurityContext是否存在認(rèn)證信息來判斷當(dāng)前請(qǐng)求是否已經(jīng)通過認(rèn)證!即:

  • 如果在SecurityContext存在有效的認(rèn)證信息,則視為“已通過認(rèn)證”

  • 如果在SecurityContext沒有有效的認(rèn)證信息,則視為“未通過認(rèn)證”

所以,在驗(yàn)證登錄成功后,需要將認(rèn)證信息存入到SecurityContext中,否則,所開發(fā)的登錄功能是沒有意義的!

其實(shí)調(diào)用AuthenticationManager(認(rèn)證管理器)對(duì)象的authenticate()方法時(shí)是可以接收到一個(gè)返回值的,可以獲取到認(rèn)證結(jié)果。

使用SecurityContextHoldergetContext()靜態(tài)方法可以獲取當(dāng)前客戶端對(duì)應(yīng)的SecurityContext對(duì)象!

[SpringBoot]Spring Security框架

?

?打印認(rèn)證方法返回的結(jié)果

[SpringBoot]Spring Security框架

?

?以上認(rèn)證方法返回的結(jié)果例如:

UsernamePasswordAuthenticationToken [
	Principal=org.springframework.security.core.userdetails.User [
		Username=root, 
		Password=[PROTECTED], 
		Enabled=true, 
		AccountNonExpired=true, 
		credentialsNonExpired=true, 
		AccountNonLocked=true, 
		Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限!??!]
	], 
	Credentials=[PROTECTED], 
	Authenticated=true, 
	Details=null, 
	Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
]

其實(shí),以上數(shù)據(jù)是基于UserDetailsSerivce實(shí)現(xiàn)類中loadUserByUsername()返回的UserDetails對(duì)象來創(chuàng)建的!

后續(xù)整個(gè)Spring Security在登錄之后每次發(fā)請(qǐng)求的時(shí)候就可以重SecurityContext的到這個(gè)數(shù)據(jù),從而識(shí)別你的身份。

以上算是實(shí)現(xiàn)了一個(gè)登錄的完整功能,但是還有一個(gè)小的問題,比如在登錄了的時(shí)候,服務(wù)端重啟了,此時(shí)登錄的信息就沒了,此時(shí)在沒有登錄信息的時(shí)候去訪問那些必須要登錄的請(qǐng)求。會(huì)得到一個(gè)403錯(cuò)誤。所以以下:

未通過認(rèn)證時(shí)拒絕訪問

當(dāng)未通過認(rèn)證(Spring Security從SecurityContext中未找到認(rèn)證信息)時(shí),嘗試訪問那些需要授權(quán)的資源(不在白名單中的,需要先登錄才可以訪問的資源),在沒有啟用http.formLogin()時(shí),默認(rèn)將響應(yīng)403錯(cuò)誤!

需要在Spring Security的配置類中進(jìn)行處理: ?

首先用http去這個(gè)方法

[SpringBoot]Spring Security框架

?這個(gè)方法需要傳進(jìn)去的參數(shù)的類型是AuthenticationEntryPoint,點(diǎn)開后發(fā)現(xiàn)也是一個(gè)接口。

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?有兩種方式,可以自己寫個(gè)類去實(shí)現(xiàn),但這個(gè)本身是一次性的使用,因?yàn)檫@個(gè)類只用在配置里,而配置本身是一次性的代碼,所以可以用匿名內(nèi)部類來寫。

[SpringBoot]Spring Security框架

?這里我們需要向客戶端去響應(yīng)一個(gè)錯(cuò)誤說你還沒有登錄,那么可以直接用response去響應(yīng),比如通過一個(gè)輸出流-寫出文本-關(guān)流響應(yīng)一個(gè)簡單內(nèi)容:

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?但是現(xiàn)在響應(yīng)的內(nèi)容太過簡單,可以響應(yīng)一個(gè)message內(nèi)容進(jìn)去。

[SpringBoot]Spring Security框架

?此時(shí)響應(yīng)出現(xiàn)顯示為一堆問號(hào),這是因?yàn)閖ava原始的服務(wù)器端的問題,默認(rèn)使用的是ISO-8859-1這個(gè)編碼格式,這種格式是不支持中文的。

[SpringBoot]Spring Security框架

?要在文檔響應(yīng)之前,設(shè)置編碼格式,例如:

[SpringBoot]Spring Security框架

此時(shí)顯示就沒有問題了:?

?[SpringBoot]Spring Security框架

?但此時(shí)任然不符合我們的設(shè)計(jì)需求。我們因該響應(yīng)給客戶端的是一個(gè)json結(jié)果,而不是一個(gè)字符串而已,需要更改文檔類型前半截:

[SpringBoot]Spring Security框架

?在寫入一個(gè)json格式的字符串(格式可以復(fù)制,手敲累容易出錯(cuò)):
[SpringBoot]Spring Security框架

?得到顯示json的結(jié)果:

[SpringBoot]Spring Security框架

?現(xiàn)在代碼惡心在需要自己去拼json這個(gè)結(jié)果,最終我們要響應(yīng)的還是和之前成功處理請(qǐng)求和處理異常時(shí)得到是一樣的結(jié)果,它依然是一個(gè)json格式的數(shù)據(jù),只不過我們之前處理請(qǐng)求,處理異常返回JsonResult就可以,為什么返回JsonResult的對(duì)象最終響應(yīng)是一個(gè)json的數(shù)據(jù)是因?yàn)閟pringMVC框架幫我們做了數(shù)據(jù)格式的轉(zhuǎn)換,轉(zhuǎn)換成json格式的字符串。但現(xiàn)在不能轉(zhuǎn),它不在springMVC的范圍之內(nèi)。

[SpringBoot]Spring Security框架

?則需要人為創(chuàng)建JSON格式的結(jié)果!可以借助fastjson工具進(jìn)行處理,這是一款可以實(shí)現(xiàn)對(duì)象與JSON格式字符串相互轉(zhuǎn)換的工具!需要添加依賴:

<fastjson.version>1.2.75</fastjson.version>
<!-- fastjson:實(shí)現(xiàn)對(duì)象與JSON的相互轉(zhuǎn)換 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>${fastjson.version}</version>
</dependency>

?便可以用這個(gè)工具做以下調(diào)整:

[SpringBoot]Spring Security框架

?最終:

[SpringBoot]Spring Security框架

?

目前,登錄算做好了,對(duì)Security使用難得部分已經(jīng)過去了,下面是一些往后推進(jìn)會(huì)設(shè)計(jì)的問題:

識(shí)別當(dāng)事人(Principal)

當(dāng)事人:當(dāng)前提交請(qǐng)求的客戶端的身份數(shù)據(jù) ?。

當(dāng)事人是一種身份數(shù)據(jù),作用是,比如你登錄一款軟件,這個(gè)軟件得知道你是誰,不然就無法做相關(guān)的操作,例如登錄之后你要修改自己的密碼,首先它得知道你是誰,然后再去改你的密碼。這份表示你到底是誰的這個(gè)數(shù)據(jù)其核心,我們就把它叫做當(dāng)事人。

當(dāng)通過登錄的驗(yàn)證后,AuthenticationManagerauthenticate()方法返回的Authentication對(duì)象中,就包含了當(dāng)事人信息!例如: ?

UsernamePasswordAuthenticationToken [
	Principal=org.springframework.security.core.userdetails.User [
		Username=root, 
		Password=[PROTECTED], 
		Enabled=true, 
		AccountNonExpired=true, 
		credentialsNonExpired=true, 
		AccountNonLocked=true, 
		Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
	], 
	Credentials=[PROTECTED], 
	Authenticated=true, 
	Details=null, 
	Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
]

?數(shù)據(jù)里面的Principal這些數(shù)據(jù)就是當(dāng)事人。

由于已經(jīng)將以上認(rèn)證結(jié)果存入到SecurityContext中,則可以在后續(xù)任何需要識(shí)別當(dāng)事人的場景中,獲取當(dāng)事人信息! ?

Spring Security提供了非常便利的獲取當(dāng)事人的做法,在控制器類中的處理請(qǐng)求的方法的參數(shù)列表中,可以聲明當(dāng)事人類型的參數(shù)(這里的user就是當(dāng)時(shí)返回的userDetails,即可以說它是userDetails類型也可以說是User類型):

[SpringBoot]Spring Security框架

并在參數(shù)上添加@AuthenticationPrincipal注解即可,例如找到管理員的controller: ?

?

[SpringBoot]Spring Security框架

上面添加@ApiIgnore是因?yàn)?寫user有一個(gè)問題是API文檔會(huì)以為你這個(gè)是請(qǐng)求參數(shù),會(huì)在API文檔中看到很多參數(shù),調(diào)試?yán)锩嬉矔?huì)有很多輸入框,需要加上這個(gè)注解來忽略。

?[SpringBoot]Spring Security框架

?此時(shí)這個(gè)user是有值的

[SpringBoot]Spring Security框架

?它的值就是在登錄成功后返回的當(dāng)事人數(shù)據(jù):?

[SpringBoot]Spring Security框架

?也就是這一截:

Principal=org.springframework.security.core.userdetails.User [
		Username=root, 
		Password=[PROTECTED], 
		Enabled=true, 
		AccountNonExpired=true, 
		credentialsNonExpired=true, 
		AccountNonLocked=true, 
		Granted Authorities=[這是一個(gè)臨時(shí)使用的山寨的權(quán)限?。?!]
	]

就可以通過get拿到當(dāng)時(shí)人的信息:?

[SpringBoot]Spring Security框架

?完成以上代碼后,重啟項(xiàng)目,可以在API文檔中使用各個(gè)賬號(hào)嘗試登錄并訪問以上“查詢管理員列表”,可以看到日志中輸出了當(dāng)次登錄的賬號(hào)的用戶名,例如:

[SpringBoot]Spring Security框架

?

通過以上做法,雖然可以獲取當(dāng)事人信息,但是,無論是UserDetails還是User類型,可以獲取的數(shù)據(jù)信息較少,且不包含當(dāng)前登錄的用戶的ID,通常并不滿足開發(fā)需求! ?

?需要記?。寒?dāng)前在控制器類中處理請(qǐng)求的方法中注入的當(dāng)事人數(shù)據(jù),就是UserDetailsService接口的實(shí)現(xiàn)類中返回的數(shù)據(jù)!

[SpringBoot]Spring Security框架

?而里面的數(shù)據(jù)來自于loginInfo

[SpringBoot]Spring Security框架

?loginInfo是從數(shù)據(jù)庫查出來的

[SpringBoot]Spring Security框架

?所以如果需要獲取當(dāng)事人的ID,需要:

?在AdminLoginInfoVO中添加ID屬性

[SpringBoot]Spring Security框架

?修改Mapper層的getLoginInfoByUsername(),需要查詢管理員ID

[SpringBoot]Spring Security框架

?現(xiàn)有的UserDetails的實(shí)現(xiàn)類User并不支持ID屬性,需要自定義類實(shí)現(xiàn)UserDetails接口,或者,自定義類繼承自User類,在自定義類中擴(kuò)展出所需的各種屬性,例如ID

因?yàn)樗旧斫o了我們user類

[SpringBoot]Spring Security框架

?

?點(diǎn)開后發(fā)現(xiàn)user實(shí)現(xiàn)了UserDetails類

[SpringBoot]Spring Security框架

?

?所以我們自定義繼承user相對(duì)于也實(shí)現(xiàn)了UserDetails,最終也可以作為這個(gè)方法的返回值。

[SpringBoot]Spring Security框架

?

在項(xiàng)目的根包下創(chuàng)建security.AdminDetails類,繼承自User類,添加基于父類的構(gòu)造方法,并擴(kuò)展出ID屬性:

[SpringBoot]Spring Security框架

?然后只用第二個(gè)多的構(gòu)造方法,第一個(gè)可以去掉,第二個(gè)包含了第一個(gè)所有的參數(shù),還有賬戶啟動(dòng)狀態(tài)等必要的信息。

[SpringBoot]Spring Security框架

但同時(shí)也用不完第二個(gè)構(gòu)造方法里面的所有參數(shù),我們需要把自己的構(gòu)造方法中不用的參數(shù)去掉,同時(shí),在調(diào)用父類的構(gòu)造方法的時(shí)候需要這個(gè)參數(shù),我們?cè)诮o個(gè)固定的值傳過去就好了。

[SpringBoot]Spring Security框架

?擴(kuò)展出id屬性,并給構(gòu)造參數(shù)加上id傳進(jìn)來給值。回頭還需要被這個(gè)值取出來,但是不能用@Data,因?yàn)長ombok需要在父類也就是user類有一個(gè)默認(rèn)的無參構(gòu)造方法,但是user沒有。所以添加@Getter注解。

[SpringBoot]Spring Security框架

?在UserDetailsService中返回?cái)?shù)據(jù)時(shí),改為返回自定義類的對(duì)象,其中將包含ID等屬性值

[SpringBoot]Spring Security框架

?里面的自定義的傳參會(huì)略有不用,之前判斷賬號(hào)是否禁用的==0,因?yàn)楫?dāng)時(shí)方法叫做disabled禁用,而自己的屬性的啟用,就用==1判斷。

[SpringBoot]Spring Security框架

?添加權(quán)限用集合,以下:

[SpringBoot]Spring Security框架

?最終代碼如下:

[SpringBoot]Spring Security框架

?在控制器類中處理請(qǐng)求的方法中,注入的當(dāng)事人類型改為自定義類型

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?

以上實(shí)現(xiàn)了可以登錄登錄后也知道你是誰的功能,登錄的效果就差不多了,而Spring Security還有一個(gè)重要的功能就是權(quán)限,我們可以區(qū)分不同的賬戶它有什么操作權(quán)限,使得某些用戶可以做特定的事情。如果要去判端當(dāng)前這個(gè)人有沒有權(quán)限去做這個(gè)事情,第一件事是把現(xiàn)在給的山寨權(quán)限換成數(shù)據(jù)庫里的真實(shí)權(quán)限。

實(shí)現(xiàn)根據(jù)權(quán)限限制訪問

?首先,需要在管理員登錄時(shí),明確此管理員的權(quán)限,則需要在Mapper層實(shí)現(xiàn)“根據(jù)用戶名查詢管理員的登錄信息,且需要包含此管理員對(duì)應(yīng)的各權(quán)限”,需要執(zhí)行的SQL語句大致是:

select
    ams_admin.id,
    ams_admin.username,
    ams_admin.password,
    ams_admin.enable,
    ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.id=ams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_id=ams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_id=ams_permission.id
where username='root';

然后,修改現(xiàn)有的查詢功能,需要先在AdminLoginInfoVO類中添加新的屬性,用于存放“權(quán)限列表”:

[SpringBoot]Spring Security框架

?

?

?然后,調(diào)整AdminMapper.xml中的配置:

<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginInfoResultMap">
    SELECT
        ams_admin.id,
        ams_admin.username,
        ams_admin.password,
        ams_admin.enable,
        ams_permission.value
    FROM ams_admin
        LEFT JOIN ams_admin_role ON ams_admin.id=ams_admin_role.admin_id
        LEFT JOIN ams_role_permission ON ams_admin_role.role_id=ams_role_permission.role_id
        LEFT JOIN ams_permission ON ams_role_permission.permission_id=ams_permission.id
    WHERE
        username=#{username}
</select>

<!-- resultMap標(biāo)簽:指導(dǎo)MyBatis封裝查詢結(jié)果 -->
<!-- resultMap標(biāo)簽的id屬性:自定義名稱,也是select標(biāo)簽上使用resultMap屬性的值 -->
<!-- resultMap標(biāo)簽的type屬性:封裝查詢結(jié)果的類型的全限定名 -->
<resultMap id="LoginInfoResultMap"
           type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
    <!-- id標(biāo)簽:配置主鍵的列與屬性的對(duì)應(yīng)關(guān)系 -->
    <!-- result標(biāo)簽:配置普通的列與屬性的對(duì)應(yīng)關(guān)系 -->
    <!-- collection標(biāo)簽:配置List集合類型的屬性與查詢結(jié)果中的數(shù)據(jù)的對(duì)應(yīng)關(guān)系 -->
    <!-- collection標(biāo)簽的ofType屬性:集合中的元素類型,取值為類型的全限定名 -->
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="enable" property="enable"/>
    <collection property="permissions" ofType="String">
        <!-- constructor標(biāo)簽:通過構(gòu)造方法來創(chuàng)建對(duì)象 -->
        <constructor>
            <!-- arg標(biāo)簽:配置構(gòu)造方法的參數(shù),如果構(gòu)造方法有多個(gè)參數(shù),依次使用多個(gè)此標(biāo)簽 -->
            <arg column="value"></arg>
        </constructor>
    </collection>
</resultMap>

補(bǔ)充解釋(關(guān)于使用resultMap標(biāo)簽):

[SpringBoot]xml文件里寫SQL用resultMap標(biāo)簽_萬物更新_的博客-CSDN博客

?配置完成后,可以通過測試進(jìn)行檢驗(yàn),查詢結(jié)果例如:

根據(jù)【username=super_admin】查詢數(shù)據(jù)完成,結(jié)果:

AdminLoginInfoVO(
	id=2, 
	username=super_admin, 
	password=$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C, 
	enable=1, 
	permissions=[/pms/product/read, /pms/product/add-new, /pms/product/delete, /pms/product/update, /pms/brand/read, /pms/brand/add-new, /pms/brand/delete, /pms/brand/update, /pms/category/read, /pms/category/add-new, /pms/category/delete, /pms/category/update, /pms/picture/read, /pms/picture/add-new, /pms/picture/delete, /pms/picture/update, /pms/album/read, /pms/album/add-new, /pms/album/delete, /pms/album/update]
)

?

?

基于方法的權(quán)限檢查

以上loginInfo已經(jīng)有真實(shí)的權(quán)限信息,從中g(shù)et出真實(shí)權(quán)限,遍歷加到權(quán)限集合里面去,加進(jìn)去后,返回的userDetails就有真正的權(quán)限信息了。

[SpringBoot]Spring Security框架

當(dāng)有了真實(shí)的權(quán)限以后,接下來就可以對(duì)所有的訪問加上權(quán)限的限制,就是某些人可以干什么,某些人不可以干什么。要實(shí)現(xiàn)這樣的效果需要做兩件事情。

第一件事情,找到配置類, 開啟權(quán)限的檢查機(jī)制

[SpringBoot]Spring Security框架

?接下來就可以做訪問什么需要什么權(quán)限,例如必須具有管理員權(quán)限的值的人,才可以查看權(quán)限列表。

[SpringBoot]Spring Security框架?

加上下面這個(gè)注解后就表示你不光要登錄,你的認(rèn)證信息的權(quán)限列表里面必須要包含hasAuthority里面的這個(gè)值,才能夠做這次的訪問,如果不包含這個(gè)權(quán)限,就訪問不了。

[SpringBoot]Spring Security框架

提示:以上使用@PreAuthorize注解檢查權(quán)限時(shí),此注解可以添加在任何方法上!例如Controller中的方法,或Service中的方法等等,由于當(dāng)前項(xiàng)目中,客戶端的請(qǐng)求第一時(shí)間都是交給了Controller,所以,更適合在Controller方法上檢查權(quán)限!

?

當(dāng)訪問不包含所需的權(quán)限時(shí),?Spring Security給了我們以下這個(gè)異常:

[SpringBoot]Spring Security框架

?有異常在全局異常處理器里面處理異常:[SpringBoot]Spring Security框架

?

ServiceCode中添加新的業(yè)務(wù)狀態(tài)碼表示“無此權(quán)限”:

[異常]401和403的區(qū)分_萬物更新_的博客-CSDN博客

[SpringBoot]Spring Security框架

?

然后,在全局異常處理器中添加處理以上異常的方法: ?

[SpringBoot]Spring Security框架

?

以上權(quán)限做好以后,還需要給它添加Token功能,這樣每次客服端在訪問過一次之后,都不用在繼續(xù)登陸。

[java]關(guān)于Session&關(guān)于Token&關(guān)于JWT_萬物更新_的博客-CSDN博客?

添加Token?

首先添加Token-JWT的依賴項(xiàng):

父項(xiàng)目添加版本管理:

[SpringBoot]Spring Security框架

?父項(xiàng)目添加依賴:

[SpringBoot]Spring Security框架

?子項(xiàng)目添加依賴:

[SpringBoot]Spring Security框架

?添加好依賴以后做兩個(gè)測試,一個(gè)生成JWT的測試,一個(gè)解析JWT 的測試:

生成JWT:


    // 不太簡單的、難以預(yù)測的字符串
    String secretKey = "jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs";

    @Test
    void generate() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 9527);
        claims.put("name", "張三");

        String jwt = Jwts.builder()
                // Header
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "JWT")
                // Payload
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 3 * 60 * 1000))//設(shè)置有效期,防止一直用.
                // Verify Signature
                .signWith(SignatureAlgorithm.HS256, secretKey)
                // 生成
                .compact();
        System.out.println(jwt);
    }

備注:

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?基于它的做法,我們可以自己傳進(jìn)去一個(gè)值:

[SpringBoot]Spring Security框架

?

?解析JWT

    // 不太簡單的、難以預(yù)測的字符串
    String secretKey = "jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs";  

   @Test
    void parse() {
        String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoi5byg5LiJIiwiaWQiOjk1MjcsImV4cCI6MTY4NDkwODUwMn0.tBo7YKRqQv6TG2cf5jeu7nNjUim5X8H6pKLF1LrYuKI";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Long id = claims.get("id", Long.class);
        String name = claims.get("name", String.class);
        System.out.println("id = " + id);
        System.out.println("name = " + name);
    }

備注:?

點(diǎn)進(jìn)Claims可以看到本質(zhì)是一個(gè)map

[SpringBoot]Spring Security框架

?獲取往里面放的值,直接給的是object,因?yàn)閙ap的value被定義死了是object,取出也是object

[SpringBoot]Spring Security框架

?但在這里Claims在原有的map之上,get方法是有擴(kuò)展的,傳入的第二個(gè)參數(shù)就是你的目標(biāo)類型是什么,這樣傳進(jìn)去是什么類型得到的就是什么類型。

[SpringBoot]Spring Security框架

?

以下是會(huì)這塊會(huì)出現(xiàn)的異常,列舉出來,回頭需要全局處理。

如果嘗試解析的JWT已經(jīng)過期,會(huì)出現(xiàn)異常:

io.jsonwebtoken.ExpiredJwtException: JWT expired at 2023-05-24T12:02:38Z. Current time: 2023-05-24T14:04:35Z, a difference of 7317175 milliseconds.  Allowed clock skew: 0 milliseconds.

?如果解析JWT時(shí)使用的secretKey有誤,會(huì)出現(xiàn)異常:

io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

如果解析JWT的數(shù)據(jù)格式錯(cuò)誤,會(huì)出現(xiàn)異常:

io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 1

補(bǔ)充:

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

注意:在不知曉secretKey的情況下,也可以解析出JWT中的數(shù)據(jù)(例如將JWT數(shù)據(jù)粘貼到官網(wǎng)),只不過驗(yàn)證簽名是失敗的,所以,不要在JWT中存放敏感信息(比如密碼,手機(jī)號(hào)碼,身份證號(hào)碼等)! [SpringBoot]Spring Security框架

?

?驗(yàn)證簽名是失敗的就是說就算你知道里面的數(shù)據(jù),但是我會(huì)告訴你不可信,比如id是9527但是也不要相信id就是9527,因?yàn)樗苡锌赡苁且粋€(gè)偽造的JWT,因?yàn)轵?yàn)證簽名失敗了。所以JWT的secretKey的價(jià)值是防止被偽造,而不是防止被解析出來,它不能做到這一點(diǎn)。[SpringBoot]Spring Security框架

?

經(jīng)過上面的測試,接下來就要在項(xiàng)目中使用JWT識(shí)別用戶的身份了

在項(xiàng)目中使用JWT識(shí)別用戶的身份

核心流程

?在項(xiàng)目中使用JWT識(shí)別用戶的身份,至少需要:

  • 當(dāng)驗(yàn)證登錄成功時(shí),生成JWT數(shù)據(jù),并響應(yīng)到客戶端去,是“賣票”的過程

    • 當(dāng)驗(yàn)證登錄成功后,不再需要(沒有必要)

    • 當(dāng)驗(yàn)證登錄成功時(shí),生成JWT數(shù)據(jù),并響應(yīng)到客戶端去,是“賣票”的過程

      • 當(dāng)驗(yàn)證登錄成功后,不再需要(沒有必要)將認(rèn)證結(jié)果存入到SecurityContext中 ,之前是這樣的:

[SpringBoot]Spring Security框架

?

  • 當(dāng)客戶端提交請(qǐng)求時(shí),需要獲取客戶端攜帶的JWT數(shù)據(jù),并嘗試解析,解析成功后,再將相關(guān)信息存入到SecurityContext中去,(因?yàn)橹拔覀冋fSecurity去檢驗(yàn)這個(gè)賬號(hào)或者說這次客戶端的訪問到底是不是一個(gè)已認(rèn)證的狀態(tài),就只是去看SecurityContext里面有沒有東西,所以一旦解析成功之后,還是要把相關(guān)信息往SecurityContext里面放,然后就沒了,后續(xù)說他有沒有登錄啊,有沒有權(quán)限啊不是這里管的事,是Security去做后續(xù)的處理)是“檢票”的過程

    • 可以調(diào)整Spring Security使用Session的策略,改為不使用Session,則不會(huì)將SecurityContext存入到Session中(不存在Session里面的好處是它就只作用在這一次請(qǐng)求中,這次請(qǐng)求結(jié)束了SecurityContext就沒了,當(dāng)下次在過來的時(shí)候就又有了,結(jié)束了又沒了。。。所以SecurityContext里面的認(rèn)證信息只作用于當(dāng)次那一次而已,在沒有調(diào)整之前是基于session的,意味著如果session的有效期是15分鐘,那你把認(rèn)證信息存上下文里面,那這個(gè)上下文的有效時(shí)間就是15分鐘,15分鐘之內(nèi)一直存在這個(gè)數(shù)據(jù)了,如果有效期是30分鐘,那就會(huì)存在30分鐘,在這個(gè)30分鐘里面肯定是會(huì)有浪費(fèi)的時(shí)間的,內(nèi)存里面存這個(gè)信息就會(huì)浪費(fèi)了,并且在你重新來訪之后時(shí)間又會(huì)重新調(diào)整為30分鐘,所以會(huì)有很長時(shí)間的浪費(fèi)

?

驗(yàn)證登錄成功時(shí)響應(yīng)JWT

需要調(diào)整的代碼大致包括:

  • IAdminService中,將login()方法的返回值類型改為String類型,重寫的方法作同樣的修改

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?

  • AdminServiceImpl中,驗(yàn)證登錄成功后,生成此管理員的信息對(duì)應(yīng)的JWT(把上文測試?yán)锩嫔蒍WT的代碼拿過來做修改),并返回

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

  • AdminController中,處理登錄時(shí),調(diào)用Service方法時(shí)獲取返回的JWT,并響應(yīng)到客戶端去

[SpringBoot]Spring Security框架

?

解析客戶端攜帶的JWT

客戶端提交若干種不同的請(qǐng)求時(shí),可能都會(huì)攜帶JWT,對(duì)應(yīng)的,在服務(wù)器,處理若干種不同的請(qǐng)求時(shí),也都需要嘗試接收并解析JWT,則應(yīng)該使用過濾器(Filter)組件進(jìn)行處理!

[web]關(guān)于過濾器Filter_萬物更新_的博客-CSDN博客

其實(shí),Spring Security框架也使用了許多不同的過濾器來解決各種問題,為了保證解析JWT是有效的,解析JWT的代碼必須運(yùn)行在Spring Security的某些過濾器之前,則接收、解析JWT的代碼也必須定義在過濾器中!

提示:過濾器(Filter)是Java服務(wù)器端應(yīng)用程序的核心組件之一,它是最早接收到請(qǐng)求的組件!過濾器可以對(duì)請(qǐng)求選擇“阻止”或“放行”!同一個(gè)項(xiàng)目中,允許存在若干個(gè)過濾器,形成“過濾器鏈(Filter Chain)”,任何請(qǐng)求必須被所有過濾器都“放行”,才會(huì)被控制器或其它組件所處理!

?按照之前的方法,實(shí)現(xiàn)javax.servlet的過濾器接口,讓后重寫doFilter方法。

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?但是重寫方法需要對(duì)類型進(jìn)行強(qiáng)轉(zhuǎn),比較麻煩,不太好用。?

[SpringBoot]Spring Security框架

?

?我們這次選擇去繼承Spring系列框架提供的OncePerRequestFilter這個(gè)類。

[SpringBoot]Spring Security框架?

?這個(gè)類是一個(gè)抽象類,這個(gè)類繼承自GenericFilterBean這個(gè)類。[SpringBoot]Spring Security框架

而GenericFilterBean這個(gè)類實(shí)現(xiàn)了Filter這個(gè)接口,?所以繼承OncePerRequestFilter這個(gè)類也算是實(shí)現(xiàn)了過濾器接口的。

[SpringBoot]Spring Security框架

繼承spring這個(gè)框架提供的OncePerRequestFilter這個(gè)類已經(jīng)幫我們做了強(qiáng)轉(zhuǎn)了,就不用我們自己強(qiáng)轉(zhuǎn)了。

[SpringBoot]Spring Security框架?

?所以在項(xiàng)目的根包下創(chuàng)建filter.JwtAuthorizationFilter類,繼承自OncePerRequestFilter類,并添加@Component注解:

@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("JwtAuthorizationFilter開始執(zhí)行……");

        // 放行
        filterChain.doFilter(request, response);
    }

}

添加@Component注解把它標(biāo)記成組件是因?yàn)橥ㄟ^注入,把解析JWT的代碼必須運(yùn)行在Spring Security的某些過濾器之前。

[SpringBoot]Spring Security框架

?到此可以測試通過API登錄請(qǐng)求常看第一步過濾器有沒有生效:

[SpringBoot]Spring Security框架

關(guān)于攜帶JWT,根據(jù)業(yè)內(nèi)慣用的做法,客戶端會(huì)將JWT放在請(qǐng)求頭(Request Header)中的Authorization屬性中,在Knife4j的API文檔中,可以:

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

關(guān)于過濾器的初步實(shí)現(xiàn):

/**
 * JWT過濾器,解決的問題:接收J(rèn)WT,解析JWT,將解析得到的數(shù)據(jù)創(chuàng)建為認(rèn)證信息并存入到SecurityContext
 */
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.debug("JwtAuthorizationFilter開始執(zhí)行……");
        // 根據(jù)業(yè)內(nèi)慣用的做法,客戶端會(huì)將JWT放在請(qǐng)求頭(Request Header)中的Authorization屬性中
        String jwt = request.getHeader("Authorization");
        log.debug("客戶端攜帶的JWT:{}", jwt);

        // 判斷客戶端是否攜帶了有效的JWT
        if (!StringUtils.hasText(jwt)) {
            // 如果JWT無效,則放行,并reture
            filterChain.doFilter(request, response);
            return;
        }

        // TODO 當(dāng)前類和AdminServiceImpl中都聲明了同樣的secretKey變量,是不合理的
        // TODO 解析JWT過程中可能出現(xiàn)異常,需要處理
        // 嘗試解析JWT
        String secretKey = "jhdSfkkjKJ3831HdsDkdfSA9jklJD749Fhsa34fdsKf08dfjFhkdfs";
        Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
        Long id = claims.get("id", Long.class);
        String username = claims.get("username", String.class);
        System.out.println("id = " + id);
        System.out.println("username = " + username);

        // TODO 需要考慮使用什么數(shù)據(jù)作為當(dāng)事人
        // TODO 需要使用真實(shí)的權(quán)限
        // 創(chuàng)建認(rèn)證信息
        Object principal = username; //當(dāng)事人 可以是任何類型,暫時(shí)使用用戶名
        Object credentials = null; //憑證 本次不需要
        Collection<GrantedAuthority> authorities = new ArrayList<>();//權(quán)限
        authorities.add(new SimpleGrantedAuthority("山寨權(quán)限"));
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                principal, credentials, authorities);

        // 將認(rèn)證信息存入到SecurityContext中
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(authentication);

        // 放行
        filterChain.doFilter(request, response);
    }

}

因?yàn)樯厦娲a中當(dāng)事人是username,此時(shí)參數(shù)再用AdminDetails 聲明是不對(duì)的,此處暫時(shí)去掉。

[SpringBoot]Spring Security框架

?

需要注意:由于Spring Security的SecurityContext默認(rèn)是基于Session的,所以,當(dāng)攜帶JWT成功登錄訪問過后,在SecurityContext中就已經(jīng)有了認(rèn)證信息,并且,在Session的有效期內(nèi),即使后續(xù)不攜帶JWT,Spring Security也能基于Session找到SecurityContext并讀取到認(rèn)證信息,并不在需要登錄就能訪問的,這可能與設(shè)計(jì)初衷并不相符!

可以將Spring Security使用(創(chuàng)建)Session的策略改為“完全不使用Session”,需要在Spring Security的配置類中添加配置:

[SpringBoot]Spring Security框架

?

備注:
1.用StringUtils.hasText的方法

[SpringBoot]Spring Security框架

?用StringUtils.hasText的方法可以同時(shí)判斷,不能為空,不能為null,和包含文本。

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?包含文本即不是空白就是包含文本:

[SpringBoot]Spring Security框架

?2.關(guān)于?Object credentials = null本此不需要憑證,因?yàn)橹皯{證的表現(xiàn)是密碼,而放在上下文里的認(rèn)證信息作用是回頭框架來識(shí)別出你是誰,有什么權(quán)限,這個(gè)過程是不需要使用密碼的。

?

關(guān)于認(rèn)證信息中的當(dāng)事人

pring Security框架并不介意你使用什么類型作為認(rèn)證信息(Authentication)中的當(dāng)事人(principal)!

在項(xiàng)目中,到底使用什么類型作為當(dāng)事人,可以自行考慮,主要考慮的因素就是:當(dāng)你需要注入當(dāng)事人數(shù)據(jù)的時(shí)候,你希望能夠得到哪些數(shù)據(jù)!

在項(xiàng)目的根包下創(chuàng)建security.LoginPrincipal作為自定義的當(dāng)事人類型:

[SpringBoot]Spring Security框架

?

并且,在解析JWT成功后,在過濾去使用此類型作為當(dāng)事人來創(chuàng)建認(rèn)證信息:

[SpringBoot]Spring Security框架

?后續(xù),在Controller中,就可以通過@AuthenticationPrincipal來注入自定義的當(dāng)事人數(shù)據(jù),例如:

[SpringBoot]Spring Security框架

?

接著處理一個(gè)小問題,因?yàn)樵谏珊徒馕鯦WT的時(shí)候?qū)π枰玫絪ecretKey這個(gè)值,并且這個(gè)值相同,如果不相同就會(huì)簽名失敗,所以一個(gè)完全相同的代碼寫兩遍是不合理的,有兩種解決方案,第一種是專門寫一個(gè)類去調(diào)取,第二個(gè)是寫在application.yml文件里面,它們的區(qū)別是在application.yml里面需要讀取在應(yīng)用,有一個(gè)讀取的過程,在類里面是直接應(yīng)用的,從執(zhí)行效率來說肯定是在類里面更快一些,但由于這個(gè)值需要甲方來定(為了防止偽造相關(guān)問題),所以必須寫在application.yml里面。

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

?

?關(guān)于secretKey必須有4位以上,否則都會(huì)被視為空值報(bào)錯(cuò)

[SpringBoot]Spring Security框架

?

以上權(quán)限還是一個(gè)假的權(quán)限,需要換成真的權(quán)限,目前我們就用把權(quán)限放在JWT中,然后再從JWT中取出權(quán)限的方式。(以替換在數(shù)據(jù)庫里查的方式,因?yàn)閺臄?shù)據(jù)庫里查數(shù)據(jù)是一個(gè)效率低下的方式,其一需要連接,傳遞SQL,然后準(zhǔn)備,準(zhǔn)備好了編譯執(zhí)行,執(zhí)行好了在給個(gè)結(jié)果一個(gè)過程。其二,數(shù)據(jù)庫里面的數(shù)據(jù)存在硬盤里面,硬盤是一個(gè)存儲(chǔ)效率非常低效的硬件。同時(shí)這段代碼只要有客戶端來訪就會(huì)執(zhí)行這段代碼,發(fā)生的非常高頻率,所以不能選擇連接數(shù)據(jù)庫這么低效的做法)?

?把集合放進(jìn)JWT里面。

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架?

?從JWT取出權(quán)限列表[SpringBoot]Spring Security框架

?這樣的取出方式看似語法沒有問題,但會(huì)出現(xiàn)類型轉(zhuǎn)換錯(cuò)誤。

[SpringBoot]Spring Security框架

?因?yàn)樵谶@一步,它獲取出來的是LinkedHashMap,但是LinkedHashMap不能強(qiáng)制轉(zhuǎn)其他類型,為什么獲取的是LinkedHashMap類型呢,因?yàn)锳PI不知道你要獲取什么類型,給你處理為了LinkedHashMap。因?yàn)槭羌霞臃盒鸵矝]有辦法向獲取id一樣在后面第二個(gè)參數(shù)加上Long.class來指定返回的類型。

[SpringBoot]Spring Security框架

?

?所以這里需要換一個(gè)做法,在生成JWT的時(shí)候不往里面放集合里,改為放Json,

[SpringBoot]Spring Security框架

?可以放Json是因?yàn)槲覀冇刑砑觙astjson的依賴,實(shí)現(xiàn)對(duì)象和Json相互轉(zhuǎn)換的依賴。

[SpringBoot]Spring Security框架

?在從JWT 取出權(quán)限的時(shí)候也取出Json字符串,然后用fastjson轉(zhuǎn)成集合

[SpringBoot]Spring Security框架

?

[SpringBoot]Spring Security框架

以上就實(shí)現(xiàn)真實(shí)權(quán)限的功能了。

?注意:此方式也不是最優(yōu)解決方案。

?

接下來處理解析JWT時(shí)可能出現(xiàn)的異常,往常我們是在全局異常處理器處理的,但是在這里不行,因?yàn)榻馕鯦WT是在過濾器里面做的,全局異常處理器只能處理controller拋出的異常。

處理解析JWT時(shí)的異常

由于解析JWT是在過濾器組件中執(zhí)行的,而過濾器是最早處理請(qǐng)求的組件,此時(shí),控制器(Controller)還沒有開始處理這次的請(qǐng)求,則全局異常處理器也無法處理解析JWT時(shí)出現(xiàn)的異常(全局異常處理器只能處理控制器拋出的異常)!這里使用最原始的try...catch處理

首先,在ServiceCode中補(bǔ)充新的狀態(tài)碼:

ERR_JWT_EXPIRED(60000),
ERR_JWT_MALFORMED(60100),
ERR_JWT_SIGNATURE(60200),

然后,在JwtAuthorizationFilter中,使用try...catch包裹嘗試解析JWT的代碼:

// 嘗試解析JWT
response.setContentType("application/json; charset=utf-8");
Claims claims = null;
try {
    claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (MalformedJwtException e) {
    String message = "非法訪問!";
    log.warn("程序運(yùn)行過程中出現(xiàn)了MalformedJwtException,將向客戶端響應(yīng)錯(cuò)誤信息!");
    log.warn("錯(cuò)誤信息:{}", message);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
    String jsonString = JSON.toJSONString(jsonResult);
    PrintWriter printWriter = response.getWriter();
    printWriter.println(jsonString);
    printWriter.close();
    return;
} catch (SignatureException e) {
    String message = "非法訪問!";
    log.warn("程序運(yùn)行過程中出現(xiàn)了SignatureException,將向客戶端響應(yīng)錯(cuò)誤信息!");
    log.warn("錯(cuò)誤信息:{}", message);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
    String jsonString = JSON.toJSONString(jsonResult);
    PrintWriter printWriter = response.getWriter();
    printWriter.println(jsonString);
    printWriter.close();
    return;
} catch (ExpiredJwtException e) {
    String message = "您的登錄信息已經(jīng)過期,請(qǐng)重新登錄!";
    log.warn("程序運(yùn)行過程中出現(xiàn)了ExpiredJwtException,將向客戶端響應(yīng)錯(cuò)誤信息!");
    log.warn("錯(cuò)誤信息:{}", message);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
    String jsonString = JSON.toJSONString(jsonResult);
    PrintWriter printWriter = response.getWriter();
    printWriter.println(jsonString);
    printWriter.close();
    return;
} catch (Throwable e) {
    String message = "服務(wù)器忙,請(qǐng)稍后再試!【在開發(fā)過程中,如果看到此提示,應(yīng)該檢查服務(wù)器端的控制臺(tái),分析異常,并在解析JWT的過濾器中補(bǔ)充處理對(duì)應(yīng)異常的代碼塊】";
    log.warn("程序運(yùn)行過程中出現(xiàn)了Throwable,將向客戶端響應(yīng)錯(cuò)誤信息!");
    log.warn("異常:", e);
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);
    String jsonString = JSON.toJSONString(jsonResult);
    PrintWriter printWriter = response.getWriter();
    printWriter.println(jsonString);
    printWriter.close();
    return;
}

注意:

以上代碼中只有response.setContentType("application/json; charset=utf-8");這串代碼可以提到最上面給每一個(gè)catch復(fù)用。?PrintWriter printWriter = response.getWriter();是不可以的

[SpringBoot]Spring Security框架

把printWriter 放在上面會(huì)導(dǎo)致本該在正常成功訪問的時(shí)候會(huì)報(bào)狀態(tài)異常錯(cuò)誤,說getWriter在本次調(diào)用中已經(jīng)被占用了。原因是我們服務(wù)端向客戶端響應(yīng)就是用printWriter 來響應(yīng)的,然后你在上圖中拿到了getWriter輸出流,控制器那邊就拿不到響應(yīng)成功的輸出流了,以至于控制器沒有辦法去響應(yīng)。[SpringBoot]Spring Security框架

?

?

?以上JWT就差不多了,以下在和前端結(jié)合的時(shí)候還需要實(shí)現(xiàn)的一些功能。

處理復(fù)雜請(qǐng)求的跨域問題

當(dāng)客戶端提交請(qǐng)求時(shí),在請(qǐng)求頭中配置了特定的屬性(例如Authorization,帶了JWT的時(shí)候),則這個(gè)請(qǐng)求會(huì)被視為“復(fù)雜請(qǐng)求”:

[SpringBoot]Spring Security框架

對(duì)于復(fù)雜請(qǐng)求,瀏覽器會(huì)先對(duì)服務(wù)器端發(fā)送OPTIONS類型的請(qǐng)求(也是和get,post一樣的請(qǐng)求方式,OPTIONS請(qǐng)求的目的是試一下服務(wù)器是不是好的,是不是可以接受),以執(zhí)行預(yù)檢(PreFlight),如果預(yù)檢通過,才會(huì)執(zhí)行本應(yīng)該發(fā)送的請(qǐng)求。

然后會(huì)看到它的請(qǐng)求就需要給它配置白名單已通過。

[SpringBoot]Spring Security框架

?在Spring Security的配置類中,可以在配置對(duì)請(qǐng)求授權(quán)時(shí),將所有OPTIONS類型的請(qǐng)求全部直接許可,例如:

[SpringBoot]Spring Security框架

?或者,調(diào)用參數(shù)對(duì)象的cors()方法也可以,例如:

[SpringBoot]Spring Security框架

提示:對(duì)于復(fù)雜請(qǐng)求的預(yù)檢,是瀏覽器的行為,并且,當(dāng)某個(gè)請(qǐng)求通過預(yù)檢后,瀏覽器會(huì)緩存此結(jié)果,后續(xù)再次發(fā)出此請(qǐng)求時(shí),不會(huì)再次執(zhí)行預(yù)檢。

實(shí)現(xiàn)單點(diǎn)登錄,以下

?

單點(diǎn)登錄

SSOSingle Sign On):單點(diǎn)登錄,表示在集群或分布式系統(tǒng)中,客戶端只需要在某1個(gè)服務(wù)器上完成登錄的驗(yàn)證,后續(xù),無論訪問哪個(gè)服務(wù)器,都不需要再次重新登錄!常見的實(shí)現(xiàn)手段主要有:共享Session,使用Token。 ?

?目前,如果希望客戶端在csmall-passport中登錄后,在csmall-product中也能夠被識(shí)別身份、權(quán)限,需要:

  • 復(fù)制依賴項(xiàng):spring-boot-starter-security、jjwt、fastjson

[SpringBoot]Spring Security框架

  • 復(fù)制LoginPrincipal

[SpringBoot]Spring Security框架

?

  • 復(fù)制ServiceCode,覆蓋此前的文件

[SpringBoot]Spring Security框架

  • 復(fù)制application-dev.yml中的自定義的配置

[SpringBoot]Spring Security框架

  • 復(fù)制JwtAuthorizationFilter

[SpringBoot]Spring Security框架

?

  • 復(fù)制SecurityConfiguration,并更改導(dǎo)包

    • 刪除PasswordEncoder@Bean方法

    • 刪除AuthenticationManager@Bean方法

    • 刪除“白名單”中管理員登錄的URL地址

完成后,在csmall-product項(xiàng)目中,也可以通過@AuthenticationPrincipal來注入當(dāng)事人數(shù)據(jù),也可以使用@PreAuthorize來配置訪問權(quán)限,這些都是通的。

[SpringBoot]Spring Security框架

[SpringBoot]Spring Security框架

?

到了這里,關(guān)于[SpringBoot]Spring Security框架的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • Spring Security 框架

    認(rèn)證鏈 Spring Security的認(rèn)證處理鏈?zhǔn)且幌盗械倪^濾器鏈,用于處理用戶的身份驗(yàn)證和授權(quán)操作。這些過濾器在請(qǐng)求處理過程中依次執(zhí)行,并在不同的階段進(jìn)行不同的認(rèn)證和授權(quán)操作,以確保應(yīng)用程序的安全性和完整性。 下面是Spring Security的標(biāo)準(zhǔn)認(rèn)證處理鏈: UsernamePasswordAuthe

    2024年02月08日
    瀏覽(21)
  • Spring Security 框架詳解

    Spring Security是一款基于Spring框架的認(rèn)證和授權(quán)框架,提供了一系列控制訪問和保護(hù)應(yīng)用程序的功能,同時(shí)也支持基于角色和權(quán)限的訪問控制,加密密碼,CSRF防范,會(huì)話管理等多種功能。Spring Security可以輕松地與其他Spring框架,如Spring Boot和Spring MVC進(jìn)行集成使用。 本文將會(huì)對(duì)

    2024年02月04日
    瀏覽(20)
  • 【Spring Security系列】一文帶你了解權(quán)限框架與Spring Security核心概念

    【Spring Security系列】一文帶你了解權(quán)限框架與Spring Security核心概念

    權(quán)限框架是軟件開發(fā)中用于管理 用戶權(quán)限和訪問控制 的工具。在企業(yè)或者我們畢設(shè)復(fù)雜的系統(tǒng)中,不同的用戶或角色需要擁有不同的訪問和操作權(quán)限,以確保系統(tǒng)的安全性和數(shù)據(jù)完整性。今天我們就討論一下Java中的安全框架! 在企業(yè)的開發(fā)中,Spring Security,Shiro都是比較流

    2024年04月16日
    瀏覽(18)
  • 【Spring Security】安全框架學(xué)習(xí)(十二)

    【Spring Security】安全框架學(xué)習(xí)(十二)

    6.0 其他權(quán)限校驗(yàn)方法 我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進(jìn)行校驗(yàn)。Spring Security還為我們提供了其它方法. 例如:hasAnyAuthority,hasRole,hasAnyRole,等。 這里我們先不急著去介紹這些方法,我們先去理解hasAuthority的原理,然后再去學(xué)習(xí)其他方法就

    2024年02月07日
    瀏覽(23)
  • 【Spring Security】安全框架學(xué)習(xí)(八)

    3.0 權(quán)限系統(tǒng)的作用 例如一個(gè)學(xué)校圖書館的管理系統(tǒng),如果是普通學(xué)生登錄就能看到借書還書相關(guān)的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個(gè)圖書館管理員的賬號(hào)登錄了,應(yīng)該就能看到并使用添加書籍信息,刪除書籍信息等功能

    2024年02月09日
    瀏覽(21)
  • Spring Security入門教程,springboot整合Spring Security

    Spring Security入門教程,springboot整合Spring Security

    Spring Security是Spring官方推薦的認(rèn)證、授權(quán)框架,功能相比Apache Shiro功能更豐富也更強(qiáng)大,但是使用起來更麻煩。 如果使用過Apache Shiro,學(xué)習(xí)Spring Security會(huì)比較簡單一點(diǎn),兩種框架有很多相似的地方。 目錄 一、準(zhǔn)備工作 創(chuàng)建springboot項(xiàng)目 pom.xml application.yml 二、創(chuàng)建相關(guān)的類

    2024年02月05日
    瀏覽(27)
  • 在Spring Boot框架中集成 Spring Security

    技術(shù)介紹 SpringSecurity的核心功能: SpringSecurity特點(diǎn): 具體實(shí)現(xiàn) 1、集成依賴 2、修改spring security 實(shí)現(xiàn)service.impl.UserDetailsServiceImpl類 代碼1具體解釋 代碼2具體解釋 實(shí)現(xiàn)config.SecurityConfig類 代碼具體解釋 總結(jié) Spring Security是一個(gè)基于Spring框架的安全性框架,它提供了一系列的安全性

    2024年02月14日
    瀏覽(30)
  • SpringBoot原理分析 | 安全框架:Security

    SpringBoot原理分析 | 安全框架:Security

    ??wei_shuo的個(gè)人主頁 ??wei_shuo的學(xué)習(xí)社區(qū) ??Hello World ! Spring Security是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架;提供一組可以在Spring應(yīng)用上下文中配置的Bean,充分利用Spring IoC,DI(控制反轉(zhuǎn)Inversion of Control ,DI:Dependency Injection

    2024年02月13日
    瀏覽(20)
  • Shiro和Spring Security安全框架對(duì)比

    Shiro和Spring Security安全框架對(duì)比

    Apache Shiro是Java的一個(gè)安全框架。目前,使用Apache Shiro的人越來越多,因?yàn)樗喈?dāng)簡單。與Spring Security對(duì)比,Shiro可能沒有Spring Security做的功能強(qiáng)大,但是在實(shí)際工作時(shí)可能并不需要那么復(fù)雜的東西,所以使用小而簡單的Shiro就足夠了。下面對(duì)這兩個(gè)安全框架進(jìn)行了對(duì)比,可以

    2024年02月10日
    瀏覽(21)
  • spring security為啥是個(gè)垃圾框架?

    古時(shí)候?qū)懘a,權(quán)限這塊寫過一個(gè)庫,基本就是一個(gè)泛型接口,里面有幾個(gè)方法: 如驗(yàn)證輸入的principal和credentials,返回token和authorities和roles,role就是一堆a(bǔ)uthorities集,也就說就是返回一堆a(bǔ)uthorities。然后每次請(qǐng)求會(huì)拿token找到authorities,然后再判斷當(dāng)前請(qǐng)求的資源(其實(shí)就是

    2024年02月08日
    瀏覽(17)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包