springboot 2.7.X 整合 spring security

  • spring security
  • 什么是spring security
  • 多的就不赘述了,直接上干货
  • 引入spring security依赖
  • 下面我们先简单看下相关源码,只提主要部分
  • 配置 WebSecurityConfig
  • UsernamePasswordAuthenticationFliter
  • UsernamePasswordAuthenticationToken
  • ProviderManager
  • AbstractUserDetailsAuthenticationProvider
  • DaoAuthenticationProvider
  • UserDetailsService
  • 简单的自定义实现
  • 引入相关依赖
  • 配置数据库
  • 创建 TestController类
  • 创建 LoginUser类 实现 UserDetails
  • 创建 MyUserDetailsService 类 继承 UserDetailsService 方便后面扩展
  • 创建 MyUserDetailsServiceImpl 类 实现 MyUserDetailsService
  • 创建 MyMobileAuthenticationFilter类
  • 创建 MobileAuthenticationToken 类
  • 创建 MyMobileAuthenticationProvider类
  • 创建 MyAuthenticationSuccessHandler类
  • 创建 MyAuthenticationFailureHandler类
  • 创建 MyMobileSecurityConfig类
  • 创建 RequestTokenFilter 类 校验token
  • 创建 MyAuthenticationEntryPointHandler 类
  • 最后完善第一步的 WebSecurityConfig 配置
  • 至此,简单的自定义登录实现就完成了,我们来测试一下


spring security

踩完坑后做个记录,以免忘记,好记性不如烂笔头。
本文是基于springboot 2.7.16 整合 spring security 5.7.11
该教程主要针对前后端分离项目,其他的就不多讲了。

什么是spring security

spring security是一个提供认证、授权,能防止常见攻击的安全框架。它支持servlet applicationreactive application。 具体的可以参考 官方文档,本文主要讲 servlet application

本质是通过一个filter链实现

spring boot security 配置 禁用 springboot security详解_spring boot


处理身份认证主要的过滤器

spring boot security 配置 禁用 springboot security详解_后端_02

以下是security的过滤器排序列表

spring boot security 配置 禁用 springboot security详解_后端_03

多的就不赘述了,直接上干货

引入spring security依赖

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

引入依赖后访问 服务器地址+端口号出现如下,表示你的接口已被 security 保护

spring boot security 配置 禁用 springboot security详解_ide_04


spring security默认的用户名是user,密码在启动服务器的时候会生成

密码:

spring boot security 配置 禁用 springboot security详解_spring_05


也可以在yml配置中自己配置

spring:
  security:
    user:
      name: admin
      password: admin

下面我们先简单看下相关源码,只提主要部分

配置 WebSecurityConfig

以前版本的 WebSecurityConfigurerAdapter已被弃用,不再去继承,现在需要以 bean 的方式注入

@EnableWebSecurity
public class WebSecurityConfig {
    /**
     * 安全过滤器,配置 URL 的安全配置
     * <p>
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                //开启跨域
//                .cors().and()
                //禁用CSRF,因为不使用 session,前后端分离项目不需要
                .csrf().disable()
                //禁用session,前后端分离项目基于token不需要
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeRequests(
                        authorizeRequests -> authorizeRequests.antMatchers("/toLogin").permitAll()
                                .anyRequest().authenticated()
                );
        return httpSecurity.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

UsernamePasswordAuthenticationFliter

上面说到security的本质是由多个过滤器组成一个链实现的,接下来我们跟踪一下源码,
只提主要部分
,从开始的图中我们看到 SecurityFilterChain 中开始会进入到一个抽象的过滤器中 AbstractAuthenticationProcessingFilter , 我们使用用户名密码登录,其中用到的过滤器是UsernamePasswordAuthenticationFliter,它继承了AbstractAuthenticationProcessingFilter ,主要用到的方法attemptAuthentication

/**
	 * 身份验证
	 */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username.trim() : "";
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        //进入 AuthenticationManager 相关处理
        return this.getAuthenticationManager().authenticate(authRequest);
    }

UsernamePasswordAuthenticationToken

一个身份认证的实现

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final Object principal;

	private Object credentials;

	/**
	 * This constructor can be safely used by any code that wishes to create a
	 * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
	 * will return <code>false</code>.
	 *
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	/**
	 * This constructor should only be used by <code>AuthenticationManager</code> or
	 * <code>AuthenticationProvider</code> implementations that are satisfied with
	 * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
	 * authentication token.
	 * @param principal
	 * @param credentials
	 * @param authorities
	 */
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}

	/**
	 * This factory method can be safely used by any code that wishes to create a
	 * unauthenticated <code>UsernamePasswordAuthenticationToken</code>.
	 * @param principal
	 * @param credentials
	 * @return UsernamePasswordAuthenticationToken with false isAuthenticated() result
	 *
	 * @since 5.7
	 */
	public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
		return new UsernamePasswordAuthenticationToken(principal, credentials);
	}

	/**
	 * This factory method can be safely used by any code that wishes to create a
	 * authenticated <code>UsernamePasswordAuthenticationToken</code>.
	 * @param principal
	 * @param credentials
	 * @return UsernamePasswordAuthenticationToken with true isAuthenticated() result
	 *
	 * @since 5.7
	 */
	public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
	}

	@Override
	public Object getCredentials() {
		return this.credentials;
	}

	@Override
	public Object getPrincipal() {
		return this.principal;
	}

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
		Assert.isTrue(!isAuthenticated,
				"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
		this.credentials = null;
	}

}

ProviderManager

进入到 ProviderManagerauthenticate方法, ProviderManager 实现了AuthenticationManager接口

@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
		//判断 AuthenticationProvider 是否支持所使用的类
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
			//进入provider的认证方法
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}

AbstractUserDetailsAuthenticationProvider

上面提到的authenticate方法

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
        //根据用户名从缓存中获取用户信息
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                //调用实现类DaoAuthenticationProvider的retrieveUser,这里会得到保存在内存中的用户信息,如密码,账号状态等
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
           	//判断用户状态
			this.preAuthenticationChecks.check(user);
            //密码比较
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        //创建一个成功的Authentication对象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
}

DaoAuthenticationProvider

DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider
获取用户信息的方法 retrieveUser

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            //查询用户信息
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
}

UserDetailsService

要自己从数据库查询用户信息需要自己实现 UserDetailsService 接口

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

这里我们主要从内存中查找,默认机制使用 InMemoryUserDetailsManagerUserDetailsManager继承了 UserDetailsService

public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    @Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //(从缓存中获取用户信息
		UserDetails user = this.users.get(username.toLowerCase());
		if (user == null) {
			throw new UsernameNotFoundException(username);
		}
        //(2)返回一个UserDetails对象
		return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
				user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
	}
}

使用的用户信息对象需要实现 UserDetails 接口

public class User implements UserDetails, CredentialsContainer {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
 
	private static final Log logger = LogFactory.getLog(User.class);
	
	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;
 
 
	public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		this(username, password, true, true, true, true, authorities);
	}
 
	public User(String username, String password, boolean enabled, boolean accountNonExpired,
			boolean credentialsNonExpired, boolean accountNonLocked,
			Collection<? extends GrantedAuthority> authorities) {
		Assert.isTrue(username != null && !"".equals(username) && password != null,
				"Cannot pass null or empty values to constructor");
		this.username = username;
		this.password = password;
		this.enabled = enabled;
		this.accountNonExpired = accountNonExpired;
		this.credentialsNonExpired = credentialsNonExpired;
		this.accountNonLocked = accountNonLocked;
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
	}
 
	@Override
    //权限
	public Collection<GrantedAuthority> getAuthorities() {
		return this.authorities;
	}
}

认证成功后会把用户信息放到 SecurityContextHolder 中,这里就不说了自行查看SecurityContextPersistenceFilter 源码

简单的自定义实现

引入相关依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.16</version>
    </dependency>
</dependencies>

配置数据库

server:
  port: 8080
spring:
  application:
    name: demo-security
  profiles:
    active: dev
  main:
    allow-bean-definition-overriding: true
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: root

创建 TestController类

/**
 * @author changq
 */
@Slf4j
@RestController
public class TestController {
    @GetMapping("/get")
    JSONObject get() {
        JSONObject json = new JSONObject();
        json.put("data", "get信息成功");
        return json;
    }
}

创建 LoginUser类 实现 UserDetails

/**
 * @author changq
 */
@Data
@TableName("sys_user")
public class LoginUser implements UserDetails {
    @TableId(type = IdType.ASSIGN_UUID)
    private String id;
    private String username;
    private String password;
    private String mobile;

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

    @Override
    public boolean isAccountNonExpired() {
        return true;//需要为true,不然认证失败
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;//需要为true,不然认证失败
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;//需要为true,不然认证失败
    }

    @Override
    public boolean isEnabled() {
        return true;//需要为true,不然认证失败
    }
}

创建 MyUserDetailsService 类 继承 UserDetailsService 方便后面扩展

扩展了简单的手机号密码登录,没用验证码,验证码的自行扩展

/**
 * @author changq
 */
public interface MyUserDetailsService extends UserDetailsService, IService<LoginUser> {
    LoginUser loadUserByMobile(String username) throws UsernameNotFoundException;
}

创建 MyUserDetailsServiceImpl 类 实现 MyUserDetailsService

/**
 * UserDetailsService 实现
 *
 * @author changq
 */
@Slf4j
@Service
public class MyUserDetailsServiceImpl extends ServiceImpl<UserMapper, LoginUser> implements MyUserDetailsService {
//    @Autowired
//    PasswordEncoder passwordEncoder;

    @Override
    public LoginUser loadUserByUsername(String username) throws UsernameNotFoundException {
        //判断用户名是否为空
        if (StrUtil.isEmpty(username)) {
            throw new NullPointerException("用户名不能为空");
        }
        LoginUser loginUser = this.getOne(Wrappers.<LoginUser>lambdaQuery().eq(LoginUser::getUsername, username));
        log.info("获取到的用户:{}", loginUser);
        if (Objects.isNull(loginUser)) {
            throw new InternalAuthenticationServiceException("用户名错误");
        }
        return loginUser;
    }

    @Override
    public LoginUser loadUserByMobile(String mobile) throws UsernameNotFoundException {
        LoginUser loginUser = this.getOne(Wrappers.<LoginUser>lambdaQuery().eq(LoginUser::getMobile, mobile));
        log.info("手机号获取到的用户:{}", loginUser);
        if (Objects.isNull(loginUser)) {
            throw new InternalAuthenticationServiceException("手机号错误");
        }
        return loginUser;
    }
}

创建 MyMobileAuthenticationFilter类

/**
 * 1.手机号认证过滤器
 *
 * @author changq
 */
public class MyMobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "mobile";

    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
	// 定义登录路径
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/mobile/toLogin",
            "POST");

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

    private boolean postOnly = true;

    public MyMobileAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public MyMobileAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username.trim() : "";
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        MobileAuthenticationToken authRequest = MobileAuthenticationToken.unauthenticated(username,
                password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * Enables subclasses to override the composition of the password, such as by
     * including additional values and a separator.
     * <p>
     * This might be used for example if a postcode/zipcode was required in addition to
     * the password. A delimiter such as a pipe (|) should be used to separate the
     * password and extended value(s). The <code>AuthenticationDao</code> will need to
     * generate the expected password in a corresponding manner.
     * </p>
     *
     * @param request so that request attributes can be retrieved
     * @return the password that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    /**
     * Enables subclasses to override the composition of the username, such as by
     * including additional values and a separator.
     *
     * @param request so that request attributes can be retrieved
     * @return the username that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    /**
     * Provided so that subclasses may configure what is put into the authentication
     * request's details property.
     *
     * @param request     that an authentication request is being created for
     * @param authRequest the authentication request object that should have its details
     *                    set
     */
    protected void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    /**
     * Sets the parameter name which will be used to obtain the username from the login
     * request.
     *
     * @param usernameParameter the parameter name. Defaults to "username".
     */
    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    /**
     * Sets the parameter name which will be used to obtain the password from the login
     * request..
     *
     * @param passwordParameter the parameter name. Defaults to "password".
     */
    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    /**
     * Defines whether only HTTP POST requests will be allowed by this filter. If set to
     * true, and an authentication request is received which is not a POST request, an
     * exception will be raised immediately and authentication will not be attempted. The
     * <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
     * authentication.
     * <p>
     * Defaults to <tt>true</tt> but may be overridden by subclasses.
     */
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

创建 MobileAuthenticationToken 类

/**
 * 4.手机号认证token
 *
 * @author changq
 */
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    private Object credentials;

    public MobileAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * @param principal
     * @param credentials
     * @param authorities
     */
    public MobileAuthenticationToken(Object principal, Object credentials,
                                     Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

    /**
     * @param principal
     * @param credentials
     */
    public static MobileAuthenticationToken unauthenticated(Object principal, Object credentials) {
        return new MobileAuthenticationToken(principal, credentials);
    }

    /**
     * @param principal
     * @param credentials
     */
    public static MobileAuthenticationToken authenticated(Object principal, Object credentials,
                                                          Collection<? extends GrantedAuthority> authorities) {
        return new MobileAuthenticationToken(principal, credentials, authorities);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

创建 MyMobileAuthenticationProvider类

/**
 * 3.手机号登录身份认证器
 *
 * @author changq
 */
@Slf4j
@Component
@Setter
public class MyMobileAuthenticationProvider implements AuthenticationProvider {
    private PasswordEncoder passwordEncoder;
    private MyUserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String phone = authentication.getPrincipal().toString();
        String password = authentication.getCredentials().toString();
        log.info("手机号:{}--,密码:{}", phone, password);
        UserDetails loginUser = userDetailsService.loadUserByMobile(phone);
        if (passwordEncoder.matches(password, loginUser.getPassword())) {
            MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(loginUser, password, loginUser.getAuthorities());
            mobileAuthenticationToken.setDetails(authentication.getDetails());
            return mobileAuthenticationToken;
        } else {
            throw new BadCredentialsException("密码错误");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }

创建 MyAuthenticationSuccessHandler类

/**
 * @author changq
 * 认证成功处理
 */
@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("认证成功处理------------");
        response.setContentType("application/json;charset=utf-8");
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", 200);
        jsonObject.put("msg", "认证成功");
        Map<String, Object> map = new HashMap<>();
        map.put("user", authentication.getPrincipal());
        String token = JWTUtil.createToken(map, "123".getBytes());
        jsonObject.put("token", token);
        PrintWriter writer = response.getWriter();
        writer.write(jsonObject.toJSONString());
    }
}

创建 MyAuthenticationFailureHandler类

/**
 * @author changq
 * 认证失败处理
 */
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("认证失败处理!!!");
        response.setContentType("application/json;charset=utf-8");
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", 403);
        jsonObject.put("data", "");
        jsonObject.put("msg", exception.getMessage());
        PrintWriter writer = response.getWriter();
        writer.write(jsonObject.toJSONString());
    }
}

创建 MyMobileSecurityConfig类

/**
 * 2.手机号登录相关Security安全配置
 *
 * @author changq
 */
@Component
public class MyMobileSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        MyMobileAuthenticationProvider provider = new MyMobileAuthenticationProvider();
        //使用自己的 UserDetailsService
        provider.setUserDetailsService(myUserDetailsService);
        //使用自定义的 passwordEncoder
        provider.setPasswordEncoder(passwordEncoder);
        httpSecurity.authenticationProvider(provider);
        MyMobileAuthenticationFilter myMobileAuthenticationFilter = new MyMobileAuthenticationFilter();
        myMobileAuthenticationFilter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
        myMobileAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
        myMobileAuthenticationFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        httpSecurity.addFilterAfter(myMobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

创建 RequestTokenFilter 类 校验token

/**
 * 校验请求token
 * 每个请求都需要校验
 *
 * @author changq
 */
@Slf4j
//@Component
public class RequestTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        log.info("进入token校验");
        SecurityContextHolder.clearContext();
        String token = request.getHeader("token");
        if (StrUtil.isEmpty(token)) {
            chain.doFilter(request, response);
            return;
        }
        JWT jwt;
        try {
            jwt = JWTUtil.parseToken(token);
        } catch (Exception e) {
            chain.doFilter(request, response);
            return;
        }
        Object user = jwt.getPayload("user");
        LoginUser loginUser = JSON.parseObject(user.toString(), LoginUser.class);
        log.info("用户信息:{}", loginUser);SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken.authenticated(loginUser, loginUser.getPassword(), loginUser.getAuthorities()));
        chain.doFilter(request, response);

    }

创建 MyAuthenticationEntryPointHandler 类

/**
 * 请求接口 401未授权处理
 *
 * @author changq
 */
@Slf4j
@Component
public class MyAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.info("401未授权处理");
        response.setContentType("application/json;charset=utf-8");
        String s = String.format("请求:%s,认证失败,无法访问该资源", request.getRequestURI());
        response.getWriter().write(JSON.toJSONString(R.error(HttpStatus.HTTP_UNAUTHORIZED, s, "")));
    }
}

最后完善第一步的 WebSecurityConfig 配置

@Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                //开启跨域
//                .cors().and()
                //禁用CSRF,因为不使用 session,前后端分离项目不需要
                .csrf().disable()
                //禁用session,前后端分离项目基于token不需要
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeRequests(
                        authorizeRequests -> authorizeRequests.antMatchers("/toLogin").permitAll()
                                .anyRequest().authenticated()
                );
        httpSecurity.apply(myMobileSecurityConfig);
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        httpSecurity.addFilterBefore(new RequestTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

至此,简单的自定义登录实现就完成了,我们来测试一下

执行登录接口 localhost:8080/mobile/login,随便输入手机号,返回403

spring boot security 配置 禁用 springboot security详解_java_06


输入正确的手机号,返回token

spring boot security 配置 禁用 springboot security详解_ide_07


我们不带token 调用一下 /get 接口,返回401

spring boot security 配置 禁用 springboot security详解_ide_08


携带 token 请求 /get 接口

spring boot security 配置 禁用 springboot security详解_java_09


以上只是个人的一些理解和简单实现,如有问题请指教!!

谢谢!!!!