【Spring Security 实战 】Spring Security 整合 jwt 附源码

Spring Security 整合 jwt 源码地址

Spring Security 快速入门

  1. 新建springBoot工程导入如下依赖
    包括了后续需要用到的数据库操作相关的组件

        <!--Spring security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    	<!--SpringBoot druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
    
        <!-- Mysql Connector -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    
        <!--SpringBoot druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
    
        <!--SpringBoot mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
    
        <!--Spring Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
  2. 增加一个接口

    @RestController
    @RequestMapping("/user")
    @RequiredArgsConstructor
    public class UserController {
    
        private final UserService userService;
    
        @GetMapping("/{id}")
        public R<Object> queryById(@PathVariable Long id){
    
            new FilterChainProxy();
            return R.ok( userService.queryById(id));
        }
    
    }
    
  3. 增加配置文件和数据库表结构

    spring:
      datasource:
        druid:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/zcct-user?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: zclvct
          # 初始连接数
          initial-size: 5
          # 最小连接数
          min-idle: 10
          # 最大连接数
          max-active: 20
          # 获取连接超时时间
          max-wait: 5000
          # 连接有效性检测时间
          time-between-eviction-runs-millis: 60000
          # 连接在池中最小生存的时间
          min-evictable-idle-time-millis: 300000
          # 连接在池中最大生存的时间
          max-evictable-idle-time-millis: 900000
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          # 检测连接是否有效
          validation-query: select 1
          # 配置监控统计
          webStatFilter:
            enabled: true
          stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
            reset-enable: false
          filter:
            stat:
              enabled: true
              # 记录慢SQL
              log-slow-sql: true
              slow-sql-millis: 1000
              merge-sql: true
            wall:
              config:
                multi-statement-allow: true
      security:
        user:
          name: admin
          password: admin
    server:
      port: 9001
    
    
    CREATE TABLE `sys_user` (
      `user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
      `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
      `tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
      `user_name` varchar(30) NOT NULL COMMENT '用户账号',
      `nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
      `user_type` tinyint DEFAULT '0' COMMENT '用户类型(0管理员用户, 01系统用户 )',
      `email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
      `phone` varchar(11) DEFAULT '' COMMENT '手机号码',
      `sex` tinyint DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
      `avatar` varchar(100) DEFAULT '' COMMENT '头像地址',
      `password` varchar(100) DEFAULT '' COMMENT '密码',
      `status` tinyint DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
      `login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
      `login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
      `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
      `update_time` datetime DEFAULT NULL COMMENT '更新时间',
      `is_deleted` tinyint DEFAULT '0' COMMENT '删除标志',
      `remark` varchar(500) DEFAULT NULL COMMENT '备注',
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';
    
  4. 启动springBoot功能 访问 /user/1 接口

    可以看到引入了 Spring Security 在访问接口是已经把接口保护了起来
    在这里插入图片描述

  5. 输入我们在配置文件中配置的账户和密码
    点击登陆后能正常访问到接口并返回数据了
    在这里插入图片描述

流程分析


上面提到的快速入门操作就是 Spring Security 提供的 HttpBasic登录验证模式 ,他是最简单的登录认证模式
他的执行流程基本如下图所示

  1. 客户端发起 / user 请求
  2. 请求走了一遍过滤器链,在最后的 FilterSecurityInterceptor 过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出 AccessDeniedException 异常
  3. 抛出的 AccessDeniedException 异常在 ExceptionTranslationFilter 过滤器中被捕获,ExceptionTranslationFilter 过滤器通过调用 LoginUrlAuthenticationEntryPoint#commence 方法给客户端返回 302,要求客户端重定向到/login 页面
  4. 客户端发送/login 请求。
  5. )/login 请求被 DefaultLoginPageGeneratingFilter 过滤器拦截下来,并在该过滤器中返
    回登录页面。所以当用户访问/hello 接口时会首先看到登录页面。
    在这里插入图片描述

原理分析

因为 spring boot 集成 Spring Security,通过 spring boot 的自动装配,其实在启动项目时做了很多操作

  • 自动创建一个名为 springSecurityFilterChain 的过滤器,幵注入到 Spring 容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等(springSecurityFilterChain 实际上代理了 Spring Security中的过滤器链
  • 创建一个 UserDetailsService 实例,UserDetailsService 负责提供用户数据
  • 给用户生成一个默认的登录页面
  • 开启 CSRF 攻击防御。
  • 开启会话固定攻击防御。
  • 集成 X-XSS-Protection。
  • 集成 X-Frame-Options 以防止单击劫持。

Spring Security 整合 JWT

JWT 简介

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

Spring Security 整合 JWT 原理

利用 Spring Security 过滤器链,根据前端传递的的token信息
整理执行流程如下流程如下:

  1. 前端请求login接口上传用户名密码,
  2. 后台接收到请求后验证账号和密码生成 token,保存登录信息到redis
  3. 前台收到返回后保存token 并在其他请求请求头中设置token
  4. 后台根据请求头中的token检查用户是否登录并验证登录信息
    在这里插入图片描述

代码实现

配置类编写

### 增加 WebSecurityConfig 配置类	

```java

    private final ApplicationContext applicationContext;
    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
 
 @Bean
    public PasswordEncoder passwordEncoder() {
        // 密码加密方式
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭 csrf
                .csrf().disable()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 不通过session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 防止iframe 造成跨域
                .and()
                .headers()
                .frameOptions()
                .disable()
                .and()
                .authorizeRequests()
                // login 允许匿名访问
                .antMatchers("/auth/login").permitAll();
    }

增加 jwtAuthenticationTokenFilter

在 jwtAuthenticationTokenFilter 实现对请求的token进行解析和验证 ,jwtAuthenticationTokenFilter 应放在 UsernamePasswordAuthenticationFilter 过滤器之前,在验证账户名和密码前设置 UsernamePasswordAuthenticationToken

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if(StrUtil.isNotBlank(token)) {
            try{
                jwtTokenProvider.parseJwt(token);
            }catch (Exception e) {
                e.printStackTrace();
                log.debug("非法Token:{}", token);
            }
        }
        filterChain.doFilter(request,response);
    }
}
@Slf4j
@Component
public class JwtTokenProvider {

    @Resource
    private RedisUtil redisUtil;
    public static final String AUTHORITIES_KEY = "user";
    private JwtParser jwtParser;
    private JwtBuilder jwtBuilder;

    private static String  BASE64_SECRET = "ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=";

    /**
     * 初始化
     */
    @PostConstruct
    public void init() {
        Key key = generalKey();
        jwtParser = Jwts.parserBuilder()
                .setSigningKey(key)
                .build();
        jwtBuilder = Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS512);
    }

    /**
     * 由字符串生成加密key
     * @return
     */
    public static Key generalKey() {
        byte[] encodedKey = Base64.decodeBase64(BASE64_SECRET);
        Key key = Keys.hmacShaKeyFor(encodedKey);
        return key;
    }

    /**
     * 创建Token
     * @param userId
     * @return
     */
    public String createToken(String userId, JwtUser jwtUser) {
        String token = jwtBuilder
                // 加入ID确保生成的 Token 都不一致
                .setId(IdUtil.simpleUUID())
                .claim(AUTHORITIES_KEY, userId)
                .setSubject(userId)
                .compact();
        redisUtil.set("login:"+userId,jwtUser,1L, TimeUnit.HOURS);
        return token;
    }

    /**
     * 解析 jtw 保存
     * @param token
     */
    public void parseJwt(String token) {
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            // 去掉令牌前缀
            token = token.replace("Bearer ", "");
        } else {
            log.debug("非法Token:{}", token);
        }

        Claims claims = jwtParser.parseClaimsJws(token).getBody();
        String userId = claims.getSubject();
        JwtUser jwtUser = (JwtUser)redisUtil.get("login:" + userId);
        if(ObjectUtil.isEmpty(jwtUser)) {
            throw new RuntimeException("请求未认证");
        }
        Object authoritiesStr = claims.get(AUTHORITIES_KEY);
        Collection<? extends GrantedAuthority> authorities =
                ObjectUtil.isNotEmpty(authoritiesStr) ?
                        Arrays.stream(authoritiesStr.toString().split(","))
                                .map(SimpleGrantedAuthority::new)
                                .collect(Collectors.toList()) : Collections.emptyList();

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtUser,"",authorities);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}

登录流程实现

用户登录流程图
在这里插入图片描述

登录接口实现

 public Map<String, Object> login(LoginUser loginUser) {
        String username = loginUser.getUsername();
        String password = loginUser.getPassword();
        // 生成一个 AuthenticationToken
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,password);
        // 使用  authenticationManager 进行认证
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(token);
        JwtUser jwtUser = (JwtUser)authentication.getPrincipal();
        Long userId = jwtUser.getUser().getUserId();
        String jwtToken = jwtTokenProvider.createToken(String.valueOf(userId),jwtUser);

        Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
            put("token", "Brear " + jwtToken);
            put("user", userId);
        }};
        return authInfo;
    }

在调用 authenticationManagerBuilder.getObject().authenticate(token); 时, 会使用UserDetailsService加载用户信息进行验证,在这里需要增加自己的查询用户信息逻辑,并生成返回前端的token

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUserName(username);
        if (user == null) {
            throw new BadRequestException("账号不存在");
        }

        if(user.getStatus() == 1) {
            throw new BadRequestException("账号已停用");
        }
        List<GrantedAuthority> authorities = roleService.getGrantedAuthorities(user);
        Set<String> roleKeys = roleService.getRoleKeys(user);
        return new JwtUser(user,roleKeys,authorities);
    }

授权流程

  1. 根据上传的token解析,设置用户相关信息到 SecurityContext
  2. 利用aop 获取请求注解信息根据注解调用SecurityUtils进行验证

代码实现

创建相关注解
在这里插入图片描述
编写通知

@Aspect
@Component
public class AuthorizeAspect {

    /**
     * 配置切入点
     */
    @Pointcut("@annotation(com.zcct.security.demo.security.annotation.RequireRules) " +
            "|| @annotation(com.zcct.security.demo.security.annotation.RequiresLogin) " +
            "|| @annotation(com.zcct.security.demo.security.annotation.RequiresPermissions) " +
            "|| @annotation(com.zcct.security.demo.security.annotation.RequireAnonymous)")
    public void pointcut() {
        // 该方法无方法体,主要为了让同类中其他方法使用此切入点
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {


        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        authorize(method);

        Object obj = joinPoint.proceed();
        return obj;
    }

    private void authorize(Method method) {
        RequireRules requireRules = method.getAnnotation(RequireRules.class);
        if(ObjectUtil.isNotEmpty(requireRules)) {
            SecurityUtils.checkRules(requireRules.value());
        }
        RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
        if(ObjectUtil.isNotEmpty(requiresLogin)) {
            SecurityUtils.checkLogin();
        }
        RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
        if(ObjectUtil.isNotEmpty(requiresPermissions)) {
            SecurityUtils.checkPermissions(requiresPermissions.value());
        }
        RequireAnonymous requireAnonymous = method.getAnnotation(RequireAnonymous.class);
        if(ObjectUtil.isNotEmpty(requireAnonymous)) {
            SecurityUtils.checkAnonymous();
        }
    }
}

增加工具类

public class SecurityUtils {

    /**
     * 获取当前登录用户
     *   建议 保存在
     * @return
     */
    public static JwtUser getCurrentUser() {
        UserDetailsService userDetailsService = SpringUtil.getBean(UserDetailsService.class);
        return (JwtUser)userDetailsService.loadUserByUsername(getCurrentUsername());
    }

    /**
     * 获取当前登录用户名
     * @return
     */
    public static String getCurrentUsername() {

        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            throw new BadRequestException(HttpStatus.FORBIDDEN,"当前登录状态过期");
        }
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            return userDetails.getUsername();
        }
        throw new BadRequestException(HttpStatus.FORBIDDEN,"找不到当前登录的信息");
    }

    /**
     * 检查角色权限
     * @param rules
     */
    public static void checkRules(String[] rules) {
        Set<String> ruleKeys = SecurityUtils.getCurrentUser().getRuleKeys();
        boolean hasAuthority = ruleKeys.contains("admin") || Arrays.stream(rules).anyMatch(ruleKeys::contains);
        if(!hasAuthority) {
            throw new BadRequestException(HttpStatus.UNAUTHORIZED,"没有权限");
        }
    }

    /**
     * 检查是否登录
     */
    public static void checkLogin() {
        getCurrentUsername();
    }

    /**
     * 检查权限
     * @param permissions
     */
    public static void checkPermissions(String[] permissions) {
        // 获取所有权限
        List<String> allPermissions = SecurityUtils.getCurrentUser().getAuthorities().
                stream().map(GrantedAuthority::getAuthority).
                collect(Collectors.toList());
        boolean hasAuthority = allPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(allPermissions::contains);
        if(!hasAuthority) {
            throw new BadRequestException(HttpStatus.UNAUTHORIZED,"没有权限");
        }
    }

    public static void checkAnonymous() {
        UserDetails userDetails = null;
        try{
            userDetails = getCurrentUser();
        }catch (Exception e){

        }
        if(ObjectUtil.isNotEmpty(userDetails)) {
            throw new BadRequestException(HttpStatus.UNAUTHORIZED,"不允许访问");
        }
    }
}
  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 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
发出的红包

打赏作者

1999

每人一点点,明天会更好

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值