life-notes/src/note/Java/SpringSecurity/之前SpringSecurity.md

45 KiB
Raw Blame History

dir
order
2

之前SpringSecurity

官方文档:https://docs.spring.io/spring-security/reference/index.html

功能:

  • 身份认证authentication
  • 授权authorization
  • 防御常见攻击protection against common attacks

身份认证:

  • 身份认证是验证谁正在访问系统资源,判断用户是否为合法用户。认证用户的常见方式是要求用户输入用户名和密码。

授权:

  • 用户进行身份认证后,系统会控制谁能访问哪些资源,这个过程叫做授权。用户无法访问没有权限的资源。

身份认证

官方代码示例:GitHub - spring-projects/spring-security-samples

项目的基本搭建

项目搭建完成后默认端口是8080直接访问localhost:8080即可。

浏览器自动跳转到登录页面:http://localhost:8080/login

项目结构

基本包

这里用到了数据库但是在项目刚开始启动时,是没有配置数据库的,这时候启动肯定会报错,所以我们现在启动类上排出连接数据库的类。

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>3.2.0</version>
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>security-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>security-demo</name>
    <url>https://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </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-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.4.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

创建启动类

在启动类上排出数据库的类。

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

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

创建IndexController

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

@Controller
public class IndexController {
    @GetMapping("/")
    public String index() {
        return "index";
    }
}

创建index.html

这里使用的动态标签th:href="@{/logout}"目的是可以自动检测路径变化,比如我们在配置文件中配置了全局路径参数。这时候动态标签会自动匹配。

这时需要访问根路径localhost:8080/demo

server:
  servlet:
    context-path: /demo

HTML模板

<html lang="zh" xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Hello Security!</title>
</head>
<body>
<h1>Hello Security</h1>
<!--通过使用@{/logout}Thymeleaf将自动处理生成正确的URL以适应当前的上下文路径。
这样无论应用程序部署在哪个上下文路径下生成的URL都能正确地指向注销功能。-->
<a th:href="@{/logout}">Log Out</a>
</body>
</html>

比如点击下面按钮会自动匹配路径并退出。

自定义Security配置

SecurityProperties修改默认用户和密码。

spring:
  security:
    user:
      name: user
      password: admin123

使用配置类

@Configuration
@EnableWebSecurity//Spring项目总需要添加此注解SpringBoot项目中不需要
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser( //此行设置断点可以查看创建的user对象
        User
        .withDefaultPasswordEncoder()
        .username("huan") //自定义用户名
        .password("password") //自定义密码
        .roles("USER") //自定义角色
        .build()
    );
    return manager;
}

基于数据库的数据源

环境准备

创建三个数据库表并插入测试数据

-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;

-- 创建用户表
CREATE TABLE `user`(
    `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `username` VARCHAR(50) DEFAULT NULL ,
    `password` VARCHAR(500) DEFAULT NULL,
    `enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`); 

-- 插入用户数据(密码是 "abc" )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);

引入依赖

这个在之前也引用过了。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.30</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.4.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置文件中

datasource:
  type: com.zaxxer.hikari.HikariDataSource
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://${bunny.datasource.host}:${bunny.datasource.port}/${bunny.datasource.sqlData}?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true
  username: ${bunny.datasource.username}
  password: ${bunny.datasource.password}

启动类

启动类记得删除排出数据库源的类。

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

配置数据库查询

创建DBUserDetailsManager类实现UserDetailsManager, UserDetailsPasswordService方法。

查询数据库字段进行匹配,如果查询到并且密码正确就可以放行。记得在方法上加上@Configuration注解。

@Configuration
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    @Resource
    private UserMapper userMapper;

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

        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    user.getEnabled(),
                    true, // 用户账号是否过期
                    true, // 用户凭证是否过期
                    true, // 用户是否未被锁定
                    authorities); // 权限列表
        }
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }

    @Override
    public void createUser(UserDetails user) {

    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }
}

配置Security默认配置

  • formLogin(withDefaults())提供默认的登录模拟页面。
  • 如果开启了formLogin(withDefaults())可以httpBasic(withDefaults())屏蔽。
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.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity// Spring项目总需要添加此注解SpringBoot项目中不需要
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // authorizeRequests():开启授权保护
        // anyRequest():对所有请求开启授权保护
        // authenticated():已认证请求会自动被授权
        httpSecurity
                .authorizeRequests(authorize -> authorize
                        .anyRequest()
                        .authenticated())
                // .formLogin(withDefaults())// 表单授权方式
                .httpBasic(withDefaults());// 基本授权方式

        return httpSecurity.build();
    }
}

添加用户

在Controller层添加接口写入添加用户的方法之后在实现接口中添加这个方法接口。

@RestController
@RequestMapping("/user")
@Tag(name = "用户请求接口")
public class UserController {
    @Autowired
    private UserService userService;

    @Operation(summary = "添加用户")
    @PostMapping("/add")
    public void addUser(@RequestBody User user) {
        userService.addUserDetails(user);
    }
}

在实现接口中实现这个方法。

  1. 注入DBUserDetailsManager之后创建这个user。
import com.atguigu.security.config.DBUserDetailsManager;
import com.atguigu.security.entity.User;
import com.atguigu.security.mapper.UserMapper;
import com.atguigu.security.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    private DBUserDetailsManager manager;

    /**
     * 添加用户
     *
     * @param user 用户信息
     */
    @Override
    public void addUserDetails(User user) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        
        // 这里的User使用的是springSecurity中的User
        UserDetails userDetails = org.springframework.security.core.userdetails.User
                .withDefaultPasswordEncoder()
                .username(user.getUsername())
                .password(user.getPassword())
                .build();

        manager.createUser(userDetails);
    }
}

在方法中添加以下内容createUser这个方法

  1. 导入了一些需要使用的类,包括User实体类和UserMapper接口。
  2. 声明了一个UserMapper类型的字段userMapper
  3. 实现了createUser方法,该方法用于创建用户。在这个示例中,该方法将传入的UserDetails对象中的用户名和密码插入到数据库中。
  4. 实现了userExists方法,该方法用于检查用户是否存在。在这个示例中,该方法始终返回false
import com.atguigu.security.entity.User;
import com.atguigu.security.mapper.UserMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;

import java.util.ArrayList;
import java.util.Collection;

@Configuration
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    @Resource
    private UserMapper userMapper;

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

        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    user.getEnabled(),
                    true, // 用户账号是否过期
                    true, // 用户凭证是否过期
                    true, // 用户是否未被锁定
                    authorities); // 权限列表
        }
    }

    @Override
    public void createUser(UserDetails userDetails) {
        // 插入数据库方法
        User user = new User();
        user.setUsername(userDetails.getUsername());
        user.setPassword(userDetails.getPassword());
        user.setEnabled(true);
        userMapper.insert(user);
    }

    @Override
    public boolean userExists(String username) {
        return false;
    }
    // 略...
}

为了方便调试springSecurity默认开启了csrf(这要求请求参数中必须有一个隐藏的**_csrf**字段),为了测试这里就暂时关闭。

在filterChain方法中添加如下代码关闭csrf攻击防御

代码示例:

//关闭csrf攻击防御
http.csrf((csrf) -> {
 csrf.disable();
});
@Configuration
@EnableWebSecurity// Spring项目总需要添加此注解SpringBoot项目中不需要
public class WebSecurityConfig {
 @Bean
 public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
     // authorizeRequests():开启授权保护
     // anyRequest():对所有请求开启授权保护
     // authenticated():已认证请求会自动被授权
     httpSecurity
             .authorizeRequests(authorize -> authorize
                     .anyRequest()
                     .authenticated())
             .formLogin(withDefaults())// 表单授权方式
             .httpBasic(withDefaults());// 基本授权方式

     // 关闭csrf攻击
     httpSecurity.csrf(AbstractHttpConfigurer::disable);
     return httpSecurity.build();
 }
}

密码加密测试

创建测试方法:

@Slf4j
public class PasswordTest {
    @Test
    void testPassword() throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

        String password = encoder.encode("password");
        log.info("Password===>:{}", password);

        // 密码校验
        Assert.isTrue(encoder.matches("password", password), "密码不一致");
    }
}

自定义登录页面

创建登录页

第一步创建Controller

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

@Controller
public class IndexController {
    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

第二步创建HTML

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
    <title>登录</title>
</head>
<body>
<h1>登录</h1>
<div th:if="${param.error}">
    错误的用户名和密码.
</div>
<!--method必须为"post"-->
<!--th:action="@{/login}" 
使用动态参数表单中会自动生成_csrf隐藏字段用于防止csrf攻击
login: 和登录页面保持一致即可SpringSecurity自动进行登录认证-->
<form method="post" th:action="@{/login}">
    <div>
        <!--name必须为"username"-->
        <input name="username" placeholder="用户名" type="text"/>
    </div>
    <div>
        <!--name必须为"password"-->
        <input name="password" placeholder="密码" type="password"/>
    </div>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

第三步配置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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity// Spring项目总需要添加此注解SpringBoot项目中不需要
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // authorizeRequests():开启授权保护
        // anyRequest():对所有请求开启授权保护
        // authenticated():已认证请求会自动被授权
        httpSecurity
                .authorizeRequests(authorize -> authorize
                        .anyRequest()
                        .authenticated())
                .formLogin(withDefaults())// 表单授权方式
                .httpBasic(withDefaults());// 基本授权方式

        // 关闭csrf攻击
        httpSecurity.csrf(AbstractHttpConfigurer::disable);

        // 配置SecurityFilterChain-自定义登录页
        httpSecurity.formLogin(form -> {
            form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
                    .usernameParameter("username")// 自定义表单用户名参数默认是username
                    .passwordParameter("password")// 自定义表单密码参数默认是password
                    .failureUrl("/login?error"); // 登录失败的返回地址
        });

        return httpSecurity.build();
    }
}

登录页的细节

WebSecurityConfig中自定义前端传递值,默认传递用户名和密码为usernamepassword,在下面示例中可以修改为自定义的用户名和密码参数。

// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
    form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
            .usernameParameter("username")// 自定义表单用户名参数默认是username
            .passwordParameter("password")// 自定义表单密码参数默认是password
            .failureUrl("/login?error"); // 登录失败的返回地址
});

自定义前端传递参数

// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
    form.loginPage("自定义登录页").permitAll()// 登录页面无需授权即可访问
            .usernameParameter("自定义用户名")// 自定义表单用户名参数默认是username
            .passwordParameter("自定义密码")// 自定义表单密码参数默认是password
            .failureUrl("自定义错误页"); // 登录失败的返回地址
});

认证响应结果

移入fastjson

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.37</version>
</dependency>

认证成功返回

import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import java.io.IOException;

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 获取用户身份信息
        Object principal = authentication.getPrincipal();
        // 获取用户凭证信息
        // Object credentials = authentication.getCredentials();
        // 获取用户权限信息
        // Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        Result<Object> result = Result.success(principal);

        // 返回
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(JSON.toJSON(result));
    }
}

WebSecurityConfig类中添加以下.successHandler(new MyAuthenticationSuccessHandler());表示成功的返回结果,自定义结果。

如果想看Result的类翻到最下面附录。

// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
    form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
            .usernameParameter("username")// 自定义表单用户名参数默认是username
            .passwordParameter("password")// 自定义表单密码参数默认是password
            .failureUrl("/login?error") // 登录失败的返回地址
            .successHandler(new MyAuthenticationSuccessHandler());// 认证成功时的处理
});

认证失败返回

和成功的返回相似,只需要修改两个地方即可。

认证失败的类

import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import java.io.IOException;

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 错误消息
        String localizedMessage = exception.getLocalizedMessage();
        Result<String> result = Result.error(localizedMessage);

        // 转成JSON
        Object json = JSON.toJSON(result);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

在WebSecurityConfig类中添加failureHandler即可.failureHandler(new MyAuthenticationFailureHandler());

// 配置SecurityFilterChain-自定义登录页
httpSecurity.formLogin(form -> {
    form.loginPage("/login").permitAll()// 登录页面无需授权即可访问
            .usernameParameter("username")// 自定义表单用户名参数默认是username
            .passwordParameter("password")// 自定义表单密码参数默认是password
            .failureUrl("/login?error") // 登录失败的返回地址
            .successHandler(new MyAuthenticationSuccessHandler())// 认证成功时的处理
            .failureHandler(new MyAuthenticationFailureHandler());// 认证失败的处理
});

注销响应

和前面成功和失败过程相似,只需要在from中再添加即可。

import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import java.io.IOException;

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 创建结果对象
        Result<Object> result = Result.success();

        // 转为JSON
        Object json = JSON.toJSON(result);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

WebSecurityConfig类中添加以下。

// 注销响应
httpSecurity.logout(logout -> {
    logout.logoutSuccessHandler(new MyLogoutSuccessHandler());// 注销成功时的处理
});

之后访问:http://localhost/demo

请求未认证接口

import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import com.atguigu.security.result.ResultCodeEnum;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import java.io.IOException;

/**
 * 请求未认证接口
 */
@Slf4j
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 错误信息
        String localizedMessage = authException.getLocalizedMessage();
        log.error("请求未认证接口:{}", localizedMessage);

        // 创建结果对象
        Result<Object> result = Result.error(ResultCodeEnum.FAIL_NEED_LOGIN);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(JSON.toJSON(result));
    }
}

WebSecurityConfig类中添加以下。

// 请求未认证接口
httpSecurity.exceptionHandling(exception -> {
    // 请求未认证的接口
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
});

访问:http://localhost/demo/

登录后

跨域访问

WebSecurityConfig类中添加以下。

// 跨域访问权限
httpSecurity.cors(withDefaults());

获取用户认证信息

USerVo类见附录。

@RestController
public class IndexController {
    @GetMapping("/")
    public Result<UserVo> index() {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();

        // 用户名
        String username = authentication.getName();
        // 身份
        Object principal = authentication.getPrincipal();
        // 凭证(脱敏)
        Object credentials = authentication.getCredentials();
        // 权限
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        // 整理返回参数
        UserVo userVo = UserVo.builder().authorities(authorities).credentials(credentials)
                .principal(principal).username(username).build();

        return Result.success(userVo);
    }
}

会话并发处理

后登录的账号会使先登录的账号失效。实现方式和之前的差不多也是实现一个接口。

import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import com.atguigu.security.result.ResultCodeEnum;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import java.io.IOException;

public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        // 创建结果对象
        Result<Object> result = Result.error(ResultCodeEnum.LOGGED_IN_FROM_ANOTHER_DEVICE);

        // 转为JSON
        Object json = JSON.toJSON(result);

        // 返回响应
        HttpServletResponse response = event.getResponse();
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

WebSecurityConfig类中添加以下。

// 后登录的账号会使先登录的账号失效
httpSecurity.sessionManagement(session -> {
    session.maximumSessions(1)
            .expiredSessionStrategy(new MySessionInformationExpiredStrategy());
});

授权访问

授权管理的实现在SpringSecurity中非常灵活可以帮助应用程序实现以下两种常见的授权需求

  • 用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表
  • 用户-角色-权限-资源:例如 张三是角色是管理员、李四的角色是普通用户,管理员能做所有操作,普通用户只能查看信息

基于request的授权

需求:

  • 具有USER_LIST权限的用户可以访问/user/list接口
  • 具有USER_ADD权限的用户可以访问/user/add接口

WebSecurityConfig添加下面内容。

// authorizeRequests():开启授权保护
httpSecurity.authorizeRequests(authorize -> {
    // 具有USER_LIST权限的用户可以访问/user/list访问路径是Controller中的路径
    authorize.requestMatchers("/user/list").hasAuthority("USER_LIST")
            // 具有USER_ADD权限的用户可以访问/user/add
            .requestMatchers("/user/add").hasAuthority("USER_ADD")
            // 对所有请求开启授权保护
            .anyRequest()
            // 已认证请求会自动被授权
            .authenticated();
});

之后在DBUserDetailsManager中授予访问权限。

在其中注释authorities.add(() -> "USER_LIST")会发现没有权限访问。

// 授予访问权限
authorities.add(() -> "USER_LIST");
authorities.add(() -> "USER_ADD");

完整代码

import com.atguigu.security.mapper.UserMapper;
import com.atguigu.security.model.entity.User;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;

import java.util.ArrayList;
import java.util.Collection;

@Configuration
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            // 授予访问权限
            authorities.add(() -> "USER_LIST");
            authorities.add(() -> "USER_ADD");
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    user.getEnabled(),
                    true, // 用户账号是否过期
                    true, // 用户凭证是否过期
                    true, // 用户是否未被锁定
                    authorities); // 权限列表
        }
    }
}

请求未授权的接口

创建未授权访问类,返回对象。

import com.alibaba.fastjson2.JSON;
import com.atguigu.security.result.Result;
import com.atguigu.security.result.ResultCodeEnum;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import java.io.IOException;

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result<Object> result = Result.error(ResultCodeEnum.FAIL_NO_ACCESS_DENIED);

        Object json = JSON.toJSON(result);

        // 返回响应
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
    }
}

WebSecurityConfig添加下面内容。

// 请求未授权的接口
httpSecurity.exceptionHandling(exception -> {
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
    // 没有权限访问
    exception.accessDeniedHandler(new MyAccessDeniedHandler());
});

更多的例子:Method Security :: Spring Security

用户-角色-资源

**需求:**角色为ADMIN的用户才可以访问/user/**路径下的资源

配置角色

filterChain方法中配置;将之前的手动添加的路径注释。

hasRole("ADMIN")可以自定义内容。

// authorizeRequests():开启授权保护
httpSecurity.authorizeRequests(authorize -> {
    // 具有USER_LIST权限的用户可以访问/user/list访问路径是Controller中的路径
    authorize
            // 具有管理员角色的用户可以访问/user/**
            .requestMatchers("/user/**").hasRole("ADMIN")
            // .requestMatchers("/user/list").hasAuthority("USER_LIST")
            // 具有USER_ADD权限的用户可以访问/user/add
            // .requestMatchers("/user/add").hasAuthority("USER_ADD")
            // 对所有请求开启授权保护
            .anyRequest()
            // 已认证请求会自动被授权
            .authenticated();
});

配置DBUserDetailsManager改变roles("USER")

  • 如果为roles("ADMIN")表示都可以访问
  • 如果为roles("USER")表示只是普通用户。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    if (user == null) {
        throw new UsernameNotFoundException(username);
    } else {
        // Collection<GrantedAuthority> authorities = new ArrayList<>();
        // // 授予访问权限
        // authorities.add(() -> "USER_LIST");
        // authorities.add(() -> "USER_ADD");
        // return new org.springframework.security.core.userdetails.User(
        //         user.getUsername(),
        //         user.getPassword(),
        //         user.getEnabled(),
        //         true, // 用户账号是否过期
        //         true, // 用户凭证是否过期
        //         true, // 用户是否未被锁定
        //         authorities); // 权限列表

        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .disabled(!user.getEnabled())
                .credentialsExpired(false)
                .accountLocked(false)
                // SecurityFilterChain中配置hasRole("ADMIN")来判断的
                .roles("USER")
                .build();
    }
}

roles("ADMIN")时全部可以访问。

为普通用户时。

RBAC设置权限表方式

RBACRole-Based Access Control基于角色的访问控制是一种常用的数据库设计方案它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计方案的示例

  1. 用户表User table包含用户的基本信息例如用户名、密码和其他身份验证信息。
列名 数据类型 描述
user_id int 用户ID
username varchar 用户名
password varchar 密码
email varchar 电子邮件地址
... ... ...
  1. 角色表Role table存储所有可能的角色及其描述。
列名 数据类型 描述
role_id int 角色ID
role_name varchar 角色名称
description varchar 角色描述
... ... ...
  1. 权限表Permission table定义系统中所有可能的权限。
列名 数据类型 描述
permission_id int 权限ID
permission_name varchar 权限名称
description varchar 权限描述
... ... ...
  1. 用户角色关联表User-Role table将用户与角色关联起来。
列名 数据类型 描述
user_role_id int 用户角色关联ID
user_id int 用户ID
role_id int 角色ID
... ... ...
  1. 角色权限关联表Role-Permission table将角色与权限关联起来。
列名 数据类型 描述
role_permission_id int 角色权限关联ID
role_id int 角色ID
permission_id int 权限ID
... ... ...

在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。

当用户尝试访问系统资源时,系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护,因为只需调整角色和权限的分配即可,而不需要针对每个用户进行单独的设置。

CREATE TABLE admin_power (
 id INT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID',
 power_name VARCHAR(50) NOT NULL COMMENT '权限名称',
 power_code VARCHAR(255) NOT NULL COMMENT '权限编码',
 description VARCHAR(100) COMMENT '描述',
 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 update_user VARCHAR(50) COMMENT '更新用户',
 is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除0-未删除1-已删除'
);

CREATE TABLE admin_user_role (
 id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
 user_id VARCHAR(50) NOT NULL COMMENT '用户id',
 role_id VARCHAR(255) NOT NULL COMMENT '角色id',
 description VARCHAR(100) COMMENT '描述',
 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 update_user VARCHAR(50) COMMENT '更新用户',
 is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除0-未删除1-已删除'
);

CREATE TABLE admin_role_power (
 id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
 role_id VARCHAR(50) NOT NULL COMMENT '角色id',
 power_id VARCHAR(255) NOT NULL COMMENT '权限id',
 description VARCHAR(100) COMMENT '描述',
 create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 update_user VARCHAR(50) COMMENT '更新用户',
 is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除0-未删除1-已删除'
);

基于方法的授权

开启方法授权

在配置文件中添加如下注解,或者在启动类上添加都可以。

  • 默认如果开启了方法授权访问同时也配置了全局角色为roles("ADMIN")那么所有接口都是可以访问的。
@EnableMethodSecurity

给用户授予角色和权限

DBUserDetailsManager中的loadUserByUsername方法添加authorities("USER_ADD", "USER_UPDATE")

  • roles("ADMIN")与authorities不能同时使用
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    if (user == null) {
        throw new UsernameNotFoundException(username);
    } else {
        // Collection<GrantedAuthority> authorities = new ArrayList<>();
        // // 授予访问权限
        // authorities.add(() -> "USER_LIST");
        // authorities.add(() -> "USER_ADD");
        // return new org.springframework.security.core.userdetails.User(
        //         user.getUsername(),
        //         user.getPassword(),
        //         user.getEnabled(),
        //         true, // 用户账号是否过期
        //         true, // 用户凭证是否过期
        //         true, // 用户是否未被锁定
        //         authorities); // 权限列表

        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .disabled(!user.getEnabled())
                .credentialsExpired(false)
                .accountLocked(false)
                // SecurityFilterChain中配置hasRole("ADMIN")来判断的
                // .roles("ADMIN")// 与authorities不能同时使用
                // 给用户授予角色和权限
                .authorities("USER_ADD", "USER_UPDATE")
                .build();
    }
}

常用授权注解

  • hasAnyRole('ADMIN')当前角色权限为ADMIN同时访问用户也要为authentication.name == 'admin'
@RestController
@RequestMapping("/user")
@Tag(name = "用户请求接口")
public class UserController {
    @Autowired
    private UserService userService;

    @Operation(summary = "查询所有用户")
    // @PreAuthorize("hasAnyRole('ADMIN')")
    @PreAuthorize("hasAnyRole('ADMIN') and authentication.name == 'admin'")// 编写逻辑表达式
    @GetMapping("/list")
    public List<User> getList() {
        return userService.list();
    }

    // 用户必须有 USER_ADD 权限 才能访问此方法
    @Operation(summary = "添加用户")
    @PreAuthorize("hasAuthority('USER_ADD')")
    @PostMapping("/add")
    public void addUser(@RequestBody User user) {
        userService.addUserDetails(user);
    }
}

如果当前用户不是admin用户访问不了。