计算机系统应用教程网站

网站首页 > 技术文章 正文

基于spring-security图形验证码、token验证

btikc 2024-09-29 09:57:39 技术文章 17 ℃ 0 评论

验证码(图形、短信、邮箱)、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进行有效性的验证

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

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

完。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表