二、spring security 短信验证 认证授权
A、自定义图片验证码验证
1、基于spring security重写图片验证码验证的过滤器ImgCodeFilter
package com.example.springsecurity.filter;
import com.example.springsecurity.exception.ImgException;
import com.example.springsecurity.handler.MyAuthenticationFailureHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author: LW
* @Date: 2021/12/26 10:24
* @Title: 数字验证过滤器,可用在图片验证码验证
*/
@Component
public class ImgCodeFilter extends OncePerRequestFilter {
@Autowired
MyAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//从请求中获取请求地址和方式进行判断是否是登录请求验证图片验证码
if("/login".equals(request.getRequestURI())&&"post".equalsIgnoreCase(request.getMethod())){
try{
verityCode(request);
}catch (ImgException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
}
}
doFilter(request,response,filterChain);
}
//验证图片验证码
public void verityCode(HttpServletRequest request) throws ImgException {
//图片验证码的在页面显示需要调用生成图片验证码的工具类,验证码生成后会先存入redis,在此略
//这里的1234是自定义的,在实际开发中是从redis获取
if(!"1234".equals(request.getParameter("code"))){
throw new ImgException("验证码错误");
}
}
}
2、重写登录异常
package com.example.springsecurity.exception; import org.springframework.security.core.AuthenticationException; public class ImgException extends AuthenticationException { public ImgException(String explanation) { super(explanation); } }
3、SecurityConfig中注入过滤器,并把过滤器加入security
@Autowired private ImgCodeFilter imgCodeFilter;
//验证用户名密码之前进行过滤验证 http.addFilterBefore(imgCodeFilter, UsernamePasswordAuthenticationFilter.class);
4、修改登录失败异常处理
if(e instanceof LockedException){ map.put("msg","账户被锁定,无法登录"); }else if(e instanceof BadCredentialsException){ map.put("msg","用户名或密码错误"); }else if(e instanceof DisabledException){ map.put("msg","账户被禁用,无法登录"); }else if(e instanceof AccountExpiredException){ map.put("msg","账户已过期,无法登录"); }else if(e instanceof CredentialsExpiredException){ map.put("msg","密码已过期,无法登录"); }else if(e instanceof ImgException){ map.put("msg",e.getMessage()); }else{ map.put("msg","登录异常,请联系管理员"); }
5、登录html加入图形验证码
B、短信验证码登录开发
1、重写AbstractAuthenticationToken中的方法
package com.example.springsecurity.handler; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * @Author: yushizhong * @Date: 2020/1/9 14:38 * @Title: 短信验证码token,对应UsernamePasswordAuthenticationToken */ public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 500L; private final Object principal; public SmsCodeAuthenticationToken(Object mobile) { super((Collection)null); this.principal = mobile; this.setAuthenticated(false); } public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } public Object getCredentials() { return null; } 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"); } else { super.setAuthenticated(false); } } public void eraseCredentials() { super.eraseCredentials(); } }
2、重写AbstractAuthenticationProcessingFilter中的方法
package com.example.springsecurity.handler; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * @Author: yushizhong * @Date: 2020/1/9 14:38 * @Title: 短信验证码token,对应UsernamePasswordAuthenticationToken */ public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 500L; private final Object principal; public SmsCodeAuthenticationToken(Object mobile) { super((Collection)null); this.principal = mobile; this.setAuthenticated(false); } public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } public Object getCredentials() { return null; } 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"); } else { super.setAuthenticated(false); } } public void eraseCredentials() { super.eraseCredentials(); } }
3、重写AbstractAuthenticationProcessingFilter中的方法
package com.example.springsecurity.filter; import com.example.springsecurity.handler.SmsCodeAuthenticationToken; import org.springframework.lang.Nullable; 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.stereotype.Component; import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @Author: yushizhong * @Date: 2020/1/9 14:40 * @Title: 短信验证码认证过滤器,对应UsernamePasswordAuthenticationFilter */ public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; private String mobileParameter = "mobile"; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/mobile", "POST")); } 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()); } else { String mobile = this.obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } @Nullable protected String obtainMobile(HttpServletRequest request) { return request.getParameter(this.mobileParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String mobileParameter) { Assert.hasText(mobileParameter, "Username parameter must not be empty or null"); this.mobileParameter = mobileParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return this.mobileParameter; } }
4、重写AuthenticationProvider 中的方法
package com.example.springsecurity.handler; import com.example.springsecurity.service.UserService; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; /** * @Author: yushizhong * @Date: 2020/1/9 14:43 * @Title: 短信验证码认证校验器,对应DaoAuthenticationProvider */ public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserService userService; public UserService getUserService() { return userService; } public void setUserService(UserService userService) { this.userService = userService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken)authentication; UserDetails user = userService.loadUserByUsername((String)smsCodeAuthenticationToken.getPrincipal()); if (user == null) { throw new InternalAuthenticationServiceException("无法获取用户信息"); } //构造认证结果 SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(user, user.getAuthorities()); result.setDetails(smsCodeAuthenticationToken.getDetails()); return result; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } }
5、短信验证码过滤器SmsCodeFilter,用于验证短信验证码是否正确
package com.example.springsecurity.filter; import com.example.springsecurity.exception.SmsCodeException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashSet; import java.util.Set; /** * @Author: yushizhong * @Date: 2020/1/9 15:00 * @Title: 用于验证短信验证码是否正确 */ @Component public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean { @Autowired private AuthenticationFailureHandler authenticationFailureHandler; private Set<String> urls = new HashSet<>(); private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); // 这里配置需要拦截的地址 urls.add("/mobile"); } @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { boolean action = false; //判断请求地址 for (String url : urls) { if (antPathMatcher.match(url, httpServletRequest.getRequestURI())) { action = true; break; } } if (action) { try { validate(httpServletRequest); } catch (SmsCodeException e) { authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); } private void validate(HttpServletRequest request) { String code= (String) request.getSession().getAttribute("code"); String smsCodeRequest = request.getParameter("smsCode"); if (code == null) { throw new SmsCodeException("短信验证码不存在"); } if(!smsCodeRequest.equalsIgnoreCase(code)) { throw new SmsCodeException("短信验证码错误"); } //清除session // request.getSession().removeAttribute("code"); } }
6、SmsCodeAuthenticationSecurityConfig 短信验证登录处理中心
package com.example.springsecurity.config;
import com.example.springsecurity.filter.SmsCodeAuthenticationFilter;
import com.example.springsecurity.handler.MyAuthenticationFailureHandler;
import com.example.springsecurity.handler.MyAuthenticationSuccessHandler;
import com.example.springsecurity.handler.SmsCodeAuthenticationProvider;
import com.example.springsecurity.service.UserService;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* @Author: yushizhong
* @Date: 2020/1/9 14:57
* @Title: 短信验证码认证安全设置,重写configure方法
*/
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private UserService userService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserService(userService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterBefore(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
7、SecurityConfig配置
package com.example.springsecurity.config; import com.example.springsecurity.domain.Auth; import com.example.springsecurity.filter.ImgCodeFilter; import com.example.springsecurity.filter.SmsCodeFilter; import com.example.springsecurity.handler.MyAuthenticationFailureHandler; import com.example.springsecurity.handler.MyAuthenticationSuccessHandler; import com.example.springsecurity.mapper.AuthMapper; import com.example.springsecurity.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 登录成功的处理 */ @Autowired private MyAuthenticationSuccessHandler successHandler; /** * 登录失败的处理 */ @Autowired private MyAuthenticationFailureHandler failureHandler; /** * 数据库验证用户信息 */ @Autowired private UserService userService; @Autowired private SmsCodeFilter smsCodeFilter; @Autowired private SmsCodeAuthenticationSecurityConfig codeAuthenticationSecurityConfig; //配置加密的方式 @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } //配置认证用户信息和授权 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } //配置拦截请求资源 @Override protected void configure(HttpSecurity http) throws Exception { http //开启HttpSecurity配置 .authorizeRequests() //指定路径 .antMatchers("/login.html").permitAll() .antMatchers("/sendCode").permitAll() .antMatchers("/mobile").permitAll() .antMatchers("/**").fullyAuthenticated() //配置认证模式 .and().formLogin() .loginPage("/login.html") .loginProcessingUrl("/mobile") //登录成功的操作 .successHandler(successHandler) //登录失败的操作 .failureHandler(failureHandler) .and() .logout() .logoutUrl("/logout") //清除身份认证信息 .clearAuthentication(true) //设置session失效 .invalidateHttpSession(true) .addLogoutHandler(new LogoutHandler() { @Override public void logout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) { } }) .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException, ServletException { //退出成功后跳转到登录 resp.sendRedirect("/login.html"); } }) //配置和登录相关的接口不需要认证 .permitAll() .and() //关闭cors .csrf() .disable(); //加载自己的配置 http.apply(codeAuthenticationSecurityConfig); http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class); } }
8、登录成功、失败异常处理
package com.example.springsecurity.handler; import com.example.springsecurity.domain.User; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; /** * @Author: yushizhong * @Date: 2020/1/7 10:11 * @Title: 登录成功处理 */ @Component public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException, ServletException { //这里可以进行页面的跳转或返回json数据给客户端浏览器 User principal = (User) auth.getPrincipal();//获取登录用户的信息 principal.setPassword(null); resp.setContentType("application/json;charset=utf-8"); PrintWriter out=resp.getWriter(); resp.setStatus(200); Map<String,Object> map=new HashMap<>(); map.put("status",200); map.put("msg",principal); ObjectMapper objectMapper = new ObjectMapper(); out.write(objectMapper.writeValueAsString(map)); out.flush();; out.close(); // resp.sendRedirect("/"); } }
package com.example.springsecurity.handler; import com.example.springsecurity.exception.ImgException; import com.example.springsecurity.exception.SmsCodeException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; /** * @Author: yushizhong * @Date: 2020/1/7 10:10 * @Title: 验证失败处理 */ @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out=resp.getWriter(); resp.setStatus(401); Map<String,Object> map=new HashMap<>(); map.put("status",401); if(e instanceof LockedException){ map.put("msg","账户被锁定,无法登录"); }else if(e instanceof BadCredentialsException){ map.put("msg","用户名或密码错误"); }else if(e instanceof DisabledException){ map.put("msg","账户被禁用,无法登录"); }else if(e instanceof AccountExpiredException){ map.put("msg","账户已过期,无法登录"); }else if(e instanceof CredentialsExpiredException){ map.put("msg","密码已过期,无法登录"); }else if(e instanceof SmsCodeException){ map.put("msg",e.getMessage()); }else{ map.put("msg","登录异常,请联系管理员"); } ObjectMapper objectMapper = new ObjectMapper(); out.write(objectMapper.writeValueAsString(map)); out.flush();; out.close(); // resp.sendRedirect("/login.html"); } }
package com.example.springsecurity.exception; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; /** * @Author: yushizhong * @Date: 2020/1/9 15:03 * @Title: 短信验证码异常类 */ public class SmsCodeException extends AuthenticationException { public SmsCodeException(String msg) { super(msg); } }
9、登录页面login.html
10、短信发送的模拟接口
package com.example.springsecurity.controller; import com.example.springsecurity.util.CodeUtil; import com.example.springsecurity.util.SendSms; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; @Controller public class UserController { @RequestMapping("/sendCode") public void sendCode(HttpServletRequest request,String mobile){ String code = CodeUtil.getCode(6); System.out.println("验证码:"+code); // SendSms.sendMsg(mobile,code); request.getSession().setAttribute("code",code); } }
11、创建UserService类,重写验证的方法loadUserByUsername。
package com.example.springsecurity.service; import com.example.springsecurity.domain.Auth; import com.example.springsecurity.domain.User; import com.example.springsecurity.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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; import java.util.ArrayList; import java.util.List; @Service public class UserService implements UserDetailsService { @Autowired private UserMapper mapper; @Override public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException { User user=mapper.loadUserByMobile(mobile); if(user==null){ throw new UsernameNotFoundException("用户不存在"); } return user; } }