整合springsecurity和springboot及redis,jwt实现前后端分离登录认证图片验证码功能
2022/1/16 19:06:40
本文主要是介绍整合springsecurity和springboot及redis,jwt实现前后端分离登录认证图片验证码功能,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
整合springsecurity和springboot及redis,jwt实现前后端分离登录认证图片验证码功能
首先我们要先明白springsecurity过滤器链的大致执行流程:
即当用户发起一个请求,那么他将进入 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(); }
这篇关于整合springsecurity和springboot及redis,jwt实现前后端分离登录认证图片验证码功能的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-05小米13T Pro系统合集:性能与摄影的极致融合,值得你升级的系统ROM
- 2024-10-01基于Python+Vue开发的医院门诊预约挂号系统
- 2024-10-01基于Python+Vue开发的旅游景区管理系统
- 2024-10-01RestfulAPI入门指南:打造简单易懂的API接口
- 2024-10-01初学者指南:了解和使用Server Action
- 2024-10-01Server Component入门指南:搭建与配置详解
- 2024-10-01React 中使用 useRequest 实现数据请求
- 2024-10-01使用 golang 将ETH账户的资产平均分散到其他账户
- 2024-10-01JWT用户校验课程:从入门到实践
- 2024-10-01Server Component课程入门指南