基本使用
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、直接启动springboot项目,此时的security就会生效,并使用自带的过滤器
3、访问当前项目的任一请求,就会出现自带的登陆页面
4、账号密码存在当前项目的内存中即 InMemoryUserDetailsManager,账号为 user,密码在控制台输出
原理
官方文档:
Architecture :: Spring Security
HTTP请求的处理程序的典型分层
一般的http请求,客户端向应用程序发送一个请求,容器创建一个FilterChain,其中包含Filter实例和Servlet,它们应该根据请求URI的路径来处理HttpServlet请求。在Spring MVC应用程序中,Servlet是DispatcherServlet的一个实例。一个Servlet最多可以处理一个HttpServlet请求和HttpServlet响应。
委托代理过滤器
在security中,使用了以下几个过滤器用于动态配置过滤器链
- DelegatingFilterProxy:Spring提供了一个名为DelegatingFilterProxy的Filter实现,它允许Servlet容器的生命周期和Spring的ApplicationContext之间进行桥接
- FilterChainProxy: 是Spring Security提供的一种特殊过滤器,允许通过SecurityFilterChain将其委托给许多过滤器实例。由于FilterChainProxy是一个Bean,它通常被包装在DelegatingFilterProxy中
- SecurityFilterChain:FilterChainProxy使用SecurityFilterChain来确定应为当前请求调用哪些Spring Security Filter实例

安全过滤器通过SecurityFilterChain API插入到FilterChainProxy中。这些过滤器可以用于多种不同的目的,如身份验证、授权、漏洞保护等。过滤器按特定顺序执行,以确保它们在正确的时间被调用,例如,执行身份验证的过滤器应在执行授权的过滤器之前被调用。通常不需要知道Spring Security的过滤器的顺序。但是,有时了解排序是有益的,如果您想了解它们,可以检查FilterOrderRegistration代码。
默认过滤器链
1、DefaultSecurityFilterChain 就是一个默认的 SecurityFilterChain
2、在 DefaultSecurityFilterChain 类中打断点测试
3、默认的15个过滤器
配置登录的账号密码
官方文档:
Username/Password Authentication :: Spring Security
1、创建配置类,并创建 UserDetailsService 的 bean,这样就会使用自定义的这个 bean 去进行校验
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
2、以上是创建了一个 InMemoryUserDetailsManager 的 bean 对象,将账号密码写在代码中
3、此时重新启动项目,登录的账号密码就被设置好了
基于内存的用户认证流程
程序启动时:
- 创建 InMemoryUserDetailsManager 对象

- 创建 User 对象,封装用户名密码

- 使用 InMemoryUserDetailsManager 将 User 存入内存

校验用户时:
- UsernamePasswordAuthenticationFilter 过滤器调用 attemptAuthentication 方法进行校验

- 调用 InMemoryUserDetailsManager 的 loadUserByUsername 方法从内存中获取 User 对象
通过数据库设置账号密码
1、查看 UserDetailsService 接口的实现类,
2、创建 UserDetailsService 实现类
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = Optional.ofNullable(username)
.map(it -> sysUserService.getOne(Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getName, it)
.last("limit 1")))
.orElseThrow(() -> new RuntimeException("获取用户信息失败"));
// 转为 UserDetails 对象
return LoginUser.builder()
.sysUser(user)
.build();
}
}
3、创建 UserDetails 实现类
@Data
@Builder
public class LoginUser implements UserDetails {
/**
* 系统用户
*/
private SysUser sysUser;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
4、增加配置类,设置密码加密的 bean
@Configuration
public class SecurityConfig {
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
5、现在登录功能就是使用数据库中获取的用户信息了
Security配置
1、创建 WebSecurityConfigurerAdapter 实现类,重写 configure(HttpSecurity http) 方法,就可以配置过滤器链,设置请求过滤、登录页、登录页、添加过滤器等操作
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 过滤请求
.authorizeRequests()
// 对以下接口放行
.antMatchers("/hello", "/login")
.permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated().and().headers().frameOptions().disable();
// 去掉这一行就没有默认的登录页面了
http.formLogin();
}
}
2、配置自定义过滤器,新增 JwtAuthenticationTokenFilter 模仿使用 jwt 进行拦截
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求携带的令牌
String token = request.getHeader("Authorization");
System.out.println(token);
//放行
filterChain.doFilter(request, response);
}
}
3、在 SecurityConfig 配置类中添加过滤器
4、这个时候把默认的登录页去掉,然后进行访问测试

5、直接访问放行的端口是正常并且每个请求都会经过自定义的过滤器
6、但是如果访问未放行的路径则会报错,提示“未获权限”
7、这个时候如果在 jwt 过滤器中往 security 的上下文设置 Authentication,则所有请求都会获取访问权限
8、再次访问未放行的路径则可以正常进入
9、只不过 jwt 过滤器中的登录信息需要从 token 中获取并验证是否有效,是否过期然后再放行
10、至此,还需要配置一个登录接口,用户登录成功之后放回一个 jwt 的 token 给前端,后续前端请求的时候携带上这个 token 即可继续访问
11、在 SecurityConfig 配置类中配置登出接口,创建 logoutSuccessHandler 登出处理类,在登出时候将 jwt 的 token 删除掉
/**
* 登出成功处理
*/
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
// 添加Logout filter
http.logout().logoutUrl("/sys/user/logout").logoutSuccessHandler(logoutSuccessHandler);
12、创建登出处理类
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private RedisService redisService;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 获取请求携带的令牌
String token = request.getHeader("Authorization");
Payload payload = jwtUtils.validate(token);
String key = payload.getUuid();
// 删除用户记录
redisService.removeKey(key);
// 继续响应
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print("注销成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
配置登录接口
方式一
在 security 中,都是通过调用 AuthenticationManager 接口的 authenticate 方法进行鉴权的,所以我们只需要在我们自己的登录接口中调用到这个鉴权方法即可
1、AuthenticationManager 是一个 security 的接口,所以我们需要找到他的实现类,而在 WebSecurityConfigurerAdapter 类中,存在一个方法用于创建 AuthenticationManager 对象,但这个对象并未被注册成为 bean,所以还无法直接使用

2、同时 WebSecurityConfigurerAdapter 也是我们配置 security 时需要继承的类,所以只需要在我们自定义的 security 配置类中重写这个方法并注册为 bean 即可
@Configuration
@EnableAuthorizationServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
3、配置自定义登录接口
@RestController
@RequestMapping("/sys/user")
public class SysUserController {
@Autowired
private SysUserService sysUserService;
@PostMapping("/login")
public ResponseData<Authentication> login(@Validated @RequestBody SysLoginBody body) {
return ResponseData.success(sysUserService.login(body));
}
}
@Service
public class SysUserService {
@Autowired
private AuthenticationManager authenticationManager;
public Authentication login(SysLoginBody body) {
Authentication authentication;
try {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
// 调用登录鉴权方法
authentication = authenticationManager.authenticate(authenticationToken);
} catch (BadCredentialsException e) {
throw new FlowException("密码有误");
} catch (Exception e) {
throw new FlowException(e.getMessage());
}
return authentication;
}
}
方式二
通过自定义过滤器进行拦截
- 创建 AbstractHttpConfigurer 继承类,添加自定义过滤器 WebPageLoginFilter,配置在 UsernamePasswordAuthenticationFilter 之前,即不用默认的登录过滤器了
@Slf4j
public class LoginFilterDsl extends AbstractHttpConfigurer<LoginFilterDsl, HttpSecurity> {
@Override
public void init(HttpSecurity builder) throws Exception {
super.init(builder);
}
@Override
public void configure(HttpSecurity builder) throws Exception {
super.configure(builder);
log.info("[{ } ] configure", getClass().getSimpleName());
ApplicationContext context = builder.getSharedObject(ApplicationContext.class);
WebLoginFilter webLoginFilter = context.getBean(WebLoginFilter.class);
builder.addFilterBefore(webLoginFilter, UsernamePasswordAuthenticationFilter.class);
}
public static LoginFilterDsl webLoginFilterDsl() {
return new LoginFilterDsl();
}
}
- 仿照 UsernamePasswordAuthenticationFilter 过滤器,新增 AbstractAuthenticationProcessingFilter 实现类的过滤器,创建构造方法,实现 attemptAuthentication() 鉴权方法
public class WebLoginFilter extends AbstractAuthenticationProcessingFilter {
private static final AntPathRequestMatcher MATCHER = new AntPathRequestMatcher("/web-login", HttpMethod.POST.name());
public WebLoginFilter() {
super(MATCHER);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
return null;
}
}
- 在 WebSecurityConfig 配置类中使用 LoginFilterDsl 配置类即可
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// 获取直接在这里添加过滤器也可以,就不用创建 LoginFilterDsl 配置类了
http.apply(webLoginFilterDsl());
}
}
原理解释
- 因为继承了 AbstractAuthenticationProcessingFilter 过滤器,并调用了父类的构造方法,所以会将我们自定义的登录路径配置进去


- 而 AbstractAuthenticationProcessingFilter 是实现了 Filter 接口

- 所以 AbstractAuthenticationProcessingFilter 就会被调用 doFilter() 方法,判断当前路径是否为登录路径,是的话就会调用 attemptAuthentication() 方法进行登录



- 所以当我们访问
/web-login路径的时候就会调用我们自定义的登陆方法

示例代码
- 本文链接:https://lxjblog.gitee.io/2024/04/09/Security/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。