Bootstrap

“全面揭秘:如何利用SpringSecurity和OAuth2构建坚不可摧的权限管理系统——从基础到高级实践的完整指南”

🎼个人主页:【Y小夜】

😎作者简介:一位双非学校的大三学生,编程爱好者,

专注于基础和实战分享,欢迎私信咨询!

🎆入门专栏:🎇【MySQLJava基础Rust

🎈热门专栏:🎊【PythonJavawebSpringboot】 

感谢您的点赞、关注、评论、收藏、是对我最大的认可和支持!❤️

目录

🎄Spring Security

🎏基本介绍

🎏入门身份验证案例

🍔注意事项

🍟@{logout}的作用

🍟页面样式无法加载的问题

🎏默认提供什么?

🎏底层原理

🍔DelegatingFilterProxy(委托过滤器代理)

🍔FilterChainProxy (过滤器链代理)

🍔SecurityFilterChain

🍔Multiple SecurityFilterChain

🎏启动与运行

🍔DefaultSecurityFilterChain

🍔SecurityProperties

🎏基于内存的用户认证

🍔创建自定义配置

🍔底层原理

🎏基于数据库的用户认证

🍔 执行Sql文件

🍔 引入依赖

🍔 配置数据源

🍔实体类

🍔Mapper

🍔Service

🍔Controller

🍔测试

🎏基于数据库的用户认证

🍔认证流程

🍔定义DBUserDetailsManager

🎏SpringSecurity的默认配置

🎏添加用户功能

🍔Controller

🍔Service

🍔修改配置

🍔使用Swagger测试

🍔关闭csrf攻击防御

🎏密码加密算法

🍔一些加密的方式

🍔Hash算法

🍔彩虹表

🍔加盐密码

🍔自适应单向函数

🎏密码加密测试

🎏DelegatingPasswordEncoder

🎏自定义登录页面

🍔创建登录Controller

🍔登录页面

🍔配置SecurityFilterChain

🎏用户认证流程(前后端分离)

🎏引入fastjson

🎏认证成功的响应

🍔成功结果处理

🍔SecurityFilterChain配置

🎏认证失败响应

🍔失败结果处理

🍔SecurityFilterChain配置

🎏注销响应

🍔注销结果处理

🍔SecurityFilterChain配置

​🎏请求未认证的接口

🍔SecurityFilterChain配置

🎏跨域

🎏用户认证信息

🍔在Controller中获取用户信息

🎏会话并发处理

🍔实现处理器接口

🍔SecurityFilterChain配置

🎏基于request的授权

🍔用户-权限-资源

🍔用户-角色-资源

🍔用户-角色-权限-资源

🎏基于方法的授权

🍔开启方法授权

🍔给用户授予角色和权限

🍔常用授权注解

🎄OAuth2

🎏简介

🍔是什么OAuth2最简向导:

🍔角色

🍔使用场景

🍟开放系统间授权

🍟现代微服务安全

🍟企业内部应用认证授权

🍔授权模式

🍟授权码

🍟隐藏式

🍟密码式

🍟凭证式

🍔授权类型的选择

🎏Spring中的OAuth2

🍔Spring中的实现

🎏相关依赖

🎏实战

🍔创建应用

🍔创建项目

🍔配置OAuth客户端属性

🍔创建Controller

🍔创建html页面

🍔启动应用程序


🎄Spring Security

🎏基本介绍

这里咱们先看官网:Spring Security :: Spring Security

这句话大概是说:Spring Security 是一个框架,它可以提供:

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

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

  • 防御常见攻击(protection against common attacks):CSRF、HTTP Headers、HTTP Requests

🎏入门身份验证案例

咱们先去它的仓库中看一下:spring-projects/spring-security-samples

这里我使用的是用传统的servlet实现的实例,在这个目录下,可以找到,所给的实例代码

a

接下来创建一个springboot工程,并选择者三个依赖

创建一个controller包,然后创建一个IndexController类

package com.yan.securitydemo.controller;

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

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

然后再资源目录下,创建一个index.html页面

<html xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Hello Security!</title>
</head>
<body>
<h1>Hello Security</h1>
<a th:href="@{/logout}">Log Out</a>
</body>
</html>

直接运行文件,这里没有配置路径,默认访问8080端口,当我们访问8080端口时,会弹出 登录页,这是springsecurity自带的安全验证,用户名为:user,密码在控制台上自动输出。

然后我们进行登录,然后点击Log out,进行退出

🍔注意事项

🍟@{logout}的作用

通过使用@{logout},Thymeleaf将自动处理生成正确的URL,以适应当前上下文路径。这样无论应用程序部署在哪个上下文路径下,生成的URL下,生成的URl都是正确的指向注销功能。

例如:如果我们在配置文件中添加如下内容

server.servlet.context-path=/demo

那么@{/logout}可以自动处理url为正确的相对路径

但是如果是普通的/logout,路径就会不正确

🍟页面样式无法加载的问题

其实页面并不是上面那个样子,而是有一个css样式,页面样式bootstrap.min.css是一个CDN地址,需要通过科学上网的方式访问

否则你的登录页会加载很久,并且看到的页面是上面那样的,这里其实需要网络代理进行访问(俗称梯子),关于怎么搞,自己去找教程,这里不多介绍,开启后,就会出现下面的样式(登录按钮没有样式文件渲染,但是不影响登录功能的执行)

🎏默认提供什么?

  • 保护应用程序URL,要求对应用程序的任何交互进行身份验证

  • 程序启动时生成一个默认用户“user”。

  • 生成一个默认的随机密码,并将此密码记录在控制台上。

  • 生成默认的登录表单和注销页面。

  • 提供基于表单的登录和注销流程。

  • 对于Web请求,重定向到登录页面;

  • 对于服务请求,返回401未经授权。

  • 处理跨站请求伪造(CSRF)攻击。

  • 处理会话劫持攻击。

  • 写入Strict-Transport-Security以确保HTTPS。

  • 写入X-Content-Type-Options以处理嗅探攻击。

  • 写入Cache Control头来保护经过身份验证的资源。

  • 写入X-Frame-Options以处理点击劫持攻击。

🎏底层原理

底层原理,可以先去看官网:

Architecture :: Spring Security

Spring Security之所以默认帮助我们做了那么多事情,它的底层原理是传统的Servlet过滤器

因为我们主要是使用的Springboot,SpringMVC、Spring的方式使用的,所以我们渴望将这些Filter对象转化为Bean对象进行管理。

🍔DelegatingFilterProxy(委托过滤器代理)

概念:DelegatingFilterProxy 是 Spring Security 提供的一个 Filter 实现,可以在 Servlet 容器和 Spring 容器之间建立桥梁。通过使用 DelegatingFilterProxy,这样就可以将Servlet容器中的 Filter 实例放在 Spring 容器中管理。

🎏

🍔FilterChainProxy (过滤器链代理)

概念:复杂的业务中不可能只有一个过滤器。因此FilterChainProxy是Spring Security提供的一个特殊的Filter,它允许通过SecurityFilterChain将过滤器的工作委托给多个Bean Filter实例。

🍔SecurityFilterChain

概念:SecurityFilterChain 被 FilterChainProxy 使用,负责查找当前的请求需要执行的Security Filter列表。

🍔Multiple SecurityFilterChain

概念:可以有多个SecurityFilterChain的配置,FilterChainProxy决定使用哪个SecurityFilterChain。如果请求的URL是/api/messages/,它首先匹配SecurityFilterChain0的模式/api/**,因此只调用SecurityFilterChain 0。假设没有其他SecurityFilterChain实例匹配,那么将调用SecurityFilterChain n。

🎏启动与运行

🍔DefaultSecurityFilterChain

其实Java有默认的securityFilterChain,我们现在来看一下:

双击shift,输入DefaultSecurityFilterChain,进行搜索,然后可以找到源码

当我们debug时,可以看到 这里面包含十五种过滤器,

🍔SecurityProperties

        默认情况下Spring Security将初始的用户名和密码存在了SecurityProperties类中。这个类中有一个静态内部类User,配置了默认的用户名(name = "user")和密码(password = uuid)

可以去看一下源码:

这个是可以自己修改密码的,SpringBoot的配置文件中:在application.properties中配置自定义用户名和密码:

spring.security.user.name=用户名
spring.security.user.password=密码

🎏基于内存的用户认证

🍔创建自定义配置

老样子,先看官方文档:Java Configuration :: Spring Security

首先创建一个配置类

package com.yan.securitydemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@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;
    }
}

 UserDetailsService用来管理用户信息,InMemoryUserDetailsManager是UserDetailsService的一个实现,用来管理基于内存的用户信息。

咱们可以看一下UserDetailService接口的源码(选中接口源码,然后点ctrl+h)

然后这里用户名和密码已经改了,自己可以去测试一下。

🍔底层原理

  • 程序启动时:

    • 创建InMemoryUserDetailsManager对象

    • 创建User对象,封装用户名密码

    • 使用InMemoryUserDetailsManager将User存入内存

  • 校验用户时:

    • SpringSecurity自动使用InMemoryUserDetailsManagerloadUserByUsername方法从内存中获取User对象

    • UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从内存中获取到的用户信息进行比较,进行用户认证

🎏基于数据库的用户认证

🍔 执行Sql文件

-- 创建数据库
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>

🍔 配置数据源

#MySQL数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security-demo
spring.datasource.username=root
spring.datasource.password=123456
#SQL日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

🍔实体类

@Data
public class User {

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

    private String username;

    private String password;

    private Boolean enabled;

}

这里设置id字段为自增字段。 

🍔Mapper

接口

package com.atguigu.securitydemo.mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

xml

resources/mapper/UserMapper.xml

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

</mapper>

🍔Service

接口

package com.atguigu.securitydemo.service;
​
public interface UserService extends IService<User> {
}

实现

package com.atguigu.securitydemo.service.impl;
​
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

🍔Controller

package com.atguigu.securitydemo.controller;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    public UserService userService;

    @GetMapping("/list")
    public List<User> getList(){
        return userService.list();
    }
}

🍔测试

然后让我们开始测试: 

这里不出意外的话要出意外了。其实按这样写,会出现一个:org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'ddlApplicationRunner' is expected to be of type 'org.springframework.boot.Runner' but was actually of type 'org.springframework.beans.factory.support.NullBean'的错误。错误原因是因为MyBatis plus和Springboot3的版本不兼容。这里我们将MyBatis plus的依赖修改一下

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.7</version>
        </dependency>

接下来再次测试,输入:localhost:8080/user/list,输入用户名和密码,这里使用的是上面自定义配置里面的用户名和密码,登入后就可以看到信息了。

🎏基于数据库的用户认证

🍔认证流程

  • 程序启动时:

    • 创建DBUserDetailsManager类,实现接口 UserDetailsManager, UserDetailsPasswordService

    • 在应用程序中初始化这个类的对象

  • 校验用户时:

    • SpringSecurity自动使用DBUserDetailsManagerloadUserByUsername方法从数据库中获取User对象

    • UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证

🍔定义DBUserDetailsManager

这里可以自己仿照着 InMemoryUserDetailsManager 这个类比葫芦画瓢。这里就不多将了,我把主要方法发给出来

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); //权限列表
        }

🍔初始化配置类

修改WebSecurityConfig中的userDetailsService方法如下

@Bean
public UserDetailsService userDetailsService() {
    DBUserDetailsManager manager = new DBUserDetailsManager();
    return manager;
}

或者直接在DBUserDetailsManager类上添加@Component注解

然后启动程序,用户名和密码都是数据库中的。

🎏SpringSecurity的默认配置

其实如果我们什么都不配置的话,其实会有一个默认的配置

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    //authorizeRequests():开启授权保护
    //anyRequest():对所有请求开启授权保护
    //authenticated():已认证请求会自动被授权
    http
        .authorizeRequests(authorize -> authorize.anyRequest().authenticated())
        .formLogin(withDefaults())//表单授权方式
        .httpBasic(withDefaults());//基本授权方式

    return http.build();
}

🎏添加用户功能

这里现在controller中写入添加用户的方法

🍔Controller

@PostMapping("/add")
public void add(@RequestBody User user){
    userService.saveUserDetails(user);
}

🍔Service

void saveUserDetails(User user);

UserServiceImpl实现中添加方法

@Resource
private DBUserDetailsManager dbUserDetailsManager;

@Override
public void saveUserDetails(User user) {

    UserDetails userDetails = org.springframework.security.core.userdetails.User
            .withDefaultPasswordEncoder()
            .username(user.getUsername()) //自定义用户名
            .password(user.getPassword()) //自定义密码
            .build();
    dbUserDetailsManager.createUser(userDetails);

}

🍔修改配置

DBUserDetailsManager中添加方法

@Override
public void createUser(UserDetails userDetails) {

    User user = new User();
    user.setUsername(userDetails.getUsername());
    user.setPassword(userDetails.getPassword());
    user.setEnabled(true);
    userMapper.insert(user);
}

🍔使用Swagger测试

<!--swagger测试-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.1.0</version>
</dependency>

登录后访问:http://localhost:8080/doc.html,找到添加的方法,用户数据添加

{
  "username": "wangwu",
  "password": "password"
}

不过这里会报一个403错误,意思是没有权限访问

这是因为Springsecurity 默认开启 csrf攻击防御功能的。

🍔关闭csrf攻击防御

//关闭csrf攻击防御
http.csrf((csrf) -> {
    csrf.disable();
});

 重试上面的操作添加成功了!

🎏密码加密算法

老样子,请先去看官方文档:Password Storage :: Spring Security

🍔一些加密的方式

明文密码:这种方式最害怕的是用户SQL注入。

        最初,密码以明文形式存储在数据库中。但是恶意用户可能会通过SQL注入等手段获取到明文密码,或者程序员将数据库数据泄露的情况也可能发生。

🍔Hash算法

        Spring Security的PasswordEncoder接口用于对密码进行单向转换,从而将密码安全地存储。对密码单向转换需要用到哈希算法,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密

        因此,数据库中存储的是单向转换后的密码,Spring Security在进行用户身份验证时需要将用户输入的密码进行单向转换,然后与数据库的密码进行比较。

        因此,如果发生数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并且在给定哈希的情况下只能通过暴力破解的方式猜测密码

🍔彩虹表

恶意用户创建称为彩虹表的查找表。

彩虹表就是一个庞大的、针对各种可能的字母组合预先生成的哈希值集合,有了它可以快速破解各类密码。越是复杂的密码,需要的彩虹表就越大,主流的彩虹表都是100G以上,目前主要的算法有LM, NTLM, MD5, SHA1, MYSQLSHA1, HALFLMCHALL, NTLMCHALL, ORACLE-SYSTEM, MD5-HALF。

这样的人太坏了,希望大家不要这样做,遵守网络环境规章制度。

🍔加盐密码

        为了减轻彩虹表的效果,开发人员开始使用加盐密码。不再只使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。盐和用户的密码将一起经过哈希函数运算,生成一个唯一的哈希。盐将以明文形式与用户的密码一起存储。然后,当用户尝试进行身份验证时,盐和用户输入的密码一起经过哈希函数运算,再与存储的密码进行比较。唯一的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希都是不同的。

🍔自适应单向函数

emmm,因为随着发展加盐也不再特别安全,就衍生出自适应单项函数。

        随着硬件的不断发展,加盐哈希也不再安全。原因是,计算机可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地破解每个密码。

        现在,开发人员开始使用自适应单向函数来存储密码。使用自适应单向函数验证密码时,故意占用资源(故意使用大量的CPU、内存或其他资源)自适应单向函数允许配置一个“工作因子”,随着硬件的改进而增加。我们建议将“工作因子”调整到系统中验证密码需要约一秒钟的时间。这种权衡是为了让攻击者难以破解密码

        自适应单向函数包括bcrypt(Springsecurity默认使用)、PBKDF2、scrypt和argon2

🎏密码加密测试

听我讲了这么多关于密码加密的理论知识,你是不是也迫不及待大展身手了。让我们一起来做一个测试吧。

测试函数

@Test
void testPassword() {

    // 工作因子,默认值是10,最小值是4,最大值是31,值越大运算速度越慢
    PasswordEncoder encoder = new BCryptPasswordEncoder(4);
    //明文:"password"
    //密文:result,即使明文密码相同,每次生成的密文也不一致
    String result = encoder.encode("password");
    System.out.println(result);

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

注意代码中的工作因子,值越大运行速度越慢。

        你会发现,运行两次,生成的密码也是不一样的。但是这里不要担心用户进行验证的时候,和数据库中的密码不同,生成的时候用的是随机salt,校验时用的存储的明文salt和加密时的一致,所以校验不会有问题。

🎏DelegatingPasswordEncoder

当我们对比上面生成的密码和数据库中的密码时,会很容易的发现不同:我们生成的密码没有前缀,而数据库中的密码有前缀。原因如下:

我们在使用默认的用户时候,使用的是 withDefaultPasswordEncoder 

目的:方便随时做密码策略的升级,兼容数据库中的老版本密码策略生成的密码

里面的 PasswordEncoderFactories方法对所有可能得密码加密算法放在了map中

🎏自定义登录页面

我相信你也这个想法很久了,就是默认的登录页太丑了!!!,早就想换掉了,那正好,现在我们就来实现一下自定义登录页面,你也可以比葫芦画瓢,设计出自己满意的登录页。

🍔创建登录Controller

        有人可能会问,这里为什么用Controller,而不是RestController,因为配置了tf肯定用普通的Controller,又不是前后端分离的,RestController返回的是json。

@Controller
public class LoginController {

    @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 th:action="@{/login}" method="post">
    <div>
        <!--name必须为"username"-->
        <input type="text" name="username" placeholder="用户名"/>
    </div>
    <div>
        <!--name必须为"password"-->
        <input type="password" name="password" placeholder="密码"/>
    </div>
    <input type="submit" value="登录" />
</form>
</body>
</html>

        现在我们进行登录,当你输入登录地址后会发现,访问的还是默认的登录页面,而不是我们自定义的,其实我们需要自己配置一下SecurityFilterChain

🍔配置SecurityFilterChain

将配置文件中的表单登录方式修改一下

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

我们可以看到,可以使用自己设置了登录页了

🎏用户认证流程(前后端分离)

  • 登录成功后调用:AuthenticationSuccessHandler

  • 登录失败后调用:AuthenticationFailureHandler

所以我们这里最重要的是重写这两个方法

🎏引入fastjson

因为在这两个类中要返回json数据,所以需要引入fastjson

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

🎏认证成功的响应

🍔成功结果处理

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //获取用户身份信息
        Object principal = authentication.getPrincipal();

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", 0);
        result.put("message", "登录成功");
        result.put("data", principal);

        //转换成json字符串
        String json = JSON.toJSONString(result);

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

🍔SecurityFilterChain配置

form.successHandler(new MyAuthenticationSuccessHandler()) //认证成功时的处理

然后进行测试,登录成功后,得到json数据

🎏认证失败响应

🍔失败结果处理

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        //获取错误信息
        String localizedMessage = exception.getLocalizedMessage();

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", -1);
        result.put("message", localizedMessage);

        //转换成json字符串
        String json = JSON.toJSONString(result);

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

🍔SecurityFilterChain配置

form.failureHandler(new MyAuthenticationFailureHandler()) //认证失败时的处理

然后进行登录测试

🎏注销响应

🍔注销结果处理

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", 0);
        result.put("message", "注销成功");

        //转换成json字符串
        String json = JSON.toJSONString(result);

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

🍔SecurityFilterChain配置

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

然后进行测试

🎏请求未认证的接口

官网:docs.spring.io

        当访问一个需要认证之后才能访问的接口的时候,Spring Security会使用AuthenticationEntryPoint将用户请求跳转到登录页面,要求用户提供登录凭证。

        这里我们也希望系统返回json结果,因此我们定义类实现AuthenticationEntryPoint接口

🍔实现AuthenticationEntryPoint接口

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        //获取错误信息
        //String localizedMessage = authException.getLocalizedMessage();

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", -1);
        result.put("message", "需要登录");

        //转换成json字符串
        String json = JSON.toJSONString(result);

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

🍔SecurityFilterChain配置

//错误处理
http.exceptionHandling(exception  -> {
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());//请求未认证的接口
});

然后进行测试 

🎏跨域

        跨域全称是跨域资源共享(Cross-Origin Resources Sharing,CORS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。

比如我们前端和后端在不同的服务器上面,ip就可能不同,这就可能导致跨域。前端一般发起的ajax请求,这个如果解决很简单,添加如下配置即可:

//跨域
http.cors(withDefaults());

🎏用户认证信息

  1. SecurityContextHolder:SecurityContextHolder 是 Spring Security 存储已认证用户详细信息的地方。

  2. SecurityContext:SecurityContext 是从 SecurityContextHolder 获取的内容,包含当前已认证用户的 Authentication 信息。

  3. Authentication:Authentication 表示用户的身份认证信息。它包含了用户的Principal、Credential和Authority信息。

  4. Principal:表示用户的身份标识。它通常是一个表示用户的实体对象,例如用户名。Principal可以通过Authentication对象的getPrincipal()方法获取。

  5. Credentials:表示用户的凭证信息,例如密码、证书或其他认证凭据。Credential可以通过Authentication对象的getCredentials()方法获取。

  6. GrantedAuthority:表示用户被授予的权限

        总结起来,SecurityContextHolder用于管理当前线程的安全上下文,存储已认证用户的详细信息,其中包含了SecurityContext对象,该对象包含了Authentication对象,后者表示用户的身份验证信息,包括Principal(用户的身份标识)和Credential(用户的凭证信息)。

🍔在Controller中获取用户信息

        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();//权限

        System.out.println(username);
        System.out.println(principal);
        System.out.println(credentials);
        System.out.println(authorities);

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", 0);
        result.put("data", username);

        return result;

进行测试,登入后,首页面显示符合预期, 

🎏会话并发处理

后登录的账号会使先登录的账号失效

🍔实现处理器接口

public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", -1);
        result.put("message", "该账号已从其他设备登录");

        //转换成json字符串
        String json = JSON.toJSONString(result);

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

🍔SecurityFilterChain配置

//会话管理
http.sessionManagement(session -> {
    session
        .maximumSessions(1)
        .expiredSessionStrategy(new MySessionInformationExpiredStrategy());
});

进行测试

现在IE浏览器上进行登录

然后再谷歌浏览器上登录

然后在IE上访问主页面,会发现提示已经在其他设备登录

🎏基于request的授权

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

  • 用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表

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

🍔用户-权限-资源

需求:

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

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

配置权限

SecurityFilterChain

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

授予权限

DBUserDetailsManager中的loadUserByUsername方法:

Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(()->"USER_LIST");
authorities.add(()->"USER_ADD");

/*authorities.add(new GrantedAuthority() {
    @Override
    public String getAuthority() {
        return "USER_LIST";
    }
});
authorities.add(new GrantedAuthority() {
    @Override
    public String getAuthority() {
        return "USER_ADD";
    }
});*/

然后咱们去测试一下:

  然后可以看到:已经授予的权限

如果是访问未授权的接口时,会出现403错误

这个可以返回json格式

请求未授权的接口

SecurityFilterChain

//错误处理
http.exceptionHandling(exception  -> {
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());//请求未认证的接口
    exception.accessDeniedHandler((request, response, e)->{ //请求未授权的接口

        //创建结果对象
        HashMap result = new HashMap();
        result.put("code", -1);
        result.put("message", "没有权限");

        //转换成json字符串
        String json = JSON.toJSONString(result);

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

当我们设置好后,进行访问没有授权的地址:

其他例子这里不过多赘述了,请去看官网:

Authorize HttpServletRequests :: Spring Security


🍔用户-角色-资源

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

配置角色

SecurityFilterChain

这里需要把上面机遇权限的操作,给注释掉,再添加如下代码:

                    //具有管理员角色的用户可以访问/user/**
                    .requestMatchers("/user/**").hasRole("ADMIN")

授予角色

DBUserDetailsManager中的loadUserByUsername方法:

这里也是需要将上面创建对象的方法注释掉,在进行添加

return org.springframework.security.core.userdetails.User
        .withUsername(user.getUsername())
        .password(user.getPassword())
        .roles("ADMIN")
        .build();

然后进行测试访问:

🍔用户-角色-权限-资源

RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。

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

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

🎏基于方法的授权

🍔开启方法授权

在配置文件中添加如下注解

@EnableMethodSecurity

然后记得将授权配置的注释掉

🍔给用户授予角色和权限

DBUserDetailsManager中的loadUserByUsername方法:

这里注意其实,权限和角色、角色和角色之间、权限和权限之间会相互覆盖,后面的会把前面的给覆盖到。

return org.springframework.security.core.userdetails.User
        .withUsername(user.getUsername())
        .password(user.getPassword())
        .roles("ADMIN")
        .authorities("USER_ADD", "USER_UPDATE")
        .build();

🍔常用授权注解

//用户必须有 ADMIN 角色 并且 用户名是 admin 才能访问此方法
@PreAuthorize("hasRole('ADMIN') and authentication.name == 'admim'")
@GetMapping("/list")
public List<User> getList(){
    return userService.list();
}

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

然后进行测试:

登录后:

访问/user/list接口:

其他例子可以看官网:Method Security :: Spring Security

🎄OAuth2

🎏简介

🍔是什么OAuth2最简向导:

https://darutk.medium.com/the-simplest-guide-to-oauth-2-0-8c71bd9a15bb

“Auth” 表示 “授权” Authorization

“O” 是 Open 的简称,表示 “开放”

连在一起就表示 “开放授权”,OAuth2是一种开放授权协议。

🍔角色

OAuth 2协议包含以下角色:

  1. 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。

  2. 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。

  3. 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。

  4. 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。

🍔使用场景

🍟开放系统间授权

社交登录

        在传统的身份验证中,用户需要提供用户名和密码,还有很多网站登录时,允许使用第三方网站的身份,这称为"第三方登录"。所谓第三方登录,实质就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。

开放API

🍟现代微服务安全

单块应用安全

微服务安全

🍟企业内部应用认证授权
  • SSO:Single Sign On 单点登录

  • IAM:Identity and Access Management 身份识别与访问管理

🍔授权模式

这里有两个相关的解释文档:

RFC6749:

RFC 6749 - The OAuth 2.0 Authorization Framework (ietf.org)

但考虑到友友们的英语以及理解能力,又找到了另一篇中文的文档。

阮一峰:

OAuth 2.0 的四种方式 - 阮一峰的网络日志 (ruanyifeng.com)

🍟授权码

授权码(authorization code),指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

        这种方式是最常用,最复杂,也是最安全的,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

🍟隐藏式

隐藏式(implicit),也叫简化模式,有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端

        RFC 6749 规定了这种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为隐藏式。这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

🍟密码式

密码式(Resource Owner Password Credentials):如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌。

        这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

🍟凭证式

        凭证式(client credentials):也叫客户端模式,适用于没有前端的命令行应用,即在命令行下请求令牌。 (机器对机器)

        这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

🍔授权类型的选择

🎏Spring中的OAuth2

🍔Spring中的实现

老样子先去看看官网:OAuth2 :: Spring Security

Spring Security

  • 客户应用(OAuth2 Client):OAuth2客户端功能中包含OAuth2 Login

  • 资源服务器(OAuth2 Resource Server)

Spring

  • 授权服务器(Spring Authorization Server):它是在Spring Security之上的一个单独的项目。

🎏相关依赖

<!-- 资源服务器 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- 客户应用 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<!-- 授权服务器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

🎏实战

听我讲了这么多关于OAuth2的相关知识,是不是想要跃跃欲试了,那我们现在就做一个GitHub社交登录实战案例吧!

🍔创建应用

首先要进行客户应用的注册:

登录GitHub,在开发者设置中找到OAuth Apps,创建一个application,为客户应用创建访问GitHub的凭据:

然后进行注册:

填写应用信息:默认的重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId}。registrationId是ClientRegistration的唯一标识符。

获取应用程序id,生成应用程序密钥:

🍔创建项目

创建一个springboot项目oauth2-login-demo,创建时引入如下依赖

github上的代码例子:spring-security-samples/servlet/spring-boot/java/oauth2/login at 6.2.x · spring-projects/spring-security-samples

🍔配置OAuth客户端属性

这里记得修改成自己的

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: 7807cc3bb1534abce9f2
            client-secret: 008dc141879134433f4db7f62b693c4a5361771b
#            redirectUri: http://localhost:8200/login/oauth2/code/github

🍔创建Controller

这里返回“index”,是返回的视图解析器的名称

package com.yan.oauthlogindemo.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(
            Model model,
            @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
            @AuthenticationPrincipal OAuth2User oauth2User) {
        model.addAttribute("userName", oauth2User.getName());
        model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
        model.addAttribute("userAttributes", oauth2User.getAttributes());
        return "index";
    }
}

🍔创建html页面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <title>Spring Security - OAuth 2.0 Login</title>
    <meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
    <div style="float:left">
        <span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
    </div>
    <div style="float:none">&nbsp;</div>
    <div style="float:right">
        <form action="#" th:action="@{/logout}" method="post">
            <input type="submit" value="Logout" />
        </form>
    </div>
</div>
<h1>OAuth 2.0 Login with Spring Security</h1>
<div>
    You are successfully logged in <span style="font-weight:bold" th:text="${userName}"></span>
    via the OAuth 2.0 Client <span style="font-weight:bold" th:text="${clientName}"></span>
</div>
<div>&nbsp;</div>
<div>
    <span style="font-weight:bold">User Attributes:</span>
    <ul>
        <li th:each="userAttribute : ${userAttributes}">
            <span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
        </li>
    </ul>
</div>
</body>
</html>

🍔启动应用程序

访问localhost:8080,会被重定向到登录页面:

然后授权登录就行了,OAuth客户端访问GitHub的获取用户信息的接口获取基本个人资料信息,并建立一个已认证的会话。

进行等出后,回到一开始的页面

知识普及:

CommonOAuth2Provider是一个预定义的通用OAuth2Provider,为一些知名资源服务API提供商(如Google、GitHub、Facebook)预定义了一组默认的属性。

例如,授权URI、令牌URI和用户信息URI通常不经常变化。因此,提供默认值以减少所需的配置。

因此,当我们配置GitHub客户端时,只需要提供client-id和client-secret属性。

好了,到这里SpringSecurity+OAuth2的内容就介绍完毕了,如果打击觉得我的产出对你有所帮助,欢迎大家“点赞评论加关注”,也欢迎大家评论区进行提问,文章最后也有我个人的联系方式,最后也欢迎大家加入我的交流群!!!

;