一、Spring Security快速入门
官方文档:
Spring Security :: Spring Security
功能:
- 身份认证(authentication)
- 授权(authorization)
- 防御常见攻击(protection against common attacks)
身份认证:
- 身份认证是验证
谁正在访问系统资源
,判断用户是否为合法用户。认证用户的常见方式是要求用户输入用户名和密码。
授权:
- 用户进行身份认证后,系统会控制
谁能访问哪些资源
,这个过程叫做授权。用户无法访问没有权限的资源。
防御常见攻击:
- CSRF
- HTTP Headers
- HTTP Requests
1.1、身份认证(authentication)
官方代码示例:GitHub - spring-projects/spring-security-samples
1.1、创建Spring Boot项目
JDK:17
SpringBoot:3.2.0(依赖了Spring Security 6.2.0)
Dependencies:Spring Web、Spring Security、Thymeleaf
1.2、创建IndexController
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
创建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>
<a href="/logout">Log Out2</a>
</body>
</html>
1.4、启动项目测试Controller
浏览器中访问:http://localhost:8080/
**浏览器自动跳转到登录页面:**http://localhost:8080/login
输入用户名:user
输入密码:在控制台的启动日志中查找初始的默认密码
点击"Sign in"进行登录,浏览器就跳转到了index页面
默认情况下Spring Security将初始的用户名和密码存在了SecurityProperties类中。这个类中有一个静态内部类User,配置了默认的用户名(name = "user")和密码(password = uuid)
我们也可以将用户名、密码配置在SpringBoot的配置文件中:在application.properties中配置自定义用户名和密码
spring.security.user.name=user
spring.security.user.password=123
1.2、注意事项
1.2.1、@{/logout}的作用
通过使用@{/logout},Thymeleaf将自动处理生成正确的URL,以适应当前的上下文路径。这样,无论应用程序部署在哪个上下文路径下,生成的URL都能正确地指向注销功能。
例如:如果我们在配置文件中添加如下内容
server.servlet.context-path=/demo
那么@{/logout}可以自动处理url为正确的相对路径
但是如果是普通的/logout,路径就会不正确
1.2.2、页面样式无法加载的问题
页面样式bootstrap.min.css是一个CDN地址,需要通过科学上网的方式访问
否则你的登录页会加载很久,并且看到的页面是这样的(登录按钮没有样式文件渲染,但是不影响登录功能的执行)
1.3、Spring Security默认做了什么
- 保护应用程序URL,要求对应用程序的任何交互进行身份验证。
- 程序启动时生成一个默认用户“user”。
- 生成一个默认的随机密码,并将此密码记录在控制台上。
- 生成默认的登录表单和注销页面。
- 提供基于表单的登录和注销流程。
- 对于Web请求,重定向到登录页面;
- 对于服务请求,返回401未经授权。
- 处理跨站请求伪造(CSRF)攻击。
- 处理会话劫持攻击。
- 写入Strict-Transport-Security以确保HTTPS。
- 写入X-Content-Type-Options以处理嗅探攻击。
- 写入Cache Control头来保护经过身份验证的资源。
- 写入X-Frame-Options以处理点击劫持攻击。
二、Spring Security自定义配置
2.1、基于内存的用户认证
1.1、创建自定义配置
实际开发的过程中,我们需要应用程序更加灵活,可以在SpringSecurity中创建自定义配置文件
官方文档:Java自定义配置
UserDetailsService用来管理用户信息,InMemoryUserDetailsManager是UserDetailsService的一个实现,用来管理基于内存的用户信息。
创建一个WebSecurityConfig文件:
定义一个@Bean,类型是UserDetailsService,实现是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;
}
}
2.2、基于内存的用户认证流程
- 程序启动时:
- 创建
InMemoryUserDetailsManager
对象 - 创建
User
对象,封装用户名密码 - 使用InMemoryUserDetailsManager
将User存入内存
- 创建
- 校验用户时:
- SpringSecurity自动使用
InMemoryUserDetailsManager
的loadUserByUsername
方法从内存中
获取User对象 - 在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中将用户输入的用户名密码和从内存中获取到的用户信息进行比较,进行用户认证
- SpringSecurity自动使用
2.3、基于数据库的数据源
创建三个数据库表并插入测试数据
-- 创建数据库
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;
}
Mapper
接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
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
接口
public interface UserService extends IService<User> {
}
实现
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
public UserService userService;
@GetMapping("/list")
public List<User> getList(){
return userService.list();
}
}
2.4、基于数据库的用户认证
基于数据库的用户认证流程
- 程序启动时:
- 创建
DBUserDetailsManager
类,实现接口 UserDetailsManager, UserDetailsPasswordService - 在应用程序中初始化这个类的对象
- 创建
- 校验用户时:
- SpringSecurity自动使用
DBUserDetailsManager
的loadUserByUsername
方法从数据库中
获取User对象 - 在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证
- SpringSecurity自动使用
定义DBUserDetailsManager
@Component
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.5、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();
}
2.6、添加用户功能
Controller
UserController中添加方法
@PostMapping("/add")
public void add(@RequestBody User user){
userService.saveUserDetails(user);
}
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);
}
修改配置
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);
}
关闭csrf攻击防御
默认情况下SpringSecurity开启了csrf攻击防御的功能,这要求请求参数中必须有一个隐藏的**_csrf**字段,如下:
在filterChain方法中添加如下代码,关闭csrf攻击防御
//关闭csrf攻击防御
http.csrf((csrf) -> {
csrf.disable();
});
2.7、密码加密算法
参考文档:[Password Storage :: Spring Security
密码加密方式
明文密码:
最初,密码以明文形式存储在数据库中。但是恶意用户可能会通过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.8、PasswordEncoder
BCryptPasswordEncoder
使用广泛支持的bcrypt算法来对密码进行哈希。为了增加对密码破解的抵抗力,bcrypt故意设计得较慢。和其他自适应单向函数一样,应该调整其参数,使其在您的系统上验证一个密码大约需要1秒的时间。BCryptPasswordEncoder的默认实现使用强度10。建议您在自己的系统上调整和测试强度参数,以便验证密码时大约需要1秒的时间。
Argon2PasswordEncoder
使用Argon2算法对密码进行哈希处理。Argon2是密码哈希比赛的获胜者。为了防止在自定义硬件上进行密码破解,Argon2是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当前的Argon2PasswordEncoder实现需要使用BouncyCastle库。
Pbkdf2PasswordEncoder
使用PBKDF2算法对密码进行哈希处理。为了防止密码破解,PBKDF2是一种故意缓慢的算法。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当需要FIPS认证时,这种算法是一个很好的选择。
SCryptPasswordEncoder
使用scrypt算法对密码进行哈希处理。为了防止在自定义硬件上进行密码破解,scrypt是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。
测试
@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), "密码不一致");
}
应用
配置
@Configuration
public class SecurityConfiguration {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
实体类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User04 {
private String userName;
private String password;
}
Service
@Slf4j
@Service
public class UserService04 {
public User04 getUser(String userName) {
return "BO".equals(userName)
? User04.builder()
.userName("BO")
.password("$2a$10$A7OcKw5xxRMh9c4ghWySr.Rjh22gpWyiWExZO5i2B32eJLQrFXcr6")
.build()
: null;
}
}
controller
@Slf4j
@RestController
@RequestMapping("/api/example04/")
@RequiredArgsConstructor
public class ExampleController04 {
private final UserService04 userService;
private final PasswordEncoder passwordEncoder;
@PostMapping("login")
public ResultVO login(@RequestBody User04 user) {
// 先查询用户是否存在
User04 u = userService.getUser(user.getUserName());
if (u == null || !passwordEncoder.matches(user.getPassword(), u.getPassword())) {
log.debug("登录失败");
return ResultVO.error(401, "用户名密码错误");
}
// 登录成功,添加token等操作
log.debug("登录成功");
return ResultVO.success(Map.of("user", u));
}
}
2.9Encryption Algorithm
加密/解密算法,适合敏感数据的加密传输
- 对称加密算法(AES等),通过相同密钥加密/解密
- 非对称加密算法(RSA等),公钥加密的数据,必须通过私钥才能解密
Spring-Security提供基于256bits PBKDF2对称算法的实现 提供不可泄露的,加密密钥SecretKey(任意长度字符串) 提供不可泄露的,16位十六进制数固定长度的的盐值(64位二进制数)
TextEncryptor Encryptors.text(secretKey, salt),基于指定密钥/盐值创建文本加密器
- String encrypt(payload), 加密指定字符串
- String decrypt(encryptString), 解密指定字符串
先验证后解密。篡改的数据无法通过验证抛出异常,避免解密浪费资源
实体类
@Data
public class MyToken {
public enum Role{
USER, ADMIN
}
private Integer uid;
private Role role;
}
组件
@Component
public class EncryptComponent05 {
private final ObjectMapper objectMapper; // 使用ObjectMapper进行JSON序列化和反序列化
@Value("${my.secretkey}")
private String secretKey; // 从配置文件中获取密钥
@Value("${my.salt}")
private String salt; // 从配置文件中获取盐值
private TextEncryptor encryptor; // 用于加密和解密的TextEncryptor对象
public EncryptComponent05(ObjectMapper objectMapper) {
this.objectMapper = objectMapper; // 构造函数,初始化ObjectMapper对象
}
/**
* 在构造函数之后,通过PostConstruct注解的方法,直接基于密钥/盐值创建单例TextEncryptor对象。避免反复创建
*/
@PostConstruct
public void getTextEncryptor() {
encryptor = Encryptors.text(secretKey, salt); // 创建TextEncryptor对象
}
public String encrypt(Map<String, Object> payload) {
try {
String json = objectMapper.writeValueAsString(payload); // 将payload转换为JSON字符串
return encryptor.encrypt(json); // 使用TextEncryptor对JSON字符串进行加密
} catch (JsonProcessingException e) {
throw new MyException(500, "服务器端错误"); // 如果发生异常,抛出自定义异常
}
}
/**
* 无法验证/解密/反序列化,说明数据被篡改,判定无权限
* @param auth
* @return
*/
public Map<String, Object> decrypt(String auth) {
try {
String json = encryptor.decrypt(auth); // 使用TextEncryptor对加密后的字符串进行解密
return objectMapper.readValue(json, Map.class); // 将解密后的JSON字符串反序列化为Map对象
} catch (Exception e) {
throw new MyException(403, "无权限"); // 如果发生异常,抛出自定义异常
}
}
}
@PostConstruct
@PostConstruct
注解是Java中的一个注解,用于标记一个方法或构造函数,表示该方法或构造函数在依赖关系注入完成后被调用。它通常用于初始化操作,例如设置默认值、打开连接等。@PostConstruct
注解的方法会在依赖注入完成后自动调用,无需手动调用。
测试
@SpringBootTest
@Slf4j
public class EncryptorTest {
@Autowired
private ObjectMapper objectMapper;
@Value("${my.secretkey}")
private String secretKey;
@Value("${my.salt}")
private String salt;
@Test
public void test_encrypt() {
MyToken Token = new MyToken();
Token.setUid(1);
Token.setRole(MyToken.Role.ADMIN);
TextEncryptor encryptor = Encryptors.text(secretKey, salt);
try {
//Map<String, Object> map = Map.of("uid", 1384896304762638307L, "role", 9);
String json = objectMapper.writeValueAsString(Token);
String r = encryptor.encrypt(json);
log.debug(r);
log.debug("{}", r.length());
log.debug(encryptor.encrypt(json));
String reJson = encryptor.decrypt(r);
Map<String, Object> reToken = objectMapper.readValue(reJson, Map.class);
log.debug(reToken.get("role").toString());
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.10自定义登录页面
创建登录Controller
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
创建登录页面
resources/templates/login.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>登录</title>
<meta charset="UTF-8">
</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
SecurityConfiguration:
.formLogin( form -> {
form
.loginPage("/login").permitAll() //登录页面无需授权即可访问
.usernameParameter("username") //自定义表单用户名参数,默认是username
.passwordParameter("password") //自定义表单密码参数,默认是password
.failureUrl("/login?error") //登录失败的返回地址
;
}); //使用表单授权方式
三、前后端分离
3.1引入fastjson
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.37</version>
</dependency>
3.2认证成功的响应
成功结果处理
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()) //认证成功时的处理
3.3认证失败响应
失败结果处理
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()) //认证失败时的处理
3.4注销响应
注销结果处理
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()); //注销成功时的处理
});
3.5请求未认证的接口
Servlet Authentication Architecture :: Spring Security
当访问一个需要认证之后才能访问的接口的时候,Spring Security会使用AuthenticationEntryPoint
将用户请求跳转到登录页面,要求用户提供登录凭证。
这里我们也希望系统返回json结果
,因此我们定义类实现AuthenticationEntryPoint接口
3.6跨域
跨域全称是跨域资源共享(Cross-Origin Resources Sharing,CORS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。
在SpringSecurity中解决跨域很简单,在配置文件中添加如下配置即可
//跨域
http.cors(withDefaults());
四、身份认证
4.1用户认证信息
基本概念
在Spring Security框架中,SecurityContextHolder、SecurityContext、Authentication、Principal和Credential是一些与身份验证和授权相关的重要概念。它们之间的关系如下:
- SecurityContextHolder:SecurityContextHolder 是 Spring Security 存储已认证用户详细信息的地方。
- SecurityContext:SecurityContext 是从 SecurityContextHolder 获取的内容,包含当前已认证用户的 Authentication 信息。
- Authentication:Authentication 表示用户的身份认证信息。它包含了用户的Principal、Credential和Authority信息。
- Principal:表示用户的身份标识。它通常是一个表示用户的实体对象,例如用户名。Principal可以通过Authentication对象的getPrincipal()方法获取。
- Credentials:表示用户的凭证信息,例如密码、证书或其他认证凭据。Credential可以通过Authentication对象的getCredentials()方法获取。
- GrantedAuthority:表示用户被授予的权限
总结起来,SecurityContextHolder用于管理当前线程的安全上下文,存储已认证用户的详细信息,其中包含了SecurityContext对象,该对象包含了Authentication对象,后者表示用户的身份验证信息,包括Principal(用户的身份标识)和Credential(用户的凭证信息)。
在Controller中获取用户信息
IndexController:
@RestController
public class IndexController {
@GetMapping("/")
public Map index(){
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;
}
}
4.2会话并发处理
后登录的账号会使先登录的账号失效
实现处理器接口
实现接口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);
}
}
SecurityFilterChain配置
//会话管理
http.sessionManagement(session -> {
session
.maximumSessions(1)
.expiredSessionStrategy(new MySessionInformationExpiredStrategy());
});
五、授权
授权管理的实现在SpringSecurity中非常灵活,可以帮助应用程序实现以下两种常见的授权需求:
-
用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表
-
用户-角色-权限-资源:例如 张三是角色是管理员、李四的角色是普通用户,管理员能做所有操作,普通用户只能查看信息
5.1基于request的授权
用户-权限-资源
需求:
- 具有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";
}
});*/
请求未授权的接口
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);
});
});
5.2用户-角色-资源
**需求:**角色为ADMIN的用户才可以访问/user/**路径下的资源
配置角色
SecurityFilterChain
//开启授权保护
http.authorizeRequests(
authorize -> authorize
//具有管理员角色的用户可以访问/user/**
.requestMatchers("/user/**").hasRole("ADMIN")
//对所有请求开启授权保护
.anyRequest()
//已认证的请求会被自动授权
.authenticated()
);
授予角色
DBUserDetailsManager中的loadUserByUsername方法:
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles("ADMIN")
.build();
5.3用户-角色-权限-资源
RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计方案的示例:
- 用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。
列名 | 数据类型 | 描述 |
---|---|---|
user_id | int | 用户ID |
username | varchar | 用户名 |
password | varchar | 密码 |
varchar | 电子邮件地址 | |
... | ... | ... |
- 角色表(Role table):存储所有可能的角色及其描述。
列名 | 数据类型 | 描述 |
---|---|---|
role_id | int | 角色ID |
role_name | varchar | 角色名称 |
description | varchar | 角色描述 |
... | ... | ... |
- 权限表(Permission table):定义系统中所有可能的权限。
列名 | 数据类型 | 描述 |
---|---|---|
permission_id | int | 权限ID |
permission_name | varchar | 权限名称 |
description | varchar | 权限描述 |
... | ... | ... |
- 用户角色关联表(User-Role table):将用户与角色关联起来。
列名 | 数据类型 | 描述 |
---|---|---|
user_role_id | int | 用户角色关联ID |
user_id | int | 用户ID |
role_id | int | 角色ID |
... | ... | ... |
- 角色权限关联表(Role-Permission table):将角色与权限关联起来。
列名 | 数据类型 | 描述 |
---|---|---|
role_permission_id | int | 角色权限关联ID |
role_id | int | 角色ID |
permission_id | int | 权限ID |
... | ... | ... |
在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。
当用户尝试访问系统资源时,系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护,因为只需调整角色和权限的分配即可,而不需要针对每个用户进行单独的设置。
5.4基于方法的授权
开启方法授权
在配置文件中添加如下注解
@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);
}
更多的例子:Method Security :: Spring Security
六、OAuth2
6.1OAuth2简介
OAuth2是什么
“Auth” 表示 “授权” Authorization
“O” 是 Open 的简称,表示 “开放”
连在一起就表示 “开放授权”,OAuth2是一种开放授权协议。
OAuth2最简向导:The Simplest Guide To OAuth 2.0
OAuth2的角色
OAuth 2协议包含以下角色:
- 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。
- 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。
- 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。
- 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。
6.2OAuth2的使用场景
开放系统间授权
社交登录
在传统的身份验证中,用户需要提供用户名和密码,还有很多网站登录时,允许使用第三方网站的身份,这称为"第三方登录"。所谓第三方登录,实质就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。
开放API
例如云冲印服务的实现
现代微服务安全
单块应用安全
微服务安全
企业内部应用认证授权
-
SSO:Single Sign On 单点登录
-
IAM:Identity and Access Management 身份识别与访问管理
6.3OAuth2的四种授权模式
四种模式:
- 授权码(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):也叫客户端模式,适用于没有前端的命令行应用,即在命令行下请求令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
总结
6.4Spring整合OAuth2
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 Login