前言
现在各大网站登录的方式是越来越多。比如:传统的用户名密码登录、快捷的邮箱、手机验证码登录,还有流行的第三方登录。那本篇呢,就给大家带来如何在 Spring Security 中定义使用邮箱验证码登录方式。看完本篇,让你学会自定义认证方式,如果公司还需要使用手机验证码登录,简简单单就能集成,毕竟流程是一致的。
编码
自定义验证方式需要使用到 Spring Security
内置的几个对象,如果各位还不了解,可以先看看这篇文章:Spring Security 中重要对象汇总
用户名密码表单登录会进入到 UsernamePasswordAuthenticationFilter
。在这整个类中,还会用到一个对象 UsernamePasswordAuthenticationToken
。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationToken
EmailVerificationCodeAuthenticationFilter
参考 UsernamePasswordAuthenticationFilter
(copy)类并进行删减, 定义 EmailVerificationCodeAuthenticationFilter
类,内容如下:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author cxyxj
*/
public class EmailVerificationCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 默认的请求参数名称
*/
public static final String EMAIL_KEY = "email";
public static final String EMAIL_CODE_KEY = "emailCode";
private String emailParameter = EMAIL_KEY;
private String emailCodeParameter = EMAIL_CODE_KEY;
/**
* 是否仅支持post方式
*/
private boolean postOnly = true;
/**
* 对请求进行过滤,只有接口为 /emil-login,请求方式为 POST,才会进入逻辑
*/
public EmailVerificationCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/email-login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// 需要是 POST 请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 判断请求格式是否 JSON
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
Map<String, String> loginData = new HashMap<>(2);
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
throw new InternalAuthenticationServiceException("请求参数异常");
}
// 获得请求参数
String email = loginData.get(emailParameter);
String emailCode = loginData.get(emailCodeParameter);
// 检查验证码
checkEmailCode(emailCode);
if(StringUtils.isEmpty(email)){
throw new AuthenticationServiceException("邮箱不能为空");
}
/**
* 使用请求参数传递的邮箱和验证码,封装为一个未认证 EmailVerificationCodeAuthenticationToken 身份认证对象,
* 然后将该对象交给 AuthenticationManager 进行认证
*/
EmailVerificationCodeAuthenticationToken authRequest = new EmailVerificationCodeAuthenticationToken(email);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
return null;
}
public void setDetails(HttpServletRequest request , EmailVerificationCodeAuthenticationToken token){
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
private void checkEmailCode(String emailCode) {
// 实际当中请从 Redis 中获取
String verifyCode = "123456";
if(StringUtils.isEmpty(verifyCode)){
throw new AuthenticationServiceException("请重新申请验证码!");
}
if (!verifyCode.equalsIgnoreCase(emailCode)) {
throw new AuthenticationServiceException("验证码错误!");
}
}
}
复制代码
简单理理上述代码:如下:
- 提供默认的请求参数名称。
- 提供无参构造方法,对请求进行过滤。
- 继承
AbstractAuthenticationProcessingFilter
,重写了attemptAuthentication
方法,对其参数进行校验,最后调用认证管理器的authenticate
方法。 - setDetails 方法设置该次请求信息,比如:调用地址、sessionId。
EmailVerificationCodeAuthenticationToken
参考 UsernamePasswordAuthenticationToken
(copy) 类并进行删减,定义 EmailVerificationCodeAuthenticationToken
类,内容如下:
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* @author cxyxj
*/
public class EmailVerificationCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public EmailVerificationCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
public EmailVerificationCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
}
复制代码
可以发现类中并没有 credentials 属性,因为验证码是有时效性的,存着没意义,那 eraseCredentials
方法也没必要重写了,凭证都没有,何需擦除呢! 类中还有两个构造方法,一个是设置是否已认证为 false。另外一个设置是否已认证为 true。
AuthenticationProvider
对于 Provider,我们直接看 AbstractUserDetailsAuthenticationProvider
,这是来处理用户名密码认证流程的。AbstractUserDetailsAuthenticationProvider
实现了 AuthenticationProvider
类, 所以重点看看该类的重写的方法。
- supports:验证传入的身份验证对象是否是
UsernamePasswordAuthenticationToken
。如果是则返回 true。
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
复制代码
- authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
//获取用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 根据用户名从缓存中获得UserDetails对象
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 如果缓存中没有信息,通过子类 DaoAuthenticationProvider 实现的 retrieveUser 方法,返回一个 UserDetails 对象
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 检查该用户对象的各种状态,比如:账户是否未锁定、账户是否启用、账户是否未过期
preAuthenticationChecks.check(user);
// 使用子类 DaoAuthenticationProvider 实现的 additionalAuthenticationChecks方法,检查密码是否输入正确
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// 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);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 检查该用户对象的各种状态,比如:凭证(密码)是否未过期
postAuthenticationChecks.check(user);
// 存入缓存
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 会调用子类方法,设置是否已认证为true,并设置权限信息
return createSuccessAuthentication(principalToReturn, authentication, user);
}
复制代码
通过对 authenticate
方法的梳理,我们知道还有两个方法在其子类中进行实现,实现类为 DaoAuthenticationProvider
。
// 检查密码是否输入正确
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
// 通过调用 UserDetailsService 的 loadUserByUsername 方法加载用户信息
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);
}
}
复制代码
OK,接下来就是 copy 阶段。我们就没必要学 Security 定义一个抽象类、子类,直接定义一个子类即可。EmailVerificationCodeAuthenticationProvider
实现 AuthenticationProvider
。
/**
* 邮箱身份验证提供者
* @author cxyxj
*/
public class EmailVerificationCodeAuthenticationProvider implements AuthenticationProvider {
private EmailVerificationCodeService service;
public EmailVerificationCodeAuthenticationProvider(EmailVerificationCodeService service) {
this.service = service;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = authentication.getName();
// 根据邮箱加载用户信息
UserDetails userDetails = service.loadUserByEmail(email);
if (userDetails == null) {
// 可以自定义异常类型
throw new InternalAuthenticationServiceException("邮箱方式登录异常");
}
EmailVerificationCodeAuthenticationToken result = new EmailVerificationCodeAuthenticationToken(userDetails,
userDetails.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return (EmailVerificationCodeAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
复制代码
再提供一个用来根据邮箱查询用户的服务。
@Service
public class EmailVerificationCodeService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
UserDetails loadUserByEmail(String email) {
SysUser user = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
if(Objects.isNull(user)){
return null;
}
// 获得用户角色信息
List<String> roles = sysRoleMapper.selectByRoleId(user.getRoleId());
// 构建 SimpleGrantedAuthority 对象
List<SimpleGrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return new SysUserDetails(user, authorities);
}
}
复制代码
数据库脚本已经放置到项目中,位于 resources/sql 下。
配置
接下来就是将上述的重写的类,配置到 Spring Security
中。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private EmailVerificationCodeService emailVerificationCodeService;
@Bean
public EmailVerificationCodeAuthenticationFilter emailVerificationCodeAuthenticationFilter() throws Exception {
EmailVerificationCodeAuthenticationFilter emailVerificationCodeAuthenticationFilter = new EmailVerificationCodeAuthenticationFilter();
// 手动设置AuthenticationManager,解决authenticationManager must be specified 启动异常
emailVerificationCodeAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
emailVerificationCodeAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
emailVerificationCodeAuthenticationFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
return emailVerificationCodeAuthenticationFilter;
}
@Bean
EmailVerificationCodeAuthenticationProvider emailVerificationCodeAuthenticationProvider() {
EmailVerificationCodeAuthenticationProvider provider = new EmailVerificationCodeAuthenticationProvider(emailVerificationCodeService);
return provider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这行需要加上,否则请求参数需要带上 _csrf
http.cors().and().csrf().disable();
http.authorizeRequests() //开启配置
// 需要放行的接口路径
.antMatchers().permitAll()
.anyRequest() //其他请求
.authenticated(); //验证 表示其他请求需要登录才能访问
// 将邮箱身份验证过滤器放置在 UsernamePasswordAuthenticationFilter 之后
http.addFilterBefore(emailVerificationCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 将自定义的 Provider 添加到 authenticationProviders 属性中
auth.authenticationProvider(emailVerificationCodeAuthenticationProvider());
}
}
复制代码
注意:在调用 addFilterBefore、addFilterAt 时,不能随意的在过滤器前后添加,只能在有排序的过滤器前后添加,否则会出现 Cannot register after unregistered Filter 异常。
已有排序的过滤器可以查看 FilterComparator
类。
接下来就可以来测试一下,我们自定义的方式是否生效了。 在请求工具中请求接口:
POST
http://localhost:8080/email-login
{
"email":"1990848@163.com",
"emailCode":"123456"
}
复制代码
如果小伙伴想断点调试,可以先在 FilterChainProxy
类的 doFilterInternal
方法打上一个断点,可以查看 Spring Security
过滤器链中的过滤器。会按照其顺序一个一个执行。
因为 EmailVerificationCodeAuthenticationFilter
继承了 AbstractAuthenticationProcessingFilter
,所以会先执行父类逻辑。
确定请求路径没有错,就会进入对应的Filter
逻辑。
在 EmailVerificationCodeAuthenticationFilter
类的 attemptAuthentication
方法最后一行会调用 this.getAuthenticationManager().authenticate(authRequest)
。 执行 ProviderManager
类的 authenticate
方法。
可以看到现在 getProviders
中有两个对象,一个是 AnonymousAuthenticationProvider
,另外一个就是我们自定义的 EmailVerificationCodeAuthenticationProvider
。 循环每个 AuthenticationProvider
对象,调用其 supports
方法进行验证,验证成功,调用其 authenticate
方法,进入到对应的 AuthenticationProvider
实现类。
EmailVerificationCodeAuthenticationProvider
进入到我们自定义的 Provider 类中,调用 EmailVerificationCodeService
的 loadUserByEmail
方法。
总结
自定义认证方式总体来说还是很简单的;需要从头重写的也就三个类,然后就是将其配置到Spring Security 中。如果需要再自定义手机号验证码登录,按照上述流程再走一遍就行。其实也是 copy 一份,然后进行小幅度的修改即可。
文章来源:https://www.toymoban.com/news/detail-667295.html
开启表单用户名密码登录
如果还需要开启用户名密码登录(JSON形式自行改造吧),需要加上如下配置:文章来源地址https://www.toymoban.com/news/detail-667295.html
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build());
manager.createUser(User.withUsername("security").password("security").roles("user").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这行需要加上,否则请求参数需要带上 _csrf
http.cors().and().csrf().disable();
// 开启表单配置
http.formLogin().permitAll();
http.authorizeRequests() //开启配置
// 需要放行的接口路径
.antMatchers().permitAll()
.anyRequest() //其他请求
.authenticated(); //验证 表示其他请求需要登录才能访问
// 将邮箱身份验证过滤器放置在 UsernamePasswordAuthenticationFilter 之后
http.addFilterAfter(emailVerificationCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置 UserDetailsService以及密码匹配器
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
// 将自定义的 Provider 添加到 authenticationProviders 属性中
auth.authenticationProvider(emailVerificationCodeAuthenticationProvider());
}
复制代码
验证
- 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞 + 收藏。
到了这里,关于在 Spring Security 中定义多种登录方式,比如邮箱、手机验证码登录的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!