自学内容网 自学内容网

SpringSecurity+OAuth2权限管理

Spring Security

零  介绍

功能:

  • 身份认证(authentication)

  • 授权(authorization)

  • 防御常见攻击(protection against common attacks)

身份认证:

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

授权:

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

防御常见攻击:

  • CSRF

  • HTTP Headers

  • HTTP Requests

一  快速入门

1  引入依赖

Spring Web、Spring Security、Thymeleaf

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

2  编写Controller

@Controller
public class IndexController {

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

3  创建index.html

在路径resources/templates中创建index.html

<html 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>

4  运行

因为没有配置tomcat端口,所以走默认8080,我们访问localhost:8080后,会自动跳转认证页面。

有随机生成的密码在后台控制台输出。默认用户名是user

点击登陆,就来到了我们所写的页面

5  分析:Spring Security默认做了什么

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

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

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

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

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

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

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

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

  • 处理会话劫持攻击。

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

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

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

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

二 Spring Security 的底层原理

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

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

1  Filter

下图展示了处理一个Http请求时,过滤器和Servlet的工作流程:

客户端发送请求,经过一系列过滤器最终请求到servlet上,也就是后端

2  DelegatingFilterProxy

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

3  FilterChainProxy

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

4  SecurityFilterChain

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

5  Multiple SecurityFilterChain

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

6  DefaultSecurityFilterChain

SecurityFilterChain接口的实现,加载了默认的16个Filter

三  Spring Security自定义配置

1  基于内存的用户认证

1.1、创建自定义配置

实际开发的过程中,我们需要应用程序更加灵活,可以在SpringSecurity中创建自定义配置文件

官方文档:Java自定义配置

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

创建一个WebSecurityConfig文件:

定义一个@Bean,类型是UserDetailsService,实现是InMemoryUserDetailsManager

package com.itheima.learnspringsercuity_oauth.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("shuaiqicjx") //自定义用户名
                        .password("123") //自定义密码
                        .roles("USER") //自定义角色
                        .build()
        );
        return manager;
    }
}

测试:使用用户名shuaiqicjx,密码123登录

1.2、基于内存的用户认证流程
  • 程序启动时:

    • 创建InMemoryUserDetailsManager对象

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

    • 使用InMemoryUserDetailsManagerUser存入内存

  • 校验用户时:

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

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

InMemoryUserDetailsManager和UsernamePasswordAuthenticationFilter组件之间的联系如下:

  • 当用户尝试登录时,UsernamePasswordAuthenticationFilter会拦截登录请求,并提取用户名和密码。
  • 然后,UsernamePasswordAuthenticationFilter会创建一个Authentication对象(通常是UsernamePasswordAuthenticationToken),包含用户提交的用户名和密码。
  • 接下来,UsernamePasswordAuthenticationFilter会调用AuthenticationManager来执行实际的认证过程。
  • AuthenticationManager会委托给配置的UserDetailsService(在这种情况下是InMemoryUserDetailsManager)来加载用户的详细信息。
  • InMemoryUserDetailsManagerloadUserByUsername方法会被调用来根据用户名查找用户详细信息。
  • 找到用户详细信息后,UsernamePasswordAuthenticationFilter会使用这些信息来验证用户提交的密码是否正确。
  • 如果密码验证成功,用户被认为是认证通过的,Spring Security会创建一个带有用户权限信息的Authentication对象,并将其添加到安全上下文中,允许用户访问受保护的资源。

2  基于数据库的用户认证

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

2.2  引入依赖

准备mysql链接,mp,lombok

<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>

2.3  配置数据源

根据自己的实际情况填写

#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

2.4  创建实体类

创建与数据库相对应的实体类

@Data
public class User {

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

    private String username;

    private String password;

    private Boolean enabled;

}

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

2.6  Service

public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

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

    @Resource
    public UserService userService;

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

2.8  定义 DBUserDetailsManager
  • 程序启动时:

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

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

  • 校验用户时:

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

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

package com.itheima.learnspringsercuity_oauth.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.itheima.learnspringsercuity_oauth.domain.User;
import com.itheima.learnspringsercuity_oauth.mapper.UserMapper;
import jakarta.annotation.Resource;
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;

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

2.9  初始化UserDetailsService

修改WebSecurityConfig中的userDetailsService方法如下

@Configuration
@EnableWebSecurity//Spring项目总需要添加此注解,SpringBoot项目中不需要
public class WebSecurityConfig {

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

3  小总结

无论是基于内存的用户认证还是基于数据库的用户认证

基于内存:

  • 校验用户时:

    • SpringSecurity自动使用InMemoryUserDetailsManagerloadUserByUsername

基于数据库

  • 校验用户时:

    • SpringSecurity自动使用DBUserDetailsManagerloadUserByUsername方法

我们可以看到他们本质上调用了同一个loadUserByUsername的方法。

这是因为InMemoryUserDetailsManager和DBUserDetailsManager本质上继承了UserDetailsManager这个类,只不过是前者是new出来的对象,这个对象的类继承,而后者是我们自己创建出来的对象,我们自己去继承了这个类

InMemoryUserDetailsManager:

DBUserDetailsManager:

而这个UserDetailsManager类的父结构就有loadUserByUsername这个方法

测试:

我们可以访问localhost:8080这个网址,使用数据库中配置的用户名和密码进行登录,可以登陆成功

四  SpringSecurity的默认配置

在WebSecurityConfig中添加如下配置

这是默认配置,就算我们没有配置,Security也会自动执行相关逻辑

formLogin,负责整个登入登出功能

默认的链路逻辑

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

    return http.build();
}

五  添加用户功能

0  添加swagger测试

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

1  Controller

UserController中添加方法

@PostMapping("/add")
    public void add(@RequestBody User user){
        userService.saveUserDetails(user);
        log.info("添加用户成功");
    }

2  Service

UserService接口中添加方法

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);

}

3  修改配置

DBUserDetailsManager中修改createUser方法

@Override
    public void createUser(UserDetails userDetails) {
        User user = new User();
        user.setEnabled(true);
        user.setUsername(userDetails.getUsername());
        user.setPassword(userDetails.getPassword());
        userMapper.insert(user);
        log.info("mapper插入用户成功");

    }

4  关闭csrf攻击防御

默认情况下SpringSecurity开启了csrf攻击防御的功能,这要求请求参数中必须有一个隐藏的_csrf字段,如下:

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

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

5  测试

六  密码加密算法

1  密码加密方式

明文密码:

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

Hash算法:

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

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

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

彩虹表:

用户恶意创建用于破解哈希密码的查找表称为彩虹表

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

加盐密码:

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

自适应单向函数:

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

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

自适应单向函数包括bcrypt、PBKDF2、scrypt和argon2

2  PasswordEncoder

BCryptPasswordEncoder

使用广泛支持的bcrypt算法来对密码进行哈希。为了增加对密码破解的抵抗力,bcrypt故意设计得较慢。和其他自适应单向函数一样,应该调整其参数,使其在您的系统上验证一个密码大约需要1秒的时间。BCryptPasswordEncoder的默认实现使用强度10。建议您在自己的系统上调整和测试强度参数,以便验证密码时大约需要1秒的时间。

Argon2PasswordEncoder

使用Argon2算法对密码进行哈希处理。Argon2是密码哈希比赛的获胜者。为了防止在自定义硬件上进行密码破解,Argon2是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当前的Argon2PasswordEncoder实现需要使用BouncyCastle库。

Pbkdf2PasswordEncoder

使用PBKDF2算法对密码进行哈希处理。为了防止密码破解,PBKDF2是一种故意缓慢的算法。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当需要FIPS认证时,这种算法是一个很好的选择。

SCryptPasswordEncoder

使用scrypt算法对密码进行哈希处理。为了防止在自定义硬件上进行密码破解,scrypt是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。

3  密码加密测试

在测试类中编写一个测试方法,自己给调整工作因子的大小,来看看具体验证的速度是多少

@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), "密码不一致");
}

4 DelegatingPasswordEncoder

  • 表中存储的密码形式:{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW

  • 通过如下源码可以知道:可以通过{bcrypt}前缀动态获取和密码的形式类型一致的PasswordEncoder对象

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

5、RSA证书

1 生成证书

首先通过终端进入到resources目录,然后执行证书生成命令。

keytool -genkeypair -alias 01star -keyalg RSA -keypass 123456 -keystore jwt.jks -storepass 123456 -validity 3650
keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12

参数帮助说明

keytool密钥和证书管理工具
命令:
-certreq            生成证书请求
-changealias        更改条目的别名
-delete             删除条目
-exportcert         导出证书
-genkeypair         生成密钥对
-genseckey          生成密钥
-gencert            根据证书请求生成证书
-importcert         导入证书或证书链
-importpass         导入口令
-importkeystore     从其他密钥库导入一个或所有条目
-keypasswd          更改条目的密钥口令
-list               列出密钥库中的条目
-printcert          打印证书内容
-printcertreq       打印证书请求的内容
-printcrl           打印 CRL 文件的内容
-storepasswd        更改密钥库的存储口令
使用 "keytool -command_name -help" 获取 command_name 的用法

keytool -genkeypair [OPTION]...
生成密钥对选项: 
-alias <alias>                  要处理的条目的别名 
-keyalg <keyalg>                密钥算法名称 
-keysize <keysize>              密钥位大小 
-sigalg <sigalg>                签名算法名称 
-destalias <destalias>          目标别名 
-dname <dname>                  唯一判别名 
-startdate <startdate>          证书有效期开始日期/时间 
-ext <value>                    X.509 扩展 
-validity <valDays>             有效天数 
-keypass <arg>                  密钥口令 
-keystore <keystore>            密钥库名称 
-storepass <arg>                密钥库口令 
-storetype <storetype>          密钥库类型 
-providername <providername>    提供方名称 
-providerclass <providerclass>  提供方类名 
-providerarg <arg>              提供方参数 
-providerpath <pathlist>        提供方类路径 
-v                              详细输出 
-protected                      通过受保护的机制的口令使用 
"keytool -help" 获取所有可用命令

2 提取公钥

通过openssl可以提取PUBLIC KEY格式,windows下需要安装 openssl

Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions

keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey
3 提取私钥

需要把证书先转成pfx格式

keytool -v -importkeystore -srckeystore jwt.jks -srcstoretype jks -srcstorepass 123456 -destkeystore jwt.pfx -deststoretype pkcs12 -deststorepass 123456 -destkeypass 123456

提取私钥

#显示在屏幕上
openssl pkcs12 -in jwt.pfx -nocerts -nodes

#输出到文件
openssl pkcs12 -in jwt.pfx -nocerts -nodes -out jwt.key

七  自定义登陆界面

1  创建登录Controller

@Controller
public class LoginController {

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

2  创建登录页面

resources/templates/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>

3  配置SecurityFilterChain

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

八  前后端分离

1  用户认证流程

  • 登录成功后调用:AuthenticationSuccessHandler

  • 登录失败后调用:AuthenticationFailureHandler

2  引入fastjson

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

3  认证成功的响应

3.1、成功结果处理

在配置包下创建一个MyAuthenticationSuccessHandler 实现 AuthenticationSuccessHandler ,重写里面的方法。

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

3.2、SecurityFilterChain配置
form.successHandler(new MyAuthenticationSuccessHandler()) //认证成功时的处理

3.3  测试

4  认证失败响应

4.1  失败结果处理
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);
    }
}

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

4.3  测试

输入密码错误后返回的页面

5  请求未认证的接口

5.1  实现AuthenticationEntryPoint接口

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

这里我们也希望系统返回json结果,因此我们定义类实现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);
    }
}

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

5.3  测试

同时有两个客户端登陆

6  跨域

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

在SpringSecurity中解决跨域很简单,在配置文件中添加如下配置即可

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

九  身份认证

1  用户认证信息

1.1、基本概念

在Spring Security框架中,SecurityContextHolder、SecurityContext、Authentication、Principal和Credential是一些与身份验证和授权相关的重要概念。它们之间的关系如下:

  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(用户的凭证信息)。

1.2、在Controller中获取用户信息
@GetMapping("/getInfo")
    public Map getInfo(){

        System.out.println("index 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;
    }

2  会话并发处理

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

2.1  实现处理器接口

实现接口SessionInformationExpiredStrategy

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

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

2.3  测试

这里我们使用不同的浏览器来模拟不同的客户端,如果同时登陆两个客户端会报错

十  授权

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

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

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

1、基于request的授权

1.1、用户-权限-资源

需求:

  • 具有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()
        );

授予权限

仅授予访问用户列表的权限

Collection<GrantedAuthority> authorities = new ArrayList<>();
            // 添加权限
            authorities.add(new GrantedAuthority() {
                @Override
                public String getAuthority() {
                    return "USER_LIST";
                }
            });

请求未授权的接口

我们为请求请求未授权的接口后返回一个自定义的返回界面,返回自定义的数据

自定义一个类去实现AccessDeniedHandler 接口

package com.itheima.learnspringsercuity_oauth.config;

import com.alibaba.fastjson2.JSON;
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;
import java.util.HashMap;

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //创建结果对象
        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());//请求未认证的接口
            exception.accessDeniedHandler(new MyAccessDeniedHandler());//请求未授权的接口
        });

测试

测试的时候为了方便,一个权限都没给,然后去请求用户的列表

去添加用户,结果因为没有权限出错

1.2、用户-角色-资源

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

配置角色

SecurityFilterChain

//开启授权保护
http.authorizeRequests(
        authorize -> authorize
                //具有管理员角色的用户可以访问/user/**
                .requestMatchers("/user/**").hasRole("ADMIN")
                //对所有请求开启授权保护
                .anyRequest()
                //已认证的请求会被自动授权
                .authenticated()
);

授予角色
org.springframework.security.core.userdetails.User
                    .withUsername(user.getUsername())
                    .password(user.getPassword())
                    .roles("ADMIN")
                    .credentialsExpired(false)//用户凭证是否过期
                    .accountLocked(false)//用户是否未被锁定
                    .build();

测试

在测试的时候为了显示效果,把给用户的权限给关掉了

最后测试的结果,会显示没有权限

1.3、用户-角色-权限-资源
介绍

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

用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。

列名数据类型描述
user_idint用户ID
usernamevarchar用户名
passwordvarchar密码
emailvarchar电子邮件地址
.........

角色表(Role table):存储所有可能的角色及其描述。

列名数据类型描述
role_idint角色ID
role_namevarchar角色名称
descriptionvarchar角色描述
.........

权限表(Permission table):定义系统中所有可能的权限。

列名数据类型描述
permission_idint权限ID
permission_namevarchar权限名称
descriptionvarchar权限描述
.........

用户角色关联表(User-Role table):将用户与角色关联起来。

列名数据类型描述
user_role_idint用户角色关联ID
user_idint用户ID
role_idint角色ID
.........

角色权限关联表(Role-Permission table):将角色与权限关联起来。

列名数据类型描述
role_permission_idint角色权限关联ID
role_idint角色ID
permission_idint权限ID
.........

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

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

实战

数据库表

DROP TABLE IF EXISTS `user_role`;
DROP TABLE IF EXISTS `role_menu`;
DROP TABLE IF EXISTS `role`;
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `menu`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
  `username` varchar(32) DEFAULT NULL COMMENT '账户名',
  `password` varchar(256) DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
  `name` varchar(32) DEFAULT NULL COMMENT '角色名',
  `keyword` varchar(64) DEFAULT NULL COMMENT '关键词',
  `description` varchar(128) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';

CREATE TABLE `user_role` (
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `role_id` int(11) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`),
  CONSTRAINT `FK_Reference_u_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
  CONSTRAINT `FK_Reference_u_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色';

CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单编号',
  `name` varchar(128) DEFAULT NULL COMMENT '菜单名',
  `link_url` varchar(128) DEFAULT NULL COMMENT '链接地址',
  `path` varchar(128) DEFAULT NULL COMMENT '路由地址',
  `priority` int(11) DEFAULT NULL COMMENT '显示优先级别',
  `icon` varchar(64) DEFAULT NULL COMMENT '图标',
  `description` varchar(128) DEFAULT NULL COMMENT '描述',
  `parent_menu_id` int(11) DEFAULT NULL COMMENT '父级菜单编号',
  `level` int(11) DEFAULT NULL COMMENT '层次级别',
  `is_enable` tinyint(4) DEFAULT '1' COMMENT '是否启用 0 禁用 1 启用',
  PRIMARY KEY (`id`),
  CONSTRAINT `FK_Reference_menu` FOREIGN KEY (`parent_menu_id`) REFERENCES `menu` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='菜单';

CREATE TABLE `role_menu` (
  `role_id` int(11) NOT NULL COMMENT '角色ID',
  `menu_id` int(11) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`,`menu_id`),
  CONSTRAINT `FK_Reference_r_menu` FOREIGN KEY (`menu_id`) REFERENCES `menu` (`id`),
  CONSTRAINT `FK_Reference_r_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色菜单关系表';

INSERT INTO `user` VALUES ('1', 'admin', '$2a$10$K7U.Xolbbz3fGsAzpIawmeQuTWt/W0TXA8DpugqRwWsE0PeRSi1Vu');
INSERT INTO `user` VALUES ('2', 'test', '$2a$10$K7U.Xolbbz3fGsAzpIawmeQuTWt/W0TXA8DpugqRwWsE0PeRSi1Vu');
INSERT INTO `role` VALUES ('1', '超级管理员', 'SUPER_ADMIN', null);
INSERT INTO `role` VALUES ('2', '普通管理员', 'ADMIN', null);
INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('2', '2');

INSERT INTO `menu` VALUES ('1', '示例查询', '/cpp/query', '2', '2', 'fa-user-md', null, null, '1', '1');
INSERT INTO `menu` VALUES ('2', '示例添加', '/cpp/add', '/2-1', '1', null, null, '1', '2', '1');
INSERT INTO `menu` VALUES ('3', '示例修改', '/cpp/modify', '/2-2', '2', null, null, '1', '2', '1');
INSERT INTO `menu` VALUES ('4', '示例删除', '/cpp/delete', '/2-3', '3', null, null, '1', '2', '1');
INSERT INTO `menu` VALUES ('5', '示例JSON', '/cpp/json', '/2-3', '3', null, null, '1', '2', '1');
INSERT INTO `role_menu` VALUES ('1', '1');
INSERT INTO `role_menu` VALUES ('1', '2');
INSERT INTO `role_menu` VALUES ('1', '3');
INSERT INTO `role_menu` VALUES ('1', '4');
INSERT INTO `role_menu` VALUES ('1', '5');
INSERT INTO `role_menu` VALUES ('2', '1');
INSERT INTO `role_menu` VALUES ('2', '5');

2、基于方法的授权

2.1、开启方法授权

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

2.2、给用户授予角色和权限
return org.springframework.security.core.userdetails.User
                    .withUsername(user.getUsername())
                    .password(user.getPassword())
                    .roles("ADMIN")//添加用户的角色
                    .authorities("USER_ADD", "USER_UPDATE")//添加用户的权限
                    .build();

2.3  常用授权注解
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Resource
    public UserService userService;

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

    //用户必须有 ADMIN 角色 才能访问此方法
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/getListByRole")
    public List<User> getListByRole(){
        return userService.list();
    }


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

2.4  测试

我们给用户授权只授权admin的角色

访问add方法

登陆之后返回用户的权限

getListByRole()访问成功

add方法没有权限

OAuth2

1  OAuth2简介

1.1  OAuth2是什么

“Auth” 表示 “授权” Authorization

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

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

OAuth2最简向导:The Simplest Guide To OAuth 2.0

1.2  OAuth2的角色

OAuth 2协议包含以下角色:

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

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

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

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

用一个例子来说,就是张三(资源所有者)要用微信(客户应用)来查看微信钱包有多少米(资源服务器)。客户应用需要在这个查看的请求上带上自己的令牌给资源服务器,令牌由授权服务器发送

1.3  OAuth2的四种授权模式

RFC6749:

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

阮一峰:

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

四种模式:

  • 授权码(authorization-code)

  • 隐藏式(implicit)

  • 密码式(password)

  • 客户端凭证(client credentials)

第一种方式:授权码

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

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

第二种方式:隐藏式

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

前端对后端

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

https://a.com/callback#token=ACCESS_TOKEN
将访问令牌包含在URL锚点中的好处:锚点在HTTP请求中不会发送到服务器,减少了泄漏令牌的风险。

第三种方式:密码式

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

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

第四种方式:凭证式

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

后端对后端

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

注意

第二种和第四种差不多,只不过是请求的对象的不同,第二种是前端对后端发送请求,第四种是后端对后端发送请求

1.4  授权类型的选择

授权码(authorization code)

隐藏式(implicit)

密码式(Resource Owner Password Credentials)

凭证式(client credentials)

2  Spring中的OAuth2

2.1  相关角色

回顾:OAuth 2中的角色

  1. 资源所有者(Resource Owner)

  2. 客户应用(Client)

  3. 资源服务器(Resource Server)

  4. 授权服务器(Authorization Server)

2.2  Spring中的实现

OAuth2 :: Spring Security

Spring Security

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

  • 资源服务器(OAuth2 Resource Server)

Spring

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

3  GiuHub社交登录案例

基于github的第三方认证

3.1  创建应用

注册客户应用:

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

登陆github账户

点击右上角图标,点击setting

划到最下面,点击开发者设置

找到OAuth

创建一个应用

配置应用的信息

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

创建之后,生成了密钥,记得保存好。

3.2  创建测试项目

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

示例代码参考:spring-security-samples/servlet/spring-boot/java/oauth2/login at 6.2.x · spring-projects/spring-security-samples (github.com)

3.3  配置OAuth客户端属性

application.yml:

id和密钥写自己的

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

3.4  创建Controller

官方示例代码

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

3.5  创建html页面

resources/templates/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>

3.6  测试

启动服务,访问localhost:8080

会跳转至github的登陆页面

登陆之后,会询问是否要认证

登陆成功,返回到是自定义的那个页面

  • 启动程序并访问localhost:8080。浏览器将被重定向到默认的自动生成的登录页面,该页面显示了一个用于GitHub登录的链接。

  • 点击GitHub链接,浏览器将被重定向到GitHub进行身份验证。

  • 使用GitHub账户凭据进行身份验证后,用户会看到授权页面,询问用户是否允许或拒绝客户应用访问GitHub上的用户数据。点击允许以授权OAuth客户端访问用户的基本个人资料信息。

  • 此时,OAuth客户端访问GitHub的获取用户信息的接口获取基本个人资料信息,并建立一个已认证的会话。


原文地址:https://blog.csdn.net/2302_80490510/article/details/143863881

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!