01_SpringSecurity学习之配置HttpSecurity

前言

  🙃😁虽然 Spring Security 不是我发明的,相关的配置方法也是整理自网络,但至少整理的工作是我做的,文章是我自己写的,所以也就算是原创吧😂

  本人菜鸟一枚,这篇文章算是我学习 Spring Security 的记录吧,文中的代码都是自己运行过的,所以放心食用🍔

1 环境

  • Intellij IDEA 2022.2
  • Spring Boot 2.7.2
    • Spring Security 5.7.2

2 配置

2.1 空

@EnableWebSecurity
public class SecurityConfig {
}

  因为 WebSecurityConfigurerAdapter 被标记了 @Deprecated ,所以,没有通过继承它来配置 Spring Security。

  启动项目,访问接口时,发现需要认证。Spring Security 提供了一个登录页面,用户名是 user,密码在IDEA的控制台里。

2.2 极简

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@EnableWebSecurity
public class SecurityConfig {
    /**
     * HttpSecurity 相关的设置
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 如果没有下面的语句, 那么任何请求都可以免认证
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // 使用默认的表单登录
                .formLogin(withDefaults())
                // 使用默认的 http basic 登录
                .httpBasic(withDefaults());
        return http.build();
    }

    /**
     * 配置要忽略的路径
     */
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        // 忽略 /error 页面
        return web -> web.ignoring().antMatchers("/error")
                // 忽略常见的静态资源路径
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }
}

  2.2 和 2.1 的运行效果相同。究其原因是 Spring Security 默认就是 2.2 这样的配置,如下代码所示:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        @Bean
        @Order(SecurityProperties.BASIC_AUTH_ORDER)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().authenticated();
            http.formLogin();
            http.httpBasic();
            return http.build();
        }
    }
}

2.3 HttpSecurity

2.3.1 简单尝试

 2.3.1.1 启用默认 formLogin & 启用默认 httpBasic
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(withDefaults())
            .httpBasic(withDefaults());
    return http.build();
}

  此时有2种登录方式——表单登录和httpBasic登录,如下图:

  1. 表单登录:Spring Security 提供了默认登录页面
    表单登录_202207300933

  2. httpBasic 登录:使用 IDEA 自带的 http client 测试

### hello 接口测试
GET http://localhost:8080/hello/say
Authorization: Basic user 079c38e0-86b3-4e2f-ac65-8429f09a1bff
 2.3.1.2 禁用 formLogin & 启用默认 httpBasic
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(form -> form.disable())
            .httpBasic(withDefaults());
    return http.build();
}

  此时,浏览器地址栏访问 http://localhost:8080/hello/say 不会重定向到 /login 表单登录页面,而是会弹出一个框,让输入用户名和密码。如下图:

弹出框_202207300943

  点击登录按钮,由F12可知,实际走的是 httpBasic 认证方式,如下图:

弹出框_202207300946
  上图中, 请求头 Authorization: Basic dXNlcjo5MzRmYjY5ZS01YmViLTQ3NTgtOTczMC00MTRmYWZlZGZjOTQ= 使用 Base64 解码后, 可知 dXNlcjo5MzRmYjY5ZS01YmViLTQ3NTgtOTczMC00MTRmYWZlZGZjOTQ= 这一串, 解码后是 user:934fb69e-5beb-4758-9730-414fafedfc94 . 也就是说, 所谓的 Base64 编码, 就是 用户名 冒号 密码

2.3.2 formLogin

  Spring Security 的配置:

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
                .formLogin(form -> form.loginPage("/login").permitAll());
        return http.build();
    }
}

  配置一个视图控制器: (点击查看 index.html)

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        // 还有一个首页的视图, 相关的 html 详见文章末尾
        registry.addViewController("/").setViewName("index");
    }
}

  引入 thymeleaf 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

  把 Spring Security 官网示例1src/main/resources/templates/login.html 复制粘贴到自己的项目中,如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">

<head>
    <title>Please Log In</title>
</head>

<body>
    <h1>Please Log In</h1>
    <div th:if="${param.error}">
        Invalid username and password.</div>
    <div th:if="${param.logout}">
        You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <input type="text" name="username" placeholder="Username" />
        </div>
        <div>
            <input type="password" name="password" placeholder="Password" />
        </div>
        <div>
            <!-- 这个是自己加的, remember me 后面会用 -->
            <input type="checkbox" name="remember-me" />Remember Me
        </div>
        <input type="submit" value="Log in" />
    </form>
</body>

</html>

  然后运行效果如下图(图示的 login.html 没有 remember me):
FORM运行效果图
  由 F12 可知,/login 登录请求,除了传用户名和密码,还传了 csrf token ,如下图:
FORM运行效果图_登录后

2.3.3 Remember Me

  Spring Security 的配置:

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
            .formLogin(form -> form.loginPage("/login").permitAll())
            // 退出登录的配置
            .logout(logout -> logout.logoutUrl("/my-logout"))
            // 记住我的设置, 注意前端 login.html 把 remember me 的标签加上
            .rememberMe(rememberMe -> rememberMe.key("myKey").tokenValiditySeconds(7*24*3600))
    ;
    return http.build();
}

  请求的时候,参数里面会有 remember-me
REMEMBER_ME
  响应头里面会让浏览器设置 remember me 相关的 Cookie
REMEMBER_ME_2

2.3.4 登录成功或失败的 Handler

  Spring Security 的配置:

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.val;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
                .formLogin(form -> form.loginPage("/login").permitAll()
                        .successHandler(jsonAuthenticationSuccessHandler())
                        .failureHandler(jsonAuthenticationFailureHandler()))
                // 退出登录的配置
                .logout(logout -> logout.logoutUrl("/my-logout"))
                // 记住我的设置
                .rememberMe(rememberMe -> rememberMe.key("myKey").tokenValiditySeconds(7 * 24 * 3600))
        ;
        return http.build();
    }

    /**
     * 认证失败的处理器
     *
     * @return 函数
     */
    private static AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
        return (request, response, exception) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            val objectMapper = new ObjectMapper();
            val data = Map.of("title", "登录失败", "status", "error");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.getWriter().println(objectMapper.writeValueAsString(data));
        };
    }

    /**
     * 认证成功的处理器
     *
     * @return 函数
     */
    private static AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
        return (request, response, authentication) -> {
            response.setStatus(HttpStatus.OK.value());
            val objectMapper = new ObjectMapper();
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.getWriter().println(objectMapper.writeValueAsString(authentication));
        };
    }
}

  登录成功如下图:

登录成功1

登录成功2

  登录失败如下图:

登录失败1

2.3.5 退出登录成功的 Handler

  Spring Security 的配置:

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            // 如果这里不设置 authorizeHttpRequests, 那么任何请求都不需要登录
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            // 使用自定义的登录页面, 注意这里需要 permitAll() 否则访问 login.html 页面的时候也会需要认证
            .formLogin(form -> form.loginPage("/login").permitAll()
                    .successHandler(jsonAuthenticationSuccessHandler())
                    .failureHandler(jsonAuthenticationFailureHandler()))
            // 退出登录的配置
            .logout(logout -> logout.logoutUrl("/my-logout")
                    .logoutSuccessHandler(jsonLogoutSuccessHandler()))
            // 记住我的设置
            .rememberMe(rememberMe -> rememberMe.key("myKey").tokenValiditySeconds(7 * 24 * 3600))
    ;
    return http.build();
}

/**
* 退出登录成功时的处理器
*
* @return 函数
*/
private static LogoutSuccessHandler jsonLogoutSuccessHandler() {
   return (request, response, authentication) -> {
       response.setStatus(HttpStatus.OK.value());
       val objectMapper = new ObjectMapper();
       val data = Map.of("title", "退出登录成功", "status", "success");
       response.setContentType(MediaType.APPLICATION_JSON_VALUE);
       response.setCharacterEncoding(StandardCharsets.UTF_8.name());
       response.getWriter().println(objectMapper.writeValueAsString(data));
   };
}

  如下图:
退出登录成功

2.3.6 自定义 Filter

  Spring Security 的配置:

// debug = true 可以看到更多的 spring security 的日志
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
    private final ObjectMapper objectMapper;
    private final AuthenticationHandler handler;
    private final ObjectPostProcessor<Object> objectPostProcessor;

    /**
     * HttpSecurity 相关的设置
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // 用自定义的 RestAuthenticationFilter 替换 UsernamePasswordAuthenticationFilter
                .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 用户是通过 json 请求登录的, 是无状态的, 可以把 csrf 禁用
                .csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }

    /**
     * 自定义的认证过滤器
     */
    private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        // 设置认证成功的处理器
        filter.setAuthenticationSuccessHandler(handler.jsonAuthenticationSuccessHandler());
        // 设置认证失败的处理器
        filter.setAuthenticationFailureHandler(handler.jsonAuthenticationFailureHandler());
        // 设置认证管理器, 如果不设置, 会报错, 说缺少 authenticationManager
        filter.setAuthenticationManager(authenticationManager());
        // 设置自定义过滤器要针对的 URL 路径
        filter.setFilterProcessesUrl("/rest/login");
        return filter;
    }

    /**
     * 构造一个认证管理器
     */
    @Bean
    AuthenticationManager authenticationManager() throws Exception {
        // 这里打日志 验证 authenticationManager 是否只初始化了一次, 因为上面有调用 authenticationManager()
        log.info("初始化 authenticationManager");
        // objectPostProcessor 是可以直接使用 spring context 的对象, 这个是参考了已废弃的 WebSecurityConfigurerAdapter 得知的
        AuthenticationManagerBuilder auth = new AuthenticationManagerBuilder(objectPostProcessor);
        // 创建一些内存中的用户, 用作测试
        auth.inMemoryAuthentication()
                .withUser("user")
                .password("{bcrypt}" + passwordEncoder().encode("password"))
                .roles("USER")
                .and()
                .withUser("admin")
                .password("{bcrypt}" + passwordEncoder().encode("password"))
                .roles("ADMIN", "USER");
        return auth.build();
    }

    /**
     * 密码编码器
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        // 也是验证 passwordEncoder 是否只初始化了一次
        log.info("初始化 passwordEncoder");
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置要忽略的路径
     */
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        // 忽略 /error 页面
        return web -> web.ignoring().antMatchers("/error")
                // 忽略常见的静态资源路径
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }
}

  自定义的过滤器类:

@RequiredArgsConstructor
public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final ObjectMapper objectMapper;

    private static final String POST = "POST";

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 只处理 POST 请求
        if (!POST.equalsIgnoreCase(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username;
        String password;
        try {
            // 从请求体中获取用户名和密码
            ServletInputStream inputStream = request.getInputStream();
            JsonNode jsonNode = objectMapper.readTree(inputStream);
            username = jsonNode.get("username").textValue();
            password = jsonNode.get("password").textValue();
        } catch (IOException e) {
            throw new BadCredentialsException("用户名或密码错误");
        }
        // 参照 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法来组装 token, setDetails(), 进行 authenticat()
        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                password);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

  自定义的认证成功失败的处理器类:

@Component
@RequiredArgsConstructor
public class AuthenticationHandler {
    private final ObjectMapper objectMapper;

    /**
     * 退出登录成功时的处理器
     *
     * @return 函数
     */
    public LogoutSuccessHandler jsonLogoutSuccessHandler() {
        return (request, response, authentication) -> {
            response.setStatus(HttpStatus.OK.value());
            val data = Map.of("title", "退出登录成功", "status", "success");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.getWriter().println(objectMapper.writeValueAsString(data));
        };
    }

    /**
     * 认证失败的处理器
     *
     * @return 函数
     */
    public AuthenticationFailureHandler jsonAuthenticationFailureHandler() {
        return (request, response, exception) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            val data = Map.of("title", "登录失败", "status", "error");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.getWriter().println(objectMapper.writeValueAsString(data));
        };
    }

    /**
     * 认证成功的处理器
     *
     * @return 函数
     */
    public AuthenticationSuccessHandler jsonAuthenticationSuccessHandler() {
        return (request, response, authentication) -> {
            response.setStatus(HttpStatus.OK.value());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.getWriter().println(objectMapper.writeValueAsString(authentication));
        };
    }
}

  在 IDEA 的 http client 中验证:

### login rest 登录测试
POST http://localhost:8080/rest/login
Content-Type: application/json

{
  "username": "user",
  "password": "password"
}

附录

1. index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>

<body>
    <h1>欢迎~</h1>
    <div>
        <form th:action="@{/my-logout}" method="post">
            <input type="submit" value="退出登录" />
        </form>
    </div>
</body>

</html>

2. 注脚


  1. Spring Security 官网示例连接 https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html ↩︎

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值