快速实现图形验证码、token验证

验证码(图形、短信、邮箱)、token机制对于系统的安全性已经是老生常谈;

本文将结合spring-security快速实现Google图形验证码、token的安全性校验。

技术储备

1、UserDetailsService接口

/**  * Core interface which loads user-specific data.  * <p>  * It is used throughout the framework as a user DAO and is the strategy used by the  * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider  * DaoAuthenticationProvider}.  *  * <p>  * The interface requires only one read-only method, which simplifies support for new  * data-access strategies.  *  * @see org.springframework.security.authentication.dao.DaoAuthenticationProvider  * @see UserDetails  *  * @author Ben Alex  */ public interface UserDetailsService { 	 	/** 	 * Locates the user based on the username. In the actual implementation, the search 	 * may possibly be case sensitive, or case insensitive depending on how the 	 * implementation instance is configured. In this case, the <code>UserDetails</code> 	 * object that comes back may have a username that is of a different case than what 	 * was actually requested.. 	 * 	 * @param username the username identifying the user whose data is required. 	 * 	 * @return a fully populated user record (never <code>null</code>) 	 * 	 * @throws UsernameNotFoundException if the user could not be found or the user has no 	 * GrantedAuthority 	 */ 	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

该接口位于org.springframework.security.core.userdetails

主要作用:加载用户信息的核心接口;被用于DaoAuthenticationProvider的策略


2、AuthenticationFailureHandler、AuthenticationSuccessHandler接口

/**  * Strategy used to handle a failed authentication attempt.  * <p>  * Typical behaviour might be to redirect the user to the authentication page (in the case  * of a form login) to allow them to try again. More sophisticated logic might be  * implemented depending on the type of the exception. For example, a  * {@link CredentialsExpiredException} might cause a redirect to a web controller which  * allowed the user to change their password.  *  * @author Luke Taylor  * @since 3.0  */ public interface AuthenticationFailureHandler { 	/** 	 * Called when an authentication attempt fails. 	 * @param request the request during which the authentication attempt occurred. 	 * @param response the response. 	 * @param exception the exception which was thrown to reject the authentication 	 * request. 	 */ 	void onAuthenticationFailure(HttpServletRequest request, 			HttpServletResponse response, AuthenticationException exception) 			throws IOException, ServletException; }
/**  * Strategy used to handle a successful user authentication.  * <p>  * Implementations can do whatever they want but typical behaviour would be to control the  * navigation to the subsequent destination (using a redirect or a forward). For example,  * after a user has logged in by submitting a login form, the application needs to decide  * where they should be redirected to afterwards (see  * {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be  * included if required.  *  * @author Luke Taylor  * @since 3.0  */ public interface AuthenticationSuccessHandler { 	/** 	 * Called when a user has been successfully authenticated. 	 * 	 * @param request the request which caused the successful authentication 	 * @param response the response 	 * @param authentication the <tt>Authentication</tt> object which was created during 	 * the authentication process. 	 */ 	void onAuthenticationSuccess(HttpServletRequest request, 			HttpServletResponse response, Authentication authentication) 			throws IOException, ServletException; }

接口位于org.springframework.security.web.authentication;

AuthenticationFailureHandler 用于处理失败的身份验证尝试的策略;

AuthenticationSuccessHandler 当用户成功通过身份验证时调用。


3、WebSecurityConfigurerAdapter 类

@Order(100) public abstract class WebSecurityConfigurerAdapter implements 		WebSecurityConfigurer<WebSecurity> { 	private final Log logger = LogFactory.getLog(WebSecurityConfigurerAdapter.class); 	private ApplicationContext context; 	private ContentNegotiationStrategy contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); 	private ObjectPostProcessor<Object> objectPostProcessor = new ObjectPostProcessor<Object>() { 		public <T> T postProcess(T object) { 			throw new IllegalStateException( 					ObjectPostProcessor.class.getName() 							+ " is a required bean. Ensure you have used @EnableWebSecurity and @Configuration"); 		} 	}; 	private AuthenticationConfiguration authenticationConfiguration; 	private AuthenticationManagerBuilder authenticationBuilder; 	private AuthenticationManagerBuilder localConfigureAuthenticationBldr; 	private boolean disableLocalConfigureAuthenticationBldr; 	private boolean authenticationManagerInitialized; 	private AuthenticationManager authenticationManager; 	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); 	private HttpSecurity http; 	private boolean disableDefaults; 	}

该类位于org.springframework.security.config.annotation.web.configuration

作用:为权限配置类,该类还实现了WebSecurityConfigurer接口;用户必须创建一个新类来继承AbstractHttpConfigurer。


我们将会用到的方法有:

1、protected void configure(AuthenticationManagerBuilder auth)

用户自定义注册权限,我们将使用我们自己的用户体系来重写。即,使用用户名+密码方式

auth.userDetailsService(customerUserDetailService).passwordEncoder(passwordEncoder());

2、protected void configure(HttpSecurity http)

配置Http权限,其中有`http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic()`配置

3、配置上面的权限认证成功、失败处理器以及登出处理器

  @Bean   public AuthenticationSuccessHandler authenticationSuccessHandler(){     return new CustomAuthenticationSuccessHandler();   }   @Bean   public AuthenticationFailureHandler authenticationFailureHandler(){     return new CustomAuthenticationFailHandler();   }   @Bean   public LogoutHandler logoutHandler(){     return new CustomLogoutSuccessHandler();   }

技术实现

1、构建用户信息Service,实现userDetailsService

目的是:用系统的用户体系构建权限的控制;即,用户名+密码

@Component public class CustomerUserDetailService implements UserDetailsService {   @Resource   private SysUserMapper sysUserMapper;   @Override   public UserDetails loadUserByUsername(String s){     SysUser sysUser = this.selectByUserName(s);     if(ObjectUtil.isNull(sysUser)){       throw new CustomAuthenticationException("用户不存在");     }     return this.getDetail(sysUser);   }   private UserDetails getDetail(SysUser sysUser){     return new CustomUserDetailsUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), Lists.newArrayList());   }   public UserDetails loadUserByUserId(Long id){     SysUser sysUser = this.selectById(id);     if(ObjectUtil.isNull(sysUser)){       throw new CustomAuthenticationException("用户不存在");     }     return this.getDetail(sysUser);   } }

2、分别构建权限认证成功处理器、失败处理器、登出处理器

用户名+密码匹配成功,我们将会登陆的用户信息进行token处理,下次客户端只需要透传token即可,不需要任何的登陆用户信息,信息更安全,可靠。

@Slf4j @Component public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {   @Resource   private StringRedisTemplate stringRedisTemplate;   private ObjectMapper objectMapper = new ObjectMapper();   @SneakyThrows   @Override   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {     String token;     Long userId = 0L;     if (authentication.getPrincipal() instanceof CustomUserDetailsUser) {       CustomUserDetailsUser userDetailsUser = (CustomUserDetailsUser) authentication.getPrincipal();       //用户名+时间       token = SecureUtil.md5(userDetailsUser.getUsername() + System.currentTimeMillis());       userId = userDetailsUser.getUserId();     } else {       token = SecureUtil.md5(String.valueOf(System.currentTimeMillis()));     }     stringRedisTemplate.opsForValue().set(Constants.AUTHENTICATION_TOKEN + token, token, Constants.TOKEN_EXPIRE, TimeUnit.SECONDS);     //返回前端的Token,V为用户的ID     stringRedisTemplate.opsForValue().set(token, Long.toString(userId), Constants.TOKEN_EXPIRE, TimeUnit.SECONDS);     response.setCharacterEncoding(CharsetUtil.UTF_8);     response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);     PrintWriter printWriter = response.getWriter();     Map<String, Object> dataMap = Maps.newLinkedHashMap();     dataMap.put(Constants.TOKEN, token);     printWriter.append(objectMapper.writeValueAsString(ResultVo.success(dataMap)));   } }

权限认证失败处理器

@Slf4j @Component public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler {   private ObjectMapper objectMapper = new ObjectMapper();   @SneakyThrows   @Override   public void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response, AuthenticationException exception){     response.setCharacterEncoding(CharsetUtil.UTF_8);     response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);     PrintWriter printWriter = response.getWriter();     printWriter.append(objectMapper.writeValueAsString(ResultVo.fail(exception.getMessage())));   } }

3、分别构建图形验证码、token的过滤器

快速实现图形验证码、token验证

图形验证码过滤器

统一过滤请求的URL,如果登陆API,需要校验图形验证码。验证码校验失败,将无权限操作,交给权限认证失败处理器处理。

@Slf4j public class AuthenticationTokenFilter extends BasicAuthenticationFilter {   private StringRedisTemplate stringRedisTemplate;   private CustomerUserDetailService customerUserDetailService;   private ObjectMapper objectMapper = new ObjectMapper();   public AuthenticationTokenFilter(AuthenticationManager authenticationManager, StringRedisTemplate template, CustomerUserDetailService customUserDetailsService) {     super(authenticationManager);     this.stringRedisTemplate = template;     this.customerUserDetailService = customUserDetailsService;   }   @Override   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {     String token = request.getHeader(Constants.TOKEN);     //如果存在Token、对Token进行校验-这个Token对应用户信息     if (!Strings.isNullOrEmpty(token)) {       String userId = stringRedisTemplate.opsForValue().get(token);       if (ObjectUtil.isNull(userId)) {         writer(response, "无效token");         return;       }       UserDetails userDetails = customerUserDetailService.loadUserByUserId(Long.valueOf(userId));       UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());       authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));       SecurityContextHolder.getContext().setAuthentication(authentication);     }     chain.doFilter(request, response);   }   @SneakyThrows   public void writer(HttpServletResponse response, String msg) {     response.setContentType("application/json;charset=UTF-8");     response.setStatus(HttpServletResponse.SC_OK);     response.getWriter()         .write(objectMapper.writeValueAsString(ResultVo.fail(HttpServletResponse.SC_UNAUTHORIZED, msg)));   }

快速实现图形验证码、token验证

token过滤器

将需要对token进行有效性的验证

@Configuration public class KaptchaConfig {   @Bean   public DefaultKaptcha producer() {     Properties properties = new Properties();     properties.put("kaptcha.border", "no");     properties.put("kaptcha.textproducer.font.color", "black");     properties.put("kaptcha.textproducer.char.space", "5");     Config config = new Config(properties);     DefaultKaptcha defaultKaptcha = new DefaultKaptcha();     defaultKaptcha.setConfig(config);     return defaultKaptcha;   } }

4、Google图形验证码配置类

加载图形验证码的bean

@Configuration public class KaptchaConfig {   @Bean   public DefaultKaptcha producer() {     Properties properties = new Properties();     properties.put("kaptcha.border", "no");     properties.put("kaptcha.textproducer.font.color", "black");     properties.put("kaptcha.textproducer.char.space", "5");     Config config = new Config(properties);     DefaultKaptcha defaultKaptcha = new DefaultKaptcha();     defaultKaptcha.setConfig(config);     return defaultKaptcha;   } }

5、案例实现

新建登陆API、验证码获取API

注入图形验证码的bean,写图片验证码

登陆接口无需任何操作,已经在权限认证成功的时候,生成了对应的token

@RestController @Slf4j public class LoginController {   @Resource   private Producer producer;   @Resource   private StringRedisTemplate stringRedisTemplate;      @SneakyThrows   @RequestMapping("/sys/code/{randomStr}")   public void captcha(@PathVariable("randomStr") String randomStr, HttpServletResponse response) {     response.setHeader("Cache-Control", "no-store, no-cache");     response.setContentType("image/jpeg");     String text = producer.createText();     log.info("【验证码生成成功】randomStr:{},captcha:{}", randomStr, text);     BufferedImage image = producer.createImage(text);     String redisKey = Constants.IMG_NUMBER_CODE_KEY + randomStr;     stringRedisTemplate.opsForValue().set(redisKey, text, Constants.TOKEN_EXPIRE, TimeUnit.SECONDS);     ServletOutputStream out = response.getOutputStream();     ImageIO.write(image, "jpg", out);     IOUtils.closeQuietly(out);   }      @PostMapping("/token/login")   @OperationLog(value = "用户登陆",type = LogOperationEnum.OTHER)   public ResultVo<?> login() {     return ResultVo.success();   } }

用户管理API

如果没有AuthIgnore方法注解,则都需要开启token验证。

@RestController @RequestMapping("/user") public class UserController {   @Resource   private SysUserService sysUserService;      @AuthIgnore   @OperationLog(value = "新增用户",type = LogOperationEnum.ADD)   @PostMapping("/add")   public ResultVo<Integer> register(@RequestBody SysUser vo){     return ResultVo.success(sysUserService.add(vo));   }    @GetMapping("/info")   @SysLog(value = "用户基本信息")   public ResultVo<SysUser> info(){     return ResultVo.success(sysUserService.info());   } }

完。

您可能还会对下面的文章感兴趣: