厲害!我帶的實習生僅用四步就整合好SpringSecurity+JWT實現登錄認證!

来源:https://www.cnblogs.com/qing-gee/archive/2022/04/07/16112239.html
-Advertisement-
Play Games

小二是新來的實習生,作為技術 leader,我還是很負責任的,有什麼鍋都想甩給他,啊,不,一不小心怎麼把心裡話全說出來了呢?重來! 小二是新來的實習生,作為技術 leader,我還是很負責任的,有什麼好事都想著他,這不,我就安排了一個整合SpringSecurity+JWT實現登錄認證的小任務交,沒 ...


小二是新來的實習生,作為技術 leader,我還是很負責任的,有什麼鍋都想甩給他,啊,不,一不小心怎麼把心裡話全說出來了呢?重來!

小二是新來的實習生,作為技術 leader,我還是很負責任的,有什麼好事都想著他,這不,我就安排了一個整合SpringSecurity+JWT實現登錄認證的小任務交,沒想到,他僅用四步就搞定了,這讓我感覺倍有面。

一、關於 SpringSecurity

在 Spring Boot 出現之前,SpringSecurity 的使用場景是被另外一個安全管理框架 Shiro 牢牢霸占的,因為相對於 SpringSecurity 來說,SSM 中整合 Shiro 更加輕量級。Spring Boot 出現後,使這一情況情況大有改觀。正應了那句古話:一人得道雞犬升天,雖然有點不大合適,就將就著用吧。

這是因為 Spring Boot 為 SpringSecurity 提供了自動化配置,大大降低了 SpringSecurity 的學習成本。另外,SpringSecurity 的功能也比 Shiro 更加強大。

二、關於 JWT

JWT,是目前最流行的一個跨域認證解決方案:客戶端發起用戶登錄請求,伺服器端接收並認證成功後,生成一個 JSON 對象(如下所示),然後將其返回給客戶端。

從本質上來說,JWT 就像是一種生成加密用戶身份信息的 Token,更安全也更靈活。

三、整合步驟

第一步,給需要登錄認證的模塊添加 codingmore-security 依賴:

<dependency>
    <groupId>top.codingmore</groupId>
    <artifactId>codingmore-security</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

比如說 codingmore-admin 後端管理模塊需要登錄認證,就在 codingmore-admin/pom.xml 文件中添加 codingmore-security 依賴。

第二步,在需要登錄認證的模塊里添加 CodingmoreSecurityConfig 類,繼承自 codingmore-security 模塊中的 SecurityConfig 類。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CodingmoreSecurityConfig extends SecurityConfig {
    @Autowired
    private IUsersService usersService;

    @Bean
    public UserDetailsService userDetailsService() {
        //獲取登錄用戶信息
        return username -> usersService.loadUserByUsername(username);
    }
}

UserDetailsService 這個類主要是用來載入用戶信息的,包括用戶名、密碼、許可權、角色集合....其中有一個方法如下:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

認證邏輯中,SpringSecurity 會調用這個方法根據客戶端傳入的用戶名載入該用戶的詳細信息,包括判斷:

  • 密碼是否一致
  • 通過後獲取許可權和角色
    public UserDetails loadUserByUsername(String username) {
        // 根據用戶名查詢用戶
        Users admin = getAdminByUsername(username);
        if (admin != null) {
            List<Resource> resourceList = getResourceList(admin.getId());
            return new AdminUserDetails(admin,resourceList);
        }
        throw new UsernameNotFoundException("用戶名或密碼錯誤");
    }

getAdminByUsername 負責根據用戶名從資料庫中查詢出密碼、角色、許可權等。

    public Users getAdminByUsername(String username) {
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_login", username);
        List<Users> usersList = baseMapper.selectList(queryWrapper);

        if (usersList != null && usersList.size() > 0) {
            return usersList.get(0);
        }

        // 用戶名錯誤,提前拋出異常
        throw new UsernameNotFoundException("用戶名錯誤");
    }

第三步,在 application.yml 中配置下不需要安全保護的資源路徑:

secure:
  ignored:
    urls: #安全路徑白名單
      - /doc.html
      - /swagger-ui/**
      - /swagger/**
      - /swagger-resources/**
      - /**/v3/api-docs
      - /**/*.js
      - /**/*.css
      - /**/*.png
      - /**/*.ico
      - /webjars/springfox-swagger-ui/**
      - /actuator/**
      - /druid/**
      - /users/login
      - /users/register
      - /users/info
      - /users/logout

第四步,在登錄介面中添加登錄和刷新 token 的方法:

@Controller
@Api(tags = "用戶")
@RequestMapping("/users")
public class UsersController {
    @Autowired
    private IUsersService usersService;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

@ApiOperation(value = "登錄以後返回token")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
        String token = usersService.login(users.getUserLogin(), users.getUserPass());

        if (token == null) {
            return ResultObject.validateFailed("用戶名或密碼錯誤");
        }

        // 將 JWT 傳遞迴客戶端
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return ResultObject.success(tokenMap);
    }

    @ApiOperation(value = "刷新token")
    @RequestMapping(value = "/refreshToken", method = RequestMethod.GET)
    @ResponseBody
    public ResultObject refreshToken(HttpServletRequest request) {
        String token = request.getHeader(tokenHeader);
        String refreshToken = usersService.refreshToken(token);
        if (refreshToken == null) {
            return ResultObject.failed("token已經過期!");
        }
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", refreshToken);
        tokenMap.put("tokenHead", tokenHead);
        return ResultObject.success(tokenMap);
    }
}

使用 Apipost 來測試一下,首先是文章獲取介面,在沒有登錄的情況下會提示暫未登錄或者 token 已過期。

四、實現原理

小二之所以能僅用四步就實現了登錄認證,主要是因為他將 SpringSecurity+JWT 的代碼封裝成了通用模塊,我們來看看 codingmore-security 的目錄結構。

codingmore-security
├── component
|    ├── JwtAuthenticationTokenFilter -- JWT登錄授權過濾器
|    ├── RestAuthenticationEntryPoint
|    └── RestfulAccessDeniedHandler
├── config
|    ├── IgnoreUrlsConfig
|    └── SecurityConfig
└── util
     └── JwtTokenUtil -- JWT的token處理工具類

JwtAuthenticationTokenFilter 和 JwtTokenUtil 在講 JWT 的時候已經詳細地講過了,這裡再簡單補充一點。

客戶端的請求頭裡攜帶了 token,服務端肯定是需要針對每次請求解析校驗 token 的,所以必須得定義一個過濾器,也就是 JwtAuthenticationTokenFilter:

  • 從請求頭中獲取 token
  • 對 token 進行解析、驗簽、校驗過期時間
  • 校驗成功,將驗證結果放到 ThreadLocal 中,供下次請求使用

重點來看其他四個類。第一個 RestAuthenticationEntryPoint(自定義返回結果:未登錄或登錄過期):

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control","no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage())));
        response.getWriter().flush();
    }
}

可以通過 debug 的方式看一下返回的信息正是之前用戶未登錄狀態下訪問文章頁的錯誤信息。

具體的信息是在 ResultCode 類中定義的。

public enum ResultCode implements IErrorCode {
    SUCCESS(0, "操作成功"),
    FAILED(500, "操作失敗"),
    VALIDATE_FAILED(506, "參數檢驗失敗"),
    UNAUTHORIZED(401, "暫未登錄或token已經過期"),
    FORBIDDEN(403, "沒有相關許可權");
    private long code;
    private String message;

    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }
}

第二個 RestfulAccessDeniedHandler(自定義返回結果:沒有許可權訪問時):

public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control","no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage())));
        response.getWriter().flush();
    }
}

第三個IgnoreUrlsConfig(用於配置不需要安全保護的資源路徑):

@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
    private List<String> urls = new ArrayList<>();
}

通過 lombok 註解的方式直接將配置文件中不需要許可權校驗的路徑放開,比如說 Knife4j 的介面文檔頁面。如果不放開的話,就被 SpringSecurity 攔截了,沒辦法訪問到了。

第四個SecurityConfig(SpringSecurity通用配置):

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
    private DynamicSecurityService dynamicSecurityService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                .authorizeRequests();

        //不需要保護的資源路徑允許訪問
        for (String url : ignoreUrlsConfig().getUrls()) {
            registry.antMatchers(url).permitAll();
        }

        // 任何請求需要身份認證
        registry.and()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                // 關閉跨站請求防護及不使用session
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 自定義許可權拒絕處理類
                .and()
                .exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler())
                .authenticationEntryPoint(restAuthenticationEntryPoint())
                // 自定義許可權攔截器JWT過濾器
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //有動態許可權配置時添加動態許可權校驗過濾器
        if(dynamicSecurityService!=null){
            registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
        }
    }
}

這個類的主要作用就是告訴 SpringSecurity 那些路徑不需要攔截,除此之外的,都要進行 RestfulAccessDeniedHandler(登錄校驗)、RestAuthenticationEntryPoint(許可權校驗)和 JwtAuthenticationTokenFilter(JWT 過濾)。

並且將 JwtAuthenticationTokenFilter 過濾器添加到 UsernamePasswordAuthenticationFilter 過濾器之前。

五、測試

第一步,測試登錄介面,Apipost 直接訪問 http://localhost:9002/users/login,可以看到 token 正常返回。

第二步,不帶 token 直接訪問文章介面,可以看到進入了 RestAuthenticationEntryPoint 這個處理器:

第三步,攜帶 token,這次我們改用 Knife4j 來測試,發現可以正常訪問:

源碼鏈接:

https://github.com/itwanger/coding-more

本篇已收錄至 GitHub 上星標 1.9k+ star 的開源專欄《Java 程式員進階之路》,據說每一個優秀的 Java 程式員都喜歡她,風趣幽默、通俗易懂。內容包括 Java 基礎、Java 併發編程、Java 虛擬機、Java 企業級開發、Java 面試等核心知識點。學 Java,就認準 Java 程式員進階之路

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 《零基礎學Java》 文件輸入/輸出流 程式運行期間,大部分數據都被存儲在記憶體中,當程式結束或被關閉時,存儲在記憶體中的數據將會消失。如果要永久保存數據,那麼最好的辦法就是把數據保存到磁碟的文件中。為此,Java提供了文件輸入/輸出流,即 FilelnputStream類 與 FilcOutputSr ...
  • 之前刷到一個視頻,老師上課點到用系統點到回答問題,然後就點名結束了。相信很多學校現在也會玩這招吧,今天就用Python給大家做一個點名系統。來吧,展示… 一.準備工作 1.Tkinter Tkinter 是 python 內置的 TK GUI 工具集。TK 是 Tcl 語言的原生 GUI 庫。作為 ...
  • SpringCloud Function SpEL註入 漏洞分析 ...
  • 前言大家拍照的時候會用到全景嗎?在拍一個環境的時候還是會有很多人用全景的吧 ,今天教大家如何用Python拼接全景圖片。 圖像的全景拼接,即“縫合”兩張具有重疊區域的圖來創建一張全景圖。其中用到了電腦視覺和圖像處理技術有:關鍵點特征檢 測、局部不變特征、關鍵特征點匹配、RANSAC(Random ...
  • (Java 演算法的 ACM 模式) 前言 經常在 LeetCode 上用核心代碼模式刷題的小伙伴突然用 ACM 模式可能會適應不過來,把時間花在輸入輸出上很浪費時間,因此本篇筆記對 Java 演算法的 ACM 模式做了個小總結; 除此之外,需要註意一些小細節: 1. 數字讀取到字元串讀取間需要用 in ...
  • 上篇文章對併發的理論基礎進行了回顧,主要是為什麼使用多線程、多線程會引發什麼問題及引發的原因,和怎麼使用Java中的多線程去解決這些問題。 正所謂,知其然知其所以然,這是學習一個知識遵循的原則。 推薦讀者先行查看併發編程的理論知識,以便可以絲滑入戲。 併發編程系列之一併發理論基礎 本篇文章重點在於J ...
  • 原創:微信公眾號 【阿Q說代碼】,歡迎分享,轉載請保留出處。 近期疫情形勢嚴峻,情形不容樂觀,周末也不敢出去浪了,躲在家裡“葛優躺”。閑來無事,又翻了遍Spring的源碼。不翻不知道,一翻嚇一跳,之前翻過的源碼已經吃進了肚子里,再見亦是陌生人。 個人建議:為了以後能快速的撿起某個知識點,最好的方法還 ...
  • 大家都看過電影《無間道》吧。在電影《無間道》中,劉建明(劉德華飾)作為黑幫的卧底在一次行動中發現了警察的卧底陳永仁(梁朝偉飾)與黃警督(黃秋生飾)通過摩斯電碼進行通訊,經過緊急的群發區域簡訊 “有內鬼,終止交易” 避免了黑幫頭目被抓。 通過動圖能看到黃警督和陳永仁僅通過手指的敲擊就能完成通訊,是不是 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...