欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Springboot+Spring Security實現(xiàn)前后端分離登錄認證及權限控制的示例代碼

 更新時間:2021年11月12日 09:02:59   作者:I_am_Rick_Hu  
本文主要介紹了Springboot+Spring Security實現(xiàn)前后端分離登錄認證及權限控制的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下

前言

    關于Spring Security的概念部分本文不進行贅述,本文主要針對于對Spring Security以及Springboot有一定了解的小伙伴,幫助大家使用Springboot + Spring Security 實現(xiàn)一個前后端分離登錄認證的過程。
    文章會一步一步循序漸進的帶大家敲一遍代碼。最終的代碼請看最后。
    代碼中我用到了插件lombok來生成實體的getter/setter,如果不想裝插件請自己補全getter/setter

本文主要的功能

1、前后端分離用戶登錄認證
2、基于RBAC(角色)的權限控制

 一、準備工作

1、統(tǒng)一錯誤碼枚舉

/**
 * @Author: Hutengfei
 * @Description: 返回碼定義
 * 規(guī)定:
 * #1表示成功
 * #1001~1999 區(qū)間表示參數(shù)錯誤
 * #2001~2999 區(qū)間表示用戶錯誤
 * #3001~3999 區(qū)間表示接口異常
 * @Date Create in 2019/7/22 19:28
 */
public enum ResultCode {
    /* 成功 */
    SUCCESS(200, "成功"),

    /* 默認失敗 */
    COMMON_FAIL(999, "失敗"),

    /* 參數(shù)錯誤:1000~1999 */
    PARAM_NOT_VALID(1001, "參數(shù)無效"),
    PARAM_IS_BLANK(1002, "參數(shù)為空"),
    PARAM_TYPE_ERROR(1003, "參數(shù)類型錯誤"),
    PARAM_NOT_COMPLETE(1004, "參數(shù)缺失"),

    /* 用戶錯誤 */
    USER_NOT_LOGIN(2001, "用戶未登錄"),
    USER_ACCOUNT_EXPIRED(2002, "賬號已過期"),
    USER_CREDENTIALS_ERROR(2003, "密碼錯誤"),
    USER_CREDENTIALS_EXPIRED(2004, "密碼過期"),
    USER_ACCOUNT_DISABLE(2005, "賬號不可用"),
    USER_ACCOUNT_LOCKED(2006, "賬號被鎖定"),
    USER_ACCOUNT_NOT_EXIST(2007, "賬號不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "賬號已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "賬號下線"),

    /* 業(yè)務錯誤 */
    NO_PERMISSION(3001, "沒有權限");
    private Integer code;
    private String message;

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

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 根據(jù)code獲取message
     *
     * @param code
     * @return
     */
    public static String getMessageByCode(Integer code) {
        for (ResultCode ele : values()) {
            if (ele.getCode().equals(code)) {
                return ele.getMessage();
            }
        }
        return null;
    }
}

2、統(tǒng)一json返回體

/**
 * @Author: Hutengfei
 * @Description: 統(tǒng)一返回實體
 * @Date Create in 2019/7/22 19:20
 */
public class JsonResult<T> implements Serializable {
    private Boolean success;
    private Integer errorCode;
    private String errorMsg;
    private T data;

    public JsonResult() {
    }

    public JsonResult(boolean success) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
    }

    public JsonResult(boolean success, ResultCode resultEnum) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
    }

    public JsonResult(boolean success, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
        this.data = data;
    }

    public JsonResult(boolean success, ResultCode resultEnum, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
        this.data = data;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(Integer errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

3、返回體構造工具

/**
 * @Author: Hutengfei
 * @Description:
 * @Date Create in 2019/7/22 19:52
 */
public class ResultTool {
    public static JsonResult success() {
        return new JsonResult(true);
    }

    public static <T> JsonResult<T> success(T data) {
        return new JsonResult(true, data);
    }

    public static JsonResult fail() {
        return new JsonResult(false);
    }

    public static JsonResult fail(ResultCode resultEnum) {
        return new JsonResult(false, resultEnum);
    }
}

4、pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.spring</groupId>
    <artifactId>security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security</name>
    <description>測試spring-security工程</description>

    <properties>
        <java.version>1.8</java.version>
        <spring.security.version>5.1.6.RELEASE</spring.security.version>
        <fastjson.version>1.2.46</fastjson.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- spring-security -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>${spring.security.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>${spring.security.version}</version>
        </dependency>
        <!-- Hikari連接池-->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <exclusions>
                <!-- 排除 tomcat-jdbc 以使用 HikariCP -->
                <exclusion>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>tomcat-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!-- Mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatisplus-spring-boot-starter</artifactId>
            <version>1.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>2.1.9</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--JSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

5、配置文件

spring:
  application:
    name: isoftstone-security
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring_security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=CTT
    username: root
    password: root
    hikari:
      minimum-idle: 5
      idle-timeout: 600000
      maximum-pool-size: 10
      auto-commit: true
      pool-name: MyHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
server:
  port: 8666

mybatis-plus:
  # 如果是放在src/main/java目錄下 classpath:/com/yourpackage/*/mapper/*Mapper.xml
  # 如果是放在resource目錄 classpath:/mapper/*Mapper.xml
  mapper-locations: classpath:mapper/*.xml, classpath:mybatis/mapping/**/*.xml
  #實體掃描,多個package用逗號或者分號分隔
  typeAliasesPackage: com.spring.**
  global-config:
    #主鍵類型  0:"數(shù)據(jù)庫ID自增", 1:"用戶輸入ID",2:"全局唯一ID (數(shù)字類型唯一ID)", 3:"全局唯一ID UUID";
    id-type: 0
    #字段策略 0:"忽略判斷",1:"非 NULL 判斷"),2:"非空判斷"
    field-strategy: 1
    #駝峰下劃線轉換
    db-column-underline: true
    #刷新mapper 調試神器
    refresh-mapper: true
    #數(shù)據(jù)庫大寫下劃線轉換
    #capital-mode: true
    #序列接口實現(xiàn)類配置,不在推薦使用此方式進行配置,請使用自定義bean注入
    #key-generator: com.baomidou.mybatisplus.incrementer.H2KeyGenerator
    #邏輯刪除配置(下面3個配置)
    logic-delete-value: 0
    logic-not-delete-value: 1
    #自定義sql注入器,不在推薦使用此方式進行配置,請使用自定義bean注入
    #sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector
    #自定義填充策略接口實現(xiàn),不在推薦使用此方式進行配置,請使用自定義bean注入
    # meta-object-handler: com.baomidou.springboot.MyMetaObjectHandler
    #自定義SQL注入器
    #sql-injector: com.baomidou.springboot.xxx
    # SQL 解析緩存,開啟后多租戶 @SqlParser 注解生效
    sql-parser-cache: true
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false

二、數(shù)據(jù)庫表設計

數(shù)據(jù)庫設計

建表語句

create table sys_user
(
    id                      int auto_increment
        primary key,
    account                 varchar(32)          not null comment '賬號',
    user_name               varchar(32)          not null comment '用戶名',
    password                varchar(64)          null comment '用戶密碼',
    last_login_time         datetime             null comment '上一次登錄時間',
    enabled                 tinyint(1) default 1 null comment '賬號是否可用。默認為1(可用)',
    not_expired             tinyint(1) default 1 null comment '是否過期。默認為1(沒有過期)',
    account_not_locked      tinyint(1) default 1 null comment '賬號是否鎖定。默認為1(沒有鎖定)',
    credentials_not_expired tinyint(1) default 1 null comment '證書(密碼)是否過期。默認為1(沒有過期)',
    create_time             datetime             null comment '創(chuàng)建時間',
    update_time             datetime             null comment '修改時間',
    create_user             int                  null comment '創(chuàng)建人',
    update_user             int                  null comment '修改人'
)
    comment '用戶表';
create table sys_role
(
    id               int auto_increment comment '主鍵id'
        primary key,
    role_name        varchar(32) null comment '角色名',
    role_description varchar(64) null comment '角色說明'
)
    comment '用戶角色表';
create table sys_permission
(
    id              int auto_increment comment '主鍵id'
        primary key,
    permission_code varchar(32) null comment '權限code',
    permission_name varchar(32) null comment '權限名'
)
    comment '權限表';
create table sys_user_role_relation
(
    id      int auto_increment comment '主鍵id'
        primary key,
    user_id int null comment '用戶id',
    role_id int null comment '角色id'
)
    comment '用戶角色關聯(lián)關系表';
create table sys_role_permission_relation
(
    id            int auto_increment comment '主鍵id'
        primary key,
    role_id       int null comment '角色id',
    permission_id int null comment '權限id'
)
    comment '角色-權限關聯(lián)關系表';
create table sys_request_path
(
    id          int auto_increment comment '主鍵id'
        primary key,
    url         varchar(64)  not null comment '請求路徑',
    description varchar(128) null comment '路徑描述'
)
    comment '請求路徑';
create table sys_request_path_permission_relation
(
    id            int null comment '主鍵id',
    url_id        int null comment '請求路徑id',
    permission_id int null comment '權限id'
)
    comment '路徑權限關聯(lián)表';

初始化表數(shù)據(jù)語句

-- 用戶
INSERT INTO sys_user (id, account, user_name, password, last_login_time, enabled, account_non_expired, account_non_locked, credentials_non_expired, create_time, update_time, create_user, update_user) VALUES (1, 'user1', '用戶1', '$2a$10$47lsFAUlWixWG17Ca3M/r.EPJVIb7Tv26ZaxhzqN65nXVcAhHQM4i', '2019-09-04 20:25:36', 1, 1, 1, 1, '2019-08-29 06:28:36', '2019-09-04 20:25:36', 1, 1);
INSERT INTO sys_user (id, account, user_name, password, last_login_time, enabled, account_non_expired, account_non_locked, credentials_non_expired, create_time, update_time, create_user, update_user) VALUES (2, 'user2', '用戶2', '$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC', '2019-09-05 00:07:12', 1, 1, 1, 1, '2019-08-29 06:29:24', '2019-09-05 00:07:12', 1, 2);
-- 角色
INSERT INTO sys_role (id, role_code, role_name, role_description) VALUES (1, 'admin', '管理員', '管理員,擁有所有權限');
INSERT INTO sys_role (id, role_code, role_name, role_description) VALUES (2, 'user', '普通用戶', '普通用戶,擁有部分權限');
-- 權限
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (1, 'create_user', '創(chuàng)建用戶');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (2, 'query_user', '查看用戶');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (3, 'delete_user', '刪除用戶');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (4, 'modify_user', '修改用戶');
-- 請求路徑
INSERT INTO sys_request_path (id, url, description) VALUES (1, '/getUser', '查詢用戶');
-- 用戶角色關聯(lián)關系
INSERT INTO sys_user_role_relation (id, user_id, role_id) VALUES (1, 1, 1);
INSERT INTO sys_user_role_relation (id, user_id, role_id) VALUES (2, 2, 2);
-- 角色權限關聯(lián)關系
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (1, 1, 1);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (2, 1, 2);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (3, 1, 3);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (4, 1, 4);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (5, 2, 1);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (6, 2, 2);
-- 請求路徑權限關聯(lián)關系
INSERT INTO sys_request_path_permission_relation (id, url_id, permission_id) VALUES (null, 1, 2);

三、Spring Security核心配置:WebSecurityConfig

    創(chuàng)建WebSecurityConfig繼承WebSecurityConfigurerAdapter類,并實現(xiàn)configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法。后續(xù)我們會在里面加入一系列配置,包括配置認證方式、登入登出、異常處理、會話管理等。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置認證方式等
        super.configure(auth);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //http相關的配置,包括登入登出、異常處理、會話管理等
        super.configure(http);
    }
}

四、用戶登錄認證邏輯:UserDetailsService

1、創(chuàng)建自定義UserDetailsService

    這是實現(xiàn)自定義用戶認證的核心邏輯,loadUserByUsername(String username)的參數(shù)就是登錄時提交的用戶名,返回類型是一個叫UserDetails 的接口,需要在這里構造出他的一個實現(xiàn)類User,這是Spring security提供的用戶信息實體。

public class UserDetailsServiceImpl  implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //需要構造出 org.springframework.security.core.userdetails.User 對象并返回
        return null;
    }
}

    這里我們使用他的一個參數(shù)比較詳細的構造函數(shù),源碼如下

User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities)

其中參數(shù):

String username:用戶名
String password: 密碼
boolean enabled: 賬號是否可用
boolean accountNonExpired:賬號是否過期
boolean credentialsNonExpired:密碼是否過期
boolean accountNonLocked:賬號是否鎖定
Collection<? extends GrantedAuthority> authorities):用戶權限列表

    這就與我們的創(chuàng)建的用戶表的字段對應起來了,Spring security都為我們封裝好了,如果用戶信息的狀態(tài)異常,登錄時則會拋出相應的異常,根據(jù)捕獲到的異常判斷是什么原因(賬號過期/密碼過期/賬號鎖定等等…),進而就可以提示前臺了。
    我們就按照該參數(shù)列表構造出我們所需要的數(shù)據(jù),然后返回,就完成了基于JDBC的自定義用戶認證。
    首先用戶名密碼以及用戶狀態(tài)信息都是從用戶表里進行單表查詢來的,而權限列表則是通過用戶表、角色表以及權限表等關聯(lián)查出來的,那么接下來就是準備service和dao層方法了

2、準備service和dao層方法

(1)根據(jù)用戶名查詢用戶信息

映射文件

    <!--根據(jù)用戶名查詢用戶-->
    <select id="selectByName" resultMap="SysUserMap">
        select * from sys_user where account = #{userName};
    </select>

service層

    /**
     * 根據(jù)用戶名查詢用戶
     *
     * @param userName
     * @return
     */
    SysUser selectByName(String userName);

(2)根據(jù)用戶名查詢用戶的權限信息

映射文件

    <select id="selectListByUser" resultMap="SysPermissionMap">
        SELECT
        p.*
        FROM
        sys_user AS u
        LEFT JOIN sys_user_role_relation AS ur
        ON u.id = ur.user_id
        LEFT JOIN sys_role AS r
        ON r.id = ur.role_id
        LEFT JOIN sys_role_permission_relation AS rp
        ON r.id = rp.role_id
        LEFT JOIN sys_permission AS p
        ON p.id = rp.permission_id
        WHERE u.id = #{userId}
    </select>

service層

    /**
     * 查詢用戶的權限列表
     *
     * @param userId
     * @return
     */
    List<SysPermission> selectListByUser(Integer userId);

    這樣的話流程我們就理清楚了,首先根據(jù)用戶名查出對應用戶,再拿得到的用戶的用戶id去查詢它所擁有的的權限列表,最后構造出我們需要的org.springframework.security.core.userdetails.User對象。
接下來改造一下剛剛自定義的UserDetailsService

public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysPermissionService sysPermissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (username == null || "".equals(username)) {
            throw new RuntimeException("用戶不能為空");
        }
        //根據(jù)用戶名查詢用戶
        SysUser sysUser = sysUserService.selectByName(username);
        if (sysUser == null) {
            throw new RuntimeException("用戶不存在");
        }
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        if (sysUser != null) {
            //獲取該用戶所擁有的權限
            List<SysPermission> sysPermissions = sysPermissionService.selectListByUser(sysUser.getId());
            // 聲明用戶授權
            sysPermissions.forEach(sysPermission -> {
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode());
                grantedAuthorities.add(grantedAuthority);
            });
        }
        return new User(sysUser.getAccount(), sysUser.getPassword(), sysUser.getEnabled(), sysUser.getAccountNonExpired(), sysUser.getCredentialsNonExpired(), sysUser.getAccountNonLocked(), grantedAuthorities);
    }
}

    然后將我們的自定義的基于JDBC的用戶認證在之前創(chuàng)建的WebSecurityConfig 中得configure(AuthenticationManagerBuilder auth)中聲明一下,到此自定義的基于JDBC的用戶認證就完成了

    @Bean
    public UserDetailsService userDetailsService() {
        //獲取用戶賬號密碼及權限信息
        return new UserDetailsServiceImpl();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置認證方式
        auth.userDetailsService(userDetailsService());
    }

五、用戶密碼加密

    新版本的Spring security規(guī)定必須設置一個默認的加密方式,不允許使用明文。這個加密方式是用于在登錄時驗證密碼、注冊時需要用到。

    我們可以自己選擇一種加密方式,Spring security為我們提供了多種加密方式,我們這里使用一種強hash方式進行加密。

加密方式.png   

 在WebSecurityConfig 中注入(注入即可,不用聲明使用),這樣就會對提交的密碼進行加密處理了,如果你沒有注入加密方式,運行的時候會報錯"There is no PasswordEncoder mapped for the id"錯誤。

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 設置默認的加密方式(強hash方式加密)
        return new BCryptPasswordEncoder();
    }

    同樣的我們數(shù)據(jù)庫里存儲的密碼也要用同樣的加密方式存儲,例如我們將123456用BCryptPasswordEncoder 加密后存儲到數(shù)據(jù)庫中(注意:即使是同一個明文用這種加密方式加密出來的密文也是不同的,這就是這種加密方式的特點)

image.png

六、屏蔽Spring Security默認重定向登錄頁面以實現(xiàn)前后端分離功能

    在演示登錄之前我們先編寫一個查詢接口"/getUser",并將"/getUser"接口規(guī)定為需要擁有"query_user"權限的用戶可以訪問,并在角色-權限關聯(lián)關系表中給user1用戶所屬角色(role_id = 1)添加權限"query_user"

image.png

    然后規(guī)定接口"/getUser"只能是擁有"query_user"權限的用戶可以訪問。后面我們基本都用這個查詢接口作為演示,就叫它"資源接口"吧。

http.authorizeRequests().
       antMatchers("/getUser").hasAuthority("query_user").

    演示登錄時,如果用戶沒有登錄去請求資源接口就會提示未登錄

    在前后端不分離的時候當用戶未登錄去訪問資源時Spring security會重定向到默認的登錄頁面,返回的是一串html標簽,這一串html標簽其實就是登錄頁面的提交表單。如圖所示

image.png

   而在前后端分離的情況下(比如前臺使用VUE或JQ等)我們需要的是在前臺接收到"用戶未登錄"的提示信息,所以我們接下來要做的就是屏蔽重定向的登錄頁面,并返回統(tǒng)一的json格式的返回體。而實現(xiàn)這一功能的核心就是實現(xiàn)AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用來處理匿名用戶訪問無權限資源時的異常(即未登錄,或者登錄狀態(tài)過期失效

/**
 * @Author: Hutengfei
 * @Description: 匿名用戶訪問無權限資源時的異常
 * @Date Create in 2019/9/3 21:35
 */
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

在WebSecurityConfig中的configure(HttpSecurity http)方法中聲明

 //異常處理(權限拒絕、登錄失效等)
 and().exceptionHandling().
 authenticationEntryPoint(authenticationEntryPoint).//匿名用戶訪問無權限資源時的異常處理

再次請求資源接口

image.png

前臺拿到這個錯誤時就可以做一些處理了,主要是退出到登錄頁面。

1、實現(xiàn)登錄成功/失敗、登出處理邏輯

    首先需要明白一件事,對于登入登出我們都不需要自己編寫controller接口,Spring Security為我們封裝好了。默認登入路徑:/login,登出路徑:/logout。當然我們可以也修改默認的名字。登錄成功失敗和登出的后續(xù)處理邏輯如何編寫會在后面慢慢解釋。
    當?shù)卿洺晒虻卿浭《夹枰祷亟y(tǒng)一的json返回體給前臺,前臺才能知道對應的做什么處理。
而實現(xiàn)登錄成功和失敗的異常處理需要分別實現(xiàn)AuthenticationSuccessHandler和AuthenticationFailureHandler接口并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中然后聲明

(1)登錄成功

/**
 * @Author: Hutengfei
 * @Description: 登錄成功處理邏輯
 * @Date Create in 2019/9/3 15:52
 */
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    SysUserService sysUserService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //更新用戶表上次登錄時間、更新人、更新時間等字段
        User userDetails = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        SysUser sysUser = sysUserService.selectByName(userDetails.getUsername());
        sysUser.setLastLoginTime(new Date());
        sysUser.setUpdateTime(new Date());
        sysUser.setUpdateUser(sysUser.getId());
        sysUserService.update(sysUser);
        
        //此處還可以進行一些處理,比如登錄成功之后可能需要返回給前臺當前用戶有哪些菜單權限,
        //進而前臺動態(tài)的控制菜單的顯示等,具體根據(jù)自己的業(yè)務需求進行擴展

        //返回json數(shù)據(jù)
        JsonResult result = ResultTool.success();
       //處理編碼方式,防止中文亂碼的情況
        httpServletResponse.setContentType("text/json;charset=utf-8");
       //塞到HttpServletResponse中返回給前臺
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

(2)登錄失敗

登錄失敗處理器主要用來對登錄失敗的場景(密碼錯誤、賬號鎖定等…)做統(tǒng)一處理并返回給前臺統(tǒng)一的json返回體。還記得我們創(chuàng)建用戶表的時候創(chuàng)建了賬號過期、密碼過期、賬號鎖定之類的字段嗎,這里就可以派上用場了.

/**
 * @Author: Hutengfei
 * @Description: 登錄失敗處理邏輯
 * @Date Create in 2019/9/3 15:52
 */
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {


    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //返回json數(shù)據(jù)
        JsonResult result = null;
        if (e instanceof AccountExpiredException) {
            //賬號過期
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
        } else if (e instanceof BadCredentialsException) {
            //密碼錯誤
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
        } else if (e instanceof CredentialsExpiredException) {
            //密碼過期
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
        } else if (e instanceof DisabledException) {
            //賬號不可用
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
        } else if (e instanceof LockedException) {
            //賬號鎖定
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用戶不存在
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
        }else{
            //其他錯誤
            result = ResultTool.fail(ResultCode.COMMON_FAIL);
        }
       //處理編碼方式,防止中文亂碼的情況
        httpServletResponse.setContentType("text/json;charset=utf-8");
       //塞到HttpServletResponse中返回給前臺
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

(3)登出

同樣的登出也要將登出成功時結果返回給前臺,并且登出之后進行將cookie失效或刪除

/**
 * @Author: Hutengfei
 * @Description: 登出成功處理邏輯
 * @Date Create in 2019/9/4 10:17
 */
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        JsonResult result = ResultTool.success();
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

2、在WebSecurityConfig中的configure(HttpSecurity http)方法中聲明

//登入
and().formLogin().
	permitAll().//允許所有用戶
	successHandler(authenticationSuccessHandler).//登錄成功處理邏輯
	failureHandler(authenticationFailureHandler).//登錄失敗處理邏輯
//登出
and().logout().
	permitAll().//允許所有用戶
	logoutSuccessHandler(logoutSuccessHandler).//登出成功處理邏輯
	deleteCookies("JSESSIONID").//登出之后刪除cookie

效果如圖:

登錄時密碼錯誤

image.png

登錄時賬號被鎖定

image.png

退出登錄之后再次請求資源接口

image.png

八、會話管理(登錄過時、限制單用戶或多用戶登錄等)

1、限制登錄用戶數(shù)量

比如限制同一賬號只能一個用戶使用

and().sessionManagement().
                    maximumSessions(1)

2、處理賬號被擠下線處理邏輯

同樣的,當賬號異地登錄導致被擠下線時也要返回給前端json格式的數(shù)據(jù),比如提示"賬號下線"、"您的賬號在異地登錄,是否是您自己操作"或者"您的賬號在異地登錄,可能由于密碼泄露,建議修改密碼"等。這時就要實現(xiàn)SessionInformationExpiredStrategy(會話信息過期策略)來自定義會話過期時的處理邏輯。

/**
 * @Author: Hutengfei
 * @Description: 會話信息過期策略
 * @Date Create in 2019/9/4 9:34
 */
@Component
public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.USER_ACCOUNT_USE_BY_OTHERS);
        HttpServletResponse httpServletResponse = sessionInformationExpiredEvent.getResponse();
        httpServletResponse.setContentType("text/json;charset=utf-8");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

3、在WebSecurityConfig中聲明

然后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后聲明,在配置同時登錄用戶數(shù)的配置下面再加一行 expiredSessionStrategy(sessionInformationExpiredStrategy)

//會話管理
and().sessionManagement().
	maximumSessions(1).//同一賬號同時登錄最大用戶數(shù)
	expiredSessionStrategy(sessionInformationExpiredStrategy);//會話信息過期策略會話信息過期策略(賬號被擠下線)

效果演示步驟
我電腦上用postman登錄
我電腦上請求資源接口,可以請求,如下左圖
在旁邊電腦上再登錄一次剛剛的賬號
在我電腦上再次請求資源接口,提示"賬號下線",如右下圖

image.png

九、實現(xiàn)基于JDBC的動態(tài)權限控制

在之前的章節(jié)中我們配置了一個

antMatchers("/getUser").hasAuthority("query_user")

    其實我們就已經(jīng)實現(xiàn)了一個所謂的基于RBAC的權限控制,只不過我們是在WebSecurityConfig中寫死的,但是在平時開發(fā)中,難道我們每增加一個需要訪問權限控制的資源我們都要修改一下WebSecurityConfig增加一個antMatchers(…)嗎,肯定是不合理的。因此我們現(xiàn)在要做的就是將需要權限控制的資源配到數(shù)據(jù)庫中,當然也可以存儲在其他地方,比如用一個枚舉,只是我覺得存在數(shù)據(jù)庫中更加靈活一點。

    我們需要實現(xiàn)一個AccessDecisionManager(訪問決策管理器),在里面我們對當前請求的資源進行權限判斷,判斷當前登錄用戶是否擁有該權限,如果有就放行,如果沒有就拋出一個"權限不足"的異常。不過在實現(xiàn)AccessDecisionManager之前我們還需要做一件事,那就是攔截到當前的請求,并根據(jù)請求路徑從數(shù)據(jù)庫中查出當前資源路徑需要哪些權限才能訪問,然后將查出的需要的權限列表交給AccessDecisionManager去處理后續(xù)邏輯。那就是需要先實現(xiàn)一個SecurityMetadataSource,翻譯過來是"安全元數(shù)據(jù)源",我們這里使用他的一個子類FilterInvocationSecurityMetadataSource。

    在自定義的SecurityMetadataSource編寫好之后,我們還要編寫一個攔截器,增加到Spring security默認的攔截器鏈中,以達到攔截的目的。

    同樣的最后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后聲明

1、權限攔截器

/**
 * @Author: Hutengfei
 * @Description: 權限攔截器
 * @Date Create in 2019/9/4 16:25
 */
@Service
public class CustomizeAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(CustomizeAccessDecisionManager accessDecisionManager) {
        super.setAccessDecisionManager(accessDecisionManager);
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        invoke(fi);
    }

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        //fi里面有一個被攔截的url
        //里面調用MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的所有權限
        //再調用MyAccessDecisionManager的decide方法來校驗用戶的權限是否足夠
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
        //執(zhí)行下一個攔截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
}

2、安全元數(shù)據(jù)源FilterInvocationSecurityMetadataSource

/**
 * @Author: Hutengfei
 * @Description:
 * @Date Create in 2019/9/3 21:06
 */
@Component
public class CustomizeFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    AntPathMatcher antPathMatcher = new AntPathMatcher();
    @Autowired
    SysPermissionService sysPermissionService;
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //獲取請求地址
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        //查詢具體某個接口的權限
        List<SysPermission> permissionList =  sysPermissionService.selectListByPath(requestUrl);
        if(permissionList == null || permissionList.size() == 0){
            //請求路徑?jīng)]有配置權限,表明該請求接口可以任意訪問
            return null;
        }
        String[] attributes = new String[permissionList.size()];
        for(int i = 0;i<permissionList.size();i++){
            attributes[i] = permissionList.get(i).getPermissionCode();
        }
        return SecurityConfig.createList(attributes);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

3、訪問決策管理器AccessDecisionManager

/**
 * @Author: Hutengfei
 * @Description: 訪問決策管理器
 * @Date Create in 2019/9/3 20:38
 */
@Component
public class CustomizeAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> iterator = collection.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute ca = iterator.next();
            //當前請求需要的權限
            String needRole = ca.getAttribute();
            //當前用戶所具有的權限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("權限不足!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

4、在WebSecurityConfig中聲明

先在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后聲明

        http.authorizeRequests().
                withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(accessDecisionManager);//訪問決策管理器
                        o.setSecurityMetadataSource(securityMetadataSource);//安全元數(shù)據(jù)源
                        return o;
                    }
                });
        http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);//增加到默認攔截鏈中

十、最終的WebSecurityConfig配置

/**
 * @Author: Hutengfei
 * @Description:
 * @Date Create in 2019/8/28 20:15
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //登錄成功處理邏輯
    @Autowired
    CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;

    //登錄失敗處理邏輯
    @Autowired
    CustomizeAuthenticationFailureHandler authenticationFailureHandler;

    //權限拒絕處理邏輯
    @Autowired
    CustomizeAccessDeniedHandler accessDeniedHandler;

    //匿名用戶訪問無權限資源時的異常
    @Autowired
    CustomizeAuthenticationEntryPoint authenticationEntryPoint;

    //會話失效(賬號被擠下線)處理邏輯
    @Autowired
    CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;

    //登出成功處理邏輯
    @Autowired
    CustomizeLogoutSuccessHandler logoutSuccessHandler;

    //訪問決策管理器
    @Autowired
    CustomizeAccessDecisionManager accessDecisionManager;

    //實現(xiàn)權限攔截
    @Autowired
    CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    private CustomizeAbstractSecurityInterceptor securityInterceptor;

    @Bean
    public UserDetailsService userDetailsService() {
        //獲取用戶賬號密碼及權限信息
        return new UserDetailsServiceImpl();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 設置默認的加密方式(強hash方式加密)
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();
        http.authorizeRequests().
                //antMatchers("/getUser").hasAuthority("query_user").
                //antMatchers("/**").fullyAuthenticated().
                withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(accessDecisionManager);//決策管理器
                        o.setSecurityMetadataSource(securityMetadataSource);//安全元數(shù)據(jù)源
                        return o;
                    }
                }).
                //登出
                and().logout().
                    permitAll().//允許所有用戶
                    logoutSuccessHandler(logoutSuccessHandler).//登出成功處理邏輯
                    deleteCookies("JSESSIONID").//登出之后刪除cookie
                //登入
                and().formLogin().
                    permitAll().//允許所有用戶
                    successHandler(authenticationSuccessHandler).//登錄成功處理邏輯
                    failureHandler(authenticationFailureHandler).//登錄失敗處理邏輯
                //異常處理(權限拒絕、登錄失效等)
                and().exceptionHandling().
                    accessDeniedHandler(accessDeniedHandler).//權限拒絕處理邏輯
                    authenticationEntryPoint(authenticationEntryPoint).//匿名用戶訪問無權限資源時的異常處理
                //會話管理
                and().sessionManagement().
                    maximumSessions(1).//同一賬號同時登錄最大用戶數(shù)
                    expiredSessionStrategy(sessionInformationExpiredStrategy);//會話失效(賬號被擠下線)處理邏輯
        http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
    }
}

十一、結束語

到現(xiàn)在為止本文就基本結束了,在本文中我們利用Springboot+Spring security實現(xiàn)了前后端分離的用戶登錄認證和動態(tài)的權限訪問控制。

最后附上github地址:
github

到此這篇關于Springboot+Spring Security實現(xiàn)前后端分離登錄認證及權限控制的示例代碼的文章就介紹到這了,更多相關Springboot SpringSecurity前后端分離登錄認證內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • IDEA配置Tomcat創(chuàng)建web項目的詳細步驟

    IDEA配置Tomcat創(chuàng)建web項目的詳細步驟

    Tomcat是一個Java?Web應用服務器,實現(xiàn)了多個Java?EE規(guī)范(JSP、Java?Servlet等),這篇文章主要給大家介紹了關于IDEA配置Tomcat創(chuàng)建web項目的詳細步驟,需要的朋友可以參考下
    2023-12-12
  • 解決springboot 2.x 里面訪問靜態(tài)資源的坑

    解決springboot 2.x 里面訪問靜態(tài)資源的坑

    這篇文章主要介紹了解決springboot 2.x 里面訪問靜態(tài)資源的坑,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • 詳解mybatis collection標簽一對多的使用

    詳解mybatis collection標簽一對多的使用

    這篇文章主要介紹了mybatis collection標簽一對多的使用,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-06-06
  • Java中Jedis基本使用

    Java中Jedis基本使用

    Redis的Java實現(xiàn)的客戶端,本文主要介紹了Java中Jedis基本使用,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-05-05
  • 舉例說明JAVA調用第三方接口的GET/POST/PUT請求方式

    舉例說明JAVA調用第三方接口的GET/POST/PUT請求方式

    在日常工作和學習中,有很多地方都需要發(fā)送請求,這篇文章主要給大家介紹了關于JAVA調用第三方接口的GET/POST/PUT請求方式的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下
    2024-01-01
  • SpringBoot實現(xiàn)阿里云短信發(fā)送的示例代碼

    SpringBoot實現(xiàn)阿里云短信發(fā)送的示例代碼

    這篇文章主要為大家介紹了如何利用SpringBoot實現(xiàn)阿里云短信發(fā)送,文中的示例代碼講解詳細,對我們學習或工作有一定幫助,需要的可以參考一下
    2022-04-04
  • SpringBoot3集成和使用Jasypt的代碼詳解

    SpringBoot3集成和使用Jasypt的代碼詳解

    隨著信息安全的日益受到重視,加密敏感數(shù)據(jù)在應用程序中變得越來越重要,Jasypt作為一個簡化Java應用程序中數(shù)據(jù)加密的工具,為開發(fā)者提供了一種便捷而靈活的加密解決方案,本文將深入解析Jasypt的工作原理,需要的朋友可以參考下
    2024-01-01
  • 淺談Java中方法參數(shù)傳遞的問題

    淺談Java中方法參數(shù)傳遞的問題

    下面小編就為大家?guī)硪黄獪\談Java中方法參數(shù)傳遞的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • ssm框架controller層返回json格式數(shù)據(jù)到頁面的實現(xiàn)

    ssm框架controller層返回json格式數(shù)據(jù)到頁面的實現(xiàn)

    這篇文章主要介紹了ssm框架controller層返回json格式數(shù)據(jù)到頁面的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-09-09
  • JDK1.8中ArrayList是如何擴容的

    JDK1.8中ArrayList是如何擴容的

    本文基于此出發(fā)講解ArrayList的擴容機制,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-12-12

最新評論