01

2022-08-29 18:03:35

在前面 SpringBoot 2.7.2 的系列文章中,已經建立了幾個 computer 相關的介面,這些介面直接通過 Spring Doc 或 POSTMAN 就可以存取。例如:

GET http://localhost:9099/computer/1

存取該服務可以獲取 id 為 1 的電腦詳情。

接下來的文章就使用 Spring Security 實現使用者認證和授權。

1 新增 Spring Security

1.1 新增依賴

Spring Boot 對 Spring Security 非常友好,它已經管理了 Spring Security 的依賴版本,並且提供 starter 的方式進行整合:

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

1.2 測試服務

新增依賴後重新啟動服務,可以在控制檯看到輸出:

在瀏覽器中存取該服務,會自動跳轉到登入頁面,該頁面是 Spring Security 預設提供的。瀏覽器中位址列自動跳轉到:

http://localhost:9099/login

在登入頁面,使用者名稱填寫 user,密碼填寫上面控制檯中輸出的密碼,點選「Sign in」,便會進入系統,顯示電腦詳情。

2 寫死設定使用者名稱和密碼

咱們使用的 Spring Boot 版本是 2.7.2,對應的 Spring Security 版本為 5.7.2,從 5.7 開始,Spring Security 的使用有一些改變,其中之一就是設定 Security 時不推薦繼承 WebSecurityConfigurerAdapter 類。

上面的 demo 中使用的使用者名稱是 user,密碼是在啟動中生成的,這肯定不符合開發的需求。設定使用者名稱密碼有三種方式:

  1. 在組態檔 application.yml 中寫死使用者名稱和密碼;
  2. 定義設定類,在該方法中寫死使用者名稱密碼;
  3. 實現 UserDetailsService 介面,然後定義設定類進行設定。

前面兩者在 demo 中玩玩就行。

2.1 在組態檔中設定使用者名稱密碼

這種方式比較簡單,直接在 application.yml 中設定即可:

spring:
  profiles:
    active: @env@

  security:
    user:
      name: hero1
      password: '111111'
      roles: 'admin'

2.2 在記憶體中設定使用者名稱密碼

刪除上面的 security 節點相關的設定,使用組態檔,設定在記憶體中生效的使用者名稱密碼。

建立設定類 SecurityConfig:

package com.yygnb.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class SecurityConfig {

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

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.builder()
                .username("hero2")
                .password(passwordEncoder().encode("111111"))
                .roles("admin")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}

重啟服務,登入介面輸入 hero2/111111 進行測試。

這種方式也是寫死的使用者名稱密碼,在實際開發中不會使用的。所以,先刪除這個檔案,然後再繼續後面的學習。

3 資料庫設定使用者名稱和密碼

在開始這個部分之前,請確保刪除了上面編寫的 SecurityConfig。

3.1 資料庫表

首先需要定義表結構。

由於我這裡是基於前面 spring boot 2.7.2 系列文章的demo進行的,我繼續沿用 Liquibase 的方式定義表結構。尊敬的讀者們可以從 GitHub 上獲取 demo 基礎工程,也可以聯絡我獲取,還可以脫離 demo 基礎工程,自己用習慣的方式建立資料庫表結構,如 SQL 語句、圖形化介面等。

resources/db/demo/ 目錄中建立檔案 demo-changelog-v2.xml,該檔案用來定義這次資料庫的變更。

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                    https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
    <changeSet id="T100-20220801-yyg-025" author="yyg">
        <createTable remarks="使用者表" tableName="user">
            <column autoIncrement="true" name="id" type="BIGINT">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="username" remarks="使用者名稱" type="VARCHAR(32)">
                <constraints nullable="false"/>
            </column>
            <column name="password" remarks="密碼" type="VARCHAR(128)">
                <constraints nullable="false"/>
            </column>
            <column name="name" remarks="姓名" type="VARCHAR(32)">
                <constraints nullable="false"/>
            </column>
        </createTable>
        <addAutoIncrement tableName="user" columnName="id" columnDataType="BIGINT"
                          incrementBy="1" startWith="1"/>

        <insert tableName="user">
            <column name="id" valueNumeric="1"/>
            <column name="username" value="hero1"/>
            <column name="password" value="111111"/>
            <column name="name" value="HERO 1"/>
        </insert>
        <insert tableName="user">
            <column name="id" valueNumeric="2"/>
            <column name="username" value="hero2"/>
            <column name="password" value="222222"/>
            <column name="name" value="HERO 2"/>
        </insert>
        <insert tableName="user">
            <column name="id" valueNumeric="3"/>
            <column name="username" value="hero3"/>
            <column name="password" value="333333"/>
            <column name="name" value="HERO 3"/>
        </insert>
    </changeSet>
</databaseChangeLog>

該檔案定義了 user 表並插入了三條資料,user 表只有四個欄位:id 主鍵、使用者名稱、密碼、姓名,主鍵自增長。

重啟服務,會自動生成表結構。

3.2 生成實體類

可以通過逆向工程生成,可以複製下面的程式碼。

com.yygnb.demo.entity.User

/**
 * 使用者表
 */
@Schema(title = "使用者")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @Schema(title = "使用者名稱")
    @NotNull(message = "不能為空")
    private String username;

    @Schema(title = "密碼")
    @NotNull(message = "不能為空")
    private String password;

    @Schema(title = "姓名")
    private String name;
}

3.3 Mapper

com.yygnb.demo.mapper.UserMapper

public interface UserMapper extends BaseMapper<User> {
}

resources/mapper/UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yygnb.demo.mapper.UserMapper">
</mapper>

到這一步,準備工作就完成了,接下來繼續回到 Spring Security。

3.4 PasswordEncoder

Spring Security 中在認證授權時,需要 PasswordEncoder 物件,該物件可以對密碼加密。

com.yygnb.demo.config.PasswordEncoderConfig

@Configuration
public class PasswordEncoderConfig {

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

3.5 實現 UserDetailsService 介面

Spring Security 提供了一個介面:UserDetailsService 。該介面只有一個方法 loadUserByUsername,在該方法中就可以查詢資料庫,根據使用者名稱查詢使用者資訊了。該方法返回 UserDetails。UserDetails 是一個介面,Spring Security 提供了一個實現類 User。

建立類:UserDetailsServiceImpl,實現 UserDetailsService 介面,並新增 @Service 註解。

com.yygnb.demo.service.impl.UserDetailsServiceImpl

package com.yygnb.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yygnb.demo.entity.User;
import com.yygnb.demo.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    //    @Autowired
    private final PasswordEncoder passwordEncoder;

    private final UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        User user = this.userMapper.selectOne(wrapper);
        if (user == null) {
            throw new UsernameNotFoundException("根據使用者名稱未查詢到使用者");
        }

        // 模擬查詢許可權
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(), passwordEncoder.encode(user.getPassword()), authorities);
    }
}

如果是以前版本的 Spring Security,還需要定義設定繼承自 WebSecurityConfigurerAdapter 類,重寫 config 方法,並在該方法中設定 UserDetailsService 等。現在的版本不需要這些無聊的操作了。

重啟服務,存取測試。

感謝你閱讀本文,如果本文給了你一點點幫助或者啟發,還請三連支援一下,點贊、關注、收藏,作者會持續與大家分享更多幹貨