Spring Security前后端分离配置以及自定义图片验证码和短信验证码登录功能

自定义图片验证和验证码验证

Spring Security原理

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()); } 

最后别忘记,自定义的这两种方式都需要在配置类中注册。我已经在最前面配置自动登录的时候配置好了提前配置了。

至此,整个自定义短信验证码登录,以及图片验证码,就已经完成了!在大多数的登录场景就已经够用了。如有错误,敬请指正!!