文章目录
- 一.什么是SpringSecurity
- 二.SpringSecurity的特征
- 三.SpringSecurity的第一个例子
- 3.1 创建SpringBoot项目
- 3.2 创建IndexController
- 3.3 创建index.html
- 3.4 启动项目
- 3.5 Spring Security默认做了什么
- 四.SpringSecurity的整体架构
- 4.1 Filter
- 4.2 DelegatingFilterProxy
- 4.3 FilterChainProxy
- 4.4 SecurityFilterChain
- 4.5 Multiple SecurityFilterChain
- 五.Spring Security自定义配置
- 5.1 基于内存的用户认证
- 5.1.1 自定义配置
- 5.1.2 基于内存的用户认证流程
- 5.2 基于数据库的数据源
- 5.2.1 SQL
- 5.2.2 引入依赖
- 5.2.3 配置数据源
- 5.2.4 实体类
- 5.2.5 Mapper
- 5.2.6 Service
- 5.2.7 Controller
- 5.3 基于数据库的用户认证
- 5.3.1 基于数据库的用户认证流程
- 5.3.2 定义DBUserDetailsManager
- 5.3.3 初始化UserDetailsService
- 5.4 SpringSecurity的默认配置
- 5.5 添加用户功能
- 5.5.1 Controller
- 5.5.2 Service
- 5.5.3 修改配置
- 5.5.4 使用Swagger测试
- 5.6 密码加密算法
- 5.6.1 密码测试
- 5.7 定义登录页面
- 5.7.1 创建Controller
- 5.7.2 准备登录页面
- 5.7.3 配置SecurityFilterChain
- 六. 前后端分离
- 6.1 认证流程
- 6.2 引入fastjson
- 6.3 认证成功的响应
- 6.3.1 成功结果处理
- 6.3.2 SecurityFilterChain配置
- 6.4 认证失败响应
- 6.4.1 失败结果处理
- 6.4.2 SecurityFilterChain配置
- 6.5 注销响应
- 6.5.1 注销结果处理
- 6.5.2 SecurityFilterChain配置
- 6.6 请求未认证的接口
- 6.6.1 实现AuthenticationEntryPoint接口
- 6.6.2 SecurityFilterChain配置
- 6.7 跨域
- 七.身份认证
- 7.1 身份认证信息
- 7.2 会话并发处理
- 实现处理器接口
- SecurityFilterChain配置
- 八.授权
- 基于request的授权
- 用户-权限-资源
- 配置权限
- 授予权限
- 请求未授权的接口
- 用户-角色-资源
- 配置角色
- 授予角色
- 用户-角色-权限-资源
- 基于方法的授权
- 开启方法授权
- 给用户授予角色和权限
- 常用授权注解
- 九.Spring Security OAuth2
- 9.1 OAuth2介绍
- 9.1.1 角色介绍
- 9.1.2 OAuth2怎么用
- 9.1.3 OAuth2的授权模式
- 9.2 为什么要用OAuth2
一.什么是SpringSecurity
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实上的标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求。
二.SpringSecurity的特征
● spring-security对spring整合较好,使用起来更加方便;
● 有更强大的spring社区进行支持;
● 支持第三方的 oauth 授权,官方网站:spring-security-oauth
三.SpringSecurity的第一个例子
3.1 创建SpringBoot项目
项目名:security-demo
JDK:17
SpringBoot:3.2.0(依赖了Spring Security 6.2.0)
Dependencies:Spring Web、Spring Security、Thymeleaf
3.2 创建IndexController
@Controller
public class IndexController {@GetMapping("/")public String index() {return "index";}
}
3.3 创建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>
3.4 启动项目
浏览器中访问:http://localhost:8080/
输入用户名:user
输入密码:在控制台的启动日志中查找初始的默认密码
点击"Sign in"进行登录,浏览器就跳转到了index页面
3.5 Spring Security默认做了什么
我们会讲述为什么,我们启动项目之后,springsecurity会发生什么?具体的思路如下:
- 当用户登录时,前端将用户输入的用户名、密码信息传输到后台,后台用一个类对象将其封装起来,通常使用的是UsernamePasswordAuthenticationToken这个类。
- 程序负责验证这个类对象。验证方法是调用Service根据username从数据库中取用户信息到实体类的实例中,比较两者的密码,如果密码正确就成功登陆,同时把包含着用户的用户名、密码、所具有的权限等信息的类对象放到SecurityContextHolder(安全上下文容器,类似Session)中去。
- 用户访问一个资源的时候,首先判断是否是受限资源。如果是的话还要判断当前是否未登录,没有的话就跳到登录页面。
- 如果用户已经登录,访问一个受限资源的时候,程序要根据url去数据库中取出该资源所对应的所有可以访问的角色,然后拿着当前用户的所有角色一一对比,判断用户是否可以访问(这里就是和权限相关)。
但SpringSecurity会帮我做什么事情呢?具体的SpringSecurity会帮我们做的事情如下:
- 保护应用程序URL,要求对应用程序的任何交互进行身份验证。
- 程序启动时生成一个默认用户“user”。
- 生成一个默认的随机密码,并将此密码记录在控制台上。
- 生成默认的登录表单和注销页面。
- 提供基于表单的登录和注销流程。
- 对于Web请求,重定向到登录页面;
- 对于服务请求,返回401未经授权。
- 处理跨站请求伪造(CSRF)攻击。
- 处理会话劫持攻击。
- 写入Strict-Transport-Security以确保HTTPS。
- 写入X-Content-Type-Options以处理嗅探攻击。
- 写入Cache Control头来保护经过身份验证的资源。
- 写入X-Frame-Options以处理点击劫持攻击。
四.SpringSecurity的整体架构
具体可以先参考官网:Spring Security的底层原理
Spring Security之所以默认帮助我们做了那么多事情,它的底层原理是传统的Servlet过滤器
4.1 Filter
下图展示了处理一个Http请求时,过滤器和Servlet的工作流程:
因此我们可以在过滤器中对请求进行修改或增强。
4.2 DelegatingFilterProxy
Spring 提供了一个Filter名为 的实现DelegatingFilterProxy,允许在 Servlet 容器的生命周期和 Spring 的ApplicationContext. Servlet容器允许Filter使用自己的标准注册实例,但它不知道Spring定义的Bean。您可以DelegatingFilterProxy通过标准 Servlet 容器机制进行注册,但将所有工作委托给实现Filter.
4.3 FilterChainProxy
Spring Security 的 Servlet 支持包含在FilterChainProxy. FilterChainProxy是 Spring Security 提供的特殊功能Filter,允许Filter通过 委托给许多实例SecurityFilterChain。由于FilterChainProxy是一个 Bean,因此它通常包装在DelegatingFilterProxy中。
4.4 SecurityFilterChain
SecurityFilterChainFilterChainProxy 使用它来确定Filter应为当前请求调用哪些 Spring
4.5 Multiple SecurityFilterChain
可以有多个SecurityFilterChain的配置,FilterChainProxy决定使用哪个SecurityFilterChain。如果请求的URL是/api/messages/,它首先匹配SecurityFilterChain0的模式/api/**,因此只调用SecurityFilterChain 0。假设没有其他SecurityFilterChain实例匹配,那么将调用SecurityFilterChain n。
五.Spring Security自定义配置
5.1 基于内存的用户认证
实际开发的过程中,我们需要应用程序更加灵活,可以在SpringSecurity中创建自定义配置文件。
官方文档:Java自定义配置
UserDetailsService用来管理用户信息,InMemoryUserDetailsManager是UserDetailsService的一个实现,用来管理基于内存的用户信息。
5.1.1 自定义配置
@Configuration
@EnableWebSecurity//Spring项目总需要添加此注解,SpringBoot项目中不需要
public class WebSecurityConfig {@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser( //此行设置断点可以查看创建的user对象User.withDefaultPasswordEncoder().username("huan") //自定义用户名.password("password") //自定义密码.roles("USER") //自定义角色.build());return manager;}
}
5.1.2 基于内存的用户认证流程
- 程序启动时:
- 创建
InMemoryUserDetailsManager
对象 - 创建
User
对象,封装用户名密码 - 使用InMemoryUserDetailsManager
将User存入内存
- 创建
- 校验用户时:
- SpringSecurity自动使用
InMemoryUserDetailsManager
的loadUserByUsername
方法从内存中
获取User对象 - 在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中将用户输入的用户名密码和从内存中获取到的用户信息进行比较,进行用户认证
- SpringSecurity自动使用
5.2 基于数据库的数据源
5.2.1 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);
5.2.2 引入依赖
<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>
5.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
5.2.4 实体类
@Data
public class User {@TableId(value = "id", type = IdType.AUTO)private Integer id;private String username;private String password;private Boolean enabled;
5.2.5 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.demo.securitydemo.mapper.UserMapper"></mapper>
5.2.6 Service
public interface UserService extends IService<User> {
}
实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
5.2.7 Controller
@RestController
@RequestMapping("/user")
public class UserController {@Resourcepublic UserService userService;@GetMapping("/list")public List<User> getList(){return userService.list();}
}
5.3 基于数据库的用户认证
5.3.1 基于数据库的用户认证流程
- 程序启动时:
- 创建
DBUserDetailsManager
类,实现接口 UserDetailsManager, UserDetailsPasswordService - 在应用程序中初始化这个类的对象
- 创建
- 校验用户时:
- SpringSecurity自动使用
DBUserDetailsManager
的loadUserByUsername
方法从数据库中
获取User对象 - 在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证
- SpringSecurity自动使用
5.3.2 定义DBUserDetailsManager
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {@Resourceprivate UserMapper userMapper;@Overridepublic 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); //权限列表}}@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {return null;}@Overridepublic void createUser(UserDetails user) {}@Overridepublic void updateUser(UserDetails user) {}@Overridepublic void deleteUser(String username) {}@Overridepublic void changePassword(String oldPassword, String newPassword) {}@Overridepublic boolean userExists(String username) {return false;}
}
5.3.3 初始化UserDetailsService
修改WebSecurityConfig中的userDetailsService方法如下
@Bean
public UserDetailsService userDetailsService() {DBUserDetailsManager manager = new DBUserDetailsManager();return manager;
}
5.4 SpringSecurity的默认配置
在WebSecurityConfig中添加如下配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {//authorizeRequests():开启授权保护//anyRequest():对所有请求开启授权保护//authenticated():已认证请求会自动被授权http.authorizeRequests(authorize -> authorize.anyRequest().authenticated()).formLogin(withDefaults())//表单授权方式.httpBasic(withDefaults());//基本授权方式return http.build();
}
5.5 添加用户功能
5.5.1 Controller
@PostMapping("/add")
public void add(@RequestBody User user){userService.saveUserDetails(user);
}
5.5.2 Service
UserService接口中添加方法
void saveUserDetails(User user);
UserServiceImp实现方法
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Resourceprivate DBUserDetailsManager dbUserDetailsManager;@Overridepublic void saveUserDetails(User user) {UserDetails userDetails = org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder()//使用默认的密码加密方案.username(user.getUsername()) //自定义用户名.password(user.getPassword()) //自定义密码.build();dbUserDetailsManager.createUser(userDetails);}}
5.5.3 修改配置
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);
}
5.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
5.6 密码加密算法
参考文档
明文密码:
最初,密码以明文形式存储在数据库中。但是恶意用户可能会通过SQL注入等手段获取到明文密码,或者程序员将数据库数据泄露的情况也可能发生。
Hash算法:
Spring Security的PasswordEncoder
接口用于对密码进行单向转换
,从而将密码安全地存储。对密码单向转换需要用到哈希算法
,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密
。
因此,数据库中存储的是单向转换后的密码
,Spring Security在进行用户身份验证时需要将用户输入的密码进行单向转换,然后与数据库的密码进行比较。
因此,如果发生数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并且在给定哈希的情况下只能通过暴力破解的方式猜测密码
。
5.6.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), "密码不一致");
}
5.7 定义登录页面
5.7.1 创建Controller
@Controller
public class LoginController {@GetMapping("/login")public String login() {return "login";}
}
5.7.2 准备登录页面
<!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>
5.7.3 配置SecurityFilterChain
SecurityConfiguration:
.formLogin( form -> {form.loginPage("/login").permitAll() //登录页面无需授权即可访问.usernameParameter("username") //自定义表单用户名参数,默认是username.passwordParameter("password") //自定义表单密码参数,默认是password.failureUrl("/login?error") //登录失败的返回地址;
}); //使用表单授权方式
六. 前后端分离
表单登录配置模块提供了successHandler()和failureHandler()两个方法,分别处理登录成功和登录失败的逻辑。其中,successHandler()方法带有一个Authentication参数,携带当前登录用户名及其角色等信息;而failureHandler()方法携带一个AuthenticationException异常参数。具体处理方式需按照系统的情况自定义。
6.1 认证流程
- 登录成功后调用:AuthenticationSuccessHandler
- 登录失败后调用:AuthenticationFailureHandler
6.2 引入fastjson
<dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.37</version>
</dependency>
6.3 认证成功的响应
6.3.1 成功结果处理
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic 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);}
}
6.3.2 SecurityFilterChain配置
form.successHandler(new MyAuthenticationSuccessHandler()) //认证成功时的处理
6.4 认证失败响应
6.4.1 失败结果处理
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic 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);}
}
6.4.2 SecurityFilterChain配置
form.failureHandler(new MyAuthenticationFailureHandler()) //认证失败时的处理
6.5 注销响应
6.5.1 注销结果处理
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic 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);}
}
6.5.2 SecurityFilterChain配置
http.logout(logout -> {logout.logoutSuccessHandler(new MyLogoutSuccessHandler()); //注销成功时的处理
});
6.6 请求未认证的接口
6.6.1 实现AuthenticationEntryPoint接口
Servlet Authentication Architecture :: Spring Security
当访问一个需要认证之后才能访问的接口的时候,Spring Security会使用AuthenticationEntryPoint
将用户请求跳转到登录页面,要求用户提供登录凭证。
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic 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);}
}
6.6.2 SecurityFilterChain配置
//错误处理
http.exceptionHandling(exception -> {exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());//请求未认证的接口
});
6.7 跨域
跨域全称是跨域资源共享(Cross-Origin Resources Sharing,CORS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。
在SpringSecurity中解决跨域很简单,在配置文件中添加如下配置即可
//跨域
http.cors(withDefaults());
七.身份认证
7.1 身份认证信息
- 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(用户的凭证信息)。
在Spring Security框架中,SecurityContextHolder、SecurityContext、Authentication、Principal和Credential是一些与身份验证和授权相关的重要概念。它们之间的关系如下:
7.2 会话并发处理
后登录的账号会使先登录的账号失效
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {@Overridepublic 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);}
}
实现处理器接口
实现接口SessionInformationExpiredStrategy
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {@Overridepublic 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中非常灵活,可以帮助应用程序实现以下两种常见的授权需求:
-
用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表
-
用户-角色-权限-资源:例如 张三是角色是管理员、李四的角色是普通用户,管理员能做所有操作,普通用户只能查看信息
基于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() {@Overridepublic String getAuthority() {return "USER_LIST";}
});
authorities.add(new GrantedAuthority() {@Overridepublic 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);});
});
用户-角色-资源
**需求:**角色为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();
用户-角色-权限-资源
RBAC基于角色的权限访问控制(Role-Based Access Control)是商业系统中最常见的权限管理技术之一。RBAC是一种思想,任何编程语言都可以实现,其成熟简单的控制思想 越来越受广大开发人员喜欢。在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。
基于方法的授权
开启方法授权
DBUserDetailsManager中的loadUserByUsername方法:
在配置文件中添加如下注解
@EnableMethodSecurity
给用户授予角色和权限
//用户必须有 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);
}
常用授权注解
//用户必须有 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);
}
九.Spring Security OAuth2
9.1 OAuth2介绍
官网:https://oauth.net/2/
一种用于授权的开放标准,用于允许用户授权第三方应用访问其受保护的资源,而无需将其凭据直接提供给第三方应用。OAuth 2.0通过使用访问令牌来实现授权,该令牌由授权服务器颁发给第三方应用,以便访问用户受保护的资源。OAuth 2.0还提供了一种用于验证用户身份和授权的流程,包括重定向用户到授权服务器以获取授权码,然后交换该授权码以获取访问令牌。OAuth 2.0已经成为许多互联网服务和应用程序的标准授权机制,包括社交媒体平台、API服务和移动应用程序。
9.1.1 角色介绍
- 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。
- 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。
- 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。
- 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。
9.1.2 OAuth2怎么用
OAuth2是目前最流行的授权协议,用来授权第三方应用,获取用户数据。 举个例子:快递员想要进入小区,有3种方式。1是业主远程开门,2是业主告诉门禁密码,3是使用令牌(Oauth2)。
令牌和密码的区别:令牌相当于火车票,密码相当于是钥匙。
● 令牌是短期的,自动失效。密码是长期有效。
● 令牌是可以撤销的,撤销立即生效。密码一般不允许他们撤销。
● 令牌有权限范围,如车票座位为10车A15座。密码一般是完整权限。
第三方登录演示(网易云客户端利用QQ扫码登录)
网易云客使用QQ扫码登录中Oauth2协议各个角色扮演者
● Rrsource Owner: 用户
● Client: 网易云
● Authorization Server: QQ
● Resource Server: QQ
● User Agent: 浏览器
9.1.3 OAuth2的授权模式
具体可以参考下面两个大佬的文章:
RFC6749:
RFC 6749 - The OAuth 2.0 Authorization Framework (ietf.org)
阮一峰:
OAuth 2.0 的四种方式 - 阮一峰的网络日志
Oauth2授权模式:
● 授权码模式:最完整和严谨的授权模式,第三方平台登录都是该模式。安全性最高。
● 简化模式:省略授权码阶段,客户端是纯静态页面采用该模式。安全性高。
● 密码模式:把用户名密码告诉客户端,对客户端高度信任,比如客户端和认证服务器是同一公司。安全性一般。
● 客户端模式:直接因客户端名义申请令牌,很少有。安全性最差。
9.2 为什么要用OAuth2
● cookie是不能跨域的,前后端分离分布式架构实现多系统SSO非常困难。
● 移动端应用没有cookie,所以对于移动端支持不友好。
● token基于header传递,部分解决了CSRF攻击。
● token比sessionID大,客户端存储在Local Storage中,可以直接被JS读取