英文网站模板改成中文,济宁 网站建设,上海企业网站开发,实时国际新闻app之所以想写这一系列#xff0c;是因为之前工作过程中使用Spring Security#xff0c;但当时基于spring-boot 2.3.x#xff0c;其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0#xff0c;结果一看Spring Security也升级为6.3.0#xff0c;关键是其风…之所以想写这一系列是因为之前工作过程中使用Spring Security但当时基于spring-boot 2.3.x其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0结果一看Spring Security也升级为6.3.0关键是其风格和内部一些关键Filter大改导致在配置同样功能时多费了些手脚因此花费了些时间研究新版本的底层原理这里将一些学习经验分享给大家。 注意由于框架不同版本改造会有些使用的不同因此本次系列中使用基本框架是 spring-boo-3.3.0默认引入的Spring Security是6.3.0JDK版本使用的是19所有代码都在spring-security-study项目上https://github.com/forever1986/spring-security-study.git 目录 1 用户读取的基本原理2 基于内存的用户配置3 基于数据库的用户配置3.1 Spring Security默认的JdbcUserDetailsManager3.2 自定义基于数据库的用户配置 4 Spring Security认证底层原理5 密码加密方式PasswordEncoder5.1 密码加密原理5.2 DelegatingPasswordEncoder原理5.3 指定PasswordEncoder 上一章中我们讲了基本入门spring-boot如何通过默认配置集成Spring Security也讲了如何自定义配置用户名和密码。在上一章中留下一个问题就是用户名密码都是配置在项目里面但实际项目中我们都是放在数据库中那么Spring Security如何配置数据库以及其认证原理是如何的我们在这一章揭晓。
1 用户读取的基本原理
我们先来了解Spring Security如何读取到我们在yml文件中的数据。
1我们看一下UserDetailsServiceAutoConfiguration这个类其中有一个inMemoryUserDetailsManager方法 从上图中我们可以看到方法inMemoryUserDetailsManager注入了一个Bean是一个InMemoryUserDetailsManager其数据来自SecurityProperties这个类在系列一中讲过是读取yml或生成默认用户名密码的。也就是说Spring Security默认构建了一个基于内存的用户密码管理类。 2我们再看看InMemoryUserDetailsManager继承哪些类或者实现哪些接口。 上图中我们可以看到InMemoryUserDetailsManager类实现了UserDetailsManager接口而UserDetailsManager接口继承了UserDetailsService接口。关键点UserDetailsService接口只有一个loadUserByUsername方法通过用户名获得UserDetails。UserDetailsManager接口只是在UserDetailsService接口的基础上增加了对用户的增删改查。 那么可以理解只需要我们自己实现一个实现UserDetailsService接口或者UserDetailsManager接口的Bean即可替换原先的配置 3UserDetails接口我们看到UserDetailsService接口的loadUserByUsername方法返回一个对象是UserDetails其也是一个接口。该接口定义了一系列获取用户信息相关的方法
4而我们在UserDetailsServiceAutoConfiguration这个类中的inMemoryUserDetailsManager方法看到使用了一个User类创建了一个用户。这个User类其实就是实现了UserDetails接口
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()5总结Spring Security是通过调用UserDetailsService接口的实现类获得一个UserDetails里面包括用户信息可以用于认证的。而Spring Security默认有2个实现类InMemoryUserDetailsManager和JdbcUserDetailsManager分别对应基于内存的用户认证和基于数据库的用户认证。另外内置一个User类实现最简单的UserDetails信息。
2 基于内存的用户配置
通过上面对其基本原理的理解我们知道只需要实现一个UserDetailsService接口的类就可以替换原来的用户密码配置下面我们就开始实现。 代码参考lesson02子模块 1新建一个子模块lesson02其pom文件引入
dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--Spring Boot 提供的 Security 启动器 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependency/dependencies2我们只需要新增一个名称service的pakage在下面新增一个类InMemoryUserDetailsServiceImpl实现UserDetailsService 接口
Service
public class InMemoryUserDetailsServiceImpl implements UserDetailsService {Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return User.withUsername(test).password({noop}4321).build();}
}注意需要将该类设置注解Service这样就纳入spring的Bean管理同时也屏蔽原先默认配置的Bean 密码部分{noop}4321前面那个{noop}代表未加密密码为什么要加入这个东西后面讲解Spring Security认证流程原理在仔细讲解 3跟lesson01一样定义一个demo的controller和启动类 4访问http://127.0.0.1:8080/demo 我们可以看见是使用新的test用户名和4321的密码才能登陆成功原先控制台或者yml配置都失效了。
3 基于数据库的用户配置
3.1 Spring Security默认的JdbcUserDetailsManager
上面分别讲述了读取用户的原理以及基于内存读取用户的方法。那么基于数据库的用户配置才是今天的主菜。我们看到中Spring Security已经有一个JdbcUserDetailsManager实现类如果我们要使用该类需要按照其默认的配置创建表创建表的语句如下位置 注意如果使用默认JdbcUserDetailsManager可以去看其源码定义了很多SQL语句且使用的是传统JdbcTemplate的方式。而在真正项目中我们一般会引入如mybatis框架以及用户表会有自己的一些格外信息所以JdbcUserDetailsManager大部分时候都是不符合我们的要求因此实际中我们会自定义自身的用户表以及基于数据库的UserDetailsService。 3.2 自定义基于数据库的用户配置 前提条件基于mysql数据库创建一个数据库名为spring_security_study创建用户表t_user -- spring_security_study.t_user definition
CREATE TABLE t_user (id bigint NOT NULL AUTO_INCREMENT,username varchar(100) NOT NULL,password varchar(100) NOT NULL,email varchar(100) DEFAULT NULL,phone varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT5 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;INSERT INTO spring_security_study.t_user (username, password, email, phone)VALUES(test, {noop}1234,testtest.com,13788888888);下面开始说明基于自定义数据库的用户配置 代码参考lesson03子模块 1新建子模块lesson03其pom引入以下依赖引入mybatis-plus、mysql-connector、druid连接池、lombok
dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--Spring Boot 提供的 Security 启动器 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-spring-boot3-starter/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdscoperuntime/scope/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactId/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactId/dependency
/dependencies2在resources下面创建yaml文件配置数据库连接和mybatis-plus相关配置
server:port: 8080
spring:# 配置数据源datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/spring_security_study?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf-8zeroDateTimeBehaviorconvertToNulluseSSLfalseallowPublicKeyRetrievaltrueusername: rootpassword: rootdruid:initial-size: 5min-idle: 5maxActive: 20maxWait: 3000timeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000validationQuery: select xtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsepoolPreparedStatements: falsefilters: stat,wall,slf4jconnectionProperties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis5000;socketTimeout10000;connectTimeout1200mybatis-plus:global-config:banner: falsemapper-locations: classpath:mappers/*.xmltype-aliases-package: com.demo.lesson03.entityconfiguration:cache-enabled: falselocal-cache-scope: statement3创建packageentity和mapper分别定义TUser和TUserMapper读取数据库用户数据。
Data
public class TUser {TableId(type IdType.ASSIGN_ID)private Long id;private String username;private String password;private String email;private String phone;
}TUserMapper定义个通过用户名获取用户的方法
Mapper
public interface TUserMapper {// 根据用户名查询用户信息Select(select * from t_user where username #{username})TUser selectByUsername(String username);}4新建packageservice新建一个JdbcUserDetailsServiceImpl通过mapper获取用户并返回
Service
public class JdbcUserDetailsServiceImpl implements UserDetailsService {Autowiredprivate TUserMapper tUserMapper;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 查询自己数据库的用户信息TUser user tUserMapper.selectByUsername(username);if(user null){throw new UsernameNotFoundException(username);}return User.builder().username(user.getUsername()).password(user.getPassword()).roles(USER).build();}}5和lesson02一样定义一个demo的controller接口和启动类SecurityLesson03Application并启动 6访问http://127.0.0.1:8080/demo 我们可以看见是使用新的test用户名和1234的密码才能登陆成功原先控制台或者yml配置都失效了。
4 Spring Security认证底层原理
上面讲解了如何通过内存或者数据库配置登录用户信息那么Spring Security是如何做用户认证的呢 首先我们要先了解到Spring Security是一系列的Filter过滤器组成的链路每个过滤器处理器对应的功能如果符合则往下走不符合则返回。关于这部分我们在下一章中在着重讲。 之前说过Spring Security支持的认证有多种还可以自定义认证。我们这里以用户名和密码的认证方式为例讲解一下其主要的原理。我们主要关注Spring Security的认证过滤器UsernamePasswordAuthenticationFilter。
1我们可以debug一下HttpSecurity下面这行代码里面可以看到Spring Security默认配置定义了哪些Filter过滤器。 2我们可以截图看到以下默认配置下有16个过滤器不同Security版本可能不同我这6.3.0默认什么都不配置是16其过滤器的各自用途我们下一章讲。这一章我们主要看UsernamePasswordAuthenticationFilter 3UsernamePasswordAuthenticationFilter是继承AbstractAuthenticationProcessingFilter我们看看AbstractAuthenticationProcessingFilter的doFilter方法里面有2个关键内容一个是调用attemptAuthentication方法做认证这个方法具体实现是在UsernamePasswordAuthenticationFilter一个successfulAuthentication方法做后续认证成功处理包括保存SecurityContext调用securityContextRepository存储session
4我们回到认证流程刚才说AbstractAuthenticationProcessingFilter调用attemptAuthentication其实就是调用UsernamePasswordAuthenticationFilter里面的attemptAuthentication方法就是执行关键。看下图解释attemptAuthentication的流程 5上图的最后一步验证其实是调用AuthenticationManager的authenticate方法AuthenticationManager是一个接口该接口实现类有几个Spring Security默认提供ProviderManager 6ProviderManager的authenticate方法中真正认证的是result provider.authenticate(authentication);这一句而provider是一个叫AuthenticationProvider接口 7AuthenticationProvider接口Spring Security默认提供抽象实现类AbstractUserDetailsAuthenticationProvider我们关注其authenticate方法方法里面有2个调用值得我们注意一个是retrieveUser这个类获得用户信息和一个additionalAuthenticationChecks做用户认证。
8我们可以看到retrieveUser和additionalAuthenticationChecks方法在AbstractUserDetailsAuthenticationProvider只是一个定义还需要其子类实现而Spring Security默认提供DaoAuthenticationProvider实现类 从上图我们就可以理解为什么我们实现UserDetailsService接口就能够修改用户信息的来源比如说数据库因为在retrieveUser方法中就是调用UserDetailsService接口去获取用户信息。 另外一个就是用户认证additionalAuthenticationChecks方法通过一个PasswordEncoder去做用户密码匹对。 9总结通过UsernamePasswordAuthenticationFilter过滤器做认证UsernamePasswordAuthenticationFilter调用AuthenticationManager管理器的authenticate方法而AuthenticationManager是调用AuthenticationProvider的authenticate方法AuthenticationProvider是通过其实现类DaoAuthenticationProvider提供最终的认证。如下流程图
5 密码加密方式PasswordEncoder
5.1 密码加密原理
我们在前面分析认证原理中DaoAuthenticationProvider的additionalAuthenticationChecks方法就是实现其密码匹配的。我们可以看到里面有一个PasswordEncoder。从名字来看就是密码加密 Spring Security提供了PasswordEncoder接口并通过实现该接口内置很多密码加密验证方式比如不加密、对称加密、非对称加密甚至可以自定义。这里我们着重了解3个实现类NoOpPasswordEncoder、BCryptPasswordEncoder和DelegatingPasswordEncoder 上图中就是3个常用的内置加密方式他们分别代表NoOpPasswordEncoder不加密、BCryptPasswordEncoder使用SHA-256 随机盐密钥对密码进行加密、DelegatingPasswordEncoder代理类通过其前缀判断不同加密方式。Spring Security默认情况下采用的是DelegatingPasswordEncoder下面我们了解一下DelegatingPasswordEncoder。
5.2 DelegatingPasswordEncoder原理
DelegatingPasswordEncoder可以通过给密码前面增加一个{前缀}同时支持多种密码加密验证而Spring Security默认的方式就是DelegatingPasswordEncoder。下图就是不同加密方式的前缀。 注意前面我们在密码之前增加{noop}其实就是采用不加密方式当然这是非常不安全的只有在演示中使用 5.3 指定PasswordEncoder
你可以指定自己的密码加密方式只需要继承PasswordEncoder接口并实现其方法即可。比如指定BCryptPasswordEncoder
Bean
public BCryptPasswordEncoder createPasswordEncoder(){return new BCryptPasswordEncoder();
}在实际项目中密码一定是要加密且最好不可逆和难以破解的方式比如SHA-256。 结语至此我们终于将Spring Security的认证底层原理讲了一遍。如果不了解的朋友可以多debug几次就能够明白其中原理。到目前为止我们只是配置了用户读取方式其它的Spring Security配置都还是默认的我们也可以看到默认情况下Spring Security就加载了16个过滤器说明有很多功能还没有讲那么下一章就是了解Spring Security底层原理以及常见Filter过滤的作用。