简单的开始
创建SpringBoot项目
首先创建一个简单的springboot项目,假设端口为8888,添加controller控制层,并在其中添加TestController
控制类,那么启动springboot项目之后,访localhost:8888/api/message
页面会显示my first message
@RestController
@RequestMapping("/api")
public TestController{@GetMapping("/messages")public String myMessage(){return "my first message";}
}
添加SpringSecurity的依赖
<dependencies><!-- ... 其他依赖元素 ... --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><!--可以通过下面的内容进行版本指定--><spring-security.version>6.2.0-SNAPSHOT</spring-security.version></dependency>
</dependencies>
SpringSecurity认证登录
运行springboot项目后,在控制台输出窗口出现:
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
尝试访问localhost:port
任意后端接口地址,可以发现出现了登录窗口,
使用user: user
password:8e557245-73e2-4286-969a-ff57fe326336
这里的密码就是控制台输出的密码。
这就是springsecurity的端口认证机制。
原理说明
Filter和FilterChain
当客户端向应用程序发送请求时,SpringSecurity会创建一系列的Filter
来过滤请求,这样的Filter
有多个,这些Filter
构成了从客户端到Servlet的一个FilterChain
,在通过FilterChain
的过滤之后,这个请求才会被Servlet处理。
需要注意的是Filter
会影响下游的 Filter
实例,当匹配到一个Filter
之后就不再匹配下面的Filter
流程如下所示。
过滤过程的伪代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {// 在过滤之前的动作chain.doFilter(request, response); // 进行过滤// 过滤之后的动作
}
FilterChainProxy和SecurityFilterChain
基本流程
当一个请求来临时,我们常常会有这样的动作,对于某一个请求接口,去查看对应的过滤器,比如对account相关的接口,我们就会给account制定对应的过滤器,当account相关的请求来临时,我们必然的就需要去通过account的过滤器去处理该请求。
FilterChainProxy
就是这样的一个角色, 用来确定当前请求应该调用哪些 Spring Security Filter
实例。
SecurityFilterChain
的作用就是将过滤器进行分类,用来被FilterChainProxy
识别调用。
因此当设计多个接口过滤器时,基本架构如下图所示
举例说明
FilterChainProxy
决定应该使用哪个 SecurityFilterChain
。只有第一个匹配的 SecurityFilterChain
被调用。
- 如果请求的URL是
/api/messages
,它首先与/api/**
的SecurityFilterChain0
模式匹配,所以只有SecurityFilterChain0
被调用,尽管它也与SecurityFilterChainn
匹配。 - 如果请求的URL是
/messages
,它与/api/**
的SecurityFilterChain_0
模式不匹配,所以FilterChainProxy
会继续顺序尝试下面的SecurityFilterChain
。假设没有其他SecurityFilterChain
实例相匹配,则调用SecurityFilterChain_n
。
工作流程
1. 自动配置的过程
-
UserDetailsServiceAutoConfiguration
类上的条件注解-
@ConditionalOnClass(AuthenticationManager.class)
确保
AuthenticationManager
类在类路径上。 -
@ConditionalOnBean(ObjectPostProcessor.class)
确保 Spring 容器中存在
ObjectPostProcessor
的 Bean -
@ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class })
确保 Spring 容器中没有定义
AuthenticationManager
、AuthenticationProvider
和UserDetailsService
的 Bean。
-
-
默认InMemoryUserDetailsManager 的Bean 的创建
如果上述条件都满足,
UserDetailsServiceAutoConfiguration
会创建一个InMemoryUserDetailsManager
的 Bean 作为默认的用户详细信息服务管理器。这个管理器会在内存中创建一个用户,通常用户名为 “user”,密码为随机生成的 UUID,这个角色为 “USER”。在创建
InMemoryUserDetailsManager
时,UserDetailsServiceAutoConfiguration
会检查SecurityProperties
中定义的用户密码。如果密码是生成的,它会记录一条日志,显示使用的密码。同时,它还会检查密码是否已经使用某种算法进行了编码,如果没有,它会使用{noop}
前缀,表示密码没有被编码。创建过程代码:可以简单浏览,之后自己创建配置时会借鉴到
@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,// springsecurity的配置文件,里面有默认的用户名,可以进入 SecurityProperties 查看详细数据ObjectProvider<PasswordEncoder> passwordEncoder) // 密码编码器{SecurityProperties.User user = properties.getUser(); // 配置文件中的用户List<String> roles = user.getRoles(); // 获取角色return new InMemoryUserDetailsManager(new UserDetails[]{User.withUsername(user.getName()) // 账号.password( // 密码this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)) //角色.build()});}
-
注册Bean
最终,
UserDetailsServiceAutoConfiguration
会将InMemoryUserDetailsManager
注册为 Spring 应用上下文中的一个 Bean,这样 Spring Security 在认证时就可以使用这个默认的用户详细信息服务。
2. UserDetailsService的作用
我们通过上面InMemoryUserDetailsManager的类,可以分析得出
-
InMemoryUserDetailsManager实现了UserDetailsManager、UserDetailsPasswordService中的方法
-
UserDetailsManager继承UserDetailsService
因此InMemoryUserDetailsManager的关键就是UserDetailsManager
、UserDetailsPasswordService
以及实现自UserDetailsService
中的方法
接口 | 方法 | 描述 |
---|---|---|
UserDetailsManager | void createUser(UserDetails user) | 根据提供的用户详情创建一个新用户账号 |
void updateUser(UserDetails user) | 更新指定的用户账号 | |
void deleteUser(String username) | 从系统中删除具有给定登录名的用户账号 | |
void changePassword(String oldPassword, String newPassword) | 修改用户账号的密码。这应该在持久的用户存储库中更改用户的密码(数据库、LDAP等) | |
boolean userExists(String username) | 检查具有给定登录名的用户账号是否存在于系统中 | |
UserDetails loadUserByUsername(String username) | 根据用户名加载用户信息,此方法从 UserDetailsService 继承 | |
UserDetailsPasswordService | UserDetails updatePassword(UserDetails user, String newPassword) | 更新用户密码。在用户登录成功后,如果检测到密码需要更新(例如,密码策略变更),则调用此方法 |
而我们可以通过上面部分自动配置过程
可以知道,假如Spring 容器中定义了 AuthenticationManager
、AuthenticationProvider
和 UserDetailsService
的 Bean,那么自动配置文件将不会生效。
3. AuthenticationManager的作用
在Spring Security中,AuthenticationManager
是一个核心接口,负责对用户的认证请求进行处理。它定义了一个 authenticate
方法,该方法接受一个 Authentication
对象作为参数,并返回一个完全认证过的 Authentication
对象。如果认证失败,则抛出 AuthenticationException
。
ProviderManager
是 AuthenticationManager
的一个常见实现,它使用一个 AuthenticationProvider
列表来处理认证请求。每个 AuthenticationProvider
都有机会对认证请求进行处理,如果一个 AuthenticationProvider
无法处理请求,ProviderManager
会尝试下一个。这个过程会一直持续,直到找到一个能够成功认证请求的 AuthenticationProvider
,或者所有的 AuthenticationProvider
都尝试完毕。
4. 手动配置账号密码
1)创建配置类、用户管理器
因此我们创建自己的WebSecurityConfig
类 ,在里面进行InMemoryUserDetailsManager
的注入,并实现构造方法。这样我们就手动创建了自己的配置内容。
@Configuration
public class WebSecurityConfig {@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager() {return new InMemoryUserDetailsManager();}
}
当然我们里面还没有给InMemoryUserDetailsManager
添加任何用户。
2)初始化用户
添加下面代码,在创建InMemoryUserDetailsManager
时新建一个用户
@Configuration
public class WebSecurityConfig {@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager() {return new InMemoryUserDetailsManager(User.withUsername("user") // 用户名.password("{noop}password") // 密码,以{noop}开头的话代表不加密.roles("a") // 使用可变参数传递角色.build());}
}
这样,当我们启动时,就可以根据上面的账号和密码进行登录
3)添加用户
当然我们也可以通过调用InMemoryUserDetailsManager
中的createUser
方法添加用户的方式,来初始化manager用户管理器,下面我们展示创建两个用户的过程。
@Configuration
public class WebSecurityConfig {@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(new User("admin", "{noop}123456", List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))));manager.createUser(new User("user", "{noop}654321", List.of(new SimpleGrantedAuthority("ROLE_USER"))));return manager;}
}
4)认证过程
我们通过上面的内容已经知道了初始化用户
添加用户
,同样的里面的updateUser
deleteUser
changePassword
userExists
方法也基本类似,不再赘述。
接下来需要弄懂的就是如何认证的呢,我们明明没有写这些相关的方法。
通过最开始的流程图,我们可以知道在配置好认证用户之后,之后程序对于每一个请求都会进行拦截。
请求拦截
AbstractAuthenticationProcessingFilter
将请求拦截,并通过调用attemptAuthentication
方法进行处理,而这个方法的具体实现存在于UsernamePasswordAuthenticationFilter
中
将请求进行拦截,然后交给授权管理器AuthenticationManager
进行控制
授权管理器认证
进入authenticate()方法发现进入到一个AuthenticationManager
接口中,而这个接口的实现类是ProviderManager
在ProviderManager
类中的authenticate
方法委派认证工作给一个或多个AuthenticationProvider
验证用户是否存在
AuthenticationProvider
仍为一个接口,其默认实现类为AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
的authenticate
方法流程如下:
- 开始认证:认证过程开始。
- 检查Authentication类型:确保传入的
Authentication
对象是UsernamePasswordAuthenticationToken
类型。 - 抛出异常:如果类型不匹配,抛出异常。
- 确定用户名:从
Authentication
对象中获取用户名。 - 从Cahce缓存获取UserDetails:尝试从Cahce缓存中获取
UserDetails
对象。 - Cahce未命中:如果Cahce未命中,从用户信息源(如数据库)检索用户信息。
- 用户不存在:如果用户不存在,根据配置抛出
UsernameNotFoundException
或BadCredentialsException
。 - 用户存在:如果用户存在,校验用户状态(如账户是否过期、是否锁定等)。
- 用户状态无效:如果用户状态无效,抛出
AuthenticationException
。 - 执行额外的认证检查:执行任何额外的认证检查(如密码过期检查)。
- 认证检查失败:如果认证检查失败,重新检索用户信息并再次执行检查。
- 执行后置认证检查:执行认证成功后的后置检查。
- 后置检查失败:如果后置检查失败,抛出
AuthenticationException
。 - 检查是否使用缓存:检查认证过程中是否使用了缓存。
- 使用了缓存:如果没有使用缓存,将用户信息放入缓存。
- 创建认证成功的Authentication对象:创建一个新的
Authentication
对象,表示认证成功。 - 返回认证成功的Authentication对象:返回认证成功的
Authentication
对象。
先看前半部分查看用户是否存在
在这里调用了retrieveUser
方法来进行用户验证获取验证结果,这个方法在DaoAuthenticationProvider
中进行验证,是否存在该用户。
在DaoAuthenticationProvider
中调用loadUserByUsername
方法进行具体内容的验证,这个方法在前面UserDetailsService的作用中看到过
验证密码是否正确
在完成用户存在验证后,我们继续看AbstractUserDetailsAuthenticationProvider
类,在这个类中使用additionalAuthenticationChecks
方法进行账号密码的验证。
具体内容的实现仍在在DaoAuthenticationProvider
中
5)请求拦截
上面我们可以知道UsernamePasswordAuthenticationFilter
拦截器,拦截的只是login的请求,那对于之后的每一次请求是个什么样的流程呢
通过拦截每一次请求,接着验证是否被授权,因此我们之后在处理请求拦截时,可以同样采用这样的方式,进行借鉴
6)汇总
7)关于加密的过程
很多配置都是通过大致流程,因此可以扩展到理解其他的一些配置项。
我们发现在上面密码验证时,是设置了编码器,那我们从来没有配置过DaoAuthenticationProvider
,这里的密码加密器是怎么配置的呢?
在DaoAuthenticationProvider
构造方法设置加密器的位置添加断点。然后执行程序时不断进入断点。
进入了InitializeUserDetailsBeanManagerConfigurer
5. 结合数据库进行用户认证
数据库和上面配置过程不同的是:
手动配置
- 首先创建springSecurity的用户
- 在登录时对用户进行认证
- 与前面创建的用户进行匹配
数据库配置
- 不需要创建用户
- 登录时直接与数据库中的用户进行匹配
经过上面的过程,我们可以知道,主要的过程就是DaoAuthenticationProvider
创建时设置的UserDetailsService
,可以控制用户的认证。
1)引入数据库
我们采用springdatajpa操作数据库
向pom中添加
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
设置yml内容
spring:datasource:url: jdbc:mysql://localhost:3306/springsecurityusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: update #自动生成数据库show-sql: true
1)创建实体类
创建好之后记得手动在数据库中添加一条数据用于测试
@Data
@Entity
@Table(name = "sys_user")
public class User {@Column(name = "user_id", unique = true, nullable = false, insertable = false, updatable = false)@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private int userId;@Column(name = "mobile")private String mobile;@Column(name = "pwd")private String password;@Column(name="identity")private int identity;@Column(name="nick_name")private String nickName;
}
2)创建Dao层
@Repository
public interface UserDao extends JpaRepository<User,Integer> {User findByMobileAndPassword(String mobile,String pwd);User findByMobile(String mobile);
}
3)仿照InMemoryUserDetailsManager创建MyUserDetailsManager
我们上面知道了,要想控制账号密码的验证,我们就需要自己注入UserDetailsService
,这样他就不会采用系统本身的验证方案了。
@Component
public class MyUserDetailsManager implements UserDetailsService {@Resourceprivate UserDao userDao;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userDao.findByMobile(username);Collection<? extends GrantedAuthority> authorities = new ArrayList<>();return new org.springframework.security.core.userdetails.User(user.getMobile(),"{noop}"+user.getPassword(),// 这里{noop}前缀代表不进行加密,也就是匹配时与数据库中的明文相同即可true,true,true,true,authorities);}
}
4)进行登录测试
6.漏洞保护
6.1 csrf跨域保护请求禁用
如果不禁用csrf,那么所有的post请求均会被拒绝
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extendsWebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) {http.csrf(csrf -> csrf.disable());}
}
springsecurity实战应用
1. 构建项目
项目框架
配置文件
pom
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><!-- https://mvnrepository.com/artifact/cn.hutool/hutool-jwt --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-jwt</artifactId><version>5.8.27</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
yml
server:port: 11012spring:datasource:url: jdbc:mysql://localhost:3306/springsecurityusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: updateshow-sql: true
实体类
@Data
@Entity
@Table(name = "sys_user")
public class User {@Column(name = "user_id", unique = true, nullable = false, insertable = false, updatable = false)@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private int userId;@Column(name = "mobile")private String mobile;@Column(name = "pwd")private String password;@Column(name="identity")private int identity;@Column(name="nick_name")private String nickName;
}
在执行项目之后,会自动构建数据库,在构建好数据库之后
记得手动插入一条数据
dao层
@Repository
public interface UserDao extends JpaRepository<User,Integer> {User findByMobileAndPassword(String mobile,String pwd);User findByMobile(String mobile);
}
服务层
public interface UserService {String login(String username,String password);
}
@Service
public class UserServiceImpl implements UserService {UserDao userDao;public UserServiceImpl(UserDao userDao) {this.userDao = userDao;}@Overridepublic String login(String username, String password) {User user = userDao.findByMobileAndPassword(username, password);if (user!= null) {return "login success"+user.getNickName();} else {return "login fail";}}
}
控制层
@RestController
@RequestMapping("/user")
public class UserController {UserService userService;public UserController(UserService userService) {this.userService = userService;}@GetMapping("/test")public String test() {return "tt";}@GetMapping("/login")public String login(@RequestParam(name = "account") String account, @RequestParam(name = "password") String password) {return userService.login(account, password);}
}
springSecurity
WebSecurityConfig
@Configuration
public class WebSecurityConfig {//加密器@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 授权管理器@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}@Bean@Order(1)public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {http.securityMatcher("/user/login").authorizeHttpRequests(authorize -> authorize.anyRequest().anonymous() // 允许匿名访问 /user/login);return http.build();}//Spring Security过滤链@Beanpublic SecurityFilterChain otherFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize -> authorize.anyRequest().hasRole("ADMIN")).httpBasic(withDefaults());return http.build();}}
Order的大小用于指明在第几层,越小越靠上,可以理解为优先级,越小越大
如果不设置order,那么会按照先后顺序进行配置
- 首先请求先通过order为1的过滤链,就是
/user/login
的请求,设置为允许匿名访问 - 而对于没有设置过滤链的请求,就会使用第二个配置
otherFilterChain
。这个配置被认为在apiFilterChain
之后,因为它的@Order
值在1
之后(没有@Order
默认为最后)
DBUserDetailsManager
继承了UserDetailsService,当加载用户的时候,就会执行这里的loadUserByUsername
@Component
public class DBUserDetailsManager implements UserDetailsService {@Resourceprivate UserDao userDao;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userDao.findByMobile(username);if (user == null) {throw new UsernameNotFoundException(username);}return new MyUserDetail(user);}
}
MyUserDetail
新建自己的UserDetails,继承原来的UserDetails,在里面添加我们自己定义的用户类,这样可以方便的存储我们自己的用户信息
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetail implements UserDetails {private User user; // 这是自己定义的用户类@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getMobile();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
2. 初步测试
在上面已经完成了接口的简单控制
我们可以通过访问localhost:11012/user/login?account=123&password=123
发现可以访问,并且登录成功
但是当我们访问localhost:11012/user/test
需要我们进行springsecurity的登录
BasicAuthenticationFilter