七、Spring Boot集成Spring Security之前后分离认证最佳实现

前言

本文介绍前后分离认证最佳实现,配合以下内容观看效果更佳!!!

一、自定义用户名密码认证过滤器RestfulUsernamePasswordAuthenticationFilter

1、注册过滤器方式

  1. 使用httpSecurity.addFilter/addFilterBefore/addFilterAfter向过滤器链中添加过滤器,其中addFilter只能添加内置的过滤器,顺序已在过滤器顺序注册器(FilterOrderRegistration)中设置;addFilterBefore/addFilterAfter可以添加自定义过滤器,添加在指定的过滤器之前/之后。该方式优点是使用简单,缺点是无法使用spring security内置的组件,与RestfulUsernamePasswordAuthenticationFilter需要使用AuthenticationManager组件冲突,故不使用该方式。
  2. 使用SecurityConfigurer通过配置类的方式向过滤器链中添加过滤器,官方使用的方式。该方式优点是可以使用spring security内置的组件,缺点是实现较为笨重,而且只能注册过滤器顺序注册器(FilterOrderRegistration)中设定的过滤器。该方式可以使用spring security内置的组件,所以采用本方式,需要修改过滤器顺序注册器添加自定义的过滤器。

2、修改并覆盖过滤器顺序注册器

  1. FilterOrderRegistration类为final类且未提供开放的注册自定义过滤器的方式,所以只能重写该类,并添加自定义过滤器的顺序
package org.springframework.security.config.annotation.web.builders;

import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.DisableEncodeUrlFilter;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.filter.CorsFilter;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

final class FilterOrderRegistration {


    private static final int INITIAL_ORDER = 100;

    private static final int ORDER_STEP = 100;

    private final Map<String, Integer> filterToOrder = new HashMap<>();

    FilterOrderRegistration() {
        Step order = new Step(INITIAL_ORDER, ORDER_STEP);
        put(DisableEncodeUrlFilter.class, order.next());
        put(ForceEagerSessionCreationFilter.class, order.next());
        put(ChannelProcessingFilter.class, order.next());
        order.next(); // gh-8105
        put(WebAsyncManagerIntegrationFilter.class, order.next());
        put(SecurityContextHolderFilter.class, order.next());
        put(SecurityContextPersistenceFilter.class, order.next());
        put(HeaderWriterFilter.class, order.next());
        put(CorsFilter.class, order.next());
        put(CsrfFilter.class, order.next());
        put(LogoutFilter.class, order.next());
        this.filterToOrder.put(
                "org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
                order.next());
        this.filterToOrder.put(
                "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
                order.next());
        put(X509AuthenticationFilter.class, order.next());
        put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
                order.next());
        this.filterToOrder.put(
                "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
                order.next());
        //添加自定义过滤器
        put(RestfulUsernamePasswordAuthenticationFilter.class, order.next());
        put(UsernamePasswordAuthenticationFilter.class, order.next());
        order.next(); // gh-8105
        this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
        put(DefaultLoginPageGeneratingFilter.class, order.next());
        put(DefaultLogoutPageGeneratingFilter.class, order.next());
        put(ConcurrentSessionFilter.class, order.next());
        put(DigestAuthenticationFilter.class, order.next());
        this.filterToOrder.put(
                "org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
                order.next());
        put(BasicAuthenticationFilter.class, order.next());
        put(RequestCacheAwareFilter.class, order.next());
        put(SecurityContextHolderAwareRequestFilter.class, order.next());
        put(JaasApiIntegrationFilter.class, order.next());
        put(RememberMeAuthenticationFilter.class, order.next());
        put(AnonymousAuthenticationFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
                order.next());
        put(SessionManagementFilter.class, order.next());
        put(ExceptionTranslationFilter.class, order.next());
        put(FilterSecurityInterceptor.class, order.next());
        put(AuthorizationFilter.class, order.next());
        put(SwitchUserFilter.class, order.next());
    }

    /**
     * Register a {@link Filter} with its specific position. If the {@link Filter} was
     * already registered before, the position previously defined is not going to be
     * overriden
     *
     * @param filter   the {@link Filter} to register
     * @param position the position to associate with the {@link Filter}
     */
    void put(Class<? extends Filter> filter, int position) {
        String className = filter.getName();
        if (this.filterToOrder.containsKey(className)) {
            return;
        }
        this.filterToOrder.put(className, position);
    }

    /**
     * Gets the order of a particular {@link Filter} class taking into consideration
     * superclasses.
     *
     * @param clazz the {@link Filter} class to determine the sort order
     * @return the sort order or null if not defined
     */
    Integer getOrder(Class<?> clazz) {
        while (clazz != null) {
            Integer result = this.filterToOrder.get(clazz.getName());
            if (result != null) {
                return result;
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    private static class Step {

        private final int stepSize;
        private int value;

        Step(int initialValue, int stepSize) {
            this.value = initialValue;
            this.stepSize = stepSize;
        }

        int next() {
            int value = this.value;
            this.value += this.stepSize;
            return value;
        }

    }

}

3、创建RestfulUsernamePasswordAuthenticationFilter

  1. 参考UsernamePasswordAuthenticationFilter
  2. 将参数获取方式从request.getParameter改为从body体中
  3. 创建UsernamePasswordAuthenticationToken
  4. 设置细节
  5. 调用getAuthenticationManager()的authenticate方法获取认证信息
package com.yu.demo.spring.filter;

import com.yu.demo.util.SpringUtil;
import org.springframework.http.HttpMethod;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * 自定义前后端分离/restful方式的用户名密码认证过滤器
 * 参考UsernamePasswordAuthenticationFilter
 */
public class RestfulUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    //是否只支持post方法
    private final boolean postOnly;
    private final String username;
    private final String password;

    public RestfulUsernamePasswordAuthenticationFilter(String username, String password, String loginUrl, String httpMethod) {
        super(new AntPathRequestMatcher(loginUrl, httpMethod));
        postOnly = HttpMethod.POST.name().equals(httpMethod);
        this.username = username;
        this.password = password;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
        if (this.postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            Map<String, String> body = SpringUtil.rawBodyToMap(request);
            String name = body.get(username);
            String pswd = body.get(password);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(name, pswd);
            setDetails(request, authRequest);
            return getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

}

4、创建自定义用户名密码认证过滤器配置类RestfulLoginConfigurer

  1. 参考FormLoginConfigurer
  2. 注册自定义用户名密码认证过滤器RestfulUsernamePasswordAuthenticationFilter
  3. 设置登录地址和请求方式
package com.yu.demo.spring.filter;

import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

/**
 * 自定义前后端分离/restful方式的用户名密码验证过滤器配置器,用于注册认证过滤器
 * 参考FormLoginConfigurer
 */
public class RestfulLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, RestfulLoginConfigurer<H>, RestfulUsernamePasswordAuthenticationFilter> {
    private final String loginMethod;

    public RestfulLoginConfigurer(RestfulUsernamePasswordAuthenticationFilter authenticationFilter, String defaultLoginProcessingUrl, String loginMethod) {
        super(authenticationFilter, defaultLoginProcessingUrl);
        this.loginMethod = loginMethod;
    }

    @Override
    public RestfulLoginConfigurer<H> loginPage(String loginPage) {
        return super.loginPage(loginPage);
    }

    @Override
    public void init(H http) throws Exception {
        super.init(http);
    }

    @Override
    protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
        return new AntPathRequestMatcher(loginProcessingUrl, loginMethod);
    }
}

二、自定义安全上下文仓库SecurityContextRepositoryImpl

  1. 基于分布式缓存实现安全上下文仓库
  2. 获取上下文时从请求头中获取token,通过token从缓存中获取上下文,不存在时返回空值安全上下文
  3. 保存上下文时从请求头或者登录用户信息中获取token,将token和上下文保存到缓存中

1、分布式缓存接口和实现

package com.yu.demo.manager;

import org.springframework.security.core.context.SecurityContext;

public interface CacheManager {

    /**
     * 通过token获取认证信息
     *
     * @param token token
     * @return 认证信息
     */
    SecurityContext getSecurityContext(String token);

    /**
     * 是否包含token
     *
     * @param token token
     * @return 是否包含token
     */
    boolean contains(String token);

    /**
     * 通过token添加认证信息
     *
     * @param token           token
     * @param securityContext 认证信息
     */
    void addSecurityContext(String token, SecurityContext securityContext);

    /**
     * 通过token删除认证信息
     *
     * @param token token
     */
    void deleteSecurityContext(String token);

}

为演示方便,这里采用过期Map,实际使用将map改为redis或者其他分布式缓存即可

package com.yu.demo.manager.impl;

import com.yu.demo.manager.CacheManager;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
public class CacheManagerImpl implements CacheManager {

    private static ExpiringMap<String, SecurityContext> SECURITY_CONTEXT_CACHE;

    @PostConstruct
    public void init() {
        SECURITY_CONTEXT_CACHE = ExpiringMap.builder().maxSize(200).expiration(30, TimeUnit.MINUTES).expirationPolicy(ExpirationPolicy.ACCESSED).variableExpiration().build();
    }

    @Override
    public SecurityContext getSecurityContext(String token) {
        return SECURITY_CONTEXT_CACHE.get(token);
    }

    @Override
    public boolean contains(String token) {
        return SECURITY_CONTEXT_CACHE.containsKey(token);
    }

    @Override
    public void addSecurityContext(String token, SecurityContext securityContext) {
        SECURITY_CONTEXT_CACHE.put(token, securityContext);
    }

    @Override
    public void deleteSecurityContext(String token) {
        SECURITY_CONTEXT_CACHE.remove(token);
    }
}

2、创建SecurityContextRepositoryImpl

package com.yu.demo.spring.impl;

import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.manager.CacheManager;
import com.yu.demo.util.SecurityUtil;
import org.apache.poi.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class SecurityContextRepositoryImpl implements SecurityContextRepository {

    private static final String AUTHORIZATION = "Authorization";
    @Autowired
    private CacheManager cacheManager;

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        //获取请求头中的token,未登录访问系统时Token为空
        String token = requestResponseHolder.getRequest().getHeader(AUTHORIZATION);
        if (StringUtil.isNotBlank(token)) {
            SecurityContext securityContext = cacheManager.getSecurityContext(token);
            //securityContext已过期时为空
            if (SecurityUtil.isNotAuthenticated(securityContext)) {
                return SecurityContextHolder.createEmptyContext();
            }
            UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
            if (token.equals(userDetails.getToken())) {
                //测试过程中伪造的Token(不修改header和body,只修改signature部分字符)有概率出现可以解析成功的情况,可能是secret太短的原因,未深究,所以这里在验证下输入的Token和缓存中的token
                return securityContext;
            }
        }
        return SecurityContextHolder.createEmptyContext();
    }

    @Override
    public void saveContext(SecurityContext securityContext, HttpServletRequest request, HttpServletResponse response) {
        //获取请求头中的token(登出时有,登录时没有)
        String token = request.getHeader(AUTHORIZATION);
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
        if (StringUtil.isBlank(token) && SecurityUtil.isNotAuthenticated(securityContext)) {
            //未登录、验证码、用户名密码校验失败
            return;
        }
        //第一次登录时Token为空
        if (StringUtil.isBlank(token)) {
            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
            //登录成功
            cacheManager.addSecurityContext(userDetails.getToken(), securityContext);
            return;
        }
        //退出或token过期(缓存中设置token过期时间)
        if (SecurityUtil.isNotAuthenticated(securityContext)) {
            cacheManager.deleteSecurityContext(token);
            return;
        }
        //更新Token
        cacheManager.addSecurityContext(token, securityContext);
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
        //本版本的Spring Security只有SessionManagementFilter中调用该方法
        //已禁用SessionManagementFilter,该方法不会被调用
        String token = request.getHeader(AUTHORIZATION);
        if (StringUtil.isBlank(token)) {
            return false;
        }
        return cacheManager.contains(token);
    }

}

三、自定义用户详情UserDetailsImpl

package com.yu.demo.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

@Setter
@Getter
@ToString
public class UserDetailsImpl implements UserDetails {
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;
    /**
     * token
     */
    private String token;

    public UserDetailsImpl(String username, String password, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, boolean enabled, Set<GrantedAuthority> grantedAuthorities) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = grantedAuthorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 账号是否未过期
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    /**
     * 账号是否未锁定
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    /**
     * 密码是否未过期
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    /**
     * 账号是否启用
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

四、自定义用户详情数据库查询UserDetailsServiceImpl

package com.yu.demo.spring.impl;

import com.yu.demo.entity.UserDetailsImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    //@Autowired
    //private UserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //TODO 通过username从数据库中获取用户,将用户转UserDetails
        //User user = userService.getByUsername(username);
        //return new User(username, user.getPassword(), user.getEnable(), user.getAccountNonExpired(), user.getCredentialsNonExpired(), user.getAccountNonLocked(), user.getAuthorities());
        //{noop}不使用密码加密器,密码123的都可以验证成功
        UserDetailsImpl userDetails = new UserDetailsImpl(username, "{noop}123", true, true, true, true, null);
        //userDetails中设置token,该token只是实现认证流程,未使用jwt
        userDetails.setToken(UUID.randomUUID().toString());
        return userDetails;
    }

}

五、自定义登录登出结果处理器

package com.yu.demo.spring.impl;


import com.yu.demo.entity.ApiResp;
import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.util.SpringUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

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

@Component
public class LoginResultHandler implements AuthenticationSuccessHandler, LogoutSuccessHandler, AuthenticationEntryPoint, AuthenticationFailureHandler {

    /**
     * 登录成功
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
        UserDetailsImpl userDetailsImpl = (UserDetailsImpl) usernamePasswordAuthenticationToken.getPrincipal();
        //登陆成功后,擦除密码
        userDetailsImpl.setPassword(null);
        //token返回到前端
        SpringUtil.respJson(response, ApiResp.success(userDetailsImpl.getToken()));
    }

    /**
     * 登录失败
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.loginFailure());
    }

    /**
     * 登出成功
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.success());
    }

    /**
     * 未登录访问需要登录的页面时
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.notLogin());
    }

}

六、过滤器链个性化配置

package com.yu.demo.config;

import com.yu.demo.spring.filter.RestfulLoginConfigurer;
import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import com.yu.demo.spring.impl.LoginResultHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextRepository;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    //登录参数用户名
    private static final String LOGIN_ARG_USERNAME = "username";
    //登录参数密码
    private static final String LOGIN_ARG_PASSWORD = "password";
    //登录请求类型
    private static final String LOGIN_HTTP_METHOD = HttpMethod.POST.name();
    //登录请求地址
    private static final String LOGIN_URL = "/login";
    //登出请求地址
    private static final String LOGOUT_URL = "/logout";

    @Autowired
    private LoginResultHandler loginResultHandler;
    @Autowired
    private SecurityContextRepository securityContextRepository;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                //禁用UsernamePasswordAuthenticationFilter、DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter
                .formLogin(FormLoginConfigurer::disable)
                //禁用BasicAuthenticationFilter
                .httpBasic(HttpBasicConfigurer::disable)
                //禁用CsrfFilter
                .csrf(CsrfConfigurer::disable)
                //禁用SessionManagementFilter
                .sessionManagement(SessionManagementConfigurer::disable)
                //异常处理配置
                .exceptionHandling(exceptionHandlingCustomizer -> exceptionHandlingCustomizer.authenticationEntryPoint(loginResultHandler))
                //http请求认证
                .authorizeHttpRequests(authorizeHttpRequestsCustomizer -> authorizeHttpRequestsCustomizer
                        //任何请求
                        .anyRequest()
                        //需要认证
                        .authenticated())
                //安全上下文配置
                .securityContext(securityContextCustomizer -> securityContextCustomizer
                        //设置自定义securityContext仓库
                        .securityContextRepository(securityContextRepository)
                        //显示保存SecurityContext,官方推荐
                        .requireExplicitSave(true))
                //登出配置
                .logout(logoutCustomizer -> logoutCustomizer
                        //登出地址
                        .logoutUrl(LOGOUT_URL)
                        //登出成功处理器
                        .logoutSuccessHandler(loginResultHandler)
                )
                //注册自定义登录过滤器的配置器:自动注册自定义登录过滤器;
                //需要重写FilterOrderRegistration的构造方法FilterOrderRegistration(){},在构造方法中添加自定义过滤器的序号,否则注册不成功
                .apply(new RestfulLoginConfigurer<>(new RestfulUsernamePasswordAuthenticationFilter(LOGIN_ARG_USERNAME, LOGIN_ARG_PASSWORD, LOGIN_URL, LOGIN_HTTP_METHOD), LOGIN_URL, LOGIN_HTTP_METHOD))
                //设置登录地址:未设置时系统默认生成登录页面,登录地址/login
                .loginPage(LOGIN_URL)
                //设置登录成功之后的处理器
                .successHandler(loginResultHandler)
                .failureHandler(loginResultHandler);

        //创建过滤器链对象
        return httpSecurity.build();
    }

}

七、其他类

package com.yu.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * 接口响应对象
 *
 * @author admin
 */
@Setter
@Getter
@ToString
@AllArgsConstructor
public class ApiResp {

    private static final String CODE_SUCCESS = "00000";
    private static final String CODE_LOGIN_FAILURE = "10000";
    private static final String MESSAGE_LOGIN_FAILURE = "登录失败";
    private static final String CODE_NOT_LOGIN = "10010";
    private static final String MESSAGE_NOT_LOGIN = "未登录";

    /**
     * 响应码
     */
    private String code;
    /**
     * 描述
     */
    private String message;
    /**
     * 数据
     */
    private Object data;

    /**
     * 成功
     */
    public ApiResp(String code) {
        this.code = code;
    }

    /**
     * 失败+失败描述
     */
    public ApiResp(String code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 成功+返回值
     */
    public ApiResp(String code, Object data) {
        this.code = code;
        this.data = data;
    }

    /**
     * 成功无返回数据
     *
     * @return 接口响应对象
     */
    public static ApiResp success() {
        return new ApiResp(CODE_SUCCESS);
    }

    /**
     * 成功有返回数据
     *
     * @param data 数据
     * @return 接口响应对象
     */
    public static ApiResp success(Object data) {
        return new ApiResp(CODE_SUCCESS, data);
    }

    /**
     * 登录失败
     *
     * @return 接口响应对象
     */
    public static ApiResp loginFailure() {
        return new ApiResp(CODE_LOGIN_FAILURE, MESSAGE_LOGIN_FAILURE);
    }

    /**
     * 未登录
     *
     * @return 接口响应对象
     */
    public static ApiResp notLogin() {
        return new ApiResp(CODE_NOT_LOGIN, MESSAGE_NOT_LOGIN);
    }

}

package com.yu.demo.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;

import java.lang.reflect.Type;
import java.util.Map;

/**
 * JSON工具类
 *
 * @author admin
 */
public class JsonUtil {
    private JsonUtil() {
        throw new AssertionError();
    }

    /**
     * 对象转json
     *
     * @param javaObject 对象或集合或者数组
     * @return json
     */
    public static String object2Json(Object javaObject) {
        return JSONObject.toJSONString(javaObject);
    }

    public static <K, V> Map<K, V> json2Map(String jsonString, Type type) {
        return JSON.parseObject(jsonString, type);
    }
}

package com.yu.demo.util;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;

/**
 * Spring框架工具类
 */
public class SecurityUtil {

    private SecurityUtil() {
        throw new AssertionError();
    }

    public static boolean isAuthenticated(SecurityContext securityContext) {
        if (securityContext == null) {
            return false;
        }
        Authentication authentication = securityContext.getAuthentication();
        if (authentication == null) {
            return false;
        }
        return authentication.isAuthenticated();
    }

    public static boolean isNotAuthenticated(SecurityContext securityContext) {
        return !isAuthenticated(securityContext);
    }

}

package com.yu.demo.util;

import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * Spring框架工具类
 */
public class SpringUtil {

    private SpringUtil() {
        throw new AssertionError();
    }

    /**
     * 请求body参数转为map
     *
     * @param request 请求
     * @return 参数map
     * @throws IOException IO流异常
     */
    public static Map<String, String> rawBodyToMap(HttpServletRequest request) throws IOException {
        BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
        StringBuilder responseStrBuilder = new StringBuilder();
        String inputStr;
        while ((inputStr = streamReader.readLine()) != null) {
            responseStrBuilder.append(inputStr);
        }
        return JsonUtil.json2Map(responseStrBuilder.toString(), Map.class);
    }


    public static void respJson(HttpServletResponse response, Map<String, Object> apiResp) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().print(JsonUtil.object2Json(apiResp));
    }

}

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.yu</groupId>
    <artifactId>spring-boot-security2-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-security2-demo</name>
    <description>Spring Boot集成Spring Security样例</description>

    <properties>
        <java.version>8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.46</version>
        </dependency>
        <!--过期map-->
        <dependency>
            <groupId>net.jodah</groupId>
            <artifactId>expiringmap</artifactId>
            <version>0.5.11</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.0</version>
        </dependency>
    </dependencies>

</project>

八、案例源码获取

posted @ 2024-11-06 21:37  那你为何对我三笑留情  阅读(132)  评论(0编辑  收藏  举报