# Spring-SpringSecurity
**Repository Path**: zhang-purui/spring-spring-security
## Basic Information
- **Project Name**: Spring-SpringSecurity
- **Description**: Spring 整合SpringSecurity
- **Primary Language**: Java
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2021-08-25
- **Last Updated**: 2022-12-05
## Categories & Tags
**Categories**: Uncategorized
**Tags**: Spring, SpringSecurity
## README
# Spring 整合SpringSecurity
## 0.背景
很多东西使用都会,但是知识点都比较零散,通过查阅资料,学习 总结下知识点。
此 案例 总结了常用到的 SpringSecurity 的知识点。
## 1.搭建 Demo
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。相关文档 🚪=》https://docs.spring.io/spring-security/site/docs/current/reference/html5/
### 1.1 maven 依赖 pom.xml
```xml
4.0.0
com.itheima
spring.spring.security
1.0-SNAPSHOT
org.apache.maven.plugins
maven-compiler-plugin
6
6
war
org.springframework.security
spring-security-taglibs
5.5.2
org.springframework.security
spring-security-config
5.5.2
org.springframework
spring-web
org.springframework
spring-jdbc
5.1.6.RELEASE
org.slf4j
slf4j-log4j12
1.7.26
org.springframework
spring-webmvc
5.1.6.RELEASE
mysql
mysql-connector-java
5.1.47
org.mybatis
mybatis
3.5.1
org.mybatis
mybatis-spring
2.0.1
javax.servlet
javax.servlet-api
4.0.1
provided
javax.servlet
jsp-api
2.0
provided
jstl
jstl
1.2
org.projectlombok
lombok
1.18.8
javax.annotation
jsr250-api
1.0
```
### 1.2 learing_security.sql
```mysql
/*
Navicat Premium Data Transfer
Source Server : local_maria
Source Server Type : MariaDB
Source Server Version : 100604
Source Host : localhost:3306
Source Schema : learing_security
Target Server Type : MariaDB
Target Server Version : 100604
File Encoding : 65001
Date: 30/08/2021 22:10:44
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for persistent_logins
-- ----------------------------
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of persistent_logins
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`permission_NAME` varchar(30) DEFAULT NULL COMMENT '菜单名称',
`permission_url` varchar(100) DEFAULT NULL COMMENT '菜单地址',
`parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单id',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` VALUES (1, 'ROLE_USER', '测试角色');
INSERT INTO `sys_role` VALUES (6, 'ROLE_ADMIN', '超级管理员');
INSERT INTO `sys_role` VALUES (7, 'ROLE_PRODUCT', '产品');
INSERT INTO `sys_role` VALUES (8, 'ROLE_ORDER', '订单');
COMMIT;
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`RID` int(11) NOT NULL COMMENT '角色编号',
`PID` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`,`PID`),
KEY `FK_Reference_12` (`PID`),
CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL COMMENT '用户名称',
`password` varchar(120) NOT NULL COMMENT '密码',
`status` int(1) DEFAULT 1 COMMENT '1开启0关闭',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 'test', '$2a$10$0AFPibMeFbsCdufbt6ZDweaCAebm8VY1CPCLJJKGmfrIjzX2NM4L.', 1);
INSERT INTO `sys_user` VALUES (2, 'zpr', '$2a$10$0AFPibMeFbsCdufbt6ZDweaCAebm8VY1CPCLJJKGmfrIjzX2NM4L.', 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`,`RID`),
KEY `FK_Reference_10` (`RID`),
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_user_role` VALUES (1, 6);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
```
### 1.3 配置 spring 容器
```xml
```
### 1.4 配置 spring-mvc
```xml
```
### 1.5 配置 spring-security
```xml
```
### 1.6 web.xml
```xml
Archetype Created Web Application
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
encodingFilter
/*
springmvc
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring-mvc.xml
springmvc
/
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
classpath:applicationContext.xml
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
```
### 1.7 项目结构截图

### 1.8 结果展示
登陆页面,实现 SpringSecurity 的方式登陆并实现 **记住我** 功能

权限不足的判断,并自定义了 403 页面。
通过 security 提供的taglibs 标签对 菜单进行显示控制。
post 表单 提供 csrf 跨站请求伪造 防范。

## 2.相关知识点
### 2.1 SpringSecurity 依赖模块
简单了解下 SpringSecurity 有哪些模块。
1. **spring-security-core**
- 本模块包含核心身份验证和访问控制类和接口、远程支持和基本预置API。它是任何使用Spring Security的应用程序所必需的。它支持独立应用程序、远程客户端、方法(服务层)安全性和JDBC用户预置。
2. spring-security-remoting
- 本模块提供与Spring Remoting的集成。除非您正在编写使用Spring Remoting的远程客户端,否则您不需要此客户端。
3. **spring-security-web**
- 如果您需要Spring Security Web身份验证服务和基于URL的访问控制,则需要它。
4. **spring-security-config**
- 本模块包含安全命名空间解析代码和Java配置代码。如果您使用Spring Security XML命名空间进行配置或Spring Security的Java配置支持,则需要它。
5. spring-security-ldap
- 本模块提供LDAP身份验证和预置代码。如果您需要使用LDAP身份验证或管理LDAP用户条目,这是必需的。
6. spring-security-oAuth2-core
- 包含为OAuth 2.0授权框架和OpenID Connect Core 1.0提供支持的核心类和接口。它是使用OAuth 2.0或OpenID Connect Core 1.0的应用程序所必需的,例如客户端、资源服务器和授权服务器。
7. spring-security-oAuth2-client
- 包含Spring Security对OAuth 2.0授权框架和OpenID Connect Core 1.0的客户端支持。它是使用OAuth 2.0登录或OAuth客户端支持的应用程序所必需的。
8. spring-security-oAuth2-jose
- 包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。JOSE框架旨在为当事人之间安全转移索赔提供一种方法。
9. spring-security-oAuth2-resource-server
- 包含Spring Security对OAuth 2.0资源服务器的支持。它用于通过OAuth 2.0承载令牌保护应用编程接口。
10. spring-security-acl
- 本模块包含一个专门的域对象ACL实现。它用于将安全性应用于应用程序中的特定域对象实例。
11. spring-security-cas
- 本模块包含Spring Security的CAS客户端集成。如果您想将 Spring Security Web 身份验证与 CAS 单点登录服务器一起使用,则应使用它。
12. spring-security-openid
- 本模块包含OpenID Web身份验证支持。它用于针对外部OpenID服务器验证用户。
13. spring-security-test
- 本模块支持使用Spring Security进行测试。
14. **spring-security-taglibs**
- 提供Spring Security的JSP标签实现。
### 2.2 过滤链
SpringSecurity 的安全🔐控制是通过 Servlet Filter 来实现。 client 的请求 经过一层一层的 filter 最终达到 处理请求的 Servlet


在 web.xml 中配置了 DelegatingFilterProxy 这是一个Spring Web 提供的 过滤器委托代理,我们在 filter-name 中指定了 被代理的 bean name 是 springSecurityFilterChain ,这是固定写法,不能写错。


initFilterBean 的初始化Filter Bean方法会把 web.xml 配置的 bean name 拿到,并根据它从WebApplicationContext 上下文中获取对应的 bean FilterChainProxy
```java
public class FilterChainProxy extends GenericFilterBean {
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
//标记过滤器是否已经执行过
private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
//注意⚠️看这里 过滤器链 可以有多个
private List filterChains;
//省略了 不关心的代码
//进行内部过滤
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//根据请求获取对应 过滤链中的 filter 列表
List filters = this.getFilters((HttpServletRequest)fwRequest);
if (filters != null && filters.size() != 0) {
//创建一个虚拟的过滤器链
FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
} else {
//这里是 没有获取到对应过滤器的情况,直接放行,比如我们的静态资源,在配置中配置了放行,就不会有filter 拦截
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
}
}
}
```
下面可以看到获取到springSecurity 提供的默认的 过滤器列表,对filers 做了一层封装 是 FilterChainProxy 的子类 VirtualFilterChain 然后执行 doFilter 方法 。
ps: 默认应该有15个,因为我把 csrf 关闭了

```java
private static class VirtualFilterChain implements FilterChain {
//原生的过滤器链
private final FilterChain originalChain;
//过滤器列表
private final List additionalFilters;
private final FirewalledRequest firewalledRequest;
//过滤器集合 size
private final int size;
//过滤器集合执行到什么位置了,主要用于记录位置
private int currentPosition;
//上面通过此构造 把 过滤器链和 默认的过滤器传入。
private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List additionalFilters) {
this.currentPosition = 0;
this.originalChain = chain;
this.additionalFilters = additionalFilters;
//默认 肯定不为0
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
//表示过滤器链已经执行完成了
if (FilterChainProxy.logger.isDebugEnabled()) {
FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain");
}
this.firewalledRequest.reset();
//调用originalChain.doFilter 进入原生过滤链
this.originalChain.doFilter(request, response);
} else {
++this.currentPosition;
Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
if (FilterChainProxy.logger.isDebugEnabled()) {
FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");
}
//执行当前过滤器,这里,因为当前 VirtualFilterChain 实现了 FilterChain
//这里又指定了 过滤链 是 this,所以 当前的 filter list 会递归进入此方法。
nextFilter.doFilter(request, response, this);
}
}
}
```
梳理一下流程:
1. web.xml 配置 DelegatingFilterProxy 并指定 代理的 bean name 为 springSecurityFilterChain
2. 打断点可得-----通过 第一步的bean name 得到 FilterChainProxy 过滤器链代理
3. 执行 FilterChainProxy 的 doFilterInternal 方法 根据请求获得 相关的 Filter
4. 传入获得的 Filter list 使用 FilteChainProxy 的内部类 VirtualFilterChain 的 doFilter 执行真正的 filter 的 doFilter 操作。
ps:过滤器链可以有多个。
### 2.3 认证流程
SpringSecurity 的表单登陆 是基于 UsernamePasswordAuthencationFilter 完成。
```java
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//指定前端传参
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
//指定 登陆请求 地址和请求方式
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//删除多余代码
//把前端传的账号密码 封装成 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//获取认证管理器,调用认证方法
return this.getAuthenticationManager().authenticate(authRequest);
}
}
```
```java
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
//提供 认证的 提供商
private List providers;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
//根据封装的 authentication 类型来找到对应的 认证提供商
//这里我们封装的 authentication 是 UsernamePasswordAuthcationFilter
if (provider.supports(toTest)) {
try {
result = provider.authenticate(authentication);
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
//删除了多余的代码
return result;
}
```
认证提供商是 AbstractUserDetailsAuthenticationProvider
```java
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { //认证方法 public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); //调用 retrieveUser 方法认证,但 当前类 并没有实现 此方法,默认的实现类是 DaoAuthenticationProvider user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); return this.createSuccessAuthentication(principalToReturn, authentication, user); } }
```
实际上调用的是 DaoAuthenticationProvider 的 retrieveUser 方法实现认证
```java
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; private PasswordEncoder passwordEncoder; private volatile String userNotFoundEncodedPassword; private UserDetailsService userDetailsService; private UserDetailsPasswordService userDetailsPasswordService; protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { //看到 loadUserByUsername 如果用过 SpringSecurity 的朋友 就很熟悉了 //这一步是要把 我们自己的用户系统接入 SpringSecurity,所有我们需要去 实现 UserDetailsService 并实现 //loadUserByUsername(username) 方法 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } } }
```
```java
@Service@Transactionalpublic class UserServiceImpl implements UserService { @Autowired private UserDao userDao; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userDao.findByName(username); if(null == sysUser){ //返回空,表示认证失败。 return null; } List authorities = new ArrayList(); //动态获取权限 List roles = sysUser.getRoles(); for (SysRole role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRoleName())); } authorities.add(new SimpleGrantedAuthority("ROLE_USER")); //{noop} 表示密码是明文,加了 passwordEncoder 就不需要了// return new User(sysUser.getUsername(),"{noop}"+sysUser.getPassword(),authorities); //enable:账号是否启用,这里使用 enable 参数判断账号是否启用 //accountNonExpired:账号没有过期? //credentialsNonExpired:密码没有过期? //accountNonLocked:账号没有锁定? //需要 把自定义的 SysUser 转换成 SpringSecurity 的 User 对象 return new User(sysUser.getUsername(), sysUser.getPassword(), sysUser.getStatus()==1, true, true, true, authorities); }}
```
ok 到这一步,已经实现了根据 传入的用户名查询出 用户信息,下面看看 如何对返回的用户信息处理
```java
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = this.determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { //这里返回用户信息 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { } } try { //这里对 用户是否锁定/是否账号过期/是否密码过期 等进行检查 this.preAuthenticationChecks.check(user); //此方法 对比 密码 是否正确 this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } //此方法会把 认证完的用户信息 重新 封装成 一个 UsernamePasswordAuthenticationToken 对象 返回 return this.createSuccessAuthentication(principalToReturn, authentication, user); } //封装 UsernamePasswordAuthenticationToken 对象 protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); this.logger.debug("Authenticated user"); return result; } }
```
在返回到 ProviderManager 最终 在 AbstractAuthenticationProcessingFilter 的下面的方法,做 成功响应 和 失败响应。
```java
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//处理返回 重新封装回来的 UsernamePasswordAutehcationFilter
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功响应
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
//认证失败响应
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
//认证失败响应
unsuccessfulAuthentication(request, response, ex);
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
//成功 把 用户信息 放入 Security 的上下文中
SecurityContextHolder.getContext().setAuthentication(authResult);
//记住我功能的相关处理
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
//观察者模式,发布事件消息
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
//使用successHandler 触发 成功的处理策略
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
```
```java
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
//ExceptionTranslationFilter 在认证开始前把 request 缓存起来
//获取请求缓存,可以跳转回 用户登陆前 想访问的 url
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
//跳转 指定的成功页面
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
```
流程梳理:
1. UsernamePasswordAuthenticationFilter 拦截到 /login 请求,执行 attemptAuthentication 方法把账号密码封装成 UsernamePasswordAuthenticationToken 对象 传入 ProviderManager authenticate 方法
2. ProviderManager authenticate 方法中 通过循环比对的方式 找到合适的 认证提供商--AbstractUserDetailsAuthenticationProvider
3. AbstractUserDetailsAuthenticationProvider 的 认证方法调用 DaoAuthenticationProvider 的 retrieveUser 方法
4. DaoAuthenticationProvider最终会调用 UserDetailsService(需要我们来实现,整合用户体系) 来返回符合条件的用户
5. AbstractUserDetailsAuthenticationProvider 中会对比密码,并把 认证成功的对象 重新封装成 UsernamePasswordAuthenticationToken对象
6. 最终 重新封装的 UsernamePasswordAuthenticationToken 会返回到 UsernamePasswordAuthenticationFilter,做出成功或失败的响应操作。
### 2.4 记住我功能实现
在 spring-security.xml 的配置文件中 配置一下,数据源,过期时间,字段名称(默认为 remember-me)
```xml
```
登陆页面
```html
```
勾选记住我登陆后,后台会产生一个 cookie,数据库 persistent_logins 表会产生一条新纪录,因为我们配置了数据源,如果不配置数据源 将不会写入数据库,表结构和名称是固定的

### 2.5 csrf 跨站请求伪造 防护机制
什么是跨站请求伪造=》 https://baike.baidu.com/item/跨站请求伪造/13777878?fr=aladdin
SpringSecurity 默认开启csrf 验证,所有页面请求的地方都需要 填写 csrf token,如果没有 将返回403 权限拒绝❌,下面在源码中 我们可以看到。
当使用了 jsp 时,SpringSecurity 默认提供了 jsp 的标签库
首先需要先引入标签库
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
使用:
```html
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
数据 - AdminLTE2定制版 | Log in
```
csrf 功能 通过 CsrfFilter 提供,可以通过配置文件 配置
```xml
```
```java
public final class CsrfFilter extends OncePerRequestFilter { //默认的 匹配器 private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final HashSet allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); @Override public boolean matches(HttpServletRequest request) { return !this.allowedMethods.contains(request.getMethod()); } @Override public String toString() { return "CsrfNotRequired " + this.allowedMethods; } } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this.tokenRepository.loadToken(request); boolean missingToken = (csrfToken == null); if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); //post,put,delete 等触发修改的请求 都将被拦截 if (!this.requireCsrfProtectionMatcher.matches(request)) { if (this.logger.isTraceEnabled()) { this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher); } //"GET", "HEAD", "TRACE", "OPTIONS" 直接放行 filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { this.logger.debug( LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request))); AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken); //csrf token 传的有问题,将返回权限拒绝错误🙅 this.accessDeniedHandler.handle(request, response, exception); return; } filterChain.doFilter(request, response); }
```
### 2.6 SpringSecurity taglibs jsp 标签库
文档 🚪=》https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/htmlsingle/#taglibs
它为访问安全信息和在JSP中应用安全约束提供了基本支持。
使用需要导入 maven依赖
```xml
org.springframework.security
spring-security-taglibs
5.5.2
```
在 jsp 中导入 taglib 标签库
```xml
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>
```
授权校验
```xml
产品管理
```
csrf token 支持
```html
```
页面上显示用户信息
```html
<%----%>
```
### 2.7 自定义登陆界面
默认SpringSecurity 提供了 默认的登陆界面
我们可以通过配置的方式 指定我们自己的 登陆页面 和 注销操作
```
```
### 2.8 处理全局异常
每当 SpringSecurity 出现 状态吗异常 都是原生的丑陋界面
受不了,用户更受不了,所以需要定义 自己的错误页面。

配置:
```xml
```

定义 Spring-mvc 全局异常类,根据异常跳转对应页面。
```java
package com.learingsecurity.controller.advice;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @className ControllerExceptionAdvice
* @Desc TODO 全局异常类
* @Author 张埔枘
* @Date 2021/8/28 9:11 下午
* @Version 1.0
*/
@ControllerAdvice
public class ControllerExceptionAdvice {
/**
* 拦截 403 异常 跳转到 403 页面
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
public String exception403(){
return "forward:/403.jsp";
}
}
```
### 2.9 加密方式
文档 🚪 =》 https://docs.spring.io/spring-security/site/docs/current/reference/html5/#authentication-password-storage
我们常用的是 BCryptPasswordEncoder 类,采用动态盐的方式加强密码的安全性。
使用需要把它放入 spring 的容器中
```xml
```
### 3.0 放行静态资源
在开发早期,单体项目时,前端代码和后端代码是在一起的。
那么SpringSecurity 对接口拦截的同时,也可能会对 静态资源拦截。所以需要 处理一下,不拦截静态资源
```xml
```
源码:https://gitee.com/zhang-purui/spring-spring-security 欢迎 Start 😄😄😄