SpringSercuiry(6)学习内容

项目应用

学习完redis后,服务器生成的jwtToken就可以存储到redis中了

一、添加redis的依赖

二、yml配置:

三、在认证成功处理器中使用

四、处理redis中乱码

但是我们发现认证成功后存储到redis中的值有乱码,不方便我们查看:

这是因为存储的时候默认以jdk的Key序列化器:JdkSerializationRedisSerializer后存储的,看到是二进制数据,但是不影响我们读取。怎么处理这样的数据呢?只需要默认更改序列化的方式就可以了:

添加一个redis的配置类

解决完成后的效果:

五、请求携带token

怎么能够保证每次请求携带这个服务器生成的token呢?

这个时候我们立马就想到了axios的拦截器,该拦截器包括请求拦截器和响应拦截器,我们在请求拦截器中获取之前在浏览器上存储的token,然后加入到对应的请求头中就好了,这样就避免了,我们每次发送请求还需要手动的添加这个请求头。

在axios实例中实现: /utils/axios.js

六、过滤器获取请求头

但是又面临一个问题?我们应该怎么获取这个请求头呢?什么时候获取呢?

以后每次请求都会发送这个请求头,如果我们写在对应的Controller中,那么每个controller的每个方法中都需要获取并验证,对吧!

这个时候,我们想到了过滤器,我们可以定义个过滤器,在这个过滤器中来获取请求头,验证这个token是否一致,同时因为spring security底层也是很多的过滤器链,定义完成后,加入到安全框架的过滤器链中就好了。

需要注意的是:登录操作是不需要验证的,所以需要直接放行,我们这里继承OncePerRequestFilter,而不是我们之前学习的实现Filter因为需要类型转换(request,response)

package com.sy.filter;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import com.sy.pojo.TUser;
import com.sy.utils.Const;
import com.sy.utils.Result;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
//如何让过滤器生效=>实例化  把过滤器添加进安全过滤器链中
@Component
@Data
public class JwtTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.secret}")
    private String secret;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 解决请求和响应乱码
        request.setCharacterEncoding("utf-8");
        response.setContentType("application/json;charset=utf-8");
        // 1. 获取请求路径 (登录请求 /login  直接放行)
        String uri = request.getRequestURI();
        // 判断当前请求路径是否为登录请求
        if ("/login".equals(uri)){
            // 放行  不拦截登录请求
            filterChain.doFilter(request,response);
        } else {
            // 2. 拦截请求  任何请求  获取请求头中的token
            String authorization = request.getHeader("authorization");
            // 判断判断当前token是否为空
            if (!StringUtils.hasText(authorization)){
                // 返回给前端 对应的错误提示信息  JSON格式
                response.getWriter().print(JSONUtil.toJsonStr(Result.error(505, "token不存在")));
            } else {
                // 3. 验证token token的合法性  截取token 将前缀去掉
                // String token = authorization.replace("Bearer", "");
                String token = authorization.substring(6);
                boolean verify = false;
                try {
                    // 借助hutool工具栏验证  根据密钥来验证
                    verify = JWTUtil.verify(token, secret.getBytes(StandardCharsets.UTF_8));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 判断验证是否成功
                if (!verify){
                    // 返回token不合法
                    response.getWriter().print(JSONUtil.toJsonStr(Result.error(506, "token不合法")));
                } else {
                    // 解析token 获取token中的有效载荷信息
                    JWT jwt = JWTUtil.parseToken(token);
                    JSONObject payloads = jwt.getPayloads();
                    String jsonTUser = payloads.get("tUser", String.class);
                    // 将字符串转成对象格式
                    TUser tUser = JSONUtil.toBean(jsonTUser, TUser.class);
                    // 获取redis中的token
                    String redis_token = (String) redisTemplate.opsForHash().get(Const.LOGIN_TOKEN_KEY, String.valueOf(tUser.getId()));
                    // 4. 获取redis中的token 与 请求头中的token 判断是否一致
                    if (!token.equals(redis_token)){
                        response.getWriter().print(JSONUtil.toJsonStr(Result.error(507, "token验证失败")));
                    } else {
                        // 将认证信息 添加到springsecurity的SecurityContextHolder中
                        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                                new UsernamePasswordAuthenticationToken(
                                        tUser,       // 认证信息
                                        List.of(),   // 密码 null
                                        List.of()   // 权限信息tUser.getAuthorities()
                                );
                        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                        // 5. 放行
                        filterChain.doFilter(request,response);
                    }
                }

            }
        }

    }
}

七、添加到过滤器链中

再一次认证通过后发送访问Index页面

前端修改:

/api/hello.js

首页向服务端发送请求获取当前认证成功的用户信息

重启项目如何让token失效

因为JWT无状态,重启项目后,jwt并没有失效,依然可以访问后端的接口;

原因是,你重启后端springboot项目后,前端sessionStorage中token没有失效,后端redis中的token也没有失效

解决办法:

1、把jwt存入redis中并设置一个过期时间,到期后jwt自动失效;(30分钟失效)

2、实现一个退出功能,用户点击退出登录,让jwt失效;(用户如果不点击退出)

3、服务关闭/重启,删除redis的所有jwt,而不是某一个用户的;

这个时候使用监听器解决,监听项目的关闭事件就可以了

设置过期时间

用户退出操作:

/api/hello.js

后端实现:

八、权限授予

和我们之前讲的一样,现在这里就说一个,万一没有权限怎么办呢?==> 提示暂无权限操作

前端实现:

后端实现:

修改 JwtTokenFilter 过滤器中 将 权限信息 添加到 添加到security上下文对象中

修改为以下内容:

我们去配置一个无权限配置的提示信息

和我们之前的成功失败的处理器是一样的也可以或者使用以下方式即可

完整写法:

package com.sy.config;


import cn.hutool.json.JSONUtil;
import com.sy.filter.JwtTokenFilter;
import com.sy.handler.LoginFailHandler;
import com.sy.handler.LoginSuccessHandler;
import com.sy.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

/**
 * 配置类
 * 认证(Authentication)
 * 授权(Authorization)
 */
@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailHandler loginFailHandler;

    @Autowired
    private JwtTokenFilter jwtTokenFilter;

    /**
     * 加密器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //定义一个bean  跨域配置
    @Bean
    public CorsConfigurationSource corsConfigurationSource(){
        //创建 CorsConfiguration 对象
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //设置需要放行的请求 和 请求头 以及请求方法  以及凭证信息
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        //允许携带的凭证信息(cookie)  这个属性设置为true  setAllowedOrigins这个一定是一个具体的域名
        corsConfiguration.setAllowCredentials(true);

        //创建CorsConfigurationSource接口对应的实现类对象
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",corsConfiguration);
        return source;
    }

    // 注册一个Bean(安全过滤器链), 定义一些我们自己的过滤逻辑(认证登录的时候去访问我们自定义的登录页面)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        return httpSecurity
                // 设置表单登录跳转的地址 发送/toLogin请求 ==> 让其跳转到自定义登录页面 ( jsp(淘汰),vue(暂时不考虑)  html(thymeleaf暂时使用) )需要引入依赖
                .formLogin(formLogin -> formLogin
                        //告知安全框架,使用自定义页面中的/login进行认证
                        .loginProcessingUrl("/login")
                        //登录成功后的处理
                        .successHandler(loginSuccessHandler)
                        //登录失败后的处理
                        .failureHandler(loginFailHandler)
                )
                //csrf跨站请求伪造 默认开启  前端发送请求的时候会带上一个csrf参数(前后端分离 目前前端没有csrf)
                //禁用csrf防御(不安全)   后期有解决方案:JWT 认证
                .csrf(csrf -> csrf.disable())
                //添加自定义过滤器 校验token过滤器  认证前校验token
                .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
                //解决跨域问题  以前方式使用过滤器解决跨域问题  安全框架解决跨域问题
                //参数配置 允许跨域访问的配置信息 在外边定义一个bean  使用调用方法的形式将跨域信息配置进来
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                //设置无权限配置
                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .accessDeniedHandler(((request, response, accessDeniedException) -> {
                            //响应给前端
                            response.getWriter().print(JSONUtil.toJsonStr(Result.error(500,"暂无权限")));
                        }))
                )
                //访问任何请求都需要认证的验证
                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                        //拦截所有请求都需要认证
                        .anyRequest().authenticated())
                .build(); // 如果只写一个build也只是空对象,因此在创建的时候加一些属性
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值