六、自定义认证filter替换掉默认的表单认证,改用json接收登录请求参数

说明

通过自定义认证filter,替换掉默认的表单认证,改用json接收登录请求参数。自定义响应处理方法,登录成功或失败后响应json数据

配置说明:

UsernamePasswordAuthenticationFilter: 以表单提交方式处理身份认证,提交用户名密码参数到POST /login完成登录认证。通过继承该类,重写attemptAuthentication方法以改写json参数接收方式和认证处理。

.addFilterAt(自定义Filter实例, UsernamePasswordAuthenticationFilter.class): 在HttpSecurity 配置自定义认证过滤器,替换掉默认的UsernamePasswordAuthenticationFilter过滤器。
这里还可以添加其他的过滤器,例如:.addFilterAfter() 、.addFilterBefore()

AuthenticationConfiguration:通过它可以获取到具体的认证处理器,本文中对应的认证方式为基于内存的认证方式。

SecurityConfig 配置

package com.example.uaa.config;

import com.example.uaa.filter.RestAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity(debug = false) // 开启调试模式,生产环境不要使用
@RequiredArgsConstructor
public class SecurityConfig {
    private final ObjectMapper objectMapper;
    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests((req) -> req
                        // 需要认证并拥有特定角色才能访问的URL
                        .antMatchers("/api/**").hasRole("USER")
                        .antMatchers("/admin/**").hasRole("ADMIN")
                        // 放行不需要认证的URL
                        .antMatchers("/authorize/**").permitAll()
                        .antMatchers("/webjars/**", "/error").permitAll()
                        // 放行通用的css、js等静态资源位置
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                        // 除了以上规则,默认拦截所有URL请求,要求认证后才能访问(该语句必须要放在末尾)
                        .anyRequest().authenticated()
                )
                // 添加自定义登录过滤器,替换掉默认的UsernamePasswordAuthenticationFilter过滤器
                .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 以下表单登录,被restAuthenticationFilter配置替换:
                // .formLogin(form-> form
                //         // 定义登陆页面
                //         .loginPage("/login").permitAll()
                //         // 登录成功后跳转的页面
                //         // .defaultSuccessUrl("/")
                //         // 登录成功后置处理器。会覆盖掉defaultSuccessUrl的配置
                //         .successHandler(((request, response, authentication) -> {
                //             ObjectMapper objectMapper = new ObjectMapper();
                //             response.setStatus(HttpStatus.OK.value());
                //             response.getWriter().println("LoginSuccess:"+
                //                     objectMapper.writeValueAsString(authentication));
                //         }))
                //         // 登录失败后置处理器,不配置,默认跳转/login?error
                //         .failureHandler(((request, response, exception) -> {
                //             response.setStatus(HttpStatus.UNAUTHORIZED.value());
                //             response.getWriter().println("LoginFailure!");
                //         }))
                // )
                // 配置退出登录的路径,配置后使用POST请求。若不配置,默认为/logout,退出成功跳到/login
                .logout(logout-> logout.logoutUrl("/perform_logout").logoutSuccessUrl("/login"))
                .httpBasic(Customizer.withDefaults())
                // 开启CSRF ,并配置不需要CSRF的路径
                // .csrf(csrf->csrf.ignoringAntMatchers("/api/**"))
                // .csrf()
                // 禁用csrf
                .csrf().disable()
                .rememberMe(Customizer.withDefaults());
        // ...
        return http.build();
    }

    /**
     * 设定认证方式 Authentication
     * 代替了配置
     * spring.security.user.name=user
     * spring.security.user.password=12345
     * spring.security.user.roles=ADMIN,USER
     *
     * 基于spring boot 2.7 后新的认证方式:
     * https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
     */

    /**
     * 基于内存的认证 InMemoryUserDetailsManager
     */
    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.builder()
                .username("user")
                .password(passwordEncoder().encode("123456"))
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        // 登录成功后置处理器。
        filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            ObjectMapper objectMapper = new ObjectMapper();
            response.setStatus(HttpStatus.OK.value());
            response.getWriter().println("LoginSuccess: "+
                    objectMapper.writeValueAsString(authentication));
        });
        // 登录失败后置处理器
        filter.setAuthenticationFailureHandler((request, response, exception) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().println("LoginFailure!");
        });
        // 获得AuthenticationManager
        filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
        filter.setFilterProcessesUrl("/authorize/login");
        return filter;
    }

}

自定义认证过滤器 RestAuthenticationFilter.java

package com.example.uaa.filter;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
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.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义登录过滤器
 */
@RequiredArgsConstructor
public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final ObjectMapper objectMapper;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("不支持的请求方法: " + request.getMethod());
        }
        UsernamePasswordAuthenticationToken authRequest;
        try {
            var jsonNode = objectMapper.readTree(request.getInputStream());
            String username = jsonNode.get("username").textValue();
            String password = jsonNode.get("password").textValue();

            authRequest = new UsernamePasswordAuthenticationToken(username, password);

        } catch (IOException e) {
            e.printStackTrace();
            throw new BadCredentialsException("用户名或密码错误");
        }

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

posted @ 2023-06-18 20:48  aesopcmc  阅读(227)  评论(0)    收藏  举报