四、自定义认证流程详解
4.1 自定义资源权限规则
- /index 公共资源
- /hello 受保护资源
– 在项目中,如果要覆盖默认权限、授权自动配置,需要让 DefaultWebSecurityCondition 这个类失效
– 添加如下配置可以实现自定义对资源权限规则设置
package com.vinjcent.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index").permitAll() // 放行资源
.anyRequest().authenticated() // 【注意】所有放行的资源放在任何拦截请求前面
.and() // 链式编程,继续拼接
.formLogin();
}
}
# 说明
- permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问
- anyRequest().authenticated() 代表所有请求必须认证之后才能访问
- formLogin() 代表开启表单认证
# 【注意】放行资源必须放在所有认证请求之前!
4.2 自定义登录界面
根据前面分析可知,校验用户名密码是根据 UsernamePasswordAuthenticationFilter 这个过滤器执行的,要求
- 用户名参数:username
- 用户名密码:password
- 请求类型:POST
- 请求路径:/login
修改默认的登录配置信息
- 导入thymeleaf依赖
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
- 定义一个跳转到login.html页面的controller
package com.vinjcent.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
}
- 在 templates 中定义登陆界面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h1>用户登录</h1>
<form th:action="@{/login}" method="post">
用户名: <input type="text" name="uname"> <br>
密码: <input type="password" name="passwd"> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
【注意】当前login.html页面的文本输入框name参数以及请求路径可根据自定义修改,但必须与 WebSecurityConfiguration 的配置一致
- 配置 SpringSecurity 配置类
package com.vinjcent.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll() // 放行登录页面
.mvcMatchers("/index").permitAll() // 放行资源
.anyRequest().authenticated() // 【注意】所有放行的资源放在任何拦截请求前面
.and() // 链式编程,继续拼接
.formLogin()
.loginPage("/toLogin") // 指定登录页面,【注意】一旦指定登陆页面之后,必须指定登录请求url
.loginProcessingUrl("/login") // 指定处理登录请求的url
.usernameParameter("uname") // 指定认证用户名参数的名称
.passwordParameter("passwd") // 指定认证密码参数的名称
// .successForwardUrl("/hello") // 认证成功后跳转的路径(转发),始终在认证成功之后跳转到指定路径
// .defaultSuccessUrl("/index", true) // 认证成功之后的跳转(重定向),如果在访问权限资源之前被拦截,那么认证之后将会跳转之前先访问的资源
.and()
.csrf().disable(); // 禁止 csrf 跨站请求保护
}
}
- successForwardUrl 与 defaultSuccessUrl 这两个方法都可以实现成功之后跳转
- successForwardUrl 默认使用 dispatchForward 跳转。【注意】不会跳转到之前i请求路径
- defaultSuccessUrl 默认使用 redirect 跳转。【注意】如果之前请求路径被拦截,认证之后会优先跳转之前请求路径,可以传入第二个参数进行修改是否跳转之前请求路径,默认为false
4.3 自定义登录成功处理(前后端分离开发解决方案)
有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面(不是交由后端去做,而是前端去做)。此时,我们只需要给前端返回一个 JSON 通知登录成功还是失败。这个时候可以通过自定义接口
-
successHandler 认证成功处理
-
该处理函数中带有一个 AuthenticationSuccessHandler 接口参数
- 根据接口描述信息,得知登陆成功会自动回调这个方法,进一步查看它的实现类可以发现,successForwardUrl、defaultSuccessUrl 也是由它的子类进一步实现的
自定义 AuthenticationSuccessHandler 实现类
- 编写 DivAuthenticationSuccessHandler 实现接口 AuthenticationSuccessHandler
package com.vinjcent.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义认证成功之后处理
*/
public class DivAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg","登陆成功");
result.put("status", 200);
result.put("authentication", authentication);
response.setContentType("application/json;charset=UTF-8");
String info = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(info);
}
}
- 在 WebSecurityConfiguration 配置类中进行成功认证配置
package com.vinjcent.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/index", true)
.successHandler(new DivAuthenticationSuccessHandler()) // 认证成功时处理,前后端分离解决方案
.and()
.csrf().disable();
}
}
- 登录测试访问
4.4 显示登录失败的信息
为了能更直观在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。Spring Security 在登陆失败之后会将异常信息存储到 request
、session
作用域中,key 为 SPRING_SECURITY_LAST_EXCEPTION
命名属性
- SimpleUrlAuthenticationFailureHandler
- 这说明,不同配置会产生作用域不同的异常
验证失败信息
- 对应页面修改,显示异常处理
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h2>
<!--request作用域-->
<!--<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>-->
<!--session作用域-->
<!--<div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>-->
</h2>
<h1>用户登录</h1>
<form th:action="@{/login}" method="post">
用户名: <input type="text" name="uname"> <br>
密码: <input type="password" name="passwd"> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
- 在 WebSecurityConfiguration 配置类中进行失败认证配置
package com.vinjcent.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/index", true)
.successHandler(new DivAuthenticationSuccessHandler())
// .failureForwardUrl("/toLogin") // 认证失败后跳转的路径(转发)
// .failureUrl("/toLogin") // 认证失败后跳转的路径(重定向)
.and()
.csrf().disable();
}
}
- 任意选择页面的一种情况作用域渲染以及对应后台配置类选择转发/重定向,进行测试
4.5 自定义登陆失败处理(前后端分离开发解决方案)
-
failureHandler 认证失败处理
-
该处理函数中带有一个 AuthenticationFailureHandler 接口参数
- 根据接口描述信息,得知登陆失败会自动回调这个方法,进一步查看它的实现类可以发现,successForwardUrl、defaultSuccessUrl 也是由它的子类进一步实现的
自定义 DivAuthenticationFailureHandler 实现类
- 编写 DivAuthenticationFailureHandler 实现接口 AuthenticationFailureHandler
package com.vinjcent.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义认证失败之后处理
*/
public class DivAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg", "登陆失败: " + exception.getMessage());
result.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
String info = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(info);
}
}
- 在 WebSecurityConfiguration 配置类中进行失败认证配置
package com.vinjcent.config;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/index", true)
.successHandler(new DivAuthenticationSuccessHandler()) // 认证成功时处理,前后端分离解决方案
// .failureForwardUrl("/toLogin")
// .failureUrl("/toLogin")
.failureHandler(new DivAuthenticationFailureHandler()) // 认证失败时处理,前后端分离解决方案
.and()
.csrf().disable();
}
}
- 登录测试访问
4.6 注销登录
SpringSecurity 中也提供了默认的注销登录(访问**/logout**),在开发时可以按照自己的需求对注销进行个性化定制
- 开启注销登录(
默认开启
)
package com.vinjcent.config;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/index", true)
.successHandler(new DivAuthenticationSuccessHandler())
// .failureForwardUrl("/toLogin")
// .failureUrl("/toLogin")
.failureHandler(new DivAuthenticationFailureHandler())
.and()
.logout() // 注销登录的logout
.logoutUrl("/logout") // 指定注销的路径url 【注意】请求方式类型必须是GET
.invalidateHttpSession(true) // 默认开启,会话清除
.clearAuthentication(true) // 默认开启,清除认证标记
.logoutSuccessUrl("/toLogin") // 注销登录成功之后跳转的页面
.and()
.csrf().disable();
}
}
- 通过 logout() 方法开启注销配置
- logoutUrl 指定退出登录请求地址,默认为 GET 请求,路径为
logout
- invalidateHttpSession 退出时是否使 session 失效,默认值为 true
- clearAuthentication 退出时是否清除认证信息,默认值为 true
- logoutSuccessUrl 退出登录时跳转地址
- 配置多个注销登录请求
- 如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求方法
package com.vinjcent.config;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/index", true)
.successHandler(new DivAuthenticationSuccessHandler())
// .failureForwardUrl("/toLogin")
// .failureUrl("/toLogin")
.failureHandler(new DivAuthenticationFailureHandler())
.and()
.logout()
// .logoutUrl("/logout")
.logoutRequestMatcher(new OrRequestMatcher( // 配置多个注销登录的请求
new AntPathRequestMatcher("/aLogout", "GET"),
new AntPathRequestMatcher("/bLogout", "POST")
))
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/toLogin")
.and()
.csrf().disable();
}
}
- 添加一个
/logout
接口
package com.vinjcent.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
@RequestMapping("/toLogout")
public String toLogout() {
return "logout";
}
}
- 添加一个logout.html页面(验证POST请求注销)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>注销登录</title>
</head>
<body>
<h1>注销登录</h1>
<form th:action="@{/bLogout}" method="post">
<input type="submit" value="注销登录">
</form>
</body>
</html>
- 测试访问
4.7 注销登录配置(前后端分离解决方案)
- 前后端分离注销登录配置
如果是前后端分离开发,注销成功后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时可以通过自定义 LogoutSuccessHandler 实现类返回内容注销之后信息
-
logoutSuccessHandler 认证失败处理
-
该处理函数中带有一个 LogoutSuccessHandler 接口参数
- 根据接口描述信息,得知注销登陆成功会自动回调这个方法
自定义 DivLogoutSuccessHandler 实现类
- 编写 DivLogoutSuccessHandler 实现接口 LogoutSuccessHandler
package com.vinjcent.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义注销成功之后处理
*/
public class DivLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> result = new HashMap<>();
result.put("msg","注销成功,当前认证对象为:" + authentication);
result.put("status", 200);
response.setContentType("application/json;charset=UTF-8");
String info = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(info);
}
}
- 在 WebSecurityConfiguration 配置类中进行失败认证配置
package com.vinjcent.config;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/index", true)
.successHandler(new DivAuthenticationSuccessHandler())
// .failureForwardUrl("/toLogin")
// .failureUrl("/toLogin")
.failureHandler(new DivAuthenticationFailureHandler())
.and()
.logout()
// .logoutUrl("/logout")
// .logoutRequestMatcher(new OrRequestMatcher(
// new AntPathRequestMatcher("/aLogout", "GET"),
// new AntPathRequestMatcher("/bLogout", "POST")
//))
.logoutSuccessHandler(new DivLogoutSuccessHandler()) // 成功退出登录时,前后端分离解决方案
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/toLogin")
.and()
.csrf().disable();
}
}
- 注销登录测试访问
当前完整所有Security配置信息
package com.vinjcent.config;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import com.vinjcent.handler.DivLogoutSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll() // 放行登录页面
.mvcMatchers("/index").permitAll() // 放行资源
.anyRequest().authenticated() // 【注意】所有放行的资源放在任何拦截请求前面
.and() // 链式编程,继续拼接
.formLogin()
.loginPage("/toLogin") // 指定登录页面,【注意】一旦指定登陆页面之后,必须指定登录请求url
.loginProcessingUrl("/login") // 指定处理登录请求的url
.usernameParameter("uname") // 指定认证用户名参数的名称
.passwordParameter("passwd") // 指定认证密码参数的名称
// .successForwardUrl("/hello") // 认证成功后跳转的路径(转发),始终在认证成功之后跳转到指定路径
// .defaultSuccessUrl("/index", true) // 认证成功之后的跳转(重定向),如果在访问权限资源之前被拦截,那么认证之后将会跳转之前先访问的资源
.successHandler(new DivAuthenticationSuccessHandler()) // 认证成功时处理,前后端分离解决方案
// .failureForwardUrl("/toLogin") // 认证失败后跳转的路径(转发)
// .failureUrl("/toLogin") // 认证失败后跳转的路径(重定向)
.failureHandler(new DivAuthenticationFailureHandler()) // 认证失败时处理,前后端分离解决方案
.and()
.logout() // 注销登录的logout
// .logoutUrl("/logout") // 指定注销的路径url 【注意】请求方式类型必须是GET
//.logoutRequestMatcher(new OrRequestMatcher( // 配置多个注销登录的请求
// new AntPathRequestMatcher("/aLogout", "GET"),
// new AntPathRequestMatcher("/bLogout", "POST")
//))
.logoutSuccessHandler(new DivLogoutSuccessHandler()) // 成功退出登录时,前后端分离解决方案
.invalidateHttpSession(true) // 默认开启,会话清除
.clearAuthentication(true) // 默认开启,清除认证标记
.logoutSuccessUrl("/toLogin") // 注销登录成功之后跳转的页面
.and()
.csrf().disable(); // 禁止 csrf 跨站请求保护
}
}
4.8 获取用户认证信息
4.8.1 SecurityContextHolder 的源码解析
SecurityContextHolder
-
SecurityContextHolder 用来获取登录之后用户信息。SpringSecurity 会将登录用户数据保存在
Session
中。但是,为了使用方便, SpringSecurity 在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,SpringSecurity 会将登录成功的用户信息保存到 SecurityContextHolder 中。 -
SecurityContextHolder 中的数据保存默认是通过
ThreadLocal
来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后, SpringSecurity 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,SpringSecurity 就会先从Session
中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security 中的 SecurityContextHolder 中的数据清空。这一策略非常方便用户在Controller、Service层以及任何代码中获取当前登录用户数据
- 实际上 SecurityContextHolder 存储的是 SecurityContext,在 SecurityContext 中存储的是 Authentication
这种设计是典型的策略设计模式
public class SecurityContextHolder {
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
// ...
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
// ...
}
MODE THREADLOCAL
:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,由于 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到MODE INHERITABLETHREADLOCAL
:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式MODE GLOBAL
:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb 开发中,这种模式很少使用到
SecurityContextHolderStrategy
通过 SecurityContextHolderStrategy 得知, SecurityContextHolderStrategy 接口用来定义存储策略方法
接口中一共定义了四个方法:
- clearContext:该方法用来清除存储的 SecurityContext 对象
- getContext:该方法用来获取存储的 SecurityContext 对象
- setContext:该方法用来设置存储的 SecurityContext 对象
- createEmptyContext:该方法用来创建一个空的 SecurityContext 对象
从上面可以看到,每一个实现类对应一种策略的实现
4.8.2 登录用户数据获取
- 代码中获取认证之后的用户数据
application.yml
文件
# 端口号
server:
port: 3035
# 服务应用名称
spring:
application:
name: SpringSecurity02
# 关闭thymeleaf缓存(用于修改完之后立即生效)
thymeleaf:
cache: false
# 配置登录用户名、密码
security:
user:
name: root
password: root
roles:
- admin
- user
- HelloController
package com.vinjcent.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
System.out.println("Hello Security");
// 1.获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
System.out.println("身份信息: " + user.getUsername());
System.out.println("权限信息: " + user.getAuthorities());
// 模拟子线程获取(默认策略为本地线程,不支持多线程获取)
new Thread(() -> {
Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
System.out.println("子线程获取: " + authentication1);
}).start();
return "Hello Security";
}
}
单线程情况下
多线程情况下
# 根据下图 SYSTEM_PROPERTY 系统属性配置
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
# MODE_THREADLOCAL
# MODE_INHERITABLETHREADLOCAL
# MODE_GLOBAL
# MODE_PRE_INITIALIZED
- 从上面可以看出,默认策略(单线程)是无法在子线程中获取用户信息的,如果需要在子线程中获取必须使用第二种策略(多线程),默认策略是通过
SYSTEM_PROPERTY
加载的,因此我们可以通过增加 VM Options 参数进行修改
4.8.3 页面上获取用户信息
- 引入依赖
<!--thymeleaf-security-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
- 页面加入命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
- 页面中使用
<ul>
<li sec:authentication="principal.username"></li>
<li sec:authentication="principal.authorities"></li>
<li sec:authentication="principal.accountNonExpired"></li>
<li sec:authentication="principal.accountNonLocked"></li>
<li sec:authentication="principal.credentialsNonExpired"></li>
</ul>