1、前言
这里只是了解每个类的含义,并且security到底是怎么使用这些类来认证的。
2、术语
注意:这里有些是比较模糊
-
SecurityContextHolder
- 安全上下文持有人 -
Authentication
- 身份验证信息
*Credentials
- 凭证,通常是一个密码
*Authorities
- 授予的权限。
*Principal
- 身份主体信息,通常是用户名和密码等
*isAuthenticated
- 是否已认证 -
SecurityContext
- 安全上下文 -
SecurityContextHolderStrategy
针对线程存储安全上下文信息的策略。(你可以理解为:SecurityContextRepository是一个参数值,SecurityContextHolderStrategy是使用什么方式存取。)- ThreadLocalSecurityContextHolderStrategy 使用线程栈方式
- GlobalSecurityContextHolderStrategy 使用全局静态变量方式,一般用于java客户端Swing
- InheritableThreadLocalSecurityContextHolderStrategy 使用可继承的线程栈方式
- ListeningSecurityContextHolderStrategy 使用监听器方式
-
GrantedAuthority
- 在身份验证上授予主体的权限(即角色、范围等)。 -
AuthenticationManager
- 定义Spring Security的过滤器如何执行身份验证的API。 这是所有入口。有以下实现类:ProviderManager
- 身份验证管理
-
AuthenticationProvider
- 身份验证提供者,可以有多个,用来支持不同的 Authentication(身份)通过support决定,如果返回非null,则表示该提供者能处理该身份验证。 -
AbstractAuthenticationProcessingFilter
- 用于身份验证的基本过滤器,处理身份验证的一个业务过程,有以下实现- UsernamePasswordAuthenticationFilter 使用表单方式身份验证,里面包含所有登录需要的处理。
-
SecurityContextRepository
保存安全上下文信息,有两种存储方式:- HttpSessionSecurityContextRepository 使用session保存
- RequestAttributeSecurityContextRepository 使用请求Attribute保存
-
DelegatingPasswordEncoder
密码编码器,里面包含很多编码器,格式是: {id}xxxxxx ,这个是为了应对系统密码从MD5升级到bcrypt或者其他类型,而设计出来的。如果系统只使用一种密码,就不需要用这个了。
Session与SecurityContext的区别:session里有securityContext,并且多了时效。
-
Session
的属性- Id - 这是SessionId
- Attribute - 这是存放其他的信息,包含有SecurityContext
- CreationTime - 创建时间
- LastAccessedTime - 最后访问时间
- MaxInactiveInterval - 最大不活动间隔,就是离开后,多久会过期。
-
SecurityContext
的属性- authentication - 这是身份信息
3、术语翻译的例子
3.1、身份存储的例子
这里模拟了security是怎么存放身份信息的
3.1.2、代码
//new AnonymousAuthenticationToken(credentials,principal,authorities);
Authentication anAuthentication = new AnonymousAuthenticationToken("密码","用户信息",new ArrayList<>());// 创建一个身份验证信息
SecurityContext context = SecurityContextHolder.createEmptyContext();// 创建一个空的安全上下文
context.setAuthentication(anAuthentication);// 把身份验证信息交给安全上下文,
SecurityContextHolder.setContext(context);// 把安全上下文放到SecurityContextHolder里,SecurityContextHolder是一个全局静态方法
3.1.3、术语
-
Authentication
身份验证信息,包含身份信息和验证信息,属性如下:- credentials 凭证,通常是一个密码
- details 其他详细信息,通常是ip地址等
- principal 身份主体信息,通常是用户名和密码等
- authorities 授予的权限
- authenticated 是否已认证
-
SecurityContextHolder
安全上下文持有人,属性如下:- strategy 存放策略,,属性类是SecurityContextHolderStrategy
-
SecurityContext
安全上下文,属性如下:- authentication 身份验证信息
-
SecurityContextHolderStrategy
针对线程存储安全上下文信息的策略。有如下实现:- ThreadLocalSecurityContextHolderStrategy 使用线程栈方式存取
- GlobalSecurityContextHolderStrategy 使用全局静态变量方式存取,一般用于java客户端Swing
- InheritableThreadLocalSecurityContextHolderStrategy 使用可继承的线程栈方式存取
- ListeningSecurityContextHolderStrategy 使用监听器方式存取
3.1.4、术语之间,它们的关系
它们的关系
它们的关系就像下面的例子:
就像是某人有100块,然后这100块,我要存到“银行、支付宝或者微信”,怎么存钱呢。
- SecurityContextHolderStrategy,是“怎么存钱”
- SecurityContextHolder,是“某人”
- authentication,是“100块”
- SecurityContext,是“银行、支付宝或者微信”,就像一个公司
3.2、身份验证的例子
这里模拟了security是怎么验证的
3.2.1、代码
TestMain 测试类
package person.twj.jwt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@Slf4j
public class TestMain {
public static void main(String[] args) {
String username = "admin";
String password = "123456";
// 1. SecurityContextHolder 创建身份A
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication anAuthentication = new UsernamePasswordAuthenticationToken(
username,
password);
context.setAuthentication(anAuthentication);
SecurityContextHolder.setContext(context);
// 2. ProviderManager implements AuthenticationManager 验证身份
// 2.1、创建身份适配器
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(NoOpPasswordEncoder.getInstance());
provider.setUserDetailsService(new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库获取用户和密码信息
if("admin".equals(username)){
return User.builder().username("admin").password("123456").build();
}else{
throw new UsernameNotFoundException("用户不存在");
}
}
});
// 2.2、加入身份provider 到 身份管理器里
ProviderManager providerManager = new ProviderManager(provider);//provider 可以多个,这里是一个适配器模式,不同的身份验证信息类型(Authentication)需要不同的适配器
try {
// 2.3、验证身份
Authentication authentication = providerManager.authenticate(// 验证身份
SecurityContextHolder.getContext().getAuthentication()// 验证身份A是否与我们数据库里的一样
);
// 2.4、最后,如果上面的适配器都处理完,还是返回null,则表示验证失败
if (authentication == null) {
log.info("验证失败");
return;
}
log.info("身份验证成功");
}catch (AuthenticationException ae){ // 捕捉认证错误信息
if(ae.getClass().isAssignableFrom(BadCredentialsException.class)){
log.error("用户名或密码错误");
}else if(ae.getClass().isAssignableFrom(CredentialsExpiredException.class)){
log.error("凭证已过期");
}else{
log.error("未知错误 ",ae);
}
}
}
}
备注:这里 SecurityContextHolder.getContext().getAuthentication() 等于 new UsernamePasswordAuthenticationToken( username, password); 可以不用纠结。
3.2.2、术语
-
AuthenticationManager
- 身份验证管理,用来作认证。有如下实现:- ProviderManager - 身份验证管理,是一个适配器模式,真正认证的是 AuthenticationProvider。
-
AuthenticationProvider
- 身份验证提供者,身份验证执行者,用来作认证,如果返回非null,则认证成功。返回null,则表示该provider处理不了,交给下个,返回AuthenticationException异常,则表示认证失败。有如下实现:- DaoAuthenticationProvider - 使用数据库的方式认证,这里只支持 UsernamePasswordAuthenticationToken 的身份验证信息。你可以去看看他的supports所支持类。
-
UserDetailsService
- 查询用户信息服务接口 -
UserDetails
- 是 principal ,表示身份主体信息,通常是用户名和密码等 -
Authentication
身份验证信息,包含身份信息和验证信息,属性如下:- credentials 凭证,通常是一个密码
- details 其他详细信息,通常是ip地址等
- principal 身份主体信息,通常是用户名和密码等
- authorities 授予的权限
- authenticated 是否已认证
3.2.3、术语之间,它们的关系
3.3、模拟 SecurityContextHolder.getContext()怎么获取redis上的用户信息
3.1.1、springboot中配置redis-session数据支持
在springboot中,引入gradle,就行了,不用任何配置
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'
配置一下,application.yml
spring:
application:
name: security-session-redis
data:
redis:
host: localhost
database: 1
password: 123456
port: 6379
timeout: 1000 # 连接超时时间
lettuce:
pool:
max-active: 50 # 连接池最大连接数(使用负值表示没有限制)
min-idle: 5 # 连接池中的最小空闲连接
max-idle: 50 # 连接池中的最大空闲连接
max-wait: 5000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
time-between-eviction-runs: 2000 #eviction线程调度时间间隔
3.3.2、连接redis,并提供Dao操作类
RedisSessionRepositoryTest.java
package person.twj.securitysessionredis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.userdetails.User;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisSessionRepository;
import java.util.Set;
/**
* 获取所有用户session信息,打印输出用户名
* 这是用于线上管理用户状态的
*/
@Slf4j
public class RedisSessionRepositoryTest {
//public static void main(String[] args) {
//RedisTemplate redisTemplate = createRedisTemplate();
//RedisSessionRepository repository = new RedisSessionRepository(redisTemplate);
//Set<String> keys = redisTemplate.keys("spring:session:sessions:*");
//keys.forEach(key->{
// String sessionId = key.substring("spring:session:sessions:".length());
// Session session = repository.findById(sessionId);
// SecurityContext context = session.getAttribute("SPRING_SECURITY_CONTEXT");
// User user = (User) context.getAuthentication().getPrincipal();
// log.info("输出 {}",user.getUsername());
// });
//}
public static RedisSessionRepository getRedisSessionRepository(){
RedisTemplate redisTemplate = createRedisTemplate();
RedisSessionRepository repository = new RedisSessionRepository(redisTemplate);
return repository;
}
private static RedisTemplate<String, Object> createRedisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
if (getDefaultRedisSerializer() != null) {
redisTemplate.setDefaultSerializer(getDefaultRedisSerializer());
}
redisTemplate.setConnectionFactory(getRedisConnectionFactory());
redisTemplate.setBeanClassLoader(RedisSessionRepositoryTest.class.getClassLoader());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
private static RedisConnectionFactory getRedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setDatabase(1);
redisStandaloneConfiguration.setPassword("123456");
redisStandaloneConfiguration.setPort(6379);
redisStandaloneConfiguration.setHostName("localhost");
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
lettuceConnectionFactory.start();
return lettuceConnectionFactory;
}
/**
* 使用Java序列化
* @return
*/
private static RedisSerializer<?> getDefaultRedisSerializer() {
return new JdkSerializationRedisSerializer();
}
}
3.3.3、SecurityContextHolder获取身份信息测试类
SecurityContextHolderTest.java
package person.twj.securitysessionredis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.util.Assert;
import java.util.function.Supplier;
@Slf4j
public class SecurityContextHolderTest {
/**
* 这里模拟了SecurityContextHolder获取用户信息的流程。
*/
public static void main(String[] args) {
// 1、创建redis连接库,并提供Dao操作
RedisSessionRepository sessionRepository = RedisSessionRepositoryTest.getRedisSessionRepository();
// 2、本地栈存放 Supplier 提供者,需要的时候才调用
ThreadLocalSecurityContextHolderStrategy holderStrategy = new ThreadLocalSecurityContextHolderStrategy();
holderStrategy.setDeferredContext(()->{
log.info("第二步,从redis获取session");
String sessionId = "cecf73be-2fd8-4665-a55d-31e2e686bcdb";
Session session = getRequestedSession(sessionId, sessionRepository);
log.info("第三步,从session中获取SecurityContext");
SecurityContext contextFromSession = session.getAttribute("SPRING_SECURITY_CONTEXT");
return contextFromSession;
});
SecurityContextHolder.setContextHolderStrategy(holderStrategy);
// 3、 SecurityContext获取用户信息
log.info("第一步,获取 SecurityContext");
SecurityContext context = SecurityContextHolder.getContext();
log.info("第四步,获取结果:SecurityContext={}",context);
Authentication authentication = context.getAuthentication();
log.info("第五步,获取结果:Authentication={}",authentication);
log.info("第六步,身份用户:{}",authentication.getPrincipal());
}
public static Session getRequestedSession(String sessionId,RedisSessionRepository sessionRepository){
return sessionRepository.findById(sessionId);
}
/**
* 基于本地线程栈存储, 我们把security里的ThreadLocalSecurityContextHolderStrategy 原封不动的搬过来
*
* @author Ben Alex
* @author Rob Winch
* @see java.lang.ThreadLocal
* @see org.springframework.security.core.context.web.SecurityContextPersistenceFilter
*/
static final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal<>();
@Override
public void clearContext() {
contextHolder.remove();
}
@Override
public SecurityContext getContext() {
return getDeferredContext().get();
}
@Override
public Supplier<SecurityContext> getDeferredContext() {
Supplier<SecurityContext> result = contextHolder.get();
if (result == null) {
SecurityContext context = createEmptyContext();
result = () -> context;
contextHolder.set(result);
}
return result;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(() -> context);
}
@Override
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
Assert.notNull(deferredContext, "Only non-null Supplier instances are permitted");
Supplier<SecurityContext> notNullDeferredContext = () -> {
SecurityContext result = deferredContext.get();
Assert.notNull(result, "A Supplier<SecurityContext> returned null and is not allowed.");
return result;
};
contextHolder.set(notNullDeferredContext);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
}
4、部分流程图
4.1、整体的流程图
4.2、过滤器链
4.3、登录身份验证处理过滤器
4.4、授权过滤器
5、问题
5.1、禁用Filter
其实security提供了很多Filter供我们使用,如果我们不用,可以使用disabled禁用,例如
http.logout(logout->logout.disable());
这里禁用了退出登录功能,security就不会帮我们加载 LogoutFilter 到Filter里了
5.2、自定义自己的Form表单过滤器
对于使用jwt来说,Form表单已经不适用了,所以我们可以禁用Form表单的Filter,然后自己写一个。
这里配置,先把Basic和form登录都禁了。
http.httpBasic(basic -> basic.disable());
http.formLogin((formLogin) ->formLogin.disable());
还有没登录访问异常,我们也要接收一下
http.exceptionHandling(exception -> {
// 未登录返回
exception.authenticationEntryPoint(new MyNotLoginAccessHandler());
});
新建一个MyUsernamePasswordAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter 类,重写 attemptAuthentication 方法
添加自己的Filter在UsernamePasswordAuthenticationFilter位置
http.addFilterAt(new MyUsernamePasswordAuthenticationFilter(
"/token",// 记得放开/token拦截
authenticationConfiguration.getAuthenticationManager()
),
UsernamePasswordAuthenticationFilter.class);
其实我们照抄UsernamePasswordAuthenticationFilter 就行了,只是我们的传参已经不是表单的类型了,我们需要从json里获取传参,剩下的校验就交给原来的代码了
// 获取json格式传参
JsonNode jsonNode =getJSON(request);
String username = jsonNode.get("username").asText();
username = (username != null) ? username.trim() : "";
String password = jsonNode.get("password").asText();
password = (password != null) ? password : "";
具体的代码
package person.twj.jwt.core.security.jwt.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import person.twj.jwt.core.security.jwt.constant.TokenConstants;
import person.twj.jwt.core.security.jwt.model.UserToken;
import person.twj.jwt.core.security.jwt.util.TokenUtil;
import person.twj.jwt.domain.vo.RsVo;
import java.io.BufferedReader;
import java.io.IOException;
public class MyUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 只允许使用post请求
private boolean postOnly = true;
public MyUsernamePasswordAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(defaultFilterProcessesUrl, authenticationManager);
LoginPostHandler loginPostHandler = new LoginPostHandler();
setAuthenticationFailureHandler(loginPostHandler);
setAuthenticationSuccessHandler(loginPostHandler);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
/*if(!requiresAuthentication(request,response)){ // 这个在 AbstractAuthenticationProcessingFilter 开始就已经过滤了
// 路径是否是 defaultFilterProcessesUrl
return null;
}*/
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 获取json格式传参
JsonNode jsonNode =getJSON(request);
String username = jsonNode.get("username").asText();
username = (username != null) ? username.trim() : "";
String password = jsonNode.get("password").asText();
password = (password != null) ? password : "";
// 组装成 没认证的token,给AuthenticationManager 去真正的验证
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken
.unauthenticated(username,password);
// 这里是使用 DaoAuthenticationProvider 来验证身份。
// 1. 验证成功后,会交给AuthenticationSuccessHandler去处理
// 2. 验证失败后,会交给AuthenticationFailureHandler去处理
return this.getAuthenticationManager().authenticate(authRequest);
}
private JsonNode getJSON(HttpServletRequest request) {
try {
// 1. 从HttpServletRequest对象中获取输入流,并读取请求正文。
StringBuilder buffer = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
String requestBody = buffer.toString();
// 2. 使用JSON库(如Jackson、Gson等)将字符串解析为JsonNode或任何其他适合你的数据结构。
ObjectMapper mapper = new ObjectMapper(); // Jackson JSON库示例
JsonNode jsonNode = mapper.readTree(requestBody); // 解析为JsonNode对象
return jsonNode;
}catch (Exception e){
e.printStackTrace();
throw new AuthenticationCredentialsNotFoundException("获取json传参失败");
}
}
class LoginPostHandler implements AuthenticationFailureHandler, AuthenticationSuccessHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
if(e instanceof BadCredentialsException) {
RsVo.failed(1,"用户名或密码错误").writeTo(response);
} else {
RsVo.failed(-1,"未知错误").writeTo(response);
}
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
UserToken userToken = (UserToken) authentication.getPrincipal();
// token里不存在用户密码
RsVo.success("登录成功", map -> {
map.put("access_token", TokenUtil.createToken(userToken));//有效期2个小时
map.put("refresh_token", TokenUtil.createRefreshToken(userToken));//有效期12小时
map.put("expires_in", TokenConstants.EXPIRES_IN);
})
.writeTo(response);
}
}
}
5.3、session管理
http.sessionManagement(sessionManagementCustomizer->{
// 用户登录成功后,信息保存在服务器Session中。SpringSecurity提供了4种方式控制会话创建。
// * always:如果一个会话尚不存在,将始终创建一个会话。
// * ifRequired:仅在需要时创建会话,默认。
// * never:框架永远不会创建会话本身,但如果它已经存在,它将使用一个。
// * stateless:不会创建或使用任何会话,完全无状态。
// 因为使用jwt,所以禁用session
sessionManagementCustomizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
});
6、使用例子
- springboot+security 使用 jwt方式登录
- springboot+security 使用 session方式登录
- springboot+security 使用 session+redis 方式登录
仓库地址:https://gitcode.com/u010101252/security-study/tree/main
7、security学习记录
流程图仓库路径:https://gitcode.com/u010101252/security-study/tree/main
security官方文档:https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html
8、学习总结
其实如果你学会了上面,其实你可以去Security官网看,我相信官网绝大部分,你都会。
例如下方的:
https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html