SpringBoot 集成 SpringSecurity 从入门到深入理解

news2024/12/23 7:31:22

完整的目录

    • 介绍 SpringSecurity
      • 简述 SpringSecurity
      • SpringSecurity 的主要功能说明
    • 项目源码
    • 入门案例
      • 项目工程路径
      • 第一步:加载依赖
      • 第二步:创建核心的配置类
      • 第三步:增加controller
      • 第三步:启动程序
      • 小结
        • 界面跳转说明
        • 密码生成说明
    • 重点内容扫盲
      • 重要的Filter
      • PasswordEncoder 接口
      • UserDetailsService 接口
    • 深入学习案例
      • 基础验证案列
        • 第一步:加载依赖
        • 第二步:初始化 SQL
        • 第三步: 添加配置
        • 第四步:添加实体类以及对应的 Mapper
        • 第五步:增加 controller 进行访问验证
        • 第六步:添加自定义的验证逻辑
      • 验证
        • 验证一:异常访问
        • 验证二:正常登录
        • 验证三:异常登录
      • 基于角色的访问控制
        • 项目结构
        • 添加菜单和角色实体类以及Mapper 文件
        • 调整 MyUserDetailsService 实现
        • 添加访问接口
        • 修改 Security 配置
        • hasRole 的源码相关说明
      • 验证
        • 验证一:使用普通用户登录验证
        • 验证二:使用管理员账户登录验证
      • 自定义界面访问
        • 添加登录界面
        • 修改 Security 的配置
      • 验证
        • 登录页面验证
      • 自定义错误页面
      • 添加错误页面
        • 修改 Security 的配置
      • 验证
        • 无访问权限验证
      • 自定义主页
        • 添加主页
        • 修改 Security 的配置
      • 验证
        • 验证跳转主页
      • remember-me 功能介绍
        • 修改 Security 配置
        • 修改Login页面
        • 创建表结构
      • 验证
        • 验证 remember-me 功能
      • 原理分析
      • 注解使用
        • @Secured注解
        • @PreAuthorize
        • @PostAuthorize
      • 总结

介绍 SpringSecurity

简述 SpringSecurity

Spring Security 是 Spring 应用程序的安全框架,它提供了认证、授权、访问控制等安全性功能。在 Spring Boot 应用程序中使用 Spring Security,可以方便地进行安全性配置。

Spring Security最初是一个独立的项目,名为Acegi Security,于2004年首次发布。Acegi Security是针对Spring框架的安全框架,旨在为Java企业应用程序提供基于认证和授权的安全功能。

2010年,Spring Security成为Spring项目的一部分,并开始在Spring框架的生态系统中广泛应用。随着时间的推移,Spring Security逐渐成为Java企业应用程序中最受欢迎的安全框架之一,用于保护Web应用程序、REST API和基于Spring的微服务架构。

在最新版本的Spring Security 5中,该框架已经得到了大量改进和更新,以支持最新的安全标准和技术,如OAuth 2.0、OpenID Connect和WebFlux。

SpringSecurity 的主要功能说明

一般Web应用关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制)。

  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。通俗的说就是系统认为用户是否能登录。
  • 授权:经过认证后判断当前用户是否有权限进行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对某一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。通俗点讲就是系统判断用户是否有权限去做某些事情

认证和授权也是 SpringSecurity 作为安全框架的核心功能。

项目源码

本文的项目地址:点击这里查看项目源码

入门案例

项目工程路径

首先,我们创建一个 SpringBoot 项目,创建项目应该来说是比较简单的,这里就略过了。下面说下简单的搭建,那就接着向下看,最后的项目工程路径是这样子的:
入门工程项目结构

第一步:加载依赖

这里主要是看下 POM 文件引入哪些依赖

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

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

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

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
      </dependency>

      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.26</version>
          <optional>true</optional>
      </dependency>

</dependencies>

第二步:创建核心的配置类

WebSecurityConfigurerAdapter 类是 Spring Security 的核心配置类,相关的权限过滤,访问地址等等都是通过这里来配置的。使用Spring Security 就离不开这个类的实现。现在呢,我们就先有一个眼熟,后面我们会一点点的深入去看是如何整合的。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * WebSecurityConfigurerAdapter就是Security的核心配置类,一般我们要用Security都会涉及到这个类,一般就是继承这个类,重写方法。
 *
 * @Author wuq
 * @Date 2021-7-16
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录
        http.formLogin()                    // 启用默认登录界面
                .failureUrl("/login?fail")     // 登录失败返回 Url
                .defaultSuccessUrl("/index")  // 登录成功跳转 URL,他会自动去根路径static文件夹下寻找login.html
                .failureForwardUrl("/fail")
                .permitAll();                   // 登录页面全部权限可以访问

        super.configure(http);
    }
}

第三步:增加controller

我这里增加controller是为了方便于看到跳转的效果
**

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @RequestMapping("index")
    public String index(){
        return "index";
    }
}

第三步:启动程序

在启动程序的时候,会发现控制台中输出了这么串密码,每次启动程序的时候,这里的密码都是不一样的
启动时会出现一串密码
访问 localhost:8080/index,这里访问的时候会自动跳转到下面的登录页面,这个并不是我们自己设置的登录页,而是 Spring Security 自己带有的登录界面,输入用户名 user (下面会介绍是怎么来的) 以及上面的密码串,就可以登录了
登录页面
登录成功之后就会跳转到下面的页面了
登录之后跳转到index
当我们访问 localhost:8080/logout 就会出现下面的界面,这个页面也是 Spring Security 自带的界面
退出登录

小结

界面跳转说明

由于我们在 WebSecurityConfig 配置了相关的界面跳转路径,所以就会实现登录之后,自动跳转到指定的页面上面。

通过上面的界面跳转就会发现,我们可以通过简单的配置就能实现请求的拦截以及,登录之后页面跳转了。

密码生成说明

在启动程序的时候会发现有这么一串密码出来,那他是怎么生成的呢?

2023-09-12 13:57:01.237  INFO 9228 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: 8026a963-4e4d-4fa2-b988-b28dcf264c33

这里我们看下上面的控制台打印,会看到有一个类 UserDetailsServiceAutoConfiguration ,的确输出的位置就是这里,这个类是自动化配置用户相关的信息的。我们访问下这个自动装配的类
源码位置
我们就继续看下是怎么调用的,通过源码可以看到这里是在生成 InMemoryUserDetailsManager bean 的时候会去调用,并且这里使用了 @ConditionalOnMissingBean 注解
调用路径
然后在这里我们看到这里是有一个 User 类,我们就去看下这个User
User 类
设置密码

看到这里,就应该清楚了这个密码是怎么生成了的,并且也知道了这个还可以去配置对应的用户名和密码:

spring.security.user.name=admin
spring.security.user.password=123456

重点内容扫盲

对于接下来需要做的深入学习之前,我们先对两块的知识点扫盲一下

重要的Filter

Spring Security采用责任链的设计模式,它有一条很长的过滤器链。通过不同的过滤器处理相应的业务流程,如登录认证、权限过滤等。

  1. org.springframework.security.web.context.SecurityContextPersistenceFilter:SecurityContextPersistenceFilter 主要是使用 SecurityContextRepository 在session中保存或更新一个SecurityContext,并将 SecurityContext 给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext 中存储了当前用户的认证以及权限信息。

  2. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter:此过滤器用于集成SecurityContext到Spring异步执行机制中的 WebAsyncManager

  3. org.springframework.security.web.header.HeaderWriterFilter:向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制

  4. org.springframework.security.web.csrf.CsrfFilter:csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果。

  5. org.springframework.security.web.authentication.logout.LogoutFilter:匹配 URL为/logout的请求,实现用户退出,清除认证信息。

  6. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter:认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

  7. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter:如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

  8. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter:由此过滤器可以生产一个默认的退出登录页面

  9. org.springframework.security.web.authentication.www.BasicAuthenticationFilter:此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

  10. org.springframework.security.web.savedrequest.RequestCacheAwareFilter:通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest

  11. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter:针对ServletRequest进行了一次包装,使得request具有更加丰富的API

  12. org.springframework.security.web.authentication.AnonymousAuthenticationFilter:当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
    spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

  13. org.springframework.security.web.session.SessionManagementFilter:SecurityContextRepository限制同一用户开启多个会话的数量

  14. org.springframework.security.web.access.ExceptionTranslationFilter:异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常

  15. org.springframework.security.web.access.intercept.FilterSecurityInterceptor:获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

PasswordEncoder 接口

关于 PasswordEncoder 接口,PasswordEncoder 主要负责的就是密码和 主题信息业务类返回的密码进行比对的时候,所要使用的加密方式。

// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);

// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);

// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword){
	return false; 
}

接着,我们看下对应的接口实现类
PasswordEncoder 实现类
我们这里就主要介绍下 BCryptPasswordEncoder 密码解析器,这个也是官方推荐使用的,

BCryptPasswordEncoder 是 Spring Security 框架提供的一种密码加密方式。它使用 bcrypt 算法对密码进行加密,该算法是一种非常安全可靠的密码加密算法。

使用 BCryptPasswordEncoder 加密用户的密码时,首先会生成一个随机“盐”(salt),并将盐值和原始密码一同进行加密。因为每个用户的盐值都是随机生成的,即使两个用户的密码相同,加密后的结果也是不同的,这样大大增加了密码破解的难度。

举一个实际的例子:密码是 123456 加密后成了 a 存到了数据库,这时候登录前端传的还是 123456 密码,然后进行加密,加密后的密文会发现根本不是a,是b,但是a和b两个密文通过加密算法提供的对比方法,在对比的时候是相等的。

具体实例

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BCryptTest {
    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

        // 对密码进行加密
        String pwd = bCryptPasswordEncoder.encode("123456");
        // 输出加密之后的字符串
        System.out.println("加密之后数据:\t"+pwd);

        // 使用 bCryptPasswordEncoder 的匹对方法
        boolean result = bCryptPasswordEncoder.matches("123456", pwd);
        // 打印比较结果
        System.out.println("比较结果:\t"+result);
    }
}

控制台输出

加密之后数据:	$2a$10$n.U/yTVF8c9mjMsPUv0fruekmbvfxAhZhcq0ymOWa/qMwr3P7LxQa
比较结果:	true

对于 BCryptPasswordEncoder 的使用,在实际项目上面,我们是会将用户的密码使用 BCrypt 加密之后保存到数据库中,然后登录的时候会将明文的密码加密之后与数据库中的密文进行对比。

UserDetailsService 接口

在上面介绍密码是如何生成的时候,有讲到 UserDetailsServiceAutoConfiguration 类,在上面的注解上面就有出现过他的身影。通过这里的 @ConditionalOnMissingBean 可以看出来,当我们没有自己的登录逻辑时(就像上面的入门示例一样),就会默认的走到这个地方来。
UserDetailsServiceAutoConfiguration
对于这个接口呢,我感觉是需要重点需要了解的,这个是涉及到我们登录的时候校验用的。也就是说 Spring Security 就是通过这个来校验登录用户信息的。 我们具体应该怎么写代码呢?这由于他是一个接口,我们实现这个接口,然后写入我们自己的逻辑就好,对应的源码如下:

package org.springframework.security.core.userdetails;

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

对于这个loadUserByUsername 具体的操作有点儿奇怪,我们这个先看整个验证流程,这里需要特别说明下:

第一步:我们根据 username 查询数据库对应的用户是否存在。
第二步:将数据库中查询的用户信息(账号+密码)封装到 UserDetail 对象中,作为方法的返回值。
第三步,将第二步中数据库中的密码与前端出入的明文密码加密之后对比,验证身份。

UserDetailsService 中loadUserByUsername 就做的事情是第二步。我们通常的情况下,是直接判断用户名和密码,看看是否能登录成功,这里和我们自己写的登录逻辑并不一样。

用户示例

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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;

@Service
public class LoginService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws
            UsernameNotFoundException {
        // 1.根据username查询数据库,判断用户名是否存在
       
        // 2.将数据库当中查出来的username和pwd封装到user对象当中返回 第三个参数表示权限
        return new User(username, pwd,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin,"));
    }
}

UserDetail
在上面出现了 UserDetail 以及一个 User类这里就再说明下他们是啥,UserDetail 是一个接口,具体的源码如下:

public interface UserDetails extends Serializable {
	// 表示获取登录用户所有权限
    Collection<? extends GrantedAuthority> getAuthorities();

	// 表示获取密码
    String getPassword();

	// 表示获取用户名
    String getUsername();

	// 表示判断账户是否过期
    boolean isAccountNonExpired();

	// 表示判断账户是否被锁定
    boolean isAccountNonLocked();

	// 表示凭证{密码}是否过期
    boolean isCredentialsNonExpired();

	// 表示当前用户是否可用
    boolean isEnabled();
}

在Spring Security 中有一个 User (不是上面文章中 SecurityProperties 的内部类 User ) 作为 UserDetails 实现,在项目上面是新建一个类实现这个接口或者直接使用这个 User 都是可以的:
User类
具体的验证调用验证的逻辑如下,这里是先将一部分源码贴出来给大伙看下,知道是怎么调用的,后面会通过示例讲到
调用验证

深入学习案例

这里呢,我们需要深入学习下 Spring Security 的登录验证了,我们先看下是怎么实现web 登录校验

先通过一个实际的例子来混个脸熟,这里呢,我把对应的源码放入到了这里:Github 项目工程地址点击这里

基础验证案列

下面最后搭建的工程项目接口是下面这样子的:
项目工程结构

第一步:加载依赖

我们这里使用 MybatisPlus 作为持久化框架,简化我们的查询。另外呢,这里需要注意的是mybatis-plus在springboot并没有版本管理,所以我们需要指定mybatis-plus版本,不然就报错。

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

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

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.26</version>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.1.2</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

第二步:初始化 SQL

初始化 SQL,这里我是用 MySQL,在虚拟机上面搭建的 docker 容器,如果是需要搭建的话,可以看下我之前写这个文章:docker 中安装 MySQL 以及使用

create table users
(
    id       bigint primary key auto_increment,
    username varchar(20) unique not null,
    password varchar(100)
);
-- 密码 123456 使用了BCrypt加密
insert into users
values (1, 'admin', '$2a$10$ZglYem2Zs8E4ETbLwaiA4OjXaTZX9w8wJ7x8LZdpGisdtI9VlIfvO');
-- 密码 123456
insert into users
values (2, 'user', '$2a$10$ZglYem2Zs8E4ETbLwaiA4OjXaTZX9w8wJ7x8LZdpGisdtI9VlIfvO');

create table role
(
    id   bigint primary key auto_increment,
    name varchar(20)
);
insert into role
values (1, '管理员');
insert into role
values (2, '普通用户');

create table role_user
(
    uid bigint,
    rid bigint
);
insert into role_user
values (1, 1);
insert into role_user
values (2, 2);

create table menu
(
    id         bigint primary key auto_increment,
    name       varchar(20),
    url        varchar(100),
    parentid   bigint,
    permission varchar(20)
);
insert into menu
values (1, '系统管理', '', 0, 'menu:system');
insert into menu
values (2, '用户管理', '', 0, 'menu:user');

create table role_menu
(
    mid bigint,
    rid bigint
);
insert into role_menu
values (1, 1);
insert into role_menu
values (2, 1);
insert into role_menu
values (2, 2);

CREATE TABLE `persistent_logins`
(
    `username`  VARCHAR(64) NOT NULL,
    `series`    VARCHAR(64) NOT NULL,
    `token`     VARCHAR(64) NOT NULL,
    `last_used` TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`series`)
) ENGINE = INNODB DEFAULT CHARSET = utf8;

第三步: 添加配置

配置文件 application.yml

# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.152.129:3306/study?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8
    username: admin
    password: 123456

# 日志打印
# 日志打印
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Spring Security 配置

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

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //  表单登录
        http.formLogin() // 表单登录
                .defaultSuccessUrl("/index")  //  登录成功之后跳转到哪个 url
                .failureForwardUrl("/fail")   //  登录失败之后跳转到哪个 url
                .and()
                .authorizeRequests() // 认证配置
                .anyRequest() // 任何请求
                .authenticated(); // 都需要身份验证

        // 关闭 csrf
        http.csrf().disable();
    }
}

Mapper 的扫码注解
由于我们使用了 mybatis plus 所以我们需要给 mapper 添加一个扫码的注解,在启动类上面添加就好

@SpringBootApplication
@MapperScan("com.demo.security.mapper")
public class SecurityApplication {

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

第四步:添加实体类以及对应的 Mapper

User 实体类以及对应的 Mapper,这个就是我们的用户,等下验证的时候,就是会去查询这个User 表中的数据

Users类

import lombok.Data;

@Data
public class Users {
    private Long id;
    private String username;
    private String password;
}

UsersMapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.demo.security.entity.Users;

public interface UsersMapper extends BaseMapper<Users> {
}

第五步:增加 controller 进行访问验证

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {

    @GetMapping("index")
    @ResponseBody
    public String index() {
        System.out.println("1111111111111");
        return "success";
    }

    @PostMapping("fail")
    @ResponseBody
    public String fail() {
        return "fail";
    }

}

第六步:添加自定义的验证逻辑

这个验证类的实现比较重要,这里也需要重点啰嗦下,上面在知识点扫盲中有讲到 UserDetailsService 这个接口,这个类就主要做了两件事请:

  • 第一:根据前端传入的用户名去查询数据库中是否存在对应的账户
  • 第二:将数据库中查询到的用户,放入到 User 对象中返回
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.demo.security.entity.Users;
import com.demo.security.mapper.UsersMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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;


/**
 * 根据账号查询用户密码,顺便判断账户是否存在。
 * 将从数据库查询出来的账号密码,放到 User 对象当中并返回。
 *
 * UserDetailsService 接口:主要作用就是返回主体,并且主体当中会携带授权(授权这个权可以是菜单权限,也可以是角色权限)。
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UsersMapper usersMapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper<Users> wrapper = new QueryWrapper();
        wrapper.eq("username", s);
        Users users = usersMapper.selectOne(wrapper);

        if (users == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        System.out.println(users);
        // 这里就是在构建权限,这里的权限可以是菜单权限也可以是角色权限
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User(users.getUsername(), users.getPassword(), auths);
    }
}

验证

验证一:异常访问

直接访问 localhost:8080/index 会发现访问的时候就会自动跳转到 localhost:8080/login 地址上面去,这里是因为没有权限就无法访问

验证二:正常登录

访问 localhost:8080/login ,输入用户名admin 和密码 123456,就会进入到下面页面
登录成功

验证三:异常登录

访问 localhost:8080/login ,输入用户名admin 然后输入错误的密码,再次登录
登录失败调整

基于角色的访问控制

前面我们已经添加了一个用户实体类,现在我们来继续增加角色和菜单相应的实体类

项目结构

我们这个地方修改之后的项目工程结构如下
项目结构

添加菜单和角色实体类以及Mapper 文件

Menu类

import lombok.Data;

@Data
public class Menu {
    private Long id;
    private String name;
    private String url;
    private Long parentId;
    private String permission;
}

Role类

import lombok.Data;

@Data
public class Role {
    private Long id;
    private String name;
}

对应的 Mapper 文件以及 xml 查询

import com.demo.security.entity.Menu;
import com.demo.security.entity.Role;

import java.util.List;

/**
 * @author wuq
 * @Time 2023-9-6 17:04
 * @Description
 */
public interface UserInfoMapper {

    /**
     * 根据用户 Id 查询用户角色
     *
     * @param userId
     * @return
     */
    List<Role> selectRoleByUserId(Long userId);

    /**
     * 根据用户 Id 查询菜单
     *
     * @param userId
     * @return
     */
    List<Menu> selectMenuByUserId(Long userId);
}
<?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.wq.security.mapper.UserInfoMapper">

    <!--根据用户 Id 查询角色信息-->
    <select id="selectRoleByUserId" resultType="com.wq.security.entity.Role">
        SELECT r.id,
               r.NAME
        FROM role r
                 INNER JOIN role_user ru ON ru.rid = r.id
        WHERE ru.uid = #{0}
    </select>
    <!--根据用户 Id 查询权限信息-->
    <select id="selectMenuByUserId" resultType="com.wq.security.entity.Menu">
        SELECT m.id,
               m.NAME,
               m.url,
               m.parentid,
               m.permission
        FROM menu m
                 INNER JOIN role_menu rm ON m.id = rm.mid
                 INNER JOIN role r ON r.id = rm.rid
                 INNER JOIN role_user ru ON r.id = ru.rid
        WHERE ru.uid = #{0}
    </select>
</mapper>

调整 MyUserDetailsService 实现

这里呢,我们将上面的源码修改下,增加权限相关的查询,前面是写死了权限,我们现在是将权限信息从数据库中查询出来再返回,按照下面的源码看,权限是封装到了 List<GrantedAuthority> 集合中。

注意:在实际开发当中,我们可能涉及不到某些接口必须用哪个角色才能访问的场景,而只是利用角色来分配菜单,然后给用户再分配角色。
如果要是这样的话,我们只需要根据用户id来关联查询角色表,再根据拥有的角色查询出来所拥有的菜单权限即可。就不需要像下面一样,还查询出来角色,把角色也放到了List当中。

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.demo.security.entity.Menu;
import com.demo.security.entity.Role;
import com.demo.security.entity.Users;
import com.demo.security.mapper.UserInfoMapper;
import com.demo.security.mapper.UsersMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.ArrayList;
import java.util.List;


/**
 * 根据账号查询用户密码,顺便判断账户是否存在。
 * 将从数据库查询出来的账号密码,放到 User 对象当中并返回。
 *
 * UserDetailsService 接口:主要作用就是返回主体,并且主体当中会携带授权(授权这个权可以是菜单权限,也可以是角色权限)。
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UsersMapper usersMapper;
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper<Users> wrapper = new QueryWrapper();
        wrapper.eq("username", s);
        Users users = usersMapper.selectOne(wrapper);

        if (users == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        // 获取用户角色、菜单列表
        List<Role> roles = userInfoMapper.selectRoleByUserId(users.getId());
        List<Menu> menus = userInfoMapper.selectMenuByUserId(users.getId());

        // 声明一个集合List<GrantedAuthority>, 将角色和菜单权限都加入进去
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        // 处理角色
        for (Role role:roles){
            // 这个地方品拼接的 "ROLE_" 不能删除
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
            grantedAuthorityList.add(simpleGrantedAuthority);
        }
        // 处理权限
        for (Menu menu:menus){
            grantedAuthorityList.add(new SimpleGrantedAuthority(menu.getPermission()));
        }
        return new User(users.getUsername(), users.getPassword(), grantedAuthorityList);
    }
}

另外这里需要注意下,在权限拼接的时候 ROLE_ 不能删除,这个后面会讲到

SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());

添加访问接口

IndexController.java 中增加下面内容

@GetMapping("findAll")
@ResponseBody
public String findAll() {
    return "findAll";
}

@GetMapping("find")
@ResponseBody
public String find() {
    return "find";
}

修改 Security 配置

下面是增加了

  • .antMatchers("/findAll").hasRole("管理员") 需要管理员权限才能访问
  • .antMatchers("/find").hasAuthority("menu:user") 需要用户具备 menu:user 这个接口的许可,才可以访问,这里的许可就是指的是菜单中的 permission 字段,如果是权限是按钮级别的控制,那么对应的接口就需要有一个唯一的 permission 许可
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单登录
        http.formLogin()
                //  登录成功之后跳转到哪个 url
                .defaultSuccessUrl("/index").permitAll()
                //  登录失败之后跳转到哪个 url
                .failureForwardUrl("/fail").permitAll();
        // 身份验证
        http.authorizeRequests()
                // // 需要用户带有管理员角色才可以访问/findAll接口
                .antMatchers("/findAll").hasRole("管理员")
                .antMatchers("/find").hasRole("管理员")
                // 需要用户具备menu:user这个接口的许可,才可以访问
                .antMatchers("/find").hasAuthority("menu:user")
                // 任何请求都需要认证
                .anyRequest().authenticated();
        // 关闭 csrf
        http.csrf().disable();
    }
}

hasRole 的源码相关说明

通过编译器,我们点击进去看 hasRole的源码时,会找到 ExpressionUrlAuthorizationConfigurer 这个类,先看下 hasRole() 最后的校验逻辑,下面都出现这个 rolePrefix 的前缀

private static String hasAnyRole(String rolePrefix, String... authorities) {
        String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','" + rolePrefix);
        return "hasAnyRole('" + rolePrefix + anyAuthorities + "')";
    }

    private static String hasRole(String rolePrefix, String role) {
        Assert.notNull(role, "role cannot be null");
        Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> {
            return "role should not start with '" + rolePrefix + "' since it is automatically inserted. Got '" + role + "'";
        });
        return "hasRole('" + rolePrefix + role + "')";
    }

那我们就全局搜索下吧,看下这个是怎么来的,看到下面就应该知道了,如果没有特殊配置的话,就走的默认前缀ROLE_
rolePrefix
在Spring Security中,可以使用GrantedAuthorityDefaults来为所有的授权授予对象指定默认的前缀。默认的前缀为ROLE_,可以使用rolePrefix属性为其指定不同的前缀,那我们具体怎么修改这个前缀呢?

WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中,可以使用以下代码来设置GrantedAuthorityDefaults的前缀:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 设置授权授予对象的默认前缀
        http.authorizeRequests().mvcMatchers("/admin/**").hasRole("ADMIN");

        // 使用自定义的前缀
        http.authorizeRequests().mvcMatchers("/user/**").hasAuthority("CUSTOMER");
    }

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("CUSTOMER_");
    }
}

在示例中,grantedAuthorityDefaults()方法返回了GrantedAuthorityDefaults对象,其构造函数中传入了一个自定义的前缀CUSTOMER_。然后,在configure(HttpSecurity http)方法中,使用hasAuthority("CUSTOMER")指定了使用自定义前缀的授权授予对象。

也可以使用默认的前缀ROLE_,只需要在grantedAuthorityDefaults()方法中不传入任何参数即可。

验证

前面的验证环节中验证过的,这里我就不再赘述了,直接做新的验证

验证一:使用普通用户登录验证

访问 localhost:8080/login 输入用户名user 和密码 123456 ,登录成功之后再去访问下 findAll接口,由于user并没有访问权限,所以这个地方会报错
user访问findAll
接下来,我们访问下 find 接口,由于我们有配置菜单的接口许可,所以这里可以访问(这里其实就是说明了,只要有一个条件满足就可以访问了)
user 访问 find

验证二:使用管理员账户登录验证

访问 localhost:8080/login 输入用户名admin 和密码 123456 ,登录成功之后再去访问下 findAll接口与 find 接口,都是可以访问的,这里就不贴图来说明了,大伙可以自己去试试就好

自定义界面访问

看到这里感觉真是不容易啊,我们还没有结束,继续向下看吧,界面添加的位置在这里
添加界面

添加登录界面

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<form action="/user/login" method="post">
    <div>
        <span>用户名:</span><input type="text" name="username">
    </div>

    <div>
        <span>密码:</span> <input type="password" name="password">
    </div>

    <div>
        <input type="submit" value="login"/>
    </div>
</form>
</body>
</html>

修改 Security 的配置

SecurityConfig.java 具体源码下的注释都比较清楚了,大伙就看下就好了

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //  表单登录
        http.formLogin()
                // 修改默认的登录页为login.html,他会自动去根路径static文件夹下寻找login.html
                .loginPage("/login.html")
                // 设置登录接口地址,这个接口不是真实存在的,还是用的security给我们提供的,之所以要有这个配置,是login.html当中form表单提交的地址我们设置的是这个
                .loginProcessingUrl("/user/login")
                // 登录成功之后跳转的 url
                .defaultSuccessUrl("/index")
                // 登录失败之后跳转的 url
                .failureForwardUrl("/fail")
                // permitAll中文意思是许可所有的:所有的都遵循上面的配置的意思
                .permitAll();

        //  身份认证
        http.authorizeRequests()
                // 该路由不需要身份认证
                .antMatchers("/user/login", "/login.html").permitAll()
                // 需要用户带有管理员权限
                .antMatchers("/findAll").hasRole("管理员")
                .antMatchers("/find").hasRole("管理员")
                // 需要用户具备这个接口的权限
                .antMatchers("/find").hasAuthority("menu:user")
                // 任何请求都需要认证
                .anyRequest().authenticated();
        // 关闭 csrf
        http.csrf().disable();
    }

验证

登录页面验证

访问 localhost:8080/login 时,会发现自动跳转到下面的页面中,输入用户名user 和密码 123456 是可以登录进去的
login.html
登录之后界面跳转
登录成功

自定义错误页面

添加错误页面

这里我们添加一个新页面 unauth.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>没有权限</title>
</head>
<body>
    <h1>没有权限</h1>
</body>
</html>

修改 Security 的配置

// 设置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");

添加错误配置

验证

无访问权限验证

访问 localhost:8080/login 输入用户名user 和密码 123456 ,登录成功之后再去访问下 findAll接口,由于user并没有访问权限
没有权限

自定义主页

这里我们是增加一个 home.html 方便于登录之后跳转,以及界面退出使用

添加主页

home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<dvi>
    登录成功
</dvi>

<div>
    <a href="/logout">退出</a>
</div>

</body>
</html>

修改 Security 的配置

// 退出,这里的/logout的请求是和前端的接口约定,是security给我们提供的,退出成功后跳转登录页/login.html
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();

增加配置

验证

验证跳转主页

访问 localhost:8080/login 输入用户名user 和密码 123456 ,再次点击退出就到主页了。
home页面

remember-me 功能介绍

简单点理解就是这样子的,当我们登录到系统,关闭掉网页,再次访问系统的接口是可以访问的。一般情况下,如果我们把网页关闭或者浏览器关闭了,即使是后端程序服务重启了,这个时候就需要重新登录。有这个 remember-me 功能,就可以不用重新登录了。

具体是怎么做的呢?其实这个是将我们的请求相关的参数持久化到了数据库中去了。这里我就直接开始说怎么做了,以及相关的报错,大伙了解下就好

修改 Security 配置

我们增加 remember-me 的配置

@Autowired
private DataSource dataSource;

@Autowired
private MyUserDetailsService myUserDetailsService;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    return jdbcTokenRepository;
}

// 注入 PasswordEncoder 类到 spring 容器中
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

remember-me 配置

修改Login页面

修改页面增加 remember-me 复选框,这里的 name="remember-me" 是不能修改的,可以理解为Spring Security 默认的

<input type="checkbox" name="remember-me">自动登录

创建表结构

CREATE TABLE `persistent_logins` (
	`username` VARCHAR ( 64 ) NOT NULL,
	`series` VARCHAR ( 64 ) NOT NULL,
	`token` VARCHAR ( 64 ) NOT NULL,
	`last_used` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY ( `series` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8;

如果不创建表结构会出现什么问题呢?我这边先不创建表结构,直接启动程序

org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)]; nested exception is java.sql.SQLSyntaxErrorException: Table 'study.persistent_logins' doesn't exist
    at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:235)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
    at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1443)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:633)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:862)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:917)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:927)

这里我们看下源码,我们是新增这部分的配置,然后这里出现了一个类 JdbcTokenRepositoryImpl, 并且我们还将 dataSource 传入进去了,我们就看下这个类

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    return jdbcTokenRepository;
}

JdbcTokenRepositoryImpl类
在这类中,就已经将这个表的相关操作全部都集成进去了,所以看到这里就明白了。

验证

验证 remember-me 功能

我们使用 admin账号之后,将浏览器页面关闭掉,依然可以访问到 findAll 接口。
remember-me 登录

原理分析

流程图,百度上面查到的地址在这里
百度找的图片

在登录成功之后,前端在浏览器上面写入了一部分信息到 cookies 中了
前端浏览器中的 cookies
开启 RememberMe 后,RememberMeAuthenticationFilter 过滤器就会被激活,我们可以看看这个过滤器的doFilter方法:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		// 查看是否有SecurityContextHolder中是否有认证信息
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			// 没有认证信息,因此尝试rememberMe认证,这也是核心方法
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);

			if (rememberMeAuth != null) {
				// rememberMeAuth不为null则表示自动登录成功,现在需要对key进行校验
				try {
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

					// 认证走到这一步就说明成功了,因为失败会抛异常
					// 将身份信息存储到SecurityContextHolder
					SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
					
					// 发布认证成功事件
					onSuccessfulAuthentication(request, response, rememberMeAuth);

					if (logger.isDebugEnabled()) {
						logger.debug("SecurityContextHolder populated with remember-me token: '"
								+ SecurityContextHolder.getContext().getAuthentication()
								+ "'");
					}

					// Fire event
					if (this.eventPublisher != null) {
						eventPublisher
								.publishEvent(new InteractiveAuthenticationSuccessEvent(
										SecurityContextHolder.getContext()
												.getAuthentication(), this.getClass()));
					}

					if (successHandler != null) {
						successHandler.onAuthenticationSuccess(request, response,
								rememberMeAuth);

						return;
					}

				}
				catch (AuthenticationException authenticationException) {
					if (logger.isDebugEnabled()) {
						logger.debug(
								"SecurityContextHolder not populated with remember-me token, as "
										+ "AuthenticationManager rejected Authentication returned by RememberMeServices: '"
										+ rememberMeAuth
										+ "'; invalidating remember-me token",
								authenticationException);
					}
					
					// 登录失败,使用该方法处理失败回调
					rememberMeServices.loginFail(request, response);

					// 发布登录失败事件
					onUnsuccessfulAuthentication(request, response,authenticationException);
				}
			}
			// 过滤器放行
			chain.doFilter(request, response);
		}
		else {
		    // 如果SecurityContextHolder中有认证信息,说明已经认证过了,则打印日志并直接放行
			if (logger.isDebugEnabled()) {
				logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}

			chain.doFilter(request, response);
		}
	}

从上面可以知道核心部分是在 rememberMeServices.autoLogin() ,最后我们找下具体的执行逻辑
在这里插入图片描述

注解使用

由于在实际开发中,当接口越来越多之后,如果都在Spring Security 的配置类中增加配置,那么就会越来越难维护,在 Spring Security 中也提供了注解来处理这样子的问题。这里我就介绍常用一部分,另外的大家可以去搜索下,问题不大。

Spring Security默认是禁用注解的,要想开启注解,要在继承 WebSecurityConfigurerAdapter 的类加 @EnableMethodSecurity 注解

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

prePostEnabled = true

  • @PreAuthorize 和 @PostAuthorize 两个注解会生效

securedEnabled =true

  • @Secured 会生效

另外还有一个 jsr250Enabled = true 这个是 java 提供的配置,这个就不在这里说了,其实都不算特别复杂的。

我们先回顾下之前的配置

// 需要用户带有管理员角色才可以访问/findAll接口
.antMatchers("/findAll").hasRole("管理员")
.antMatchers("/find").hasAuthority("menu:user")

@Secured注解

需要开启配置

@EnableGlobalMethodSecurity(securedEnabled=true)

具体使用如下,我们可以在 Controller 上面去增加一个注解就可以了,当然前缀 ROLE_ 不能少!!!
在这里插入图片描述

这个也可以使用在方法上面:

@Secured({"ROLE_管理员","ROLE_普通用户"})
@GetMapping("find")
@ResponseBody
public String find() {
    return "find";
}

这个就是和 .antMatchers("/findAll").hasRole("管理员") 等效

@PreAuthorize

@PreAuthorize:注解适合进入方法前的权限验证, 可以将登录用户的 roles/permissions 参数传到方法中。

需要开启配置

@EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("index")
@ResponseBody
@PreAuthorize("hasAnyAuthority('menu:system')")
public String index() {
    System.out.println("1111111111111");
    return "success";
}

这个就是和 .antMatchers("/find").hasAuthority("menu:user") 等效。

@PostAuthorize

@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值
的权限。配置都是差不多的,这里就不多说了。

@EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("index")
@ResponseBody
@PostAuthorize("hasAnyAuthority('menu:system')")
public String index() {
    System.out.println("1111111111111");
    return "success";
}

总结

看到这里了,应该是对 Spring Security 大致的用法是怎么来的就有一个清晰的了解了,在项目开发中看到上面,应该是应对一般的开发工作是没有问题了的。

另外呢,使用 SpringBoot Security + Jwt + Redis + Mybatis Plus 做为登录校验可以点击看下项目工程源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1006706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【2023集创赛】信诺达杯:基于Sinodyne ST3020的音频功放芯片LM386N-1的测试

本文为2023年第七届全国大学生集成电路创新创业大赛&#xff08;“集创赛”&#xff09;信诺达杯全国三等奖作品分享&#xff0c;参加极术社区的【有奖征集】分享你的2023集创赛作品&#xff0c;秀出作品风采&#xff0c;分享2023集创赛作品扩大影响力&#xff0c;更有丰富电子…

唯品会的两个常用API分享(商品详情和关键字搜索)

万邦vip API 接入说明 API地址:https://api-test.cn/vip/ 调用示例&#xff1a; -- 请求示例 url 默认请求参数已经URL编码处理 curl -i "https://api-服务器.cn/vip/item_get/?key<您自己的apiKey>&secret<您自己的apiSecret>&num_iid1710613157-…

YSA Toon (Anime/Toon Shader)

这是一个Toon着色器/Cel阴影着色器,用于Unity URP 此着色器的目的是使角色或物体阴影实时看起来尽可能接近真实的动画或卡通效果 可以用于游戏,渲染,插图等 着色器特性,如:面的法线平滑、轮廓修复、先进的边缘照明、镜面照明、完全平滑控制 这个文档包括所有的功能https:/…

基于SSM的健康综合咨询问诊平台设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

【算法专题突破】滑动窗口- 将 x 减到 0 的最小操作数(12)

目录 1. 题目解析 2. 算法原理 3. 代码编写 写在最后&#xff1a; 1. 题目解析 题目链接&#xff1a;1658. 将 x 减到 0 的最小操作数 - 力扣&#xff08;Leetcode&#xff09; 这道题并不难理解&#xff0c;其实就是在数组里找值&#xff0c;直到把x减成0&#xff0c; 这…

电压放大器和电荷放大器的区别是什么

电压放大器和电荷放大器一样吗&#xff1f; 电压放大器和电荷放大器不是一样的。你要明白&#xff0c;电压放大器是将输入信号的电压进行放大的装置&#xff0c;通常直接连接到信号源&#xff0c;不需要电容来耦合信号。而电荷放大器是将输入信号的电荷量进行放大的装置&#x…

更安全、更清晰、更高效——《C++ Core Guidelines解析》

由资深技术专家Rainer Grimm撰著的《C Core Guidelines解析》&#xff0c;从内容上说&#xff0c;选取了现代C语言最核心的相关规则;从篇幅上说&#xff0c;对软件工程师非常友好。以“八二原则”看&#xff0c;这个精编解析版是一-个非常聪明的选择。同时&#xff0c;Rainer G…

软件报错提示vcomp140.dll丢失怎么办?这5个修复方法可帮到你

随着科技的飞速发展&#xff0c;电脑已经成为人们日常生活和工作中不可或缺的重要工具。然而&#xff0c;在使用电脑的过程中&#xff0c;难免会遇到一些问题&#xff0c;如电脑报错 vcomp140.dll 丢失。这给许多用户带来了困扰&#xff0c;那么&#xff0c;究竟该如何解决这个…

『吴秋霖赠书活动 | 第二期』《ChatGPT原理与实战》

文章目录 1. 写在前面2. Tansformer架构模型3. ChatGPT原理4. 提示学习与大模型能力的涌现4.1 提示学习4.2 上下文学习4.3 思维链 5. 行业参考与建议5.1 拥抱变化5.2 定位清晰5.3 合规可控5.4 经验沉淀 千模大战正酣&#xff0c;吃透ChatGPT是制胜关键&#xff01; 声明&#x…

Python使用pygame设计一幅冷冷的雪落动图

文章目录 基础代码实现雪花飘落动图更换雪景背景转换GIF动图完整实现代码推荐阅读 看到很多小伙伴使用python实现了很多动态的效果&#xff0c;非常漂亮。 闲来无事&#xff0c;也参考做法&#xff0c;自己做了一幅雪落动图。过程中&#xff0c;遇到了一些问题&#xff0c;花了…

《Docker 容器化的艺术:深入理解容器技术》

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f405;&#x1f43e;猫头虎建议程序员必备技术栈一览表&#x1f4d6;&#xff1a; &#x1f6e0;️ 全栈技术 Full Stack: &#x1f4da…

Mysql免安装版的root密码是多少

免安装版的Mysql在初始化后root是没有密码的 1、下载免安装版Mysql 下载链接&#xff1a;MySQL :: Download MySQL Community Server 下载后解压 里面的目录是这样的 2、添加配置文件和系统环境 在系统变量中添加Mysql的bin的path路径 在Mysql的目录下添加my.ini配置文件 [my…

苏宁suningAPI接入说明获得suning商品详情

API地址:https://o0b.cn/anzexi 参数说明 通用参数说明 version:API版本key:调用key,测试key:test_api_keyapi_name:API类型[item_search,seller_info]cache:[yes,no]默认yes&#xff0c;将调用缓存的数据&#xff0c;速度比较快result_type:[json,xml,serialize,var_export]…

新零售商城模式与传统电商和零售的痛点的对比

新零售是一种以消费者体验为中心的数据驱动的泛零售形态&#xff0c;它通过运用大数据、人工智能等先进技术手段&#xff0c;对商品的生产、流通与销售过程进行升级改造&#xff0c;进而重塑业态结构与生态圈&#xff0c;并对线上服务、线下体验以及现代物流进行深度融合的零售…

【Linux】Linux常用命令60条(含完整命令语句)

Linux是一个强大的操作系统&#xff0c;它提供了许多常用的命令行工具&#xff0c;可以帮助我们用于管理文件、目录、进程、网络和系统配置等。以下是一些常用的Linux命令&#xff1a; 1. ls&#xff1a;列出当前目录中的文件和子目录 ls2. pwd&#xff1a;显示当前工作目录的…

什么是 BSD 协议?

BSD开源协议是一个给于使用者很大自由的协议。可以自由的使用&#xff0c;修改源代码&#xff0c;也可以将修改后的代码作为开源或者专有软件再发布。当你发布使用了BSD协议的代码&#xff0c;或者以BSD协议代码为基础做二次开发自己的产品时&#xff0c;需要满足三个条件&…

解决 SLF4J: Class path contains multiple SLF4J bindings.

1. 异常现象 启动springboot项目&#xff0c;抛出警告信息&#xff1a; SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/Users/quanll5/Documents/java_repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.j…

国内最佳的Respond替代品——SaleSmartly(ss客服)

如果响应工具最近让您失望&#xff0c;那么可能是时候开始检查一些响应替代方案以保持您的客服系统策略正常运行了&#xff01;选择正确的工具对于执行高性能的营销策略至关重要&#xff0c;该策略将为您提供最佳的投资回报率 &#xff08;ROI&#xff09;。 Respond也是一个得…

C++算法进阶系列之倍增算法解决求幂运算

1. 引言 学习倍增算法&#xff0c;先了解什么是倍增以及倍增算法的优势。如果面前有一堆石子&#xff0c;要求计算出石子的总数量。 这是一个简单的数数问题&#xff0c;可以&#xff1a; 一颗石子一颗石子的数。两颗石子两颗石子的数。三颗石子三颗石子的数。或者更多颗石子…

一志愿复录比接近1:1,计算机专业招生名额近百人,杭州师范大学考情分析

杭州师范大学 考研难度&#xff08;☆☆&#xff09; 内容&#xff1a;23考情概况&#xff08;拟录取和复试分析&#xff09;、院校概况、23初试科目、23复试详情、各专业考情分析、各科目考情分析。 正文893字预计阅读&#xff1a;3分钟 2023考情概况 杭州师范大学计算机相…