Spring-Security 6.x版本入门讲解

Spring-Security

配置代码
package magnus.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.core.MediaType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@EnableWebSecurity
public class MagnusSecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
                                                   ObjectMapper objectMapper) throws Exception {
        // 定义安全请求拦截规则
        httpSecurity.authorizeHttpRequests(request -> request.anyRequest().authenticated());
        // 给SpringSecurity注入 /login登录页面及用户密码表单处理的登录请求
        httpSecurity.formLogin(Customizer.withDefaults());
        // 登出请求注册
        httpSecurity.logout(LogoutConfigurer::permitAll);
//        httpSecurity.addFilterBefore(magnusUsernamePasswordFilter(objectMapper),
//                                     UsernamePasswordAuthenticationFilter.class);
        // 关闭csrf
        httpSecurity.csrf(AbstractHttpConfigurer::disable);
        return httpSecurity.build();
    }

    @Bean
    public UsernamePasswordAuthenticationFilter magnusUsernamePasswordFilter(ObjectMapper objectMapper) {
        // 配置自定义的UsernamePasswordAuthenticationFilter
        MagnusUsernamePasswordFilter magnusUsernamePasswordFilter = new MagnusUsernamePasswordFilter();
        magnusUsernamePasswordFilter.setAuthenticationManager(providerManager());
        magnusUsernamePasswordFilter.setObjectMapper(objectMapper);
        magnusUsernamePasswordFilter.initialize();
        return magnusUsernamePasswordFilter;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails build = User.withUsername("username").password("{noop}password").roles("user").build();
        return new InMemoryUserDetailsManager(build);
    }

    @Bean
    public AuthenticationManager providerManager() {
        List<AuthenticationProvider> providers = new ArrayList<>();
        // 自定义Provider,此处可以定义多数据源。
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService());
        providers.add(provider);
        return new ProviderManager(providers);
    }
}
原理

​ 配置spring-security包,需要使用@EnableWebSecurity注解。

@EnableWebSecurity注解通过@Import注解注入了四个类: WebSecurityConfigurationSpringWebMvcImportSelectorOAuth2ImportSelectorHttpSecurityConfiguration

WebSecurityConfiguration主要是注入FilterChainProxy对象,这个对象是SpringSecurity调用链的入口代理。

HttpSecurityConfiguration主要是注入不同的SecurityFilterChain对象。这些对象包含不同的过滤规则,但是最后都被注册到FilterChainProxy内作为内部的调用链流程进行请求过滤。

SpringSecurity中最重要的两个概念,一个是FilterChainProxy,一个是SecurityFilterChainFilterChainProxy作为SpringSecurity安全控制的总入口,而SecurityFilterChain则定义了安全校验的各种规则。

SpringSecurity

SpringWeb框架会使用GenericFilterBean来标识一个Bean为过滤器,在初始化Web容器过程中,会扫描这些bean,并将他们统一注册到SpringFilterChain中,最后把这个SpringFilterChain注册为Tomcat的Filter。

授权流程

​ SpringSecurity 授权流程主要涉及UsernamePasswordAuthenticationFilter类,通过HttpSecurity.formLogin来自动注入这个过滤器到FilterChainProxy中。这个类会校验是否需要生成默认登录页面,并自动生成/login GET开头的登录html网页和/login POST的登录处理器,所以这里依赖spring-mvc环境。UsernamePasswordAuthenticationFilter类调用doFilter()方法判断当前请求是否是POST,如果符合条件,会调用attemptAuthentication方法来进行权限校验。权限校验过程中会生成UsernamePasswordAuthenticationToken对象,并且以这个对象为授权处理类。UsernamePasswordAuthenticationFilter主要逻辑是从请求Request中获取用户登录的参数,然后将具体的校验逻辑委派给AuthenticationManager来验证,即调用this.getAuthenticationManager().authenticate(authRequest)

AuthenticationManager的一般实现类是ProviderManagerProviderManager类会把校验委派给注册的所有provider。只要有一个provider实现类能成功校验,就直接返回校验成功。ProviderManager中一般会注册一个JDBC相关的Provider,即DaoAuthenticationProviderDaoAuthenticationProvider里面会注册UserDetailsService.loadUserByUsername()来去数据库中查询用户相关数据。

ProviderManager验证用户身份正确后,UsernamePasswordAuthenticationFilter会继续执行父类的doFilter方法。调用this.successfulAuthentication(request, response, chain, authenticationResult)方法来进行验证通过的后续处理。将Authentication验证结果放到SecurityContextHolder对象中(可以在后续的处理中通过SecurityContextHolder.getContext来获取)。同时会调用this.securityContextRepository.saveContext(context, request, response)来定制化的保存Authentication。其中,默认的SecurityContextRepository实现有HttpSessionSecurityContextRepositoryRequestAttributeSecurityContextRepositoryHttpSessionSecurityContextRepository的作用是,判断当前请求是否有Session字段,如果有Session,则清空当前的Session,并创建一个新的Session,放到Response的报文头中。RequestAttributeSecurityContextRepository的作用是,在SPRING_CONTEXT中加入字段SPRING_SECURITY_CONTEXT,可以在Spring定义的html中通过此字段访问这个变量。在所有的SecurityContextRepository调用结束后,会执行AbstractAuthenticationProcessingFilter中定义的成功回调this.successHandler.onAuthenticationSuccess(request, response, authResult)。此回调方法可以通过setAuthenticationSuccessHandler方法实现自定义。

​ 至此,SpringSecurity基于用于身份、密码的校验流程已经完成。

权限验证

​ SpringSecurity经过上述的授权流程后,会在HTTP请求的返回报文中附上SessionID,即Cookies:JSESSIONID=74E183225F3A22A4FEF14A32E7717584; Path=/; HttpOnly;。客户端下一次请求资源的时候,就会带上这个JSESSIONID作为自己的身份验证标识。以请求路径为/为例,客户端授权后发起/的路径请求默认页面。

​ SpringSecurity在构造SecurityFilterChain的时候,会默认加上HttpSecurity.securityContext()方法。此方法给SecurityFilterChain对象注册了SecurityContextHolderFilter对象。这个对象就是默认权限验证的关键类。

​ SpringSecurity在构造SecurityFilterChain的时候,会要求指定要过滤的链接,即HttpSecurity.authorizeHttpRequest()构造器,此构造器会给SecurityFilterChain对象注册AuthorizationFilter

​ 请求进来之后,经过FilterChainProxydoFilter方法,会通过代理,遍历执行FilterChainProxy中注册的所有filters链。当请求被SecurityContextHolderFilter过滤时,这个过滤器会给securityContextHolderStrategy设置一个deferredContext回调方法。这个方法内部实现是校验当前请求是否有Session,如果有Session则判断这个Session是不是Security使用的Session。这里不会立即执行,而是存放到securityContextHolderStrategy中供后续调用。请求过滤器链继续执行,最后会来到AuthorizationFilterAuthorizationFilter会调用this::getAuthentication方法来获取当前验证的结果。而this::getAuthentication方法就是通过调用this.securityContextHolderStrategy.getContext().getAuthentication();方法来实现,也即上述在SecurityContextHolderFilter流程中注册的对象。这里调用了延迟函数deferredContext的内部实现:HttpSessionSecurityContextRepository.readSecurityContextFromSession()。读取了Session后,将Session转换为Authentication对象,来继续进行权限过滤校验。如果通过,则直接放行,否则会抛出Access Denied异常。抛出的Access Denied异常将被捕获并提交给ExceptionHandling来处理。默认的ExceptionHandling处理是使用Http403AuthenticationEntrypoint,即返回403报错。

​ 至此,权限校验流程结束。

扩展

​ 当前分布式环境下,已经不再使用Session作为客户端信息保存的场景。SpringSecurity引入JWT(JSON Web Token)来保存登录状态是更好的选择。

​ 扩展SpringSecurity默认的实现,有两个需要扩展的点:权限验证,授权。

​ 首先是扩展授权,也就是我们需要自定义实现类似于UsernamePasswordAuthenticationFilter的登录验证。授权的目标是,监听/login POST请求,读取ReqBody中用户上送的身份验证相关数据,和数据库中存储的用户信息做对比。如果验证无问题,则生成JWT的TOKEN码值,并存放到HttpServletResponse的自定义Header中。

package magnus.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.core.MediaType;
import lombok.Setter;
import magnus.utils.jwt.JwtAuthentication;
import magnus.utils.jwt.JwtPayload;
import magnus.utils.jwt.JwtTokenService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

/**
 * Jwt授权,判断当前路径是否是/login POST,如果满足条件则给予授权服务
 */
@Setter
public class MagnusJwtAuthorizationFilter extends AbstractAuthenticationProcessingFilter {

    private String tokenHeader;
    private ObjectMapper objectMapper;
    private JwtTokenService jwtTokenService;

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
                                                                                                            HttpMethod.POST);
    private static final String USERNAME_KEY = "username";
    private static final String PASSWORD_KEY = "password";

    protected MagnusJwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    protected MagnusJwtAuthorizationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        super(requiresAuthenticationRequestMatcher);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // 取 登录请求的数据
        if (!request.getMethod().equals(HttpMethod.POST)) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        ServletInputStream inputStream = request.getInputStream();
        byte[] b = new byte[1024];
        int read;
        StringBuilder stringBuilder = new StringBuilder();
        while ((read = inputStream.read(b)) > 0) {
            stringBuilder.append(new String(b, 0, read));
        }
        String reqBody = stringBuilder.toString();
        Map map = objectMapper.readValue(reqBody, Map.class);
        //
        String username = (String) map.get(USERNAME_KEY);
        String password = (String) map.get(PASSWORD_KEY);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                username, password);
        this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
        return usernamePasswordAuthenticationToken;
    }

    public void initialize() {
        // todo 设置success handler 和 failure handler
        super.setAuthenticationSuccessHandler(((request, response, authentication) -> {
            JwtPayload build = JwtPayload.builder().expireTime(Instant.now().plusSeconds(300).toEpochMilli())
                                         .userid((String) authentication.getPrincipal())
                                         .allocationTime(Instant.now().toEpochMilli()).build();
            String token = jwtTokenService.generateToken(build);
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
            usernamePasswordAuthenticationToken.setDetails(build);
            response.setContentType(MediaType.APPLICATION_JSON);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            Map<String, Object> map = new HashMap<>();
            map.put("result", "登录成功");
            map.put("authentication", authentication);
            response.getWriter().write(objectMapper.writeValueAsString(map));
            response.setHeader(tokenHeader, token);
        }));

        super.setAuthenticationFailureHandler(((request, response, exception) -> {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }));
    }
}

​ 需要将上面的MagnusJwtAuthorizationFilter注入到SecurityFilterChain中,如下所示:

package magnus.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import magnus.utils.jwt.JwtTokenService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@EnableWebSecurity
@ConfigurationProperties(prefix = "magnus.jwt")
public class MagnusSecurityConfiguration {

    String tokenHeader;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, ObjectMapper objectMapper,
                                                   JwtTokenService jwtTokenService) throws Exception {
        httpSecurity.authorizeHttpRequests(
                request -> request.requestMatchers("/restful/**").permitAll().anyRequest().authenticated());
        httpSecurity.logout(LogoutConfigurer::permitAll);
        httpSecurity.addFilterAfter(magnusJwtAuthenticationFilter(jwtTokenService, objectMapper),
                                    SecurityContextHolderFilter.class);
        httpSecurity.csrf(AbstractHttpConfigurer::disable);
        return httpSecurity.build();
    }

    public MagnusJwtAuthorizationFilter magnusJwtAuthorizationFilter(JwtTokenService jwtTokenService,
                                                                     ObjectMapper objectMapper) {
        MagnusJwtAuthorizationFilter magnusJwtAuthorizationFilter = new MagnusJwtAuthorizationFilter(providerManager());
        magnusJwtAuthorizationFilter.setObjectMapper(objectMapper);
        magnusJwtAuthorizationFilter.setTokenHeader(tokenHeader);
        magnusJwtAuthorizationFilter.setJwtTokenService(jwtTokenService);
        magnusJwtAuthorizationFilter.initialize();
        return magnusJwtAuthorizationFilter;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails build = User.withUsername("username").password("{noop}password").roles("user").build();
        return new InMemoryUserDetailsManager(build);
    }

    @Bean
    @Qualifier("providerManager")
    public AuthenticationManager providerManager() {
        List<AuthenticationProvider> providers = new ArrayList<>();
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService());
        MagnusJwtUserInfoProvider magnusJwtUserInfoProvider = new MagnusJwtUserInfoProvider();
        providers.add(provider);
        providers.add(magnusJwtUserInfoProvider);
        return new ProviderManager(providers);
    }

    public void setTokenHeader(String tokenHeader) {
        this.tokenHeader = tokenHeader;
    }
}

​ 需要注意的一点是,这个类不可以注册到Spring容器中,因为这个Bean是默认实现了GenericFilterBean,会被SpringWeb捕获到作为SpringWeb的过滤器,这样就会被执行两遍了!

​ 更多扩展点包括,可以自定义从数据库中读取用户参数(上面代码是内存中配置的用户),只要修改UserDetailsService。可以自定义请求头的key值,只需配置文件中修改magnus.jwt.tokenHeader等。同时也可以配置多数据源校验,就是在ProviderManager中注册多个DaoAuthenticationProvider

​ 授权完成,当用户再次请求某个服务器资源时,服务器就需要校验客户是否有足够的权限,也就是客户请求携带的token是否符合服务器的要求。

package magnus.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.MediaType;
import lombok.NonNull;
import lombok.Setter;
import magnus.utils.jwt.JwtPayload;
import magnus.utils.jwt.JwtTokenService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@Setter
public class MagnusJwtAuthenticationFilter extends OncePerRequestFilter {

    private String tokenHeader;
    private ObjectMapper objectMapper;
    private JwtTokenService jwtTokenService;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws IOException, ServletException {
        if (requireAuthentication(request)) {
            // 如果是POST请求,则获取对应的token报文头
            String header = request.getHeader(tokenHeader);
            if (header != null) {
                try {
                    // 验证成功
                    JwtPayload jwtPayload = jwtTokenService.verifyToken(header);
                    // 直接放行
                    UsernamePasswordAuthenticationToken authenticated = UsernamePasswordAuthenticationToken.authenticated(
                            jwtPayload.getUserid(), jwtPayload.getPassword(), jwtPayload.getAuthorities());
                    // todo 查询出用户相关信息,放到Authentication里面
                    SecurityContextHolder.getContext().setAuthentication(authenticated);
                } catch (Exception e) {
                    // 校验失败,交给后续的步骤来校验
                    response.setHeader(tokenHeader, null);
                }
            }
        }
        filterChain.doFilter(request, response);
    }

    public boolean requireAuthentication(HttpServletRequest request) {
        if (request.getHeader(tokenHeader) != null) {
            return true;
        }
        return false;
    }

    private void errorHandler(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 报错直接返回登录异常
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType(MediaType.APPLICATION_JSON);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        Map<String, Object> map = new HashMap<>();
        map.put("result", "登录失败");
        response.getWriter().write(objectMapper.writeValueAsString(map));
    }
}

​ 同样是定义一个过滤器,每次读取请求的报文头中是否包含自定义的tokenHeader字段。如果包含,则校验包含的token是否有误。校验通过,将Authentication结果存到SecurityContextHolder中,并继续后续的过滤规则。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
org.springframework.security.authentication.InternalAuthenticationServiceException: null at org.springframework.security.authentication.dao.DaoAuthenticationProvider.retrieveUser(DaoAuthenticationProvider.java:123) ~[spring-security-core-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:144) ~[spring-security-core-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:199) ~[spring-security-core-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:95) ~[spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77) [spring-security-web-5.3.4.RELEASE.jar:5.3.4.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.9.RELEASE.jar:5.2.9.
07-20

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值