• 【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>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
    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));
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    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
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      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='用户信息表';
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
    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();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    增加 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    @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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    登录流程实现

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

    登录接口实现

     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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在调用 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    授权流程

    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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    增加工具类

    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,"不允许访问");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
  • 相关阅读:
    PIE-engine 教程 ——矢量集合的循环计算使用for循环(中国各省市面积统计)
    go编译问题收集
    GitHub上标星120k的Java进阶面试教程等!(建议收藏)
    在Ubuntu系统 搭建企业GIT服务器
    [leetcode] 264. 丑数 II
    JAVA实现PDF转图片
    港澳通行证识别易语言代码
    【java_wxid项目】【第一章】【Spring Boot快速构建应用】
    CSAPP实验记录(1)--------- DataLab
    php错误/异常/日志及常见调试和排错
  • 原文地址:https://blog.csdn.net/qq_44808472/article/details/126611112