Appearance
Spring Security-初认识
Spring Security 是Spring官方提供的安全框架,提供了身份验证、授权和防止常见攻击功能。
先读Spring security的整体框架,了解大致的工作流程:https://docs.spring.io/spring-security/reference/servlet/architecture.html
参考视频:
1. 架构介绍
Spring Security通过servlet技术栈中的过滤器Filter发挥作用的,根据不同的路径配置不同的过滤器链。
过滤器链中每个过滤器作用不同,并按照次序依次发挥作用,过滤器的顺序按照功能排序的话,大致顺序可以认为是防止常见攻击、身份验证和授权。

2. 环境搭建与引入依赖
首先在Spring Boot项目中搭建web环境,只需要简单的Controller:
java
@RestController
public class HelloController {
@GetMapping("/hello")
public R hello(){
return R.ok("hello spring security");
}
}java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class R<T> {
private int code;
private String msg;
@JsonRawValue
private T data;
public static <T> R ok(T data){
return new R<T>(1, "success", data);
}
public static <T> R error(String msg){
return new R<T>(-1, msg, null);
}
}然后引入Spring Security依赖:
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>当我们使用浏览器访问'/hello'路径时,会跳转到登录界面:

用户名是user,密码在控制台中有打印。输入账号密码后,会显示接口'/hello'的结果。
3. 为什么?
在上面的实验中,仅仅是引入了Spring Security 依赖,整个程序就被保护起来了,Spring Security做了什么?
首先,再次明确Spring Security 是通过servlet技术栈中的过滤器Filter起作用的,换句话说,Spring Security引入了一系列过滤器。
我们可以通过在application.yml中更改日志打印级别:
yaml
logging:
level:
org:
springframework:
security: trace可以配置账号密码,这样就不用使用Spring Security 自动生成的一长串密码:
javaspring: security: user: name: test password: test
这样,在程序启动时会输出以下的内容:
txt
Will secure any request with filters:
DisableEncodeUrlFilter,
WebAsyncManagerIntegrationFilter,
SecurityContextHolderFilter,
HeaderWriterFilter,
CsrfFilter,
LogoutFilter,
UsernamePasswordAuthenticationFilter,
DefaultResourcesFilter,
DefaultLoginPageGeneratingFilter,
DefaultLogoutPageGeneratingFilter,
BasicAuthenticationFilter,
RequestCacheAwareFilter,
SecurityContextHolderAwareRequestFilter,
AnonymousAuthenticationFilter,
ExceptionTranslationFilter,
AuthorizationFilter3.1 问题一:请求的路径/hello,为什么地址栏变成了/login?
答案当然是浏览器进行了重定向:

可是重定向这个工作是怎么完成的呢?
首先,在FormLoginConfigurer初始化时,会调用父类AbstractAuthenticationFilterConfigurer的构造方法,其中设置了登录界面的URL为/login:
java
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
// 1. FormLoginConfigurer 构造方法
public FormLoginConfigurer() {
// 2. 调用父类AbstractAuthenticationFilterConfigurer构造方法
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
}
public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter>
extends AbstractHttpConfigurer<T, B> {
// 3. 构造方法-1
protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
// 4. 调用自身构造方法-2
this();
this.authFilter = authenticationFilter;
if (defaultLoginProcessingUrl != null) {
loginProcessingUrl(defaultLoginProcessingUrl);
}
}
// 5.构造方法-2
protected AbstractAuthenticationFilterConfigurer() {
setLoginPage("/login");
}
// 6.设置登录界面URL
private void setLoginPage(String loginPage) {
this.loginPage = loginPage;
// 7. LoginUrlAuthenticationEntryPoint 注意该类
this.authenticationEntryPoint = new LoginUrlAuthenticationEntryPoint(loginPage);
}
}在ExceptionTranslationFilter过滤器中,它的作用主要是处理身份认证和授权过程中出现的异常,我们从doFilter方法跟踪,然后进入handleAuthenticationException()方法查看身份认证失败的处理逻辑:
java
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
this.securityContextHolderStrategy.setContext(context);
this.requestCache.saveRequest(request, response);
// 我们的关注点
this.authenticationEntryPoint.commence(request, response, reason);
}还记得上面初始化的配置吗,authenticationEntryPoint是LoginUrlAuthenticationEntryPoint实例,我们查看其commence实现:
java
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
if (!this.useForward) {
// redirect to login page. Use https if forceHttps true
String redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
// 重定向
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
return;
}
// 以下代码省略
......
}在RedirectStrategy默认实现中,实现重定向逻辑:
java
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
// 1. 获取重定向地址
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
// 2. 状态码校验 HttpStatus.FOUND 就是302重定向
if (this.statusCode == HttpStatus.FOUND) {
// 直接重定向
response.sendRedirect(redirectUrl);
}
else {
response.setHeader(HttpHeaders.LOCATION, redirectUrl);
response.setStatus(this.statusCode.value());
response.getWriter().flush();
}
}所以这就解决了我们的问题,为什么请求的是/hello路径,但浏览器地址栏变成了/login。
3.2 问题二:登录界面是怎么来的?
浏览器重定向后,发起第二次请求/login。
在上面的拦截器中,有DefaultLoginPageGeneratingFilter,见名知意,该过滤器是用于创建默认的登录界面的,在其拦截方法中可以找到相关代码:
java
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 是否登录错误
boolean loginError = isErrorPage(request);
// 是否退出登录成功
boolean logoutSuccess = isLogoutSuccess(request);
// 当前请求的是登录界面 或 登录错误 或 退出登录成功
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
// 创建登录界面
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
// 写响应并返回
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
return;
}
// 如果不是上述情况,则放行
chain.doFilter(request, response);
}3.3 问题三:Spring Security是如何校验账号密码的?
我们关注两个过滤器:
UsernamePasswordAuthenticationFilter:用于校验表单登录;BasicAuthenticationFilter:用户请求头中Authorization方式登录;
并且,要在AuthorizationFilter的配合下发挥作用,我们在下一篇文章中讲解。
3.4 问题四:为什么登录成功后,返回了/hello路径的结果
主要流程如图:

- 首先浏览器访问
/hello路径,Spring Security身份验证失败,重定向到/login,并将初始请求URL保存到session中,在响应中设置session id; - 浏览器接收到重定向响应后,重新请求
/login(GET方式)页面,DefaultLoginPageGeneratingFilter过滤器返回登录界面; - 用户输入账号密码后,请求
/login(POST方式),UsernamePasswordAuthenticationFilter校验表单登录,如果登录成功,则获取请求头中携带的session id ,将session中保存的URL提取出来重定向;登录成功后,会创建一个新的session,然后将登录信息保存到新的session中,响应携带最新的session id; - 浏览器收到重定向响应后,重新请求
/hello接口,并携带最新的session id,Spring Security通过session id 校验用户是否登录,然后返回结果。