自定义验证成功处理器
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private UserMapper userMapper;
@SneakyThrows
@Override
public void onAuthenticationSuccess(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));
自定义验证失败处理器
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
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) {
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);
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;
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;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
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 {
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();
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);
@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);
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
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());
最后别忘记,自定义的这两种方式都需要在配置类中注册。我已经在最前面配置自动登录的时候配置好了提前配置了。
至此,整个自定义短信验证码登录,以及图片验证码,就已经完成了!在大多数的登录场景就已经够用了。如有错误,敬请指正!!