首先 我们需要了解spring security的基本工作流程
当登录请求进来时,会在UsernamePasswordAuthenticationFilter 里构建一个没有权限的 Authentication ,
然后把Authentication 交给 AuthenticationManager 进行身份验证管理
而AuthenticationManager 本身不做验证 ,会交给 AuthenticationProvider 进行验证
AuthenticationProvider 会调用 UserDetailsService 对用户信息进行校验
UserDetailsService 校验成功后 会返回 UserDetails类,里面存放着用户祥细信息
验证成功后 会重新构造 Authentication 把 UserDetails 传进去,并把认证 改为 true super.setAuthenticated(true)
验证成功后来到 AuthenticationSuccessHandler
验证成功处理器 ,在里面可以返回数据给前端
对基本的流程熟悉后,我们就可以仿照密码登录 来自己定义短信验证方法
根据上图 我们要重写 SmsAuthenticationFilter、SmsAuthenticationProvider、UserDetailsService、UserDetails,来模拟用户密码登录
在写一些配置类来启用我们的短信业务流程
SmsSecurityConfigurerAdapter
MySecurityConfig extends WebSecurityConfigurerAdapter
还有自定义成功方法
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler
自定义 用户认证失败异常
MyAuthenticationEntryPointImpl implements AuthenticationEntryPoint
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 设置拦截/sms/login短信登录接口
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
// 认证参数
private String phoneParameter = "phone";
private String smsCodeParameter = "code";
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
@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.obtainPhone(request);
phone = phone != null ? phone : "";
phone = phone.trim();
String smsCode = this.obtainSmsCode(request);
smsCode = smsCode != null ? smsCode : "";
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phone, smsCode);
this.setDetails(request, authRequest);
// 认证信息
return this.getAuthenticationManager().authenticate(authRequest);
}
}
// request.getParameter 只能获取json格式的数据
@Nullable
protected String obtainSmsCode(HttpServletRequest request) {
return request.getParameter(this.smsCodeParameter);
}
@Nullable
protected String obtainPhone(HttpServletRequest request) {
return request.getParameter(this.phoneParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setPhoneParameter(String phoneParameter) {
Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
this.phoneParameter = phoneParameter;
}
public void setSmsCodeParameter(String smsCodeParameter) {
Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");
this.smsCodeParameter = smsCodeParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.phoneParameter;
}
public final String getPasswordParameter() {
return this.smsCodeParameter;
}
}
验证手机号和短信是否匹配 核心业务
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
private static final String REDIS_LONGIN_PRE = "login:";
private SmsDetailsServiceImpl smsUserDetailsService;
private StringRedisTemplate stringRedisTemplate;
public SmsAuthenticationProvider (SmsDetailsServiceImpl userDetailsServiceImpl,StringRedisTemplate stringRedisTemplate) {
this.smsUserDetailsService = userDetailsServiceImpl;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
String phone = "";
if (principal instanceof String) {
phone = (String) principal;
}
String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码
// 1. 检验Redis手机号的验证码
String redisCode =stringRedisTemplate.opsForValue().get(REDIS_LONGIN_PRE+phone);
if (StringUtils.isEmpty(redisCode)) {
throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
}
if (!inputCode.equals(redisCode)) {
throw new BadCredentialsException("输入的验证码不正确,请重新输入");
}
// 2. 短信验证成功后要删除redis中的验证码
stringRedisTemplate.delete(REDIS_LONGIN_PRE+phone);
// 3. 根据手机号查询用户信息
LoginUser userDetails = (LoginUser) smsUserDetailsService.loadUserByUsername(phone);
if (userDetails == null) {
throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
}
// 4. 重新创建已认证对象,
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,inputCode, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
查询用户信息
@Service
public class SmsDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
System.out.println("正在使用SmsDetailsServiceImpl。。。。。");
// 查询用户信息
QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
System.out.println("手机号为:" + phone);
wrapper.eq("mobile",phone);
UserEntity userEntity = userMapper.selectOne(wrapper);
if(Objects.isNull(userEntity)){
throw new RuntimeException("用户不存在");
}
//TODO 查询对应权限信息
LoginUser user = new LoginUser(userEntity, Arrays.asList("test","admin"));
return user;
}
}
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// 手机号
private final Object principal;
// 验证码
private Object credentials;
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(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
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
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");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
自定义UserDetails类 方便数据获取
@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
private UserEntity userEntity;
public LoginUser(UserEntity userEntity) {
this.userEntity = userEntity;
}
private List<String> permissions;
@JSONField(serialize = false)
List<SimpleGrantedAuthority> authorities;
public LoginUser(UserEntity userEntity, List<String> permissions) {
this.userEntity = userEntity;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null)
return authorities;
authorities = permissions.stream().map(item -> {
return new SimpleGrantedAuthority(item);
}).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
单独配置短信验证
@Component
public class SmsSecurityConfigurerAdapter extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private SmsDetailsServiceImpl userDetailsService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void configure(HttpSecurity http) throws Exception {
//自定义SmsCodeAuthenticationFilter过滤器
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
//设置自定义SmsCodeAuthenticationProvider的认证器userDetailsService
SmsAuthenticationProvider smsCodeAuthenticationProvider = new SmsAuthenticationProvider(userDetailsService,redisTemplate);
//在UsernamePasswordAuthenticationFilter过滤前执行
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
把 短信验证配置添加到总配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
MyAuthenticationEntryPointImpl myAuthenticationEntryPoint;
@Autowired
MyAccessDeniedHandlerImpl myAccessDeniedHandler;
@Autowired
SmsSecurityConfigurerAdapter smsSecurityConfigurerAdapter;
@Autowired
UsernamePassSecurityConfAdapter usernamePassSecurityConfAdapter;
// 创建盐值加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// 不通过session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 对于登录接口 运行匿名访问
.authorizeRequests()
.antMatchers("/login","/sms/login","/sms/login/test","/sms/sendcode").anonymous()
.antMatchers("/").hasAnyAuthority("admin")
// 除上面外的所有请求全部要鉴权认证
.anyRequest().authenticated()
// .and().apply(usernamePassSecurityConfAdapter)
.and().apply(smsSecurityConfigurerAdapter);
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理器
http.exceptionHandling()
// 配置认证失败处理器
.authenticationEntryPoint(myAuthenticationEntryPoint);
// // 配置权限处理器
// .accessDeniedHandler(myAccessDeniedHandler);
// 允许跨域
http.cors();
}
}
自定义认证成功方法 主要用来生成token 后在返回给前端
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
static StringRedisTemplate stringRedisTemplate;
private static final String REDIS_LONGIN_PRE = "login:";
private static final String REDIS_LONGIN_TOKEN = "login:token:";
// 解决 @Component 下 @Autowired 注入为null的情况
@Autowired
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
MyAuthenticationSuccessHandler.stringRedisTemplate = stringRedisTemplate;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// logger.info("登录成功");
LoginUser principal = (LoginUser) authentication.getPrincipal();
// 认证通过了,使用userid生成一个jwt jwt 返回给前端
String token = new JWTEasyUtil().createToken(principal.getUserEntity().getId());
// 把token存入redis 并设过期时间
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_TOKEN+principal.getUserEntity().getId(),token,1, TimeUnit.HOURS);
// 把用户的完整信息存入redis
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_PRE+principal.getUserEntity().getId(), JSONObject.toJSONString(principal));
// 返回token给前端 前端拿到token 每次请求都要在head里携带token
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(token));
}
}
自定义 用户认证失败异常
// 自定义 用户认证失败异常
@Component
public class MyAuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println(""
+authException.getMessage()+"");
}
}
此过滤器最先执行,用来校验 前端发送请求的token
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
StringRedisTemplate redisTemplate;
private static final String REDIS_LONGIN_PRE = "login:";
private static final String REDIS_LONGIN_TOKEN = "login:token:";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
filterChain.doFilter(request, response);
return;
}
String userId = null;
// 解析token
try {
Claims parseToken = new JWTEasyUtil().parseToken(token);
userId = parseToken.getSubject();
} catch (Exception e) {
e.printStackTrace();
}
// 首先判断token值过期没
String redisToken = redisTemplate.opsForValue().get(REDIS_LONGIN_TOKEN + userId);
if(StringUtils.isEmpty(redisToken) || (!redisToken.equals(token))){
throw new AccountExpiredException("token过期,请重新登录");
}
// TODO 从redis中获取用户信息
String userJson = redisTemplate.opsForValue().get(REDIS_LONGIN_PRE + userId);
// 判断用户是否已经注销了
if(Objects.isNull(userJson)){
throw new AccountExpiredException("请重新登录");
}
LoginUser loginUser = JSONObject.parseObject(userJson, LoginUser.class);
// 存入SecurityContextHolder
// 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
@RestController
public class LoginController {
@Autowired
LoginService loginService;
@GetMapping("/sms/sendcode")
public BaseResult sendCode(@RequestParam("phone") String phone) {
return loginService.sendCode(phone);
}
@GetMapping("/loginout")
public BaseResult loginOut() {
return loginService.logout();
}
}
public interface LoginService extends IService<UserEntity> {
BaseResult logout();
BaseResult sendCode(String phone);
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
System.out.println("用户名:"+username);
wrapper.eq("username",username);
UserEntity userEntity = userMapper.selectOne(wrapper);
if(Objects.isNull(userEntity)){
throw new RuntimeException("用户不存在");
}
//TODO 查询对应权限信息
LoginUser user = new LoginUser(userEntity,Arrays.asList("test","admin"));
return user;
}
}
SmsAuthenticationFilter
,用来根据手机号来查找用户信息, 此类 仿写UsernamePasswordAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 设置拦截/sms/login短信登录接口(短信登录接口)
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
// 认证 请求参数
private String phoneParameter = "phone";
private String smsCodeParameter = "code";
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
@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.obtainPhone(request);
phone = phone != null ? phone : "";
phone = phone.trim();
String smsCode = this.obtainSmsCode(request);
smsCode = smsCode != null ? smsCode : "";
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phone, smsCode);
this.setDetails(request, authRequest);
// 认证信息
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainSmsCode(HttpServletRequest request) {
return request.getParameter(this.smsCodeParameter);
}
@Nullable
protected String obtainPhone(HttpServletRequest request) {
return request.getParameter(this.phoneParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setPhoneParameter(String phoneParameter) {
Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
this.phoneParameter = phoneParameter;
}
public void setSmsCodeParameter(String smsCodeParameter) {
Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");
this.smsCodeParameter = smsCodeParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.phoneParameter;
}
public final String getPasswordParameter() {
return this.smsCodeParameter;
}
}
SmsAuthenticationFilter
里的return this.getAuthenticationManager().authenticate(authRequest);
来到了ProviderManager
执行authenticate(Authentication authentication)
方法。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;
boolean debug = logger.isDebugEnabled();
// 遍历验证管理
// 此时的toTest 为 SmsCodeAuthenticationToken
// 需要provider遍历到支持SmsCodeAuthenticationToken 的验证方法
// 此时血药provider == SmsAuthenticationProvidery 至于为什么请往下看
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
AuthenticationProvider
后,会执行上面Authentication authenticate(Authentication authentication)
方法里的 result = provider.authenticate(authentication);
也就来到了 SmsAuthenticationProvider
里,执行里面的 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
方法 ,在方法里进行短信验证码校验。
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
private static final String REDIS_LONGIN_PRE = "login:";
private SmsDetailsServiceImpl smsUserDetailsService;
private StringRedisTemplate stringRedisTemplate;
public SmsAuthenticationProvider (SmsDetailsServiceImpl userDetailsServiceImpl,StringRedisTemplate stringRedisTemplate) {
this.smsUserDetailsService = userDetailsServiceImpl;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
String phone = "";
if (principal instanceof String) {
phone = (String) principal;
}
String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码
// 1. 检验Redis手机号的验证码
String redisCode =stringRedisTemplate.opsForValue().get(REDIS_LONGIN_PRE+phone);
if (StringUtils.isEmpty(redisCode)) {
throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
}
if (!inputCode.equals(redisCode)) {
throw new BadCredentialsException("输入的验证码不正确,请重新输入");
}
// 2. 短信验证成功后要删除redis中的验证码
stringRedisTemplate.delete(REDIS_LONGIN_PRE+phone);
// 3. 根据手机号查询用户信息
LoginUser userDetails = (LoginUser) smsUserDetailsService.loadUserByUsername(phone);
if (userDetails == null) {
throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
}
// 4. 重新创建已认证对象,
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,inputCode, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
// 当类型为SmsCodeAuthenticationToken的认证实体进入时才走此Provider
// 正是这个设置 在ProviderManager寻找处理认证时才能找个这个类
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
经过 SmsAuthenticationProvider
里的Authentication authenticate(Authentication authentication)
方法后,会返回到 ProviderManager
里继续执行sessionStrategy.onAuthentication(authResult, request, response)
方法。
然后来到AbstractAuthenticationProcessingFilter
类里,执行successHandler.onAuthenticationSuccess(request, response, authResult)
此时就来到了自定义认证成功方法MyAuthenticationSuccessHandler
里,执行里面的onAuthenticationSuccess
方法。
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
static StringRedisTemplate stringRedisTemplate;
private static final String REDIS_LONGIN_PRE = "login:";
private static final String REDIS_LONGIN_TOKEN = "login:token:";
// 解决 @Component 下 @Autowired 注入为null的情况
@Autowired
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
MyAuthenticationSuccessHandler.stringRedisTemplate = stringRedisTemplate;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// logger.info("登录成功");
LoginUser principal = (LoginUser) authentication.getPrincipal();
// 认证通过了,使用userid生成一个jwt jwt 返回给前端
String token = new JWTEasyUtil().createToken(principal.getUserEntity().getId());
// 把token存入redis 并设过期时间
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_TOKEN+principal.getUserEntity().getId(),token,1, TimeUnit.HOURS);
// 把用户的完整信息存入redis
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_PRE+principal.getUserEntity().getId(), JSONObject.toJSONString(principal));
// 返回token给前端 前端拿到token 每次请求都要在head里携带token
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(token));
}
}
发送验证码
验证码登录
6、项目源码
地址 https://github.com/Anan-X/spring_security_demo/tree/master