springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理

【springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理】目录
前言
依赖引入:
JWT工具类
修改CustomizeAuthenticationSuccessHandler代码
修改登录失败处理器CustomizeAuthenticationFailureHandler
redis工具类和验证码配置
编写获取验证码的接口:
验证码过滤器CaptchaFilter:
JWT过滤器JwtAuthenticationFilter
jwt认证失败处理器
无权限访问处理
Spring Security全局配置:WebSecurityConfig
测试:

前言 本篇文章是基于上一篇文章进行的整理扩展,没有看过的可以看一下上一篇文章
SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客
本篇文章的思路是基于这位博主的博客进行的开发,一些对于jwt的描述,session和token的不同描述的很不错,原文地址:
【全网最细致】SpringBoot整合Spring Security + JWT实现用户认证_小灵宝的博客-CSDN博客_springboot整合security+jwt
依赖引入: 由于我也使用了验证码的,所以引入了redis,因为我的jdk11版本,所以需要引入一些依赖,如果jdk1.8就不需要那些依赖,可以自行删除

org.springframework.boot spring-boot-starter-data-redis io.jsonwebtoken jjwt 0.9.0 com.github.axet kaptcha 0.0.9 javax.xml.bind jaxb-api 2.3.0 com.sun.xml.bind jaxb-impl 2.3.0 com.sun.xml.bind jaxb-core 2.3.0 javax.activation activation 1.1.1 cn.hutool hutool-all 5.3.3 org.apache.commons commons-lang3 3.8.1 commons-codec commons-codec 1.15 org.springframework.boot spring-boot-configuration-processor true

JWT工具类 首先写一个JWT工具类JwtUtils,该工具类需要有3个功能:生成JWT、解析JWT、判断JWT是否过期。在此我们使用了@ConfigurationProperties注解,方便读取application.yml文件里的内容。
/** * @Author: zm * @Description: jwt工具类 * @Date: 2022/4/24 16:48 */ @Data @Component @ConfigurationProperties(prefix = "zm.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 getClaimsByToken(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是否存在与有效 * @param jwtToken * @return */ public boolean checkToken(String jwtToken) { if(StringUtils.isEmpty(jwtToken)) return false; try { Jwts.parser().setSigningKey(secret).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 根据request判断token是否存在与有效(也就是把token取出来罢了) * @param request * @return */ public boolean checkToken(HttpServletRequest request) { try { String jwtToken = request.getHeader(header); if(StringUtils.isEmpty(jwtToken)) return false; Jwts.parser().setSigningKey(secret).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; }/** * 根据token获取用户的account * @param request * @return */ public String getMemberAccountByJwtToken(HttpServletRequest request) { String jwtToken = request.getHeader(header); if(StringUtils.isEmpty(jwtToken)) return ""; try{ Jws claimsJws = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwtToken); Claims claims = claimsJws.getBody(); return claims.getSubject(); }catch (Exception e){ e.printStackTrace(); return ""; } } }

对应的配置文件:
zm: jwt: header: Authorization expire: 604800 # 7天,s为单位 secret: abcdefghabcdefghabcdefghabcdefgh

修改CustomizeAuthenticationSuccessHandler代码 springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理
文章图片

代码:
/** * @Author: zm * @Description:登录成功处理逻辑 * @Date: 2022/4/24 10:18 */ @Component public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Autowired private SysUserService sysUserService; @Autowired JwtUtils jwtUtils; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { //更新用户表上次登录时间、更新人、更新时间等字段 User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); SysUser sysUser = sysUserService.getUserDetails(userDetails.getUsername()); //sysUser.setLastLoginTime(new Date()); sysUser.setUpdateDate(LocalDateTime.now()); sysUser.setUpdateBy(sysUser.getAccound()); sysUserService.update(sysUser); //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限, //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展// 根据用户的id和account生成token并返回 String jwtToken = jwtUtils.generateToken(sysUser.getAccound()); Map results = new HashMap<>(); results.put(jwtUtils.getHeader(),jwtToken); //返回json数据 JsonResult result = ResultTool.success(results); //处理编码方式,防止中文乱码的情况 httpServletResponse.setContentType("text/json; charset=utf-8"); //塞到HttpServletResponse中返回给前台 httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }

修改登录失败处理器CustomizeAuthenticationFailureHandler onAuthenticationFailure方法用于向前端返回错误信息,登录失败有可能是用户名密码错误,有可能是验证码错误,这里我们自定义了验证码错误的异常,它继承了Spring Security的AuthenticationException:
/** * @Author: zm * @Description:自定义验证码错误异常 * @Date: 2022/4/25 10:12 */ public class CaptchaException extends AuthenticationException {public CaptchaException(String msg) { super(msg); } }

onAuthenticationFailure()方法里修改登录失败处理逻辑,添加验证码异常的失败处理

springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理
文章图片

代码:
/** * @Author: zm * @Description:登录失败处理逻辑 * @Date: 2022/4/24 10:39 */ @Component public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { //返回json数据 JsonResult result = null; if (e instanceof CaptchaException) { //验证码错误 result = ResultTool.fail(ResultCode.USER_CAPTCHA_ERROR); } else if (e instanceof AccountExpiredException) { //账号过期 result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED); } else if (e instanceof BadCredentialsException) { //密码错误 result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR); } else if (e instanceof CredentialsExpiredException) { //密码过期 result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED); } else if (e instanceof DisabledException) { //账号不可用 result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE); } else if (e instanceof LockedException) { //账号锁定 result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED); } else if (e instanceof InternalAuthenticationServiceException) { //用户不存在 result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST); }else{ //其他错误 result = ResultTool.fail(ResultCode.COMMON_FAIL); } //处理编码方式,防止中文乱码的情况 httpServletResponse.setContentType("text/json; charset=utf-8"); //塞到HttpServletResponse中返回给前台 httpServletResponse.getWriter().write(JSON.toJSONString(result)); } }

redis工具类和验证码配置 redis工具类网上有很多,我是用的是这个网址的RedisUtil: 最全的Java操作Redis的工具类,使用StringRedisTemplate实现,封装了对Redis五种基本类型的各种操作!???
大家可以自行选择。如果自己项目有就用自己项目里的就可以。
验证码生成使用的是谷歌的验证码工具类,配置类如下:
/** * @Author: zm * @Description:验证码工具类 * @Date: 2022/4/25 10:17 */ @Configuration public class KaptchaConfig { @Bean 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; } }

编写获取验证码的接口:
@Autowired private RedisUtil redisUtil; @Autowired Producer producer; /** * 获取验证码 * @return * @throws IOException */ @GetMapping("/captcha") public JsonResult Captcha() throws IOException { String key = UUID.randomUUID().toString(); String code = producer.createText(); BufferedImage image = producer.createImage(code); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(image, "jpg", outputStream); String str = "data:image/jpeg; base64,"; String base64Img = str + Base64.encodeBase64String(outputStream.toByteArray()); redisUtil.hPut(Constant.CAPTCHA, key, code); return ResultTool.success( MapUtil.builder() .put("userKey", key) .put("captcherImg", base64Img) .build() ); }

Constant是自定义常量池:
public class Constant {/** * 验证码常量 */ public final static String CAPTCHA="captcha"; }

验证码过滤器CaptchaFilter: 在验证码过滤器中,需要先判断请求是否是登录请求,若是登录请求,则进行验证码校验,从redis中通过userKey查找对应的验证码,看是否与前端所传验证码参数一致,当校验成功时,因为验证码是一次性使用的,一个验证码对应一个用户的一次登录过程,所以需用hdel将存储的HASH删除。当校验失败时,则交给登录认证失败处理器LoginFailureHandler进行处理。
CaptchaFilter继承了OncePerRequestFilter抽象类,该抽象类在每次请求时只执行一次过滤,即它的作用就是保证一次请求只通过一次filter,而不需要重复执行。CaptchaFilter需要重写其doFilterInternal方法来自定义处理逻辑
/** * @Author: zm * @Description: 对请求进行过滤,判断JWT token是否有效 * 验证码过滤器 * @Date: 2022/4/24 17:08 */ @Component public class CaptchaFilter extends OncePerRequestFilter {@Autowired RedisUtil redisUtil; @Autowired CustomizeAuthenticationFailureHandler loginFailureHandler; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String url = request.getRequestURI(); if ("/login".equals(url) && request.getMethod().equals("POST")) { // 校验验证码 try { validate(request); } catch (CaptchaException e) { // 交给认证失败处理器 loginFailureHandler.onAuthenticationFailure(request, response, e); return; } } filterChain.doFilter(request, response); } // 校验验证码逻辑 private void validate(HttpServletRequest httpServletRequest) { String code = httpServletRequest.getParameter("code"); String key = httpServletRequest.getParameter("userKey"); if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) { throw new CaptchaException("验证码错误"); }if (!code.equals(redisUtil.hGet(Constant.CAPTCHA, key))) { throw new CaptchaException("验证码错误"); }// 若验证码正确,执行以下语句 // 一次性使用 redisUtil.hDelete(Constant.CAPTCHA, key); } }

JWT过滤器JwtAuthenticationFilter 在首次登录成功后,LoginSuccessHandler将生成JWT,并返回给前端。在之后的所有请求中(包括再次登录请求),都会携带此JWT信息。我们需要写一个JWT过滤器JwtAuthenticationFilter,当前端发来的请求有JWT信息时,该过滤器将检验JWT是否正确以及是否过期,若检验成功,则获取JWT中的用户名信息,检索数据库获得用户实体类,并将用户信息告知Spring Security,后续我们就能调用security的接口获取到当前登录的用户信息。
??若前端发的请求不含JWT,我们也不能拦截该请求,因为一般的项目都是允许匿名访问的,有的接口允许不登录就能访问,没有JWT也放行是安全的,因为我们可以通过Spring Security进行权限管理,设置一些接口需要权限才能访问,不允许匿名访问
/** * @Author: zm * @Description: JWT token过滤器 * @Date: 2022/4/25 11:47 */ public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Autowired JwtUtils jwtUtils; @Autowired SysUserService sysUserService; /** * 直接将我们前面写好的service注入进来,通过service获取到当前用户的权限 * */ @Autowired private UserDetailsServiceImpl userDetailsService; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); }@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 获取当请求头中的token,其实这里多余,完全可以使用HttpServletRequest来获取 String authToken = request.getHeader(jwtUtils.getHeader()); //String jwt = request.getHeader(jwtUtils.getHeader()); // 获取到当前用户的account String account = jwtUtils.getMemberAccountByJwtToken(request); System.out.println("自定义JWT过滤器获得用户名为"+account); Authentication a=SecurityContextHolder.getContext().getAuthentication(); // 当token中的username不为空时进行验证token是否是有效的token if (!account.equals("") && SecurityContextHolder.getContext().getAuthentication() == null) { // token中username不为空,并且Context中的认证为空,进行token验证// 获取到用户的信息,也就是获取到用户的权限 UserDetails userDetails = this.userDetailsService.loadUserByUsername(account); if (jwtUtils.checkToken(authToken)) {// 验证当前token是否有效UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //将authentication放入SecurityContextHolder中 SecurityContextHolder.getContext().setAuthentication(authentication); } } // 放行给下个过滤器 chain.doFilter(request, response); } }

若JWT验证成功,我们构建了一个UsernamePasswordAuthenticationToken对象,用于保存用户信息,之后将该对象交给SecurityContextHolder,set进它的context中,这样后续我们就能通过调用SecurityContextHolder.getContext().getAuthentication().getPrincipal()等方法获取到当前登录的用户信息了。
jwt认证失败处理器
/** * @Author: Administrator * @Description:jwt认证失败处理器 * @Date: 2022/4/25 12:00 */ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN); response.setContentType("text/json; charset=utf-8"); response.getWriter().write(JSON.toJSONString(result)); } }

无权限访问处理
/** * @Author: zm * @Description: 没有权限设置 * @Date: 2022/4/24 16:59 */ @Component public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {@Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { JsonResult result = ResultTool.fail(ResultCode.NO_PERMISSION); response.setContentType("text/json; charset=utf-8"); response.getWriter().write(JSON.toJSONString(result)); } }

Spring Security全局配置:WebSecurityConfig 因为是结合的前一篇文章,如果有没有的依赖,请参考前一篇文章:
SpringBoot整合Spring Security实现前后端分离登录权限处理_zmgst的博客-CSDN博客
WebSecurityConfig整体配置:
/** * spring security 配置类 * @Author: zm * @Description: * @Date: 2022/4/22 13:48 */ @Configuration @EnableWebSecurity//开启Spring Security的功能 //prePostEnabled属性决定Spring Security在接口前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true,会拦截加了这些注解的接口 @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {/** * 自定义用户登录操作 */ @Autowired private UserDetailsServiceImpl userDetailsService; /** * 匿名用户访问无权限资源时的异常 */ @Autowired private CustomizeAuthenticationEntryPoint authenticationEntryPoint; /** * 登录成功执行方法 */ @Autowired private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler; /** * 登陆失败执行方法 */ @Autowired private CustomizeAuthenticationFailureHandler authenticationFailureHandler; /** * 没有权限设置 */ @Autowired private CustomizeAccessDeniedHandler customizeAccessDeniedHandler; /** * 登出成功执行方法 */ @Autowired private CustomizeLogoutSuccessHandler logoutSuccessHandler; /** * 会话过期策略处理 */ @Autowired private CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy; //自定义权限访问设置 =======开始========= /** * 访问决策管理器 */ @Autowired private CustomizeAccessDecisionManager accessDecisionManager; /** * 安全元数据源 */ @Autowired private CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource; /** * 权限拦截器 */ @Autowired private CustomizeAbstractSecurityInterceptor securityInterceptor; //自定义权限访问设置 =======结束=========/** * 验证码过滤 */ @Autowired private CaptchaFilter captchaFilter; /** * JWT token过滤器 * @return * @throws Exception */ @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()); return jwtAuthenticationFilter; } /** * 指定加密方式 * @return */ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); }@Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(); http .authorizeRequests() //自定义权限控制器 .withObjectPostProcessor(new ObjectPostProcessor() { @Override public O postProcess(O o) { o.setAccessDecisionManager(accessDecisionManager); //访问决策管理器 o.setSecurityMetadataSource(securityMetadataSource); //安全元数据源 return o; } }) .antMatchers(HttpMethod.POST, "/sysUser/addUser").permitAll() // 允许post请求/add-user,而无需认证 .antMatchers("/sysUser/captcha").permitAll()//验证码放过 .anyRequest().authenticated() //有请求都需要验证//登入 .and().formLogin(). permitAll().//允许所有用户 successHandler(authenticationSuccessHandler).//登录成功处理逻辑 failureHandler(authenticationFailureHandler).//登录失败处理逻辑//登出 and().logout(). permitAll().//允许所有用户 logoutSuccessHandler(logoutSuccessHandler)//登出成功处理逻辑 //deleteCookies("JSESSIONID")//登出之后删除cookie//异常处理(权限拒绝、登录失效等) .and() .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理 .accessDeniedHandler(customizeAccessDeniedHandler)// 无状态session,不进行存储 禁用session .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //设置一个账号只能一个用户使用 //.maximumSessions(1) ////会话信息过期策略会话信息过期策略(账号被挤下线) //.expiredSessionStrategy(sessionInformationExpiredStrategy)// 配置自定义的过滤器 .and() .addFilter(jwtAuthenticationFilter()) // 验证码过滤器放在UsernamePassword过滤器之前 .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(securityInterceptor,FilterSecurityInterceptor.class); //http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } }

测试: 验证码接口:
访问后会返回唯一标识和base64编码的图片,这里我直接从redis里获取了
springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理
文章图片

redis里的验证码:
springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理
文章图片

访问登录接口:
提示请求成功,返回了一个token
springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理
文章图片
访问请求权限接口:
将刚才返回的token添加到请求头上,进行请求,返回成功
springBoot|SpringBoot整合Spring Security+JWT实现前后端分离登录权限处理
文章图片

此文章只是简单操作了Spring Security整合jwt的流程,具体的原理并没有讲解,望各位大佬多多指教!

    推荐阅读