短信登录过滤器 SmsAuthenticationFilter
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
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.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 短信登录过滤器
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 设置拦截/sms/login短信登录接口
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
// 认证参数
private String principalParameter = "phone"; //对应手机号
private String credentialsParameter = "code"; //对应验证码
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String phone = this.obtainPrincipal(request);
phone = phone != null ? phone : "";
phone = phone.trim();
String code = this.obtainCredentials(request);
code = code != null ? code : "";
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, code);
this.setDetails(request, authRequest);
// 认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainCredentials(HttpServletRequest request) {
return request.getParameter(this.credentialsParameter);
}
@Nullable
protected String obtainPrincipal(HttpServletRequest request) {
return request.getParameter(this.principalParameter);
}
protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setPrincipalParameter(String principalParameter) {
Assert.hasText(principalParameter, "principal parameter must not be empty or null");
this.principalParameter = principalParameter;
}
public void setCredentialsParameter(String credentialsParameter) {
Assert.hasText(credentialsParameter, "credentials parameter must not be empty or null");
this.credentialsParameter = credentialsParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getPrincipalParameter() {
return this.principalParameter;
}
public final String getCredentialsParameter() {
return this.credentialsParameter;
}
}
短信登录令牌 SmsAuthenticationToken
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
import java.util.Collection;
/**
* 短信登录令牌
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
private final Object phone;
private Object code;
public SmsAuthenticationToken(Object phone, Object credentials) {
super((Collection) null);
this.phone = phone;
this.code = credentials;
this.setAuthenticated(false);
}
public SmsAuthenticationToken(Object phone, Object code, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.phone = phone;
this.code = code;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.code;
}
@Override
public Object getPrincipal() {
return this.phone;
}
@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.code = null;
}
}
短信登录校验器
import cn.hutool.core.util.StrUtil;
import com.ruoyi.common.constant.Constants;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Principal;
/**
* 短信登录校验器
*/
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
private SmsUserDetailsService smsUserDetailsService;
private RedisTemplate redisTemplate;
public SmsAuthenticationProvider(@Qualifier("smsUserDetailsService") SmsUserDetailsService smsUserDetailsService, RedisTemplate redisTemplate) {
this.smsUserDetailsService = smsUserDetailsService;
this.redisTemplate = redisTemplate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
String mobile = "";
if (principal instanceof UserDetails) {
mobile = ((UserDetails) principal).getUsername();
} else if (principal instanceof AuthenticatedPrincipal) {
mobile = ((AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof Principal) {
mobile = ((Principal) principal).getName();
} else {
mobile = principal == null ? "" : principal.toString();
}
String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码
// 1. 根据手机号查询用户信息
UserDetails userDetails = smsUserDetailsService.loadUserByUsername(mobile);
if (userDetails == null) {
throw new InternalAuthenticationServiceException("用户不存在");
}
// 2. 检验Redis手机号的验证码
String verifyKey = Constants.PHONE_CODE_KEY + mobile;
String redisCode = redisTemplate.opsForValue().get(verifyKey);
if (StrUtil.isEmpty(redisCode)) {
throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
}
if (!inputCode.equals(redisCode)) {
throw new BadCredentialsException("输入的验证码不正确,请重新输入");
}
redisTemplate.delete(verifyKey);//用完即删
// 3. 重新创建已认证对象,
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(principal, inputCode, userDetails.getAuthorities());
authenticationResult.setDetails(userDetails);
return authenticationResult;
}
@Override
public boolean supports(Class> aClass) {
return SmsAuthenticationToken.class.isAssignableFrom(aClass);
}
}
查询短信登录信息并封装为UserDetails
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.system.service.ISysUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
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;
/**
* 查询短信登录信息并封装为UserDetails 这里可以抽取一个抽象类,权限加载和校验租户等逻辑交给父类处理
*/
@Service("smsUserDetailsService")
public class SmsUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(SmsUserDetailsService.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
SysUser user = userService.selectUserByPhone(phone);
if (StringUtils.isNull(user)) {
log.info("手机号:{} 不存在.", phone);
throw new InternalAuthenticationServiceException("手机号:" + phone + " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", phone);
throw new ServiceException("对不起,您的账号:" + phone + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phone);
throw new DisabledException("对不起,您的账号:" + phone + " 已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}
适配器 SmsSecurityConfigurerAdapter
import com.ruoyi.framework.sms.handle.FailAuthenticationHandler;
import com.ruoyi.framework.sms.handle.SuccessAuthenticationHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* Author: LiuLin
* Date: 2022/5/27 16:25
* Description:
*/
@Component
public class SmsSecurityConfigurerAdapter extends SecurityConfigurerAdapter {
@Autowired
private SuccessAuthenticationHandler successHandler;
@Autowired
private FailAuthenticationHandler failureHandler;
@Autowired
private SmsAuthenticationProvider smsAuthenticationProvider;
@Override
public void configure(HttpSecurity builder) throws Exception {
SmsAuthenticationFilter filter = new SmsAuthenticationFilter();
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationFailureHandler(failureHandler);
builder.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
filter.setAuthenticationManager(authenticationManager);
builder.authenticationProvider(smsAuthenticationProvider);
super.configure(builder);
}
}
登录失败
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Author: LiuLin
* Date: 2022/5/30 10:19
* Description:
*/
@Component
public class FailAuthenticationHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ISysUserService userService;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String mobile = request.getParameter("phone");
SysUser sysUser = userService.selectUserByPhone(mobile);
if (sysUser == null) {
AsyncManager.me().execute(AsyncFactory.recordLogininfor("未知", Constants.LOGIN_FAIL, "手机号:" + mobile + "不存在"));
} else {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(sysUser.getUserName(), Constants.LOGIN_FAIL, exception.getMessage()));
}
JSONObject res = new JSONObject();//返回前端数据
res.put("code", com.ruoyi.common.constant.HttpStatus.ERROR);
res.put("msg", exception.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(String.valueOf(res));
}
}
登录成功
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Author: LiuLin
* Date: 2022/5/30 10:18
* Description:
*/
@Component
public class SuccessAuthenticationHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private TokenService tokenService;
@Autowired
private ISysUserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
LoginUser loginUser = (LoginUser) authentication.getDetails();
recordLoginInfo(loginUser.getUserId());
AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
JSONObject res = new JSONObject();//返回前端数据
res.put("msg", "操作成功");
res.put("code", HttpStatus.SUCCESS);
res.put("token", tokenService.createToken(loginUser));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(String.valueOf(res));
//response.getWriter().write(objectMapper.writeValueAsString(res));
}
public void recordLoginInfo(Long userId) {
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
sysUser.setLoginDate(DateUtils.getNowDate());
userService.updateUserProfile(sysUser);
}
}
spring security配置
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private SmsSecurityConfigurerAdapter smsSecurityConfigurerAdapter;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/sms/login", "/sendCode").anonymous()
// 添加手机号短信登录
httpSecurity.apply(smsSecurityConfigurerAdapter);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
}