目录

前言

依赖引入:

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就不需要那些依赖,可以自行删除

<!--        redis  -->        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!--        jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>
<!-- 解决jdk1.8之后删除jar报错 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
<!-- hutool工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>
<!-- 编码工具包-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>
<!-- 解决 idea@ConfigurationProperties()注解报错 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

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<Claims> 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代码

 代码:

/**
 * @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<String,String> 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()方法里修改登录失败处理逻辑,添加验证码异常的失败处理

代码:

/**
 * @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<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> 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里获取了

 redis里的验证码:

访问登录接口:

提示请求成功,返回了一个token

 访问请求权限接口:

将刚才返回的token添加到请求头上,进行请求,返回成功

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

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐