iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
Software Development

掌握Java神器,駕馭SpringBoot猛獸系列 第 25

第25日 用Spring Security做登入驗證

  • 分享至 

  • xImage
  •  

昨日啟用了Spring Security針對特定規則路由進行保護,但目前驗證功能尚未完整,還欠缺了身分驗證的方式,今天來套用之前寫好的JWT工具類,添加自訂過濾器實現身分驗證

身分驗證和授權流程

去銀行或郵局辦事情時,都會需要抽號碼牌排隊,要辦理業務的窗口會叫號,臨櫃出示抽到的號碼,必須與當窗口服務的號碼相符合,才可以執行相關業務,若要執行要本人親洽才能辦理的業務,此時就要另外出示證明本人的文件,來識別申請人是否有權可以執行相關業務,在進一步做後續操作

  • 身分驗證

    1. 抽取的號碼牌 => 系統核發的憑證
    2. 窗口叫號臨櫃檢查號碼牌 => 驗證請求攜帶的憑證是否允許使用
    3. 出示證明文件 => 進行身分驗證
  • 授權

    每個窗口都有專職的業務範疇,會根據申請者的資料,決定是否可以辦理業務
    ,轉成程式邏輯來看就是,當前使用者有相應資料,可以操作當前API,能執行後續操作

Spring Security 實作身分驗證流程

銜接上個段落提到的情境,程式並不知道「已經被授權」的使用者狀態,此時可以建立一個類別,用來當前使用者請求的狀態,Spring Security建立下列元件,用來記錄使用者及請求驗證資訊

  • UserDetails

    負責存放當前請求的使用資訊

  • UsernamePasswordAuthenticationToken

    封裝當前請求使用者資訊(UserDetails),以及請求是否被授權的狀態

此外 Spring Security是基於Filter,實現進入核心業務邏輯前,進行相關邏輯處理,實際操作流程是先撰寫過濾器,並實作身分驗證邏輯,在將自訂過濾器嵌入登入驗證過濾鏈中

取得 UserDetails

依照使用規範,必須將系統紀錄使用者資料,轉換為 UserDetails物件,Spring Security提供了UserDetailsService 介面,規範要實作的抽象方法 loadUserByUsername,根據識別User唯一名稱來產生 UserDetails

由於是用 account 當做唯一鍵,取得資料依據會以 account為主,先來做前置作業

  1. MemberDao建立新的查詢方法
public Member getByAccount(String account);
  1. mappers/Member.xml 定義相應查詢邏輯
<select id="getByAccount" resultType="com.app.demo.dto.Member">
    SELECT * FROM members WHERE account = #{account} LIMIT 1
</select>

系統互動的元件建立後,在來就是新增 Service依照介面UserDetailsService,來實作相應方法

@Service
@Service
public class DBUserDetailsService implements UserDetailsService {
    @Autowired
    private MemberDao memberMapper;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {

        Member member = memberMapper.getByAccount(account);
        if (member == null) {
            throw new UsernameNotFoundException("User not found");
        }

        return new User(
            member.getAccount(),
            member.getPassword(),
            // 這裡先套空陣列,後面會說實際用途
            Collections.emptyList()
        );
    }
}

有了Service Layer測試好後,來針對 DBUserDetailsService進行測試

@SpringBootTest
@ActiveProfiles("test") // 定義測試的環境設定套用 application-test.properties
public class UserDetailsServiceTest {

    @Autowired
    private DBUserDetailsService userDetialServ;

    @Test
    void checkUserByAccount() {
        // 記得先在資料庫添加相應 account為test的資料
        UserDetails member = userDetialServ.loadUserByUsername("test");
        assertEquals("test", member.getUsername());
    }
}

執行 mvn test -Dtest=UserDetailsServiceTest#checkUserByAccount
檢查將使用者資料封裝UserDetails是否正常

自訂帳號驗證過濾器

身分過濾器的實現邏輯就是,利用請求中攜帶的 Token 套用到,套用到寫好的模組中,根據實際需求決定是否需要阻止請求

@Component
public class UserAuthFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private DBUserDetailsService userDetialServ;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        String authToken = request.getHeader("Authorization");
        String headerPrefix = "Bearer ";
        if (authToken != null && authToken.startsWith(headerPrefix)) {
            try {
                authToken = authToken.substring(headerPrefix.length());
                jwtUtil.validateToken(authToken);

                Claims payload = jwtUtil.parseToken(authToken);

                // 取得 Token綁定識別身分資料
                String account = payload.getSubject();
                System.out.printf("account is %s\n", account);

                // 根據 stage 判斷查找前台還是後台使用者
                UserDetails userDetials = userDetialServ.loadUserByUsername(account);
                System.out.printf("UserDetails Username %s\n", userDetials.getUsername());

                if (userDetials != null) {
                    Authentication authenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetials, null, userDetials.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }

            } catch (Exception ex) {
                System.out.println(ex.getMessage());
                SecurityContextHolder.getContext().setAuthentication(null);
            }

        }

        filterChain.doFilter(request, response);

    }

}

調整 Spring Security配置

在 Spring Security Config 嵌入自訂過濾鍊並建立新的 Bean

@Bean
public UserAuthFilter jwtAuthenticationFilter() {
    return new UserAuthFilter();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> {
            authorize
                    .requestMatchers("/api/member/**").authenticated()
                    .anyRequest().permitAll();
        })
        // 構造器添加 addFilterBefore 配置
        .addFilterBefore(
            jwtAuthenticationFilter(), // 此處要用Bean註釋的方法取得自定義 Filter
            UsernamePasswordAuthenticationFilter.class  // Spring Security 預設
        );
    return http.build();
}

建立一個API 取得登入使用者資料

@GetMapping("/member")
public Map getMmeber() {
    Authentication authUser = SecurityContextHolder.getContext().getAuthentication();
    Map<String, Object> responseData = new HashMap<>();
    responseData.put("account", authUser.getName());
    return responseData;
}

現在來調整getMember的測試,並套用Bearer Toekn

@Test
public void getMember() throws Exception {
        // 為了掌控篇幅,這裡省略透過登入取得Token的流程,直接用JwtTuil產生新Token
        String token = jwtUtil.generateToken("test");
        String bearerToken = String.format("Bearer %s", token);
        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
                        .get("/api/member") // 測試取得當前使用者資料的接口
                        .header("authorization", bearerToken);

        mockMvc.perform(requestBuilder)
                        .andExpect(MockMvcResultMatchers.status().isOk())
                        .andExpect(MockMvcResultMatchers
                                        .jsonPath("$.account")
                                        .value("test"));
}

從運行測試打印的內容查看Token的資料,與期望的帳號符合
https://ithelp.ithome.com.tw/upload/images/20231008/20161920G5vU87tBK8.png

參考

  1. 掘土-Spring Security授權認證流程
  2. 菜豬筆記

上一篇
第24日 安裝Spring Security
下一篇
第26日 從實務看IoC容器與Bean
系列文
掌握Java神器,駕馭SpringBoot猛獸30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言