自定义图片验证和验证码验证
Spring Security原理
绿:检查请求中是否包含这些信息
蓝:处理异常
橙:决定该请求是否能访问到服务
自定义登录
原始的Spring Security采用的是登录方式在前后端分离的项目中是不适用的,所以需要我们自定义登录方式。
自定义验证成功处理器
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private UserMapper userMapper;
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,颁发令牌,更改数据库数据等等,
//进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展
//返回json数据
CommonReturnType result = CommonReturnType.success("登录成功");
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
复制代码
自定义验证失败处理器
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
//返回json数据
CommonReturnType result = null;
if (e instanceof AccountExpiredException) {
//账号过期
result = CommonReturnType.fail("账号过期");
} else if (e instanceof InternalAuthenticationServiceException) {
//密码错误
result = CommonReturnType.fail("用户不存在");
} else if(e instanceof BadCredentialsException) {
//用户不存在
result = CommonReturnType.fail(e.getMessage());
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = CommonReturnType.fail("密码过期");
} else if (e instanceof DisabledException) {
//账号不可用
result = CommonReturnType.fail("账号被禁用");
} else if (e instanceof LockedException) {
//账号锁定
result = CommonReturnType.fail("账号锁定");
} else if(e instanceof NonceExpiredException) {
//异地登录
result = CommonReturnType.fail("异地登录");
} else if(e instanceof SessionAuthenticationException) {
//session异常
result = CommonReturnType.fail("session错误");
} else if(e instanceof ValidateCodeException) {
//验证码异常
result = CommonReturnType.fail(e.getMessage());
} else {
//其他未知异常
result = CommonReturnType.fail(e.getMessage());
}
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
复制代码
匿名访问(未登录访问)处理器
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
CommonReturnType result = CommonReturnType.fail("访问服务需要登录");
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
复制代码
访问权限拒绝处理器
@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
CommonReturnType result = CommonReturnType.fail("访问服务需要管理员身份");
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
复制代码
登出成功处理器
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
CommonReturnType result = CommonReturnType.success("登出成功");
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
复制代码
security配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//数据库查询用户服务
@Autowired
private UserNameDetailService userdetailservice;
//未登录处理器(匿名访问无权限处理)
@Autowired
private CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;
//会话过期策略处理器(异地登录)
@Autowired
private CustomizeSessionInformationExpiredStrategy customizeSessionInformationExpiredStrategy;
//登录成功处理器
@Autowired
private CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler;
//登录失败处理器
@Autowired
private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;
//权限拒绝处理器
@Autowired
private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;
//登出成功处理器
@Autowired
private CustomizeLogoutSuccessHandler customizeLogoutSuccessHandler;
//图片验证码过滤器
@Autowired
private ValidateImageCodeFilter validateImageCodeFilter;
//短信验证码过滤器
@Autowired
private SmsFilter smsFilter;
//短信验证码配置
@Autowired
private SmsAuthenticationConfig smsAuthenticationConfig;
/**
* 自定义数据库查寻认证
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userdetailservice).passwordEncoder(passwordEncoder());
}
/**
* 设置加密方式
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置登录
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启跨域以及关闭防护
http.csrf().disable().cors();
//注册自定义图片验证码过滤器
http.addFilterBefore(validateImageCodeFilter, UsernamePasswordAuthenticationFilter.class);
//短信验证顾虑器
http.addFilterBefore(smsFilter, ValidateImageCodeFilter.class);
//将短信验证码认证配置到spring security中
http.apply(smsAuthenticationConfig);
//更改未登录或者登录过期默认跳转
http.exceptionHandling().authenticationEntryPoint(customizeAuthenticationEntryPoint);
//路径权限
http.authorizeRequests()
.antMatchers("/api/v1/user/login","/doc.html"
,"/aip/v1/qrs/cc","/api/v1/user/mobile"
,"/api/v1/user/sms","/api/v1/user/image")
.permitAll()
.antMatchers("/usr/add").hasAnyAuthority("admin")
.anyRequest().authenticated();
//退出登录
http.logout()
.logoutUrl("/logout").logoutSuccessUrl("/test/hello").deleteCookies("JSESSIONID")
.logoutSuccessHandler(customizeLogoutSuccessHandler) //登出成功逻辑处理
.and()
.formLogin()
.successHandler(customizeAuthenticationSuccessHandler) //登录成功逻辑处理
.failureHandler(customizeAuthenticationFailureHandler) //登录失败逻辑处理
.and()
.exceptionHandling()
.accessDeniedHandler(customizeAccessDeniedHandler) //权限拒绝逻辑处理
.authenticationEntryPoint(customizeAuthenticationEntryPoint) //匿名访问无权限访问资源异常处理
//会话管理
.and()
.sessionManagement()
.maximumSessions(1) //同一个用户最大的登录数量
.expiredSessionStrategy(customizeSessionInformationExpiredStrategy); //异地登录(会话失效)处理逻辑
}
public SmsAuthenticationConfig getSmsAuthenticationConfig() {
return smsAuthenticationConfig;
}
}
复制代码
数据库查询用户服务
@Service("userdetailservice")
public class UserNameDetailService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleRelationService userRoleRelationService;
@Autowired
private RolePermissionRelationService rolePermissionRelationService;
@Autowired
private SysPermissionService sysPermissionService;
@SneakyThrows
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(null == username|| "".equals(username)) {
throw new UsernameNotFoundException("用户名不能为空");
}
//查询用户
//查找用户权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(s1);
return new User(user.getUsername(), new BCryptPasswordEncoder().encode(user.getPassword()), user.getEnabled(),user.getAccountNotExpired(),user.getCredentialsNotExpired(),user.getAccountNotLocked(),auths);
}
}
复制代码
自定义图片验证码
原理:
首先,我们通过一个接口获取图片验证码,同时将服务端将图片验证码存起来,然后我们在UsernamePasswordAuthenticationFilter前面添加过滤器来对验证码进行验证
图片验证码
public class ImageCode implements Serializable {
//图片验证码
private BufferedImage image;
//验证码
private String code;
//过期时间
private LocalDateTime expireTime;
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpire() {
return LocalDateTime.now().isAfter(expireTime);
}
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
}
复制代码
自定义验证异常
校验过程中需要抛出自定义的异常
public class ValidateCodeException extends AuthenticationException {
private static final long serialVersionUID = 5022575393500654458L;
public ValidateCodeException(String message) {
super(message);
}
}
复制代码
随机生成验证码
public class ImageCodeUtil {
/**
* 创建图片验证码
* @return
*/
public static ImageCode createImageCode() {
int width = 100; // 验证码图片宽度
int height = 36; // 验证码图片长度
int length = 4; // 验证码位数
int expireIn = 120; // 验证码有效时间 120s
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200,250));
g.fillRect(0,0,width,height);
g.setFont(new Font("Times New Roman",Font.ITALIC, 35));
g.setColor(getRandColor(160,200));
for(int i = 0; i< 155; i++){
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
StringBuilder sRand = new StringBuilder();
String rand = null;
for(int i = 0; i<length; i++){
int anInt = random.nextInt(57);
if(anInt >= 10) {
if(anInt + 65 >=91 && anInt + 65 <= 96) {
anInt += 6;
}
char ch = (char) (anInt + 65);
rand = String.valueOf(ch);
} else {
rand = String.valueOf(anInt);
}
sRand.append(rand);
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 15 * i + 15, 28);
}
g.dispose();
return new ImageCode(image, sRand.toString(),expireIn);
}
private static Color getRandColor(int fc, int bc) {
Random random = new Random();
if(fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
复制代码
图片验证码过滤器
这里继承OncePerRequestFilter和继承BasicAuthenticationFilter是一样的,因为BasicAuthenticationFilter也是继承了OncePerRequestFilter。
@Slf4j
@Component
public class ValidateImageCodeFilter extends OncePerRequestFilter {
@Autowired
//自定义验证失败处理器
private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;
//这里我选择将验证码存放在HttpSessionSessionStrategy中(可以使用redis等进行存储)
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//请求路径中是否包含login这个关键词 && 发送的请求必须是post
if (StringUtils.contains(request.getRequestURI(), "login")
&& StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
try {
//开始验证
validateCode(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
//如果验证失败,就使用自定义验证处理器
customizeAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
//验证实现
private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {
//从SessionStrategy中拿出验证码
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE");
//从请求路径中拿出验证码
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
//验证码判空
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码不能为空 ");
}
//验证码颁发方验证
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在!");
}
//验证码是否过期
if (codeInSession.isExpire()) {
sessionStrategy.removeAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE");
throw new ValidateCodeException("验证码已过期!");
}
//验证码正确性
if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不正确!");
}
//移除服务端的验证码存储
sessionStrategy.removeAttribute(servletWebRequest,"SESSION_KEY_IMAGE_CODE");
}
}
复制代码
获取验图片证码接口
@RequestMapping("/image")
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = ImageCodeUtil.createImageCode();
ImageCode codeInRedis = new ImageCode(null,imageCode.getCode(),imageCode.getExpireTime());
new HttpSessionSessionStrategy().setAttribute(new ServletWebRequest(request), "SESSION_KEY_IMAGE_CODE", codeInRedis);
response.setContentType("image/jpeg;charset=utf-8");
response.setStatus(HttpStatus.OK.value());
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
复制代码
自定义短信验证码
和图片验证码不同,短信验证码是一种登录方式,而图片验证码是账户密码登录的一个参数。
这里,我们需要定义一个新的登录验证的方式。我们借鉴账户密码的验证方式来写。
短信验证码过滤器
拦截短信验证码登录请求,组成一个验证token,然后进行验证。最后将这一整套流程注册进spring security
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String MOBILE_KEY = "mobile";
private String mobileParameter = MOBILE_KEY;
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/v1/user/mobile", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
//生成一个验证token,但是没有经过验证
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
复制代码
SmsAuthenticationToken
在上一步的拦截器中,我们拦截了短信验证码登录请求,我们需要组装一个AuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public SmsAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
@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");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
复制代码
SmsAuthenticationProvider
对上面组装的验证token进行验证。
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Autowired
private MobileDetailService mobileDetailService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
UserDetails userDetails = mobileDetailService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (null == userDetails) {
throw new InternalAuthenticationServiceException("未找到与该手机号对应的用户");
}
//标记这个验证结果为已验证
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsAuthenticationToken.class.isAssignableFrom(aClass);
}
public UserDetailsService getUserDetailService() {
return mobileDetailService;
}
public void setUserDetailService(MobileDetailService mobileDetailService) {
this.mobileDetailService = mobileDetailService;
}
}
复制代码
配置短信验证码流程到spring security
@Component
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private MobileDetailService mobileDetailService;
@Override
public void configure(HttpSecurity http) {
//一个验证拦截器
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
//给这个验证拦截器设置一个管理器
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//设置验证成功的处理器
smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
//设置验证失败的处理器
smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
//一个验证provider实现验证功能(将权限等信息加进去)
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
//给这个provider设置我的登录账户信息获取service
smsAuthenticationProvider.setUserDetailService(mobileDetailService);
//将这个验证器加到用户名登录的后面
http.authenticationProvider(smsAuthenticationProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
复制代码
获取验证码接口
通过接口直接返回代替了短信服务,这里仍然采用了sessionstrategy存放,根据需要可以采用redis等第三方数据库存取。
@RequestMapping("/sms")
public void createSms(HttpServletRequest request,HttpServletResponse response,String mobile) throws IOException {
SmsCode smsCode = RandomSmsUtil.createSMSCode();
new HttpSessionSessionStrategy().setAttribute(new ServletWebRequest(request),"SESSION_KEY_SMS_CODE" + mobile,smsCode);
response.getWriter().write(smsCode.getCode());
System.out.println("您的验证码信息为:" + smsCode.getCode() + "有效时间为:" + smsCode.getExpireTime());
}
复制代码
最后别忘记,自定义的这两种方式都需要在配置类中注册。我已经在最前面配置自动登录的时候配置好了提前配置了。
至此,整个自定义短信验证码登录,以及图片验证码,就已经完成了!在大多数的登录场景就已经够用了。如有错误,敬请指正!!