当前位置: 首页 > article >正文

【springboot】整合spring security 和 JWT

目录

  • 1. 整合spring security
    •       1. 导入依赖
    •       2. 配置类
    •       3. 实体类实现UserDetails接口
    •       4. 业务逻辑实现类实现UserDetailsService接口
    •       5. 控制类实现登录功能
    •       6. JWT工具类
    •       7. 测试登录功能
  • 2. 分析源码
    •       1. UsernamePasswordAuthenticationToken
    •       2. Authentication接口
    •       3. SecurityContextHolder类
  • 3. 认证过滤器
    •       1. 使用OncePerRequestFilter实现认证过滤器
    •       2. 测试认证功能

1. 整合spring security

      1. 导入依赖

<!--        security依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <scope>compile</scope>
    </dependency>
        <!--        该模块包含 security 命名空间解析代码和Java配置代码,如需要将XML命名空间进行配置或 Spring Security 的 Java 配置支持 -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
<!--        该模块包含过滤器和相关的web安全基础设施代码,如servlet API依赖的东西,认证服务和基于URL的访问控制-->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
    </dependency>

在这里插入图片描述在这里插入图片描述在这里插入图片描述

      2. 配置类

@Configuration
public class SecurityConfig{
    @Bean //配置加密器
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();// 加密方式
    }
    /*
       resUserBizImpl必须实现UserDetailsService接口
       UserDetailsService接口是spring security的用户认证接口
       用于从数据库中读取用户信息
     */
    private ResUserBizImpl resUserBiz;
    @Autowired //这里使用set方法注入,可以避免循环依赖
    public void setResUserBiz(ResUserBizImpl resUserBiz) {
        this.resUserBiz = resUserBiz;
    }
    @Bean //认证服务 组装组件
    public AuthenticationProvider authenticatorProvider() {DaoAuthenticationProvider provider =
                new DaoAuthenticationProvider();
        //设置密码加密器
        provider.setPasswordEncoder(passwordEncoder());
        //设置用户信息获取服务 获取用户信息
        provider.setUserDetailsService(resUserBiz);
        return provider;
    }
    @Bean //认证管理器
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
            throws Exception {
        //获取认证管理器
        return configuration.getAuthenticationManager();
    }
    @Bean //配置安全过滤器链
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {
        //取消默认的登录页面
        http.formLogin(AbstractHttpConfigurer::disable)
                //取消默认的登出页面
                .logout(AbstractHttpConfigurer::disable)
                //将自己的认证服务加入
                .authenticationProvider(authenticatorProvider())
                //禁用csrf保护
                .csrf(AbstractHttpConfigurer::disable)
                //禁用session,因为使用token
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                //禁用http基本认证,因为传输数据用的post,且请求体为JSON
                .httpBasic(AbstractHttpConfigurer::disable)
                //开放接口,除开放的接口外,其他接口都需要认证
                .authorizeHttpRequests(request -> request
                        .requestMatchers(HttpMethod.POST, "/user/login","/user/register","/user/logout").permitAll()
                        .requestMatchers(HttpMethod.GET, "/captcha/getCaptcha").permitAll()
                        .anyRequest().authenticated());
        return http.build();
    }
}

      3. 实体类实现UserDetails接口

      UserDetails接口是spring security提供的,用于封装用户信息,包括用户名、密码、角色等。通过这个接口,可以将用户的详细信息封装到UserDetails对象中,并通过UserDetailsService接口的实现类来提供用户信息。

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Resuser implements Serializable, UserDetails {
    @TableId(type = IdType.AUTO)
    private Integer userid;
    private String username;
    // UserDetails接口中定义的密码字段名必须为password
    @TableField(value = "pwd")
    private String password; 
    private String email;
    //表中没有该字段 用于封装角色
    @TableField(exist = false)
    private String role="user";
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 返回一个SimpleGrantedAuthority对象,表示用户的角色
        return List.of(new SimpleGrantedAuthority(role));
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;//表示账号没有过期
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;//表示账号没有被锁定
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;//表示密码没有过期
    }

    @Override
    public boolean isEnabled() {
        return true;//表示账号可用
    }
}

      4. 业务逻辑实现类实现UserDetailsService接口

      这里实现了UserDetailsService接口中的loadUserByUsername方法,用于根据用户名查询用户信息。

@Service
@Slf4j
public class ResUserBizImpl implements ResUserBiz, UserDetailsService {
    @Autowired
    private ResUserMapper resUserMapper;
    // 注入加密器
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override //UserDetailsService接口的方法
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<Resuser> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Resuser::getUsername,username);
        try{
            Resuser resuser = resUserMapper.selectOne(queryWrapper);
            return resuser;
        }catch (Exception e){
            log.error("用户不存在");
            return null;
        }
    }
}

      5. 控制类实现登录功能

@RestController
@RequestMapping("/user")
public class ResUserController {
    @Autowired
    private ResUserBizImpl resUserBiz;
    @Autowired // 注入认证管理器
    private AuthenticationManager authenticationManager;
    @Autowired // 注入jwt工具类
    private JwtUtil jwtUtil;
    @RequestMapping("/login")// /user/login
    public ResponseResult login(
            @RequestBody // 将请求体中的json数据映射为ResUserVO对象
            @Valid // 开启校验,必须有resuserVO对象才能通过校验
            ResUserVO user,HttpSession session){
        //加入验证码 因为将验证码存在在session中,所以需要从session中取出来
        String captcha = (String) session.getAttribute("captcha");
        //判断验证码是否正确
        if(!captcha.equalsIgnoreCase(user.getCaptcha())){
            return ResponseResult.error("验证码错误");
        }
        //使用authenticate()方法接收UsernamePasswordAuthenticationToken对象,并返回一个包含用户详细信息的 Authentication 对象。
        Authentication authentication = authenticationManager.authenticate(
                //将用户名和密码[和证书]封装到UsernamePasswordAuthenticationToken对象中,用来包装认证信息
                new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPwd())
        );
        //getContext()获取当前线程的SecurityContext,并返回一个SecurityContext对象。
        //setAuthentication()设置当前线程的SecurityContext,并将Authentication对象作为参数传入。
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //使用getPrincipal()方法获取认证信息,并转换为UserDetails对象
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        Resuser resuser = (Resuser) userDetails;
        //生成JWT负载
        Map payload = new HashMap();
        payload.put("userid",String.valueOf(resuser.getUserid()));
        payload.put("username",userDetails.getUsername());
        payload.put("role",resuser.getRole());
        payload.put("email",resuser.getEmail());
        //生成JWT令牌
        String token = jwtUtil.getToken(payload);
        //返回token,后面使用token进行认证
        return ResponseResult.ok("登录成功").setData(token);
    }
}

      登录认证流程图:

image.png

      6. JWT工具类

/**
 * 提供Jwt工具类
 * 提供Token生成和验证方法
 */
@Component //托管spring容器
public class JwtUtil {
    // 密钥
    private static final String key = "3f2e1d4c5b6a79808f7e6d5c4b3a29181716151413121110";
    // 令牌过期时间                           分  秒  毫秒
    private static final long EXPIRE_TIME = 5*60*1000;

    /**
     * 生成JWT令牌
     * @param payload
     * @return
     */
    public static String getToken(Map payload){
        //设置头部信息
        Map headers = new HashMap();
        //设置签名算法
        headers.put("alg", "HS256");
        //设置令牌类型
        headers.put("typ", "JWT");
        //                   系统当前时间(毫秒)
        Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
        //设置过期时间
        //方案一 将过期时间放在负载中
        payload.put("exp",date);

        //生成令牌
        String jwt = Jwts.builder()
                //设置头信息
                .setHeaderParams(headers)
                //设置负载信息
                .setClaims(payload)
                //方案二 通过jwtBuilder设置过期时间
                .setExpiration(date)
                //使用HS256算法和密钥对JWT进行签名
                .signWith(SignatureAlgorithm.HS256,key)
                //将之前设置的头部信息、负载信息和签名信息组合成一个完整的JWT,并以字符串形式返回
                .compact();
        System.out.println(jwt);
        return jwt;
    }

    /**
     * 解析JWT令牌
     * @param token
     * @return
     */
    public static Claims parseToken(String token){
        try{
            System.out.println("开始解析令牌...");
            /*
                以下代码的作用:
                使用指定的密钥(key)来解析JWT。
                验证JWT的签名是否正确。
                检查JWT是否过期或被篡改。
             */
            Claims claims = Jwts.parser()//创建一个解析器
                    .setSigningKey(key)//设置验证签名的密钥
                    .parseClaimsJws(token)//传入需要进行解析的token并解析
                    .getBody();//从Jws<Claims>对象中获取负载信息
            //获取过期时间
            //方案一 获取负载中的expiration字段,得到的是毫秒值的字符串
            String expiration = claims.get("exp").toString();//1694786829000
            Date date = new Date(Long.parseLong(expiration));
            //方案二 获取jwtBuilder中设置的过期时间,得到的是"EEE MMM dd HH:mm:ss zzz yyyy"格式的字符串
            String expiration1 = claims.getExpiration().toString();//Mon Sep 16 15:17:09 CST 2024
            Date date1 = (new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy")).parse(expiration1);
            if (date.before(new Date())) //判断当前时间是否在过期时间之前
                return claims;
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("令牌过期!");
        }
        return null;
    }
}

      使用JWT可参考:JWT生成、解析token

      7. 测试登录功能

在这里插入图片描述


2. 分析源码

      1. UsernamePasswordAuthenticationToken

      将用户的认证信息(如用户名和密码)封装成一个对象,以便在认证过程中使用。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 620L;
    private final Object principal;
    private Object credentials;
    /*
        常用的构造函数
        principal:认证的主体信息,通常为用户名或用户对象。
        credentials:认证的凭证信息,通常为密码。
     */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }
    /*
        常用的构造函数
        authorities:用户的权限列表。
     */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
    /*
        ....
     */
}

      2. Authentication接口

       Authentication 对象包含了用户的身份信息(如用户名)、凭证(如密码)以及用户的权限列表。通过SecurityContextHolder.getContext().setAuthentication(authentication)语句设置认证信息。认证成功后,Authentication 对象会被存储在 SecurityContext 中,以便在应用程序的其他部分获取当前用户的认证信息。

public interface Authentication extends Principal, Serializable {
    // 获取权限
    Collection<? extends GrantedAuthority> getAuthorities();
    // 获取凭证
    Object getCredentials();
    // 获取详情
    Object getDetails();
    // 获取主体信息
    Object getPrincipal();
    // 是否已认证
    boolean isAuthenticated();
    // 设置是否已认证
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

      3. SecurityContextHolder类

      SecurityContextHolder 是 Spring Security 中的一个核心类,用于存储和获取当前用户的安全上下文信息。它提供了一系列静态方法,用于管理 SecurityContext 对象,该对象包含了用户的认证信息和授权信息。

public class SecurityContextHolder {
    public static SecurityContext getContext() {
        return strategy.getContext();
    }
    /*
        ....
     */
}

      SecurityContext接口提供了获取和设置认证信息的方法。

public interface SecurityContext extends Serializable {
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
}

3. 认证过滤器

      1. 使用OncePerRequestFilter实现认证过滤器

      OncePerRequestFilter 是 Spring 提供的一个抽象类,用于确保过滤器在每个请求中只执行一次。继承 OncePerRequestFilter 类,重写 doFilterInternal 方法,实现认证逻辑。这里用来验证token是否有效。

/**
 * 过滤请求头中是否包含token信息
 * 并判断token是否有效:1. 是否过期
 *                     2. 解析token是否正确
 */
@Component
public class JwtFilter extends OncePerRequestFilter {
    /**
     *
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");
        System.out.println("authorizationHeader="+authorizationHeader);//Bearer xxx.yyy.zzz
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
            // 获取token
            String token = authorizationHeader.substring(7);
            // 解析token 这里验证了token是否有效
            Claims claims = JwtUtil.parseToken(token);
            // 打印Claims信息
            System.out.println("subject="+claims.getSubject()//获取负载信息,和过期时间等
                    +"\t expiration="+claims.getExpiration()//获取过期时间
                    +"\t issuer="+claims.getIssuer()//获取签发者
                    +"\t id="+claims.getId()//获取jti
                    +"\t username="+claims.get("username")//获取自定义信息
                    +"\t password="+claims.get("password"));//获取自定义信息
            //在JWT验证通过后,将用户信息注入到Spring Security的上下文中,从而实现无状态的用户认证。
            if (claims != null) {
                String user = claims.getSubject();//获取负载信息
                // 创建认证信息
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
                // 将认证信息放入SecurityContext中
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        // 如果没有该语句,则无法进入到controller层
        filterChain.doFilter(request, response);
    }
}

      2. 测试认证功能

      随便写一个控制器方法,测试token是否有效。

    @RequestMapping("/checkToken")
    public ResponseResult checkToken(){
        System.out.println("检查token");
        return ResponseResult.ok("token有效");
    }

      使用postman工具进行测试。

在这里插入图片描述


链接:

spring security中文文档:https://springdoc.cn/spring-security/index.html

参考文章:https://blog.csdn.net/m0_71273766/article/details/132942056?spm=1001.2014.3001.5501


http://www.kler.cn/news/306943.html

相关文章:

  • Vue接入高德地图并实现基本的路线规划功能
  • Redis基础,常用操作命令,主从复制,一主两从,事务数据库操作
  • day01 - Java基础语法
  • [Golang] Sync
  • HarmonyOS开发之全局状态管理
  • 天融信把桌面explorer.exe删了,导致开机之后无windows桌面,只能看到鼠标解决方法
  • C++基础面试题 | 什么是C++中的虚继承?
  • LabVIEW机动车动态制动性能校准系统
  • spring项目中如何通过redis的setnx实现互斥锁解决缓存缓存击穿问题
  • [项目][WebServer][HttpServer]详细讲解
  • 一码空传临时网盘PHP源码,支持提取码功能
  • 数据中台进化为数据飞轮的必要
  • 【笔记】自动驾驶预测与决策规划_Part2_基于模型的预测方法
  • 初学Linux(学习笔记)
  • Vue.js入门系列(二十九):深入理解编程式路由导航、路由组件缓存与路由守卫
  • 【C++】入门基础(下)
  • Java项目基于docker 部署配置
  • 关于新版本 tidb dashboard API 调用说明
  • 评价类——熵权法(Entropy Weight Method, EWM),完全客观评价
  • ansible安全优化篇
  • 在深圳停车场我居然能看到很漂亮的瓦房
  • 707. 设计链表
  • SQL,从每组中的 json 字段中提取唯一值
  • 鸿蒙开发基础
  • Rust Web开发框架对比:Warp与Actix-web
  • SpringBoot + MySQL + MyBatis 实操示例教学
  • 从冯唐的成事心法 看SAP协助企业战略落地到信息化
  • 车载软件架构 --- SOA设计与应用(上)
  • DAY20240913 VUE:深入解析 Vue Router 局部路由守卫:路由独享与组件内部守卫的妙用与区别
  • 自修C++PrimerPlus--类型转换、右值引用、引用中的类对象