springboot 2.7.X 整合 spring security 详解,实现自定义登录

spring security

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

什么是spring security

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

本质是通过一个filter链实现
在这里插入图片描述
处理身份认证主要的过滤器
在这里插入图片描述

以下是security的过滤器排序列表
在这里插入图片描述

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

引入spring security依赖

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

引入依赖后访问 服务器地址+端口号出现如下,表示你的接口已被 security 保护
在这里插入图片描述
spring security默认的用户名是user,密码在启动服务器的时候会生成
密码:在这里插入图片描述
也可以在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
在这里插入图片描述
输入正确的手机号,返回token
在这里插入图片描述
我们不带token 调用一下 /get 接口,返回401
在这里插入图片描述
携带 token 请求 /get 接口
在这里插入图片描述
以上只是个人的一些理解和简单实现,如有问题请指教!!
谢谢!!!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 2.7 还没有发布,可能是您想说的是 Spring Boot 2.4。 在 Spring Boot 2.4 中,配置 Spring Security 和 JWT 的步骤如下: 1. 添加依赖 在 pom.xml 文件中添加 Spring Security 和 JWT 的依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> ``` 2. 配置 Spring SecuritySpring Security 的配置类中,添加以下代码: ```java @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenFilter jwtTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/authenticate").permitAll() .anyRequest().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password(passwordEncoder().encode("password")).roles("USER"); } } ``` 这个配置类中,我们首先禁用了 CSRF 防护,然后配置了访问控制,允许 `/api/authenticate` 路径的请求不需要认证,其他路径的请求需要进行认证。同时,我们还配置了使用 JWT 进行认证,通过 `JwtTokenFilter` 过滤器来实现。 3. 配置 JWT 在 JWT 的配置类中,添加以下代码: ```java @Configuration public class JwtConfig { @Value("${jwt.secret}") private String secret; @Bean public JwtParser jwtParser() { return Jwts.parser().setSigningKey(secret); } @Bean public JwtEncoder jwtEncoder() { return Jwts.builder().setSigningKey(secret).build(); } @Bean public JwtTokenFilter jwtTokenFilter() { return new JwtTokenFilter(jwtParser()); } } ``` 在这个配置类中,我们首先从配置文件中读取了 JWT 的密钥,然后配置了 `JwtParser` 和 `JwtEncoder` 对象,用于解析和生成 JWT。最后,我们还配置了 `JwtTokenFilter` 过滤器。这个过滤器用于在每个请求中解析 JWT,并将解析后的信息传递给 Spring Security 进行认证。 4. 编写 JwtTokenFilter 最后,我们需要编写 `JwtTokenFilter` 过滤器。这个过滤器用于在每个请求中解析 JWT,并将解析后的信息传递给 Spring Security 进行认证。具体代码如下: ```java public class JwtTokenFilter extends OncePerRequestFilter { private final JwtParser jwtParser; public JwtTokenFilter(JwtParser jwtParser) { this.jwtParser = jwtParser; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token); String username = claimsJws.getBody().getSubject(); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } catch (Exception e) { SecurityContextHolder.clearContext(); } } chain.doFilter(request, response); } } ``` 在这个过滤器中,我们首先从请求头中获取 JWT,然后使用 `JwtParser` 对象对 JWT 进行解析。如果解析成功,我们就从 JWT 中获取用户名,并创建一个 `UsernamePasswordAuthenticationToken` 对象,用于在 Spring Security 中进行认证。最后,我们将认证信息保存到 `SecurityContextHolder` 中,以便于后续的处理。如果解析失败,我们就清空 `SecurityContextHolder` 中的认证信息。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值