整合springsecurity和springboot及redis,jwt实现前后端分离登录认证图片验证码功能

整合springsecurity和springboot及redis,jwt实现前后端分离登录认证图片验证码功能

首先我们要先明白springsecurity过滤器链的大致执行流程:
整合springsecurity和springboot及redis,jwt实现前后端分离登录认证图片验证码功能
即当用户发起一个请求,那么他将进入 Security 过滤器链。

  • 当到 LogoutFilter 的时候判断是否是退出路径,如果是退出路径则到 logoutHandler 退出处理器,如果退出成功则到logoutSuccessHandler 退出成功处理。如果不是退出路径则直接进入下一个过滤器。
  • 当到 UsernamePasswordAuthenticationFilter的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到AuthenticationSuccessHandler登录成功处理器处理,如果不是登录请求则不进入该过滤器。
  • 进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
  • 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理

LogoutFilter - 登出过滤器
logoutSuccessHandler - 登出成功之后的操作类
UsernamePasswordAuthenticationFilter - from提交用户名密码登录认证过滤器
AuthenticationFailureHandler - 登录失败操作类
AuthenticationSuccessHandler - 登录成功操作类
BasicAuthenticationFilter - Basic身份认证过滤器
SecurityContextHolder - 安全上下文静态工具类
AuthenticationEntryPoint - 认证失败入口
ExceptionTranslationFilter - 异常处理过滤器
AccessDeniedHandler - 权限不足操作类
FilterSecurityInterceptor - 权限判断拦截器、出口

了解大致流程后,我们第一步自然是导入他们的jar包。

  • 对于图片验证码功能,既然要实现那么我们首先先需要一个生成图片验证码的类,在里面设置了图片的的大小以及尺寸等等:
@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", "4");
      properties.put("kaptcha.image.height", "40");
      properties.put("kaptcha.image.width", "120");
      properties.put("kaptcha.textproducer.font.size", "30");
      Config config = new Config(properties);
      DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
      defaultKaptcha.setConfig(config);
      return defaultKaptcha;
   }
}
  • 接着我们需要在controller层编写生成验证码的controller:
@Slf4j
@RestController
public class AuthController extends BaseController{
   @Autowired
   private Producer producer;
   /**
    * 图片验证码
    */
   @GetMapping("/captcha")
   public Result captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
      String code = producer.createText();
      String key = UUID.randomUUID().toString();
      BufferedImage image = producer.createImage(code);
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      ImageIO.write(image, "jpg", outputStream);
      BASE64Encoder encoder = new BASE64Encoder();
      String str = "data:image/jpeg;base64,";
      String base64Img = str + encoder.encode(outputStream.toByteArray());

      // 存储到redis中
      redisUtil.hset(Const.captcha_KEY, key, code, 120);
      log.info("验证码 -- {} - {}", key, code);
      return Result.succ(
            MapUtil.builder()
            .put("token", key)
            .put("base64Img", base64Img)
            .build()
      );
   }
}
  • 由于我们是前后端分离,所以要禁用session,所以这里验证码放在redis中做缓存处理,接着每次生成验证码要随机生成一个key给前端,前端提交表单时再把key和验证码一起提交上来。
  • 然后因为图片验证码的方式,所以我们进行了encode,把图片进行了base64编码,这样前端就可以显示图片了。

由于springsecurity的配置,我们需要在原先的登录过滤器之前设置一个前置过滤器先验证验证码是否正确:

/**
 * 图片验证码校验过滤器,在登录过滤器前
 */
@Slf4j
@Component
public class CaptchaFilter extends OncePerRequestFilter {
   private final String loginUrl = "/login";
   @Autowired
   RedisUtil redisUtil;
   @Autowired
   LoginFailureHandler loginFailureHandler;
   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
         throws ServletException, IOException {
      String url = request.getRequestURI();
      if (loginUrl.equals(url) && request.getMethod().equals("POST")) {
         log.info("获取到login链接,正在校验验证码 -- " + url);
         try {
            validate(request);
         } catch (CaptchaException e) {
            log.info(e.getMessage());
            // 交给登录失败处理器处理
            loginFailureHandler.onAuthenticationFailure(request, response, e);
         }
      }
      filterChain.doFilter(request, response);
   }
   private void validate(HttpServletRequest request) {
      String code = request.getParameter("code");
      String token = request.getParameter("token");
      if (StringUtils.isBlank(code) || StringUtils.isBlank(token)) {
         throw new CaptchaException("验证码不能为空");
      }
      if(!code.equals(redisUtil.hget(Const.captcha_KEY, token))) {
         throw new CaptchaException("验证码不正确");
      }
      // 一次性使用
      redisUtil.hdel(Const.captcha_KEY, token);
   }
}
  • 在了解前面的过滤器链执行流程后,我们知道对于登录失败我们会有一个处理器进行处理:
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

		response.setContentType("application/json;charset=UTF-8");
		ServletOutputStream outputStream = response.getOutputStream();

		outputStream.write(JSONUtil.toJsonStr(new ApiResponse<>().setReMsg(exception.getMessage())).getBytes("UTF-8"));

		outputStream.flush();
		outputStream.close();
	}
}
  • 同样对于登录成功我们也会有一个处理器:
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {


	@Autowired
	JwtUtils jwtUtils;

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {


		response.setContentType("application/json;charset=UTF-8");
		ServletOutputStream outputStream = response.getOutputStream();
		// 生成jwt,并放置到请求头中
		String jwt = jwtUtils.generateToken(authentication.getName());
		response.setHeader(jwtUtils.getHeader(), jwt);
		outputStream.write(JSONUtil.toJsonStr(new ApiResponse<>().setReMsg("登录成功")).getBytes("UTF-8"));
		outputStream.flush();
		outputStream.close();
	}

}
  • 然后我们为了结合jwt,需要一个jwt工具类:
@Data
@Component
@ConfigurationProperties(prefix = "matrix.jwt")
public class JwtUtils {

	private long expire;
	private String secret;
	private String header;

	// 生成jwt
	public String generateToken(String username) {

		Date nowDate = new Date();
		Date expireDate = new Date(nowDate.getTime() + 1000 * expire);

		return Jwts.builder()
				.setHeaderParam("typ", "JWT")
				.setSubject(username)
				.setIssuedAt(nowDate)
				.setExpiration(expireDate)// 7天過期
				.signWith(SignatureAlgorithm.HS512, secret)
				.compact();
	}

	// 解析jwt
	public Claims getClaimByToken(String jwt) {
		try {
			return Jwts.parser()
					.setSigningKey(secret)
					.parseClaimsJws(jwt)
					.getBody();
		} catch (Exception e) {
			return null;
		}
	}

	// jwt是否过期
	public boolean isTokenExpired(Claims claims) {
		return claims.getExpiration().before(new Date());
	}

}
  • 这个工具类实现了生成token,验证token,且密钥信息我们要自己在yml配置文件中书写:
matrix:
  jwt:
    header: authorization
    expire: 604800
    secret: ji8n3439n439n43ld9ne9343fdfer49h
  • 因为我们实现了jwt技术,所以我们还需要一个jwt过滤器来处理有token的情况:
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

	@Autowired
	UserService userService;

	@Autowired
	JwtUtils jwtUtils;
	@Autowired
	UserDetailServiceImpl userDetailService;
	public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(authenticationManager);
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

		String jwt = request.getHeader(jwtUtils.getHeader());
		if (StrUtil.isBlankOrUndefined(jwt)) {
			// 没有jwt直接放行
			chain.doFilter(request, response);
			return;
		}
		System.out.println(12312);

		Claims claim = jwtUtils.getClaimByToken(jwt);
		if (claim == null) {
			throw new JwtException("token 异常");
		}
		if (jwtUtils.isTokenExpired(claim)) {
			// 会注入进验证失败的异常中
			throw new JwtException("token已过期");
		}

		String username = claim.getSubject();
		// 获取用户的权限等信息
		User user = userService.getByUsername(username);

		// 将token信息存放
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(user.getId()));


		SecurityContextHolder.getContext().setAuthentication(token);
		// 放行
		chain.doFilter(request, response);
	}
}
  • 我们还需要一个处理器用来解决匿名用户访问无权限资源时的异常:
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

		response.setContentType("application/json;charset=UTF-8");
		response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
		ServletOutputStream outputStream = response.getOutputStream();

		outputStream.write(JSONUtil.toJsonStr(new ApiResponse<>().setReMsg("请先登录")).getBytes("UTF-8"));

		outputStream.flush();
		outputStream.close();
	}
}
  • 同样,我们的密码不能明文存储,我们需要配置加密,因此我们需要注入加密与验证策略:
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
   return new BCryptPasswordEncoder();
}
上一篇:dart系列之:和null说再见,null使用最佳实践


下一篇:Requsets模块与反射