Spring Security + OAuth2
第一章 Spring Security 快速入门
第二章 Spring Security 自定义配置
第三章 Spring Security 前后端分离配置
第四章 Spring Security 身份认证
第五章 Spring Security 授权
第六章 OAuth2
文章目录
1、基于内存的用户认证
1.1、创建自定义配置
实际开发的过程中,我们需要应用程序更加灵活,可以在SpringSecurity中创建自定义配置文件。
官方文档:Java自定义配置
UserDetailsService用来管理用户信息,InMemoryUserDetailsManger是UserDetailsService的一个实现,用来管理基于内存的用户信息。
创建一个WebSeacurityConfig文件:
定义一个@Bean,类型是UserDetailService,实现是InMemoryUserDetailsManger
@Configuration //配置类
//开起SpringSecurity自定义配置(在SpringBoot项目中可以省略此注解)
//@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
//创建基于内存的用户信息管理器
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//使用manger管理UserDetails对象
manager.createUser(
//创建UserDetails对象,用于管理用户名,用户密码,用户角色,用户权限等内容。
User
.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER").build()
);
return manager;
}
}
1.2、基于内存的用户认证流程
程序启动时:
- 创建InMemoryUserDetailsManager 对象
- 创建User对象,封装用户名密码
- 使用InMemoryUserDetailsManager 将User存入内存
校验用户时:
- SpringSecurity自动使用InMemoryUserDetailsManager 的loadingUserByUsername方法从内存中获取User对象。
- 在UsernamePasswordAuthenticationFilrer过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从内存中获取到的用户信息进行比较,进行用户认证
2、基于数据库的数据源
2.1、SQL
创建数据库表并插入测试数据
-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;
DROP TABLE IF EXISTS `user`;
--创建用户表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(500) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
--唯一索引
UNIQUE KEY `user_username_uindex` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
--插入用户数据(密码是abc)
insert into `user`(`id`,`username`,`password`,`enabled`) values
(1,'admin','{bcrypt}$2a$10$GRLdNijSQMUv1/au9ofL.eDwmoohzz$7.rmNSJZ.BFxO/BTK76KIW',1),
(2,'Helen','{bcrypt}$2a$10$GRLdNijSQMUv1/au9ofL.eDwmoohzz$7.rmNSJZ.BFxO/BTK76KIW',1),
(3,'Tom','{bcrypt}$2a$10$GRLdNijSQMUv1/au9ofL.eDwmoohzz$7.rmNSJZ.BFxO/BTK76KIW',1);
2.2、引入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.3、配置数据源
#sql数据源
spring.datasource.url=jdbc:mysql://localhost:3306/security-demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#Sql日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.4、实体类
package com.security.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
@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
package com.security.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.security.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
<?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.security.demo.mapper.UserMapper">
</mapper>
2.6、Service
package com.security.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.security.demo.entity.User;
public interface UserService extends IService<User> {
}
package com.security.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.security.demo.entity.User;
import com.security.demo.mapper.UserMapper;
import com.security.demo.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
2.7、Controller
package com.security.demo.controller;
import com.security.demo.entity.User;
import com.security.demo.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
public UserService userService;
@GetMapping(path = "/list")
public List<User> getList(){
return userService.list();
}
}
3、基于数据库的用户认证
3.1、基于数据库的用户认证流程
程序启动时:
- 创建 DBUserDetailsManager 对象,实现接口UserDetailsManager, UserDetailsPasswordService
- 在应用程序中初始化这个类对象。
校验用户时: - SpringSecurity自动使用DBUserDetailsManager 的loadingUserByUsername方法从数据库中获取User对象。
- 在UsernamePasswordAuthenticationFilrer过滤器中的attemptAuthentication方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证
3.2、定义DBUserDetailsManager
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Resource
private UserMapper userMapper;
@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;
}
/**
* 从数据库中获取用户配置信息。
* @param username
* @return
* @throws UsernameNotFoundException
*/
@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//权限列表
);
}
}
}
3.3、初始化UserDetailsService
@Configuration //配置类
//@EnableWebSecurity //开起SpringSecurity自定义配置(在SpringBoot项目中可以省略此注解)
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
//使用manger管理UserDetails对象
DBUserDetailsManager manager = new DBUserDetailsManager();
return manager;
}
}
也可以直接在DBUserDetailsManager加入@Component注解,以省略在WebSecurityConfig 类中的配置。
@Component
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService
4、Spring Security默认配置
在WebSecurityConfig中会有默认配置行为
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
// authorizeHttpRequests() 开启授权保护
// anyRequest() 对所以请求开启授权保护
// authenticated()已认证请求会自动授权。
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated() )
.formLogin(withDefaults());//表单授权方式
// .httpBasic(withDefaults());//基本授权方式,浏览器弹窗方式。
return http.build();
}
5、添加用户功能
5.1、Controller
@PostMapping("/add")
public void add(@RequestBody User user){
userService.saveUserDetails(user);
}
5.2、Service
UserService 接口中添加方法
void saveUserDetails(User user);
UserServiceImpl实现方法
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@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())
.roles("USER").build();
dbUserDetailsManager.createUser(userDetails);
}
}
5.3、DBUserDetailsManager
在DBUserDetailsManager 中重写 createUser方法
@Resource
private UserMapper userMapper;
public void createUser(UserDetails userDetails) {
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setEnabled(true);
userMapper.insert(user);
}
5.4、使用Swagger测试
添加swagger来测试添加用户功能
<!--swagger 测试-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
Swagger测试地址:http://localhost:8080/demo/doc.html
使用swagger测试添加用户接口。会报错403,这时需要我们关闭关闭csrf攻击防御
5.5、关闭csrf攻击防御
在WebSecurityConfig的filterChain中关闭csrf。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
// authorizeHttpRequests() 开启授权保护
// anyRequest() 对所以请求开启授权保护
// authenticated()已认证请求会自动授权。
http
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated() )
.formLogin(withDefaults());//表单授权方式
// .httpBasic(withDefaults());//基本授权方式
//关闭csrf攻击防御
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
此时再次添加用户可以添加成功
6、密码加密算法
6.1、密码加密方式
明文密码:
最初,密码以明文形式存储在数据库中,但是恶意用户可能会通过SQL注入等手段获取到
Hash算法:
Spring Security的PasswordEncoder用于对密码进行单向转换,从而将密码安全地存。对密码单向转
换需要用到哈希算法,例如MD5、SHA-2560、SHA-512等,哈希算法是单向的,只能加密,不能解密。
因此,数据库中存储的是单向转换后的密码,SpringSecurity在进行用户身份验证时需要将用户输入的密码进行
单向转换,然后与数据库的密码进行比较。
因此,如果发生数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并且在给定哈希的情况下只能通
过暴力破解的方式猜测密码。
彩虹表:
恶意用户创建称为彩虹表的查找表。
彩虹表就是一个庞大的、针对各种可能的字母组合预先生成的哈希值集合,有了它可以快速破解各类密码。越是复杂的密码,需要
的彩虹表就越大,主流的彩虹表都是100G以上,目前主要的算法有LM,NTLM,MD5,SHA1,MYSQLSHA1,HALFLMCHALL,
NTLMCHALL, ORACLE-SYSTEM, MD5-HALF。
加盐密码:
为了减轻彩虹表的效果,开发人员开始使用加盐密码。不再只使用密码作为哈希函数的输人,而是为每个用户的密码生成随机字节(称为盐)。盐和用户的密码将一起经过哈希函数运算,生成一个唯一的哈希。盐将以明文形式与用户的密码一起存储。然后,当用户尝试进行身份验证时,盐和用户输入的密码一起经过哈希函数运算,再与存储的密码进行比较。唯一的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希都是不同的。
自适应单向函数:
随着硬件的不断发展,加盐哈希也不再安全。原因是,计算机可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地破解每个密码。
现在,开发人员开始使用自适应单向函数来存储密码。使用自适应单向函数验证密码时,故意占用资源(故意使用大量的CPU、内存或其他资源)。自适应单向函数允许配置一个“工作因子”,随着硬件的改进而增加。我们建议将-工作因子调整到系统中验证密码需要约一秒钟的时间。这种权衡是为了让攻击者难以破解密码。自适应单向函数包括bcrypt、PBKDF2、scrypt和argon2
6.2、PasswordEncoder
BCryptPasswordEncoder
使用广泛支持的bcrypt算法来对密码进行哈希。为了增加对密码破解的抵抗力,bcrypt故意设计得较慢。和其他自适应单向函数一样,应该调整其参数,使其在您的系统上验证一个密码大约需要1秒的时间。
BCryptPasswordEncoder的默认实现使用强度10。建议您在自己的系统上调整和测试强度参数,以便验证密码时大约需要1秒的时间。
Argon2PasswordEncoder
使用Argon2算法对密码进行哈希处理。Argon2是密码哈希比赛的获胜者。为了防止在自定义硬件上进行密码破
解,Argon2是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大3约1秒来验证一个密码。当前的Argon2PasswordEncoder实现需要使用BouncyCastle库。
Pbkdf2PasswordEncoder
使用PBKDF2算法对密码进行哈希处理。为了防止密码破解,PBKDF2是一种故意缓慢的算法。与其他自适应单向
函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当需要FIPS认证时,这种算法是一个很好
择
6.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),"密码不一致");
}
6.4、DelegatingPasswordEncoder
- 表中存储的密码形式:{bcrypt}$2a 10 10 10GRLdNijSQMUv1/au9ofL.eDwmoohzz$7.rmNSJZ.BFxO/BTK76KIW
- 通过如下源码可以知道:可以通过{bcrypt}前缀动态获取和密码的形式类型一致的PasswordEncoder 对象
- 目的:方便随时做密码策略的升级,兼容数据库中的老版本密码策略生成的密码
7、 自定义登录页面
7.1、创建登录Controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login(){
return "login";
}
}
7.2、创建登录页面
<html xmlns: xmlns:th="http://www.w3.org/1999/xhtml" th="https:/www.thymeleaf.org">
<head>
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<div th:if="${param.error}">
错误的用户名和密码.</div>
<!--method必须为"post"–>-->
<!--th:action="@{/login]",-->
<!--使用动态参数,表单中会自动生成_cgrf隐藏字段:用于防止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>
7.3、配置SecurityFilterChain
要想使自定义的表单跳转成功,需要修改WebSecurityConfig在的配置。
http
.authorizeHttpRequests(authorize ->
authorize
.anyRequest()
.authenticated())
.formLogin(form -> {
form
.loginPage("/login") //设置新的登录页。
.permitAll() //无需授权即可访问。
//配置自定义的表单用户名参数,默认为username
.usernameParameter("myU")
//配置自定义的表单密码参数,默认为password
.passwordParameter("myP")
//检验失败时跳转的地址,默认是error
.failureUrl("/login?failure");
});//表单授权方式
如果修改了failureUrl配置,也要修改页面上显示错误的链接,否则错误信息不显示。
<div th:if="${param.failure}">错误的用户名和密码.</div>