SpringBoot整合Spring Security实现用户角色验证以及权限控制

文章目录

    • 一。Spring Security介绍
    • 二。工程搭建
      • 1.工程结构图:
      • 2.导入依赖
      • 3.编写security配置类
      • 4.编写UserDetailsService验证类
      • 5.Dao层
      • 6.model层
      • 7.控制器层
      • 8.工具类
      • 9.配置文件
      • 10.jsp示例登录页面
      • 11.启动类
    • 三。数据库结构
      • t_user(用户表)
      • t_role(角色表)
      • t_permission(权限表)
      • t_user_role(用户角色表)
      • t_permission(角色权限表)
    • 四。测试
      • 1.用户登录验证
      • 2.用户权限验证
    • 五。项目地址


一。Spring Security介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它 是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。

二。工程搭建

1.工程结构图:

SpringBoot整合Spring Security实现用户角色验证以及权限控制_第1张图片

2.导入依赖

pom.xml内容如下所示:


<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.itheima.securitygroupId>
    <artifactId>security-springbootartifactId>
    <version>1.0-SNAPSHOTversion>

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.3.RELEASEversion>
    parent>

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
    properties>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>


        
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>javax.servlet-apiartifactId>
            <scope>providedscope>
        dependency>
        
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>jstlartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
            <scope>providedscope>
        dependency>
        
        <dependency>
            <groupId>org.apache.tomcat.embedgroupId>
            <artifactId>tomcat-embed-jasperartifactId>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.0version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.47version>
        dependency>
    dependencies>
    <build>
        <finalName>security-springbootfinalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.mavengroupId>
                    <artifactId>tomcat7-maven-pluginartifactId>
                    <version>2.2version>
                plugin>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <configuration>
                        <source>1.8source>
                        <target>1.8target>
                    configuration>
                plugin>

                <plugin>
                    <artifactId>maven-resources-pluginartifactId>
                    <configuration>
                        <encoding>utf-8encoding>
                        <useDefaultDelimiters>trueuseDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resourcesdirectory>
                                <filtering>truefiltering>
                                <includes>
                                    <include>**/*include>
                                includes>
                            resource>
                            <resource>
                                <directory>src/main/javadirectory>
                                <includes>
                                    <include>**/*.xmlinclude>
                                includes>
                            resource>
                        resources>
                    configuration>
                plugin>
            plugins>
        pluginManagement>
    build>

project>

其中,整合spring security最主要的依赖如下所示:

<!-- 以下是>spring security依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

3.编写security配置类

新建WebConfig配置类,实现WebMvcConfigurer接口,配置项目启动默认跳转页面,这里是配置到login.jsp页面:

package com.security.springboot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration//就相当于springmvc.xml文件
public class WebConfig implements WebMvcConfigurer {

    //默认Url根路径跳转到/login,此url为spring security提供
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login-view");
        registry.addViewController("/login-view").setViewName("login");

    }

}

新建WebSecurityConfig配置类,实现WebSecurityConfigurerAdapter接口,配置安全拦截机制:

package com.security.springboot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //密码编码器(用户密码存入数据为BCryptPasswordEncoder加密的方式存入)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
                .and()
                .formLogin()//允许表单登录
                .loginPage("/login-view")//登录页面
                .loginProcessingUrl("/login")
                .successForwardUrl("/login-success")//自定义登录成功的页面地址
        .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login-view?logout");


    }
}

4.编写UserDetailsService验证类

什么是UserDetailsService ?

现在咱们现在知道DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个 Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份 信息就是一个 Object ,大多数情况下它可以被强转为UserDetails对象。

DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息 UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交 的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为spring bean来定 义自定义身份验证。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定 的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认 证流程,同时会把UserDetails填充至Authentication。

上面一直提到UserDetails是用户信息,咱们看一下它的真面目:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与 UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证 其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形 成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。

通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。 Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。

编写SpringDataUserDetailsService类,实现UserDetailsService接口,从数据库查询用户信息。(认证逻辑会首先通过这个验证类来验证用户是否存在

package com.security.springboot.service;

import com.security.springboot.dao.UserDao;
import com.security.springboot.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    //根据 账号查询用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //将来连接数据库根据账号查询用户信息
        User user = userDao.getUserByUsername(username);
        if(user == null){
            //如果用户查不到,返回null,由provider来抛出异常
            return null;
        }
        //根据用户的id查询用户的权限
        List<String> permissions = userDao.findPermissionsByUserId(user.getId());
        //将permissions转成数组
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }
}

5.Dao层

编写UserDao类,从数据库获取信息:

package com.security.springboot.dao;

import com.security.springboot.model.Permission;
import com.security.springboot.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;

@Repository
public class UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    //根据账号查询用户信息
    public User getUserByUsername(String username){
        String sql = "select id,username,password,fullname,mobile from t_user where username = ?";
        //连接数据库查询用户
        List<User> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(User.class));
        if(list !=null && list.size()==1){
            return list.get(0);
        }
        return null;
    }

    //根据用户id查询用户权限
    public List<String> findPermissionsByUserId(String userId){
        String sql = "SELECT * FROM t_permission WHERE id IN(\n" +
                "\n" +
                "SELECT permission_id FROM t_role_permission WHERE role_id IN(\n" +
                "  SELECT role_id FROM t_user_role WHERE user_id = ? \n" +
                ")\n" +
                ")\n";

        List<Permission> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(Permission.class));
        List<String> permissions = new ArrayList<>();
        list.forEach(c -> permissions.add(c.getCode()));
        return permissions;
    }
}

6.model层

User:

package com.security.springboot.model;

import lombok.Data;

/**
 * 用户实体
 */
@Data
public class User {
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

Permission:

package com.security.springboot.model;

import lombok.Data;

/**
 * 用户关联权限实体
 */
@Data
public class Permission {

    private String id;
    private String code;         //用户权限
    private String description;  //权限描述
    private String url;          //资源url
}

7.控制器层

LoginController:

package com.security.springboot.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {
    /**
     * 登录成功执行接口
     * @return
     */
    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        //提示具体用户名称登录成功
        return getUsername()+" 登录成功";
    }

    /**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/r/r1",produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限才可以访问
    public String r1(){
        return getUsername()+" 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/r/r2",produces = {"text/plain;charset=UTF-8"})
    @PreAuthorize("hasAuthority('p2')")//拥有p2权限才可以访问
    public String r2(){
        return getUsername()+" 访问资源2";
    }

    /**
     * 无需认证测试接口
     * @return
     */
    @GetMapping(value = "test",produces = {"text/plain;charset=UTF-8"})
    public String test(){
        return "无需认证测试接口 ";
    }

    //获取当前用户信息
    private String getUsername(){
        String username = null;
        //当前认证通过的用户身份
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //用户身份
        Object principal = authentication.getPrincipal();
        if(principal == null){
            username = "匿名";
        }
        if(principal instanceof org.springframework.security.core.userdetails.UserDetails){
            UserDetails userDetails = (UserDetails) principal;
            username = userDetails.getUsername();
        }else{
            username = principal.toString();
        }
        return username;
    }


}

8.工具类

因为SpringSecurity框架中,数据库存放的用户密码是以加密的方式来进行存储的,在本项目我们采用BCryptPasswordEncoder加密。

首先我们需要在之前的WebSecurityConfig配置类中声明:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第2张图片
BCryptPasswordEncoder加密工具类BCPEUtils代码如下所示:

package com.security.springboot.util;
 
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
/**
 * SpringSecurity中的BCryptPasswordEncoder加密
 */
public class BCPEUtils {
 
    private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    /**
     *  将原生密码进行加密
     * @return 加密后的字符串密码
     */
    public static String encode(String rawPWD){
        String hashedPassword = passwordEncoder.encode(rawPWD);
        return hashedPassword;
    }
 
    /**
     * 比较原生密码与密文是否一致
     * @param rawPWD 原生密码
     * @param encodePWD 密文
     * @return true/false
     */
    public static boolean matches(String rawPWD,String encodePWD){
        boolean result = passwordEncoder.matches(rawPWD, encodePWD);
        return result;
    }


    public static void main(String[] args) {
        String password="123456";
        String rePassword=BCPEUtils.encode(password);
        System.out.println(rePassword);
        System.out.println("----------------------");
        System.out.println(BCPEUtils.matches(password,rePassword));
    }
}

9.配置文件

application.yml配置内容如下:

server:
  port: 8080
  servlet:
    context-path: /security-springboot  #访问前缀
spring:
  application:
    name: security-springboot
  mvc:
    view:   #视图解析器
      prefix: /WEB-INF/view/
      suffix: .jsp
  datasource:  #连接数据库信息
    url: jdbc:mysql://localhost:3306/user_db
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver




10.jsp示例登录页面

login.jsp

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录title>
head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>   码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
form>
body>
html>

11.启动类

SecuritySpringBootApp:

package com.security.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecuritySpringBootApp {
    public static void main(String[] args) {
        SpringApplication.run(SecuritySpringBootApp.class,args);
    }
}

三。数据库结构

由于本项目中的验证是从数据库中读取用户数据,所以我们需要创建一个名为user_db的数据库,并创建五张数据表

t_user(用户表)

DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (
  `id` bigint(20) NOT NULL COMMENT '用户id',
  `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `fullname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户姓名',
  `mobile` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `t_user` VALUES (1, 'zhangsan', '$2a$10$3MYHLGvYkYgCKRAXNeEDTe1g7AznV/Ni7pmXP2zD0V0YS/HosBamu', '张三', '12345674891');

创建完成后如下所示:
在这里插入图片描述

t_role(角色表)

DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `create_time` datetime NULL DEFAULT NULL,
  `update_time` datetime NULL DEFAULT NULL,
  `status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `unique_role_name`(`role_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `t_role` VALUES ('1', '管理员', NULL, NULL, NULL, '');

创建完成后如下所示:
在这里插入图片描述

t_permission(权限表)

DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限标识符',
  `description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `url` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `t_permission` VALUES ('1', 'p1', '测试资源 1', '/r/r1');
INSERT INTO `t_permission` VALUES ('2', 'p3', '测试资源2', '/r/r2');

创建完成后如下所示:
在这里插入图片描述

t_user_role(用户角色表)

DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role`  (
  `user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `create_time` datetime NULL DEFAULT NULL,
  `creator` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `t_user_role` VALUES ('1', '1', NULL, NULL);

创建完成后如下所示:
在这里插入图片描述

t_permission(角色权限表)

DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission`  (
  `role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `permission_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`role_id`, `permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `t_role_permission` VALUES ('1', '1');
INSERT INTO `t_role_permission` VALUES ('1', '2');

创建完成后如下所示:
在这里插入图片描述

四。测试

1.用户登录验证

1.Debug启动项目,在SpringDataUserDetailsService上面打断点:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第3张图片
2.在浏览器访问 http://localhost:8080/security-springboot,自动跳到登录页面:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第4张图片
3.输入用户名以及用户密码,点击登录按钮:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第5张图片
4.跳到后台SpringDataUserDetailsService用户验证逻辑中,拿到用户信息:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第6张图片
5.用户验证成功后:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第7张图片

2.用户权限验证

1.访问http://localhost:8080/security-springboot/r/r1,由于张三角色含有p1,p3权限,所以可以成功进行访问:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第8张图片
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第9张图片
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第10张图片
2.访问http://localhost:8080/security-springboot/r/r2,访问失败:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第11张图片
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第12张图片
3.无需权限认证访问接口,访问成功:
SpringBoot整合Spring Security实现用户角色验证以及权限控制_第13张图片SpringBoot整合Spring Security实现用户角色验证以及权限控制_第14张图片

五。项目地址

该项目已经上传至百度网盘,有需要的小伙伴可自行下载:

链接:https://pan.baidu.com/s/1g6EqQMvje653zTjySy-sCA
提取码:lig3

你可能感兴趣的:(#,Spring,Security,#,SpringBoot,springboot,spring,security)