验证码(图形、短信、邮箱)、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的过滤器
图形验证码过滤器
统一过滤请求的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进行有效性的验证
@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()); } }
完。