springboot3+Spring Security+JWT+redis+MySQL实现登陆验证

springboot3+Spring Security+JWT+redis实现登陆验证

完整代码连接:gitee完整代码跳转

背景

多年来,我虽积累了丰富的工作经验,但从未有机会独立负责实现一个完整的登录注册功能。由于登录注册功能是系统架构中的基石,通常在公司项目初期就已构建完成,并作为标准模块集成至后续开发的系统中。即便在技术迭代升级的过程中,这类核心功能也鲜少进行大幅调整,更多的是通过增加配置选项或优化技术框架来保持其稳定性和兼容性。鉴于此,我决定借此机会,运用最新的技术框架(如SpringBoot 3),亲自梳理并实现这一基础功能,以加深对系统底层逻辑的理解与掌握。

环境

JDK17、maven3.x、SpringBoot3

流程

<?xml version="1.0" standalone="no"?><?xml-stylesheet type="text/css" href="https://wps.processon.com/themes/default/wps/mind/icons/icons.css" ?> 1.引入依赖 3.配置Spring Security (SecurityConfig) 4. 创建JWT实现类 (JwtServiceImpl) 5. 创建JWT过滤器 (JwtAuthenticationFilter 2.定义角色和权限实体类 6.创建登陆用户bean (User) 7.User实现UserDetails接口 拦截访问 是否携带 token Y N token有效性 校验 8.实现登陆接口 (Controller,Service等) 9.配置认证策略 (AuthenticationConfig 10创建一个普通接口(测 试token有效性) 开始 结束 token有效性校 验逻辑 返回403 结束 redis中的token是否过期 &&Token是否过期 N过期 Y有效 其他流程

1.引入依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
        </dependency>

2.定义用户实体类


@Data
@TableName("sys_user")
public class SysUserDO {
    /**
     * id
     */
    private Integer id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 状态
     */
    private Integer status;
}

3.配置Spring Security(SecurityConfig)

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfig {

    /**
     * 白名单路径
     */
    private static final String[] WHITE_LIST_URL = {"/user/login"};
    /**
     * 过滤器:请求处理之前验证JWT
     */
    private final JwtAuthenticationFilter jwtAuthFilter;

    private final AuthenticationProvider authenticationProvider;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        System.out.println(" =======安全策略启动===== ");
        System.out.printf(" =======白名单%s===== %n", Arrays.toString(WHITE_LIST_URL));
        // 禁用CSRF保护 globally
        http.csrf(AbstractHttpConfigurer::disable)
                // 除白名单外,都需要身份验证
                .authorizeHttpRequests(req ->
                        req.requestMatchers(WHITE_LIST_URL)
                                .permitAll()
                                // 拒绝访问除以上URL以外的所有请求
                                .anyRequest()
                                // 要求身份验证
                                .authenticated())
                // 使用不存储会话的会话策略
                .sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
                // 使用JWT认证提供者
                .authenticationProvider(authenticationProvider)
                // 在JWT认证过滤器之前添加用户名密码认证过滤器
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
                // 配置注销流程
                .logout(logout ->
                        // 设置注销URL
                        logout.logoutUrl("/user/logout")
                                // 在注销成功后清除安全上下文
                                .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()));

        //单用户登录,如果有一个登录了,同一个用户在其他地方不能登录
        //http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
        //单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
        //http.sessionManagement().maximumSessions(1).expiredUrl("/toLogin");

        // 返回配置好的HTTP安全策略
        return http.build();
    }
}

4. 创建JWT实现类(JwtServiceImpl)


@Service
public class JwtServiceImpl {

    @Value("${application.security.jwt.secret-key}")
    private String secretKey;
    @Value("${application.security.jwt.expiration}")
    private long jwtExpiration;
    @Value("${application.security.jwt.refresh-token.expiration}")
    private long refreshExpiration;
    @Resource
    private RedisTemplate<String, String> redisTemplate;
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {

        return buildToken(extraClaims, userDetails, jwtExpiration);
    }

    /**
     * 刷新token:token续命且刷新redis
     *
     * @param token
     * @return
     */
    public String generateRefreshToken(String token) {
        System.out.println("=== generateRefreshToken === " );
        try {
            DecodedJWT jwt = JWT.decode(token);
            String username = jwt.getSubject();
            Algorithm algorithm = Algorithm.HMAC256(secretKey);
            Date now = new Date();
            Date expiryDate = new Date(now.getTime() + refreshExpiration);
            String redisKey = generateUserRedisKey(username, token);
            redisTemplate.opsForValue().set(redisKey, username, refreshExpiration, TimeUnit.MILLISECONDS);
            return JWT.create().withSubject(username).withIssuedAt(now).withExpiresAt(expiryDate).sign(algorithm);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) {
        System.out.println("=== 创建token === " );
        //结合自身业务场景,有些系统将用户的一些基本信息(id,username,roles)全部放在了以json的形式放在了token中
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        String token =  JWT.create()
                .withSubject(JSON.toJSONString(userDetails))
                .withIssuedAt(now)
                .withExpiresAt(expiryDate)
                .sign(algorithm);
        //将token存入redis中,并设置过期时间
        String redisKey = generateUserRedisKey(userDetails.getUsername(), token);
        redisTemplate.opsForValue().set(redisKey, userDetails.getUsername(), expiration, TimeUnit.MILLISECONDS);

        return token;
    }



    public boolean isTokenValid(String token) {
        System.out.println("=== token有效性校验 === " );
        try {
            Algorithm algorithm = Algorithm.HMAC256(secretKey);
            JWT.require(algorithm).build().verify(token);
            System.out.println(" === 有效的token === " );
            return true;
        } catch (Exception e) {
            System.err.println(" === 无效的token === " );
            return false;
        }
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder().setSigningKey(getSignInKey()).build().parseClaimsJws(token).getBody();
    }

    public String getUsernameFromToken(String token) {
        System.out.println("=== getUsernameFromToken === ");
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getSubject();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    private Key getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    public String generateUserRedisKey(String username,String token) {
        return username+"::"+token;
    }
}

5. 创建JWT过滤器(JwtAuthenticationFilter)


@Component
@RequiredArgsConstructor

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Resource
    private JwtServiceImpl jwtServiceImpl;
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println(" =======拦截器启动===== ");
        System.out.printf(" =======拦截器路径%s===== %n", request.getRequestURL().toString());
        // 从HttpServletRequest请求中获取"Authorization"头部的值
        String token = request.getHeader("Authorization");
        // 判断token是否不为null,并且以"Bearer "开头
        if (token != null && token.startsWith("Bearer ")) {
            // 将token从"Bearer "开头截取
            token = token.substring(7);
            String username = jwtServiceImpl.getUsernameFromToken(token);
            String redisKey = jwtServiceImpl.generateUserRedisKey(username, token);
            String redisUser = redisTemplate.opsForValue().get(redisKey);
            if (jwtServiceImpl.isTokenValid(token) && StringUtils.hasText(redisUser)) {
                //没有用户信息,就需要从数据库中获取用户信息
                //有效的token
                System.out.println("=====有效的token = " + token);
                // 创建UsernamePasswordAuthenticationToken类型的authentication
                //将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。这表明当前这个用户是登录过的,后续的拦截器就不用再拦截了
                UserDetails userDetails = JSON.parseObject(redisUser, UserDetails.class);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, new ArrayList<>());
                // 将authentication设置到当前SecurityContextHolder中的Authentication
                SecurityContextHolder.getContext().setAuthentication(authentication);
                //刷新token,结合自身系统场景而定:相当于每次访问都进行续命
                refreshToken(token);
            } else {
                System.err.println("===无效token===");
            }
        }
        // 执行过滤器链中的下一个过滤器
        chain.doFilter(request, response);
    }

    /**
     * 刷新token
     */
    private void refreshToken(String token) {
        jwtServiceImpl.generateRefreshToken(token);
    }
}

6.创建登陆用户bean(User)

这里要注意区别实体类SysUserDO,这个bean需要去实现security的UserDetails接口

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
    /**
     * id
     */
    private Integer id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 角色列表
     */
    @TableField(exist = false)
    private List<SysRoleDO> roles;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }

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

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

8.实现登陆接口(Controller,Service等)

Controller略,
ServiceImpl主要代码:
在generateRefreshToken方法中包含有redis的刷新

  @Override
    public Result login(String username, String password) {
        System.out.println("=== login ===" );
        User user = getUserByUsername(username).get();
        String token = null;
        //验证密码是否正确
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username,password);
        try {
            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
            if(authenticate==null){
                throw new RuntimeException("用户名或密码错误");
            }
            token = jwtServiceImpl.generateToken(user);
            System.out.println("token = " + token);
            String refreshToken = jwtServiceImpl.generateRefreshToken(token);
            System.out.println("refreshToken = " + refreshToken);
        }catch (Exception e){
            log.error("用户名或密码错误!",e);
            throw new RuntimeException("用户名或密码错误");
        }
        return Result.success().ok(token);
    }

9.配置认证策略(AuthenticationConfig)

主要作用:
1.从库中获取登陆用户的信息
2.密码加密
3.填充认证切面


@Configuration
@RequiredArgsConstructor
public class AuthenticationConfig {


  @Resource
  private SysUserService userService;

  @Bean
  public UserDetailsService userDetailsService() {
    System.out.println(" ====userDetailsService === ");
    return username -> userService.getUserByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
  }

  @Bean
  public AuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService());
    authProvider.setPasswordEncoder(passwordEncoder());
    return authProvider;
  }

  @Bean
  public AuditorAware<Integer> auditorAware() {
    return new ApplicationAuditAware();
  }

  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

}

10创建一个普通接口(测试token有效性)

访问的使用header中需要带有token

@RequestMapping("/test")
    public Result test() {
        return Result.success();
    }

测试

工具:postma
1.登陆接口

2.header中带Authorization访问测试接口

在这里插入图片描述
或者
注意: 一定要以:“Bearer ”开头,Bearer后有个空格
在这里插入图片描述
不带token或者错误的token
在这里插入图片描述
在这里插入图片描述

完整代码连接:gitee完整代码跳转

  • 40
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。它是一种安全的传输方式,将用户的身份信息进行编码并生成一个令牌,可以在客户端和服务器之间进行传递。JWT通常由三部分组成:头部,载荷和签名。头部包含了令牌的类型和加密算法,载荷包含了用户的身份信息,签名用于验证令牌的合法性和完整性。 Spring Security是一个基于Spring框架的安全性解决方案,提供了一套全面的认证和授权机制。它集成了JWT作为一种认证方式,可以通过JWT来进行用户身份验证和授权。Spring Security可以提供用户认证、授权、密码加密、会话管理等功能。 Redis是一种内存数据库,它支持高性能的键值对存储,并提供了多种数据结构的支持。在商城系统中,Redis通常被用作缓存,用于存储用户的登录信息、购物车数据、商品库存等。通过将数据存储在内存中,Redis能够提供非常快速的读写性能,从而提升系统的响应速度和并发能力。 综上所述,JWT是一种用于身份验证和授权的开放标准,可以与Spring Security集成来实现安全认证和授权机制。而Redis则可以作为缓存数据库,用于提升系统性能和数据访问速度。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [基于SpringBoot2+MybatisPlus+SpringSecurity+jwt+redis+Vue的前后端商城系统源码](https://download.csdn.net/download/2301_76965813/87778818)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [SpringBoot2+MybatisPlus+SpringSecurity+jwt+redis+Vue的前后端分离的商城系统](https://download.csdn.net/download/weixin_47367099/85250567)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [通用权限管理系统+springboot+mybatis plus+spring security+jwt+redis+mysql](https://download.csdn.net/download/qq_37049128/87842802)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值