• SpringSecurity前后端分离


    SpringSecurity前后端分离(动态鉴权)

    一、认证流程讲解

    1、原始认证流程

    原始认证流程通常会配合Session一起使用,但前后端分离后就用不到Session了

    SpringSecurity默认的认证流程如下图(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)

    图片描述

    DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider抽象类,而AbstractUserDetailsAuthenticationProvider抽象类又实现了AuthenticationProvider这个接口。

    AuthenticationProvider接口和AuthenticationManager接口都有 authenticate() 这个方法

    认证流程:

    1、传入用户名和密码

    2、UsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象

    3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

    4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

    5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法

    6、loadUserByUsername()方法会返回UserDetails对象,认证成功逐一返回上一层

    2、前后端分离认证流程

    前后端分离后,我们要求在认证成功或者失败的时候能够返回对应的状态码,这时我们不再使用Session进行认证管理,而常采用jwt(JSON Web Token)的方式进行认证,这里引出两种前后端分离的写法

    图片描述

    (该图是B站UP主“三更草堂”讲SpringSecurity课程的图)

    无论使用下面哪一种写法,这里都需要在UsernamePasswordAuthenticationFilter前面添加一个过滤器,用于进行Token认证,如果Token认证成功,则表示该用户已登录;Token认证失败则表明未登录或者登陆已过期。

    2.1、继承UsernamePasswordAuthenticationFilter的写法

    图片描述

    认证流程:

    1、传入用户名和密码

    2、MyUsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象

    3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写的authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

    4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

    5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法

    6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象

    7、在继承WebSecurityConfigurerAdapter的类中设置登陆成功、失败处理器,处理器内部定义好返回的状态码等信息

    2.2、自定义写法

    图片描述

    UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口

    认证流程:

    1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层

    2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象

    3、然后调用AuthenticationManagerauthenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

    4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

    5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法

    6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象

    2.3、区别

    1、使用UsernamePasswordAuthenticationFilter的写法需要使用登陆成功、失败处理器,自定义的写法不需要,自定义的写法可以自定义失败处理器(包括认证异常和授权异常,即登陆失败和没有权限)

    2、使用UsernamePasswordAuthenticationFilter的写法对于扩展写法没那么友好,比如说添加手机验证码

    二、数据库的设计

    该示例是上面自定义的前后端分离的写法

    这里使用的是Oracle数据库,这里没有权限的表,但是使用角色来判断也差不多

    图片描述

    1、用户表

    图片描述

    2、用户角色关系表

    图片描述

    3、角色表

    图片描述

    4、图片表

    图片描述

    5、点赞表

    图片描述

    三、初始配置

    SpringBoot 版本是 2.6.0

    1、项目结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tfbztDyA-1659406016673)(https://dn-simplecloud.shiyanlou.com/courses/uid1534017-20220213-1644749790627)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cEOZZevH-1659406016674)(https://dn-simplecloud.shiyanlou.com/courses/uid1534017-20220213-1644749819365)]

    2、导入依赖

    
        
            org.springframework.boot
            spring-boot-starter-web
        
    
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.springframework.security
            spring-security-test
            test
        
    
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.1
        
    
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
    
        
        
            com.alibaba
            fastjson
            1.2.74
        
    
        
        
            cn.hutool
            hutool-all
            5.5.6
        
        
        
            com.baomidou
            mybatis-plus-generator
            3.4.1
        
        
        
            org.apache.commons
            commons-lang3
            3.7
        
        
        
            org.apache.velocity
            velocity-engine-core
            2.2
        
        
        
            io.springfox
            springfox-swagger-ui
            2.7.0
        
    
        
            io.springfox
            springfox-swagger2
            2.7.0
        
    
        
        
            io.jsonwebtoken
            jjwt
            0.9.0
        
    
        
        
            mysql
            mysql-connector-java
            runtime
        
    
        
        
            com.oracle.database.jdbc
            ojdbc8
            runtime
        
    
    
    
    • 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
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106

    3、代码生成器

    代码生成器这里最开始使用的是mysql 8.X版本的,读者需要自己修改一下数据库的名字,如果是mysql 5.X还需要修改一下驱动

    后面才改用Oracle数据库,这里的代码就懒得改了

    package com.guet.APPshareimage;
    
    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
    import com.baomidou.mybatisplus.generator.AutoGenerator;
    import com.baomidou.mybatisplus.generator.config.*;
    import com.baomidou.mybatisplus.generator.config.rules.DateType;
    import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
    import org.apache.commons.lang3.StringUtils;
    
    import java.util.Scanner;
    
    /**
     * @Author LZDWTL
     * @Date 2021-12-15 17:09
     * @ClassName CodeGenerator
     * @Description 代码生成器
     */
    public class CodeGenerator {
    
        /**
         * 

    * 读取控制台内容 *

    */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 创建代码生成器对象 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(scanner("请输入你的项目路径") + "/src/main/java"); //作者 gc.setAuthor("LZDWTL"); //生成之后是否打开资源管理器 gc.setOpen(false); //重新生成时是否覆盖文件 gc.setFileOverride(false); //%s 为占位符 //mp生成service层代码,默认接口名称第一个字母是有I gc.setServiceName("%sService"); //设置主键生成策略 自动增长 gc.setIdType(IdType.AUTO); //设置Date的类型 只使用 java.util.date 代替 gc.setDateType(DateType.ONLY_DATE); //开启实体属性 Swagger2 注解 gc.setSwagger2(true); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/shareimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("shareimage"); dsc.setPassword("888888"); //使用mysql数据库 dsc.setDbType(DbType.MYSQL); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); //pc.setModuleName(scanner("请输入模块名")); pc.setParent("com.guet.APPshareimage"); pc.setController("controller"); pc.setService("service"); pc.setServiceImpl("service.impl"); pc.setMapper("mapper"); pc.setEntity("entity"); pc.setXml("mapper"); mpg.setPackageInfo(pc); // 策略配置 StrategyConfig strategy = new StrategyConfig(); //设置哪些表需要自动生成 strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); //实体类名称驼峰命名 strategy.setNaming(NamingStrategy.underline_to_camel); //列名名称驼峰命名 strategy.setColumnNaming(NamingStrategy.underline_to_camel); //使用简化getter和setter strategy.setEntityLombokModel(true); //设置controller的api风格 使用RestController strategy.setRestControllerStyle(true); //驼峰转连字符 strategy.setControllerMappingHyphenStyle(true); //忽略表中生成实体类的前缀 //strategy.setTablePrefix("t_"); mpg.setStrategy(strategy); mpg.execute(); } }
    • 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
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109

    运行代码生成器,复制路径输入,然后依次输入数据库中表的名字

    D:WorkSpaceJavaWorkSpceidealAPP-shareimageAPP-shareimage
    
    t_user,t_picture,t_like,t_user_role,t_role
    
    • 1
    • 2
    • 3

    图片描述

    4、配置application.yml

    根据自己的数据库和redis进行配置

    server:
      port: 8080
    
    spring:
      # 数据库配置
      datasource:
        driver-class-name: oracle.jdbc.driver.OracleDriver
        url: jdbc:oracle:thin:@120.77.80.135:1521:orcl
        username: XXXXXX
        password: XXXXXX
        # 连接池
        hikari:
          # 连接池名
          pool-name: DateHikariCP
          # 最小空闲连接数
          minimum-idle: 5
          # 空闲连接最大存活时间,默认600000(10分钟)
          idle-timeout: 180000
          # 最大连接数,默认10
          maximum-pool-size: 10
          # 从连接池返回的连接自动提交
          auto-commit: true
          # 连接最大存活时间,1800000(30分钟)
          max-lifetime: 1800000
          # 连接超时时间,默认30000(30秒)
          connection-timeout: 30000
          # 测试连接是否可用的查询语句
          #connection-test-query: SELECT 1   #这个是mysql的测试语句
          connection-test-query: SELECT * from dual  #这个是oracle的测试语句
    
      #redis配置
      redis:
        #服务器地址
        host: 120.77.80.135
        #端口
        port: 6379
        #redis密码
        password: XXXXXX
        #数据库,默认是0
        database: 0
        #超时时间
        timeout: 1209600000ms
        lettuce:
          pool:
            #最大链接数,默认8
            max-active: 8
            #最大连接阻塞等待时间,默认-1
            max-wait: 10000ms
            #最大空闲连接,默认8
            max-idle: 200
            #最小空闲连接,默认0
            min-idle: 5
    
    mybatis-plus:
      mapper-locations: classpath:/mapper/*Mapper.xml
      type-aliases-package: com.guet.APPshareimage.entity
    
    logging:
      level:
        com.guet.shareimage.mapper: debug
    
    jwt:
      # JWT存储的请求头
      tokenHeader: Authorization
      # JWT 加解密使用的密钥
      secret: lzdwtl
      # JWT的超期限时间(1000*60*60*24*14)14天,即两周
      expiration: 1209600000
      # JWT 负载中拿到开头
      tokenHead: Bearer
    
    
    role:
      roleid: 1
    
    • 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

    5、其他配置、工具类

    5.1、SpringSecurity配置类

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private MyOncePerRequestFilter myOncePerRequestFilter;
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            //1、关闭csrf,关闭Session
            http
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            //2、设置不需要认证的URL
            http
                    .authorizeRequests()
                    //允许未登录的用户进行访问
                    .antMatchers("/doLogin").anonymous()
                    //其余url都要认证才能访问
                    .anyRequest().authenticated();
        }
    }
    
    • 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

    5.2、JSON格式返回配置类

    public abstract class JSONAuthentication {
        /**
         * 输出JSON
         *
         * @param request
         * @param response
         * @param obj
         * @throws IOException
         * @throws ServletException
         */
        protected void WriteJSON(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object obj) throws IOException, ServletException {
            //这里很重要,否则页面获取不到正常的JSON数据集
            response.setContentType("application/json;charset=UTF-8");
    
            //跨域设置
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Method", "POST,GET");
            //输出JSON
            PrintWriter out = response.getWriter();
            out.write(JSON.toJSONString(obj));
            out.flush();
            out.close();
        }
    }
    
    • 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

    5.3、密码编码类

    @Component
    public class BCryptPasswordEncoderUtil extends BCryptPasswordEncoder {
    
        @Override
        public String encode(CharSequence rawPassword) {
            return super.encode(rawPassword);
        }
    
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
    
            return super.matches(rawPassword,encodedPassword);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    5.4、JWT工具类

    @Component
    public class JwtUtil {
    
        private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
    
        private static String SECRET_KEY;
    
        private static Long EXPIRATION_TIME;
    
    
        //对于静态变量,需要使用set方法才能使用设置好的字段值
        @Value("${jwt.secret}")
        public void setSECRET_KEY(String SECRET_KEY) {
            this.SECRET_KEY = SECRET_KEY;
        }
    
        @Value("${jwt.expiration}")
        public void setEXPIRATION_TIME(Long expiration) {
            this.EXPIRATION_TIME = expiration;
        }
        
        public static String getUUID() {
            String token = UUID.randomUUID().toString().replaceAll("-", "");
            return token;
        }
    
        /**
         * 生成jtw
         *
         * @param subject token中要存放的数据(json格式)
         * @return
         */
        public static String createJWT(String subject) {
            JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
            return builder.compact();
        }
    
        /**
         * 生成jtw
         *
         * @param subject   token中要存放的数据(json格式)
         * @param ttlMillis token超时时间
         * @return
         */
        public static String createJWT(String subject, Long ttlMillis) {
            JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
            return builder.compact();
        }
    
        private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
            SecretKey secretKey = generalKey();
            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);
            if (ttlMillis == null) {
                ttlMillis = EXPIRATION_TIME;
            }
            long expMillis = nowMillis + ttlMillis;
            Date expDate = new Date(expMillis);
            return Jwts.builder()
                    .setId(uuid)              //唯一的ID
                    .setSubject(subject)   // 主题  可以是JSON数据
                    .setIssuer("LZDWTL")     // 签发者
                    .setIssuedAt(now)      // 签发时间
                    .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                    .setExpiration(expDate);
        }
    
        /**
         * 创建token
         *
         * @param id
         * @param subject
         * @param ttlMillis
         * @return
         */
        public static String createJWT(String id, String subject, Long ttlMillis) {
            JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
            return builder.compact();
        }
    
        /**
         * 生成加密后的秘钥 secretKey
         *
         * @return
         */
        public static SecretKey generalKey() {
            byte[] encodedKey = Base64.getDecoder().decode(SECRET_KEY);
            SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
            return key;
        }
    
        /**
         * 解析
         *
         * @param jwt
         * @return
         * @throws Exception
         */
        public static Claims parseJWT(String jwt) throws Exception {
            SecretKey secretKey = generalKey();
            return Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(jwt)
                    .getBody();
        }
    }
    
    • 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
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    5.5、Redis工具类

    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    @Component
    public class RedisCache
    {
        @Autowired
        public RedisTemplate redisTemplate;
    
        /**
         * 缓存基本的对象,Integer、String、实体类等
         *
         * @param key 缓存的键值
         * @param value 缓存的值
         */
        public  void setCacheObject(final String key, final T value)
        {
            redisTemplate.opsForValue().set(key, value);
        }
    
        /**
         * 缓存基本的对象,Integer、String、实体类等
         *
         * @param key 缓存的键值
         * @param value 缓存的值
         * @param timeout 时间
         * @param timeUnit 时间颗粒度
         */
        public  void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
        {
            redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
        }
    
        /**
         * 设置有效时间
         *
         * @param key Redis键
         * @param timeout 超时时间
         * @return true=设置成功;false=设置失败
         */
        public boolean expire(final String key, final long timeout)
        {
            return expire(key, timeout, TimeUnit.SECONDS);
        }
    
        /**
         * 设置有效时间
         *
         * @param key Redis键
         * @param timeout 超时时间
         * @param unit 时间单位
         * @return true=设置成功;false=设置失败
         */
        public boolean expire(final String key, final long timeout, final TimeUnit unit)
        {
            return redisTemplate.expire(key, timeout, unit);
        }
    
        /**
         * 获得缓存的基本对象。
         *
         * @param key 缓存键值
         * @return 缓存键值对应的数据
         */
        public  T getCacheObject(final String key)
        {
            ValueOperations operation = redisTemplate.opsForValue();
            return operation.get(key);
        }
    
        /**
         * 删除单个对象
         *
         * @param key
         */
        public boolean deleteObject(final String key)
        {
            return redisTemplate.delete(key);
        }
    
        /**
         * 删除集合对象
         *
         * @param collection 多个对象
         * @return
         */
        public long deleteObject(final Collection collection)
        {
            return redisTemplate.delete(collection);
        }
    
        /**
         * 缓存List数据
         *
         * @param key 缓存的键值
         * @param dataList 待缓存的List数据
         * @return 缓存的对象
         */
        public  long setCacheList(final String key, final List dataList)
        {
            Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
            return count == null ? 0 : count;
        }
    
        /**
         * 获得缓存的list对象
         *
         * @param key 缓存的键值
         * @return 缓存键值对应的数据
         */
        public  List getCacheList(final String key)
        {
            return redisTemplate.opsForList().range(key, 0, -1);
        }
    
        /**
         * 缓存Set
         *
         * @param key 缓存键值
         * @param dataSet 缓存的数据
         * @return 缓存数据的对象
         */
        public  BoundSetOperations setCacheSet(final String key, final Set dataSet)
        {
            BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
            Iterator it = dataSet.iterator();
            while (it.hasNext())
            {
                setOperation.add(it.next());
            }
            return setOperation;
        }
    
        /**
         * 获得缓存的set
         *
         * @param key
         * @return
         */
        public  Set getCacheSet(final String key)
        {
            return redisTemplate.opsForSet().members(key);
        }
    
        /**
         * 缓存Map
         *
         * @param key
         * @param dataMap
         */
        public  void setCacheMap(final String key, final Map dataMap)
        {
            if (dataMap != null) {
                redisTemplate.opsForHash().putAll(key, dataMap);
            }
        }
    
        /**
         * 获得缓存的Map
         *
         * @param key
         * @return
         */
        public  Map getCacheMap(final String key)
        {
            return redisTemplate.opsForHash().entries(key);
        }
    
        /**
         * 往Hash中存入数据
         *
         * @param key Redis键
         * @param hKey Hash键
         * @param value 值
         */
        public  void setCacheMapValue(final String key, final String hKey, final T value)
        {
            redisTemplate.opsForHash().put(key, hKey, value);
        }
    
        /**
         * 获取Hash中的数据
         *
         * @param key Redis键
         * @param hKey Hash键
         * @return Hash中的对象
         */
        public  T getCacheMapValue(final String key, final String hKey)
        {
            HashOperations opsForHash = redisTemplate.opsForHash();
            return opsForHash.get(key, hKey);
        }
    
        /**
         * 删除Hash中的数据
         *
         * @param key
         * @param hkey
         */
        public void delCacheMapValue(final String key, final String hkey)
        {
            HashOperations hashOperations = redisTemplate.opsForHash();
            hashOperations.delete(key, hkey);
        }
    
        /**
         * 获取多个Hash中的数据
         *
         * @param key Redis键
         * @param hKeys Hash键集合
         * @return Hash对象集合
         */
        public  List getMultiCacheMapValue(final String key, final Collection hKeys)
        {
            return redisTemplate.opsForHash().multiGet(key, hKeys);
        }
    
        /**
         * 获得缓存的基本对象列表
         *
         * @param pattern 字符串前缀
         * @return 对象列表
         */
        public Collection keys(final String pattern)
        {
            return redisTemplate.keys(pattern);
        }
    }
    
    • 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
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226

    5.6、Redis配置类

    package com.guet.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * @Author LZDWTL
     * @Date 2022-01-30 19:39
     * @ClassName
     * @Description
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        @SuppressWarnings(value = { "unchecked", "rawtypes" })
        public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
        {
            RedisTemplate template = new RedisTemplate<>();
            template.setConnectionFactory(connectionFactory);
    
            FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
    
            // 使用StringRedisSerializer来序列化和反序列化redis的key值
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(serializer);
    
            // Hash的key也采用StringRedisSerializer的序列化方式
            template.setHashKeySerializer(new StringRedisSerializer());
            template.setHashValueSerializer(serializer);
    
            template.afterPropertiesSet();
            return template;
        }
    }
    
    • 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

    5.7、序列化工具

    public class FastJsonRedisSerializer implements RedisSerializer
    {
    
        public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    
        private Class clazz;
    
        static
        {
            ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        }
    
        public FastJsonRedisSerializer(Class clazz)
        {
            super();
            this.clazz = clazz;
        }
    
        @Override
        public byte[] serialize(T t) throws SerializationException
        {
            if (t == null)
            {
                return new byte[0];
            }
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }
    
        @Override
        public T deserialize(byte[] bytes) throws SerializationException
        {
            if (bytes == null || bytes.length <= 0)
            {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);
    
            return JSON.parseObject(str, clazz);
        }
    
    
        protected JavaType getJavaType(Class clazz)
        {
            return TypeFactory.defaultInstance().constructType(clazz);
        }
    }
    
    • 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

    四、全局异常处理

    1、公用返回对象

    1.1、拓展接口

    使公用返回对象枚举类和自定义异常方便扩展

    /**
     * @Author LZDWTL
     * @Date 2021-12-06 15:59
     * @ClassName CommonResp
     * @Description 返回对象的接口,装饰者模式
     */
    public interface CommonResp {
        Integer getCode();
    
        String getMsg();
    
        CommonResp setMsg(String msg);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.2、公用返回对象

    @Data
    public class RespBean implements Serializable {
        private static final long serialVersionUID = 1L;
        private Integer code;
        private String msg;
        private Object obj;
    
        public RespBean(RespBeanEnum respBeanEnum, Object obj) {
            this.code = respBeanEnum.getCode();
            this.msg = respBeanEnum.getMsg();
            this.obj = obj;
        }
    
        public RespBean(RespBeanEnum respBeanEnum) {
            this.code = respBeanEnum.getCode();
            this.msg = respBeanEnum.getMsg();
        }
    
        public RespBean(RespBeanEnum respBeanEnum, String msg) {
            this.code = respBeanEnum.getCode();
            this.msg = msg;
        }
    
        public RespBean() {
            this.code = RespBeanEnum.ERROR.getCode();
            this.msg = RespBeanEnum.ERROR.getMsg();
        }
    
        public RespBean(String msg) {
            this.code = RespBeanEnum.ERROR.getCode();
            this.msg = msg;
        }
    
        //自定义的业务异常错误码和信息
        public RespBean(ServicesException e) {
            this.code = e.getCode();
            this.msg = e.getMsg();
        }
    
    }
    
    • 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

    1.3、枚举类

    public enum RespBeanEnum implements CommonResp{
    
        SUCCESS(200,"请求成功!"),
        ERROR(500,"服务器响应错误!"),
    
        /** 10XX 表示用户错误*/
        USER_REGISTER_FAILED(1001, "注册失败"),
        USER_ACCOUNT_EXISTED(1002,"用户名已存在"),
        USER_ACCOUNT_NOT_EXIST(1003,"用户名不存在"),
        USERNAME_PASSWORD_ERROR(1004,"用户名或密码错误"),
        PASSWORD_ERROR(1005,"密码错误"),
        USER_ACCOUNT_EXPIRED(1006,"账号过期"),
        USER_PASSWORD_EXPIRED(1007,"密码过期"),
        USER_ACCOUNT_DISABLE(1008,"账号不可用"),
        USER_ACCOUNT_LOCKED(1009,"账号锁定"),
        USER_NOT_LOGIN(1010,"用户未登陆"),
        USER_NO_PERMISSIONS(1011,"用户权限不足"),
        USER_SESSION_INVALID(1012,"会话已超时"),
        USER_ACCOUNT_LOGIN_IN_OTHER_PLACE(1013,"账号超时或账号在另一个地方登陆"),
        TOKEN_VALIDATE_FAILED(1014,"Token令牌验证失败"),
        LIKE_ALREADY_GICED(1015,"请勿重复点赞"),
    
    
    
        /** 20XX 表示服务器错误 */
        PICTURE_UPLOAD_FAILED(2001,"上传图片失败"),
        GIVE_LIKE_FAILED(2002,"点赞失败"),
        PICTURE_LOAD_FAILED(2003,"图片加载失败"),
        UPDATE_USER_INFO_FAILED(2004,"修改用户信息失败"),
        UPDATE_USER_PASSWORD_FAILED(2005,"修改密码失败"),
        ;
    
        private Integer code;
        private String msg;
    
        RespBeanEnum(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        @Override
        public Integer getCode() {
            return this.code;
        }
    
        @Override
        public String getMsg() {
            return this.msg;
        }
    
        @Override
        public CommonResp setMsg(String msg) {
            this.msg=msg;
            return this;
        }
    }
    
    • 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

    2、全局异常

    2.1、自定义异常

    实现CommonResp接口,方便自定义异常后续修改错误信息

    public class ServicesException extends RuntimeException implements CommonResp {
    
        private CommonResp commonResp;
    
        //直接接收RespBeanEnum的传参用于构造业务异常
        public ServicesException(CommonResp commonResp) {
            super();    //调用父类的无参构造方法
            this.commonResp = commonResp;
        }
    
        //接收自定义msg的方式构造业务异常
        public ServicesException(String msg, CommonResp commonResp) {
            super();
            this.commonResp = commonResp;
            this.commonResp.setMsg(msg);
        }
    
    
        @Override
        public Integer getCode() {
            return this.commonResp.getCode();
        }
    
        @Override
        public String getMsg() {
            return this.commonResp.getMsg();
        }
    
        @Override
        public CommonResp setMsg(String msg) {
            this.commonResp.setMsg(msg);
            return this;
        }
    }
    
    • 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

    2.2、全局异常处理器

    @RestControllerAdvice注解表示捕获控制层抛出的异常

    @ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常

    在这里插入图片描述

    (图片来源:https://blog.csdn.net/weixin_43702146/article/details/118606502)

    因为使用了@RestControllerAdvice注解,自动去捕获控制层抛出的异常,AuthenticationException异常和AccessDeniedException异常也被捕获了,但是我不想在这里处理,所以将这两个异常往外抛给失败处理器去处理。

    @RestControllerAdvice  //捕获controller层的异常
    public class GlobalExceptionHandler {
        private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
        /**
         * @Author: LZDWTL
         * @param: [e]
         * @return: com.guet.shareimage.response.RespBean
         * @Description: 业务异常
         */
        @ExceptionHandler(value = ServicesException.class)
        public RespBean servicesExceptionHandler(ServicesException e){
            logger.error("发生业务异常! 原因是:{}",e.getMsg());
            return new RespBean(e);
        }
    
    
        /**
         * @Author: LZDWTL
         * @param: [e]
         * @return: com.guet.shareimage.response.RespBean
         * @Description: 其他异常
         */
        @ExceptionHandler(value = Exception.class)
        public RespBean exceptionHandler(Exception e){
            logger.error("未知异常! 原因是:",e);
            return new RespBean();
        }
    
    
        /**
         * @Author: LZDWTL
         * @Date: 2022/2/11
         * @param: [authException]
         * @return: void
         * @Description: 将 AuthenticationException 异常往上抛,让认证处理器去处理
         */
        @ExceptionHandler(value = AuthenticationException.class)
        public void accountExpiredExceptionHandler(AuthenticationException authException){
            throw authException;
        }
    
        //将 AccessDeniedException 异常往上抛,让授权处理器去处理
        @ExceptionHandler(value = AccessDeniedException.class)
        public void accessDeniedExceptionHandler(AccessDeniedException accDenException){
            throw accDenException;
        }
    }
    
    • 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

    五、登陆认证

    图片描述

    UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口

    认证流程:

    1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层

    2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象

    3、然后调用AuthenticationManagerauthenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

    4、AbstractUserDetailsAuthenticationProviderauthenticate()方法中调用了抽象方法retrieveUser()方法

    5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法

    6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象

    1、登陆模块

    包括登陆和登出功能

    1.1、控制器LoginController

    /**
     * @Author LZDWTL
     * @Date 2021-12-17 8:57
     * @ClassName LoginController
     * @Description 登陆控制器
     * 这个控制器没有用到,“/login”这个url是SpringSecurity中的UsernamePasswordAuthenticationFilter拦截器中自己设定的
     * 同时它还设置了必须使用POST方式才能进行登陆
     */
    @RestController
    public class LoginController {
    
        @Autowired
        private LoginService loginService;
    
        @PostMapping("/doLogin")
        public RespBean doLogin(@RequestBody LoginDTO loginDTO){
            return loginService.doLogin(loginDTO);
        }
    
        @RequestMapping("/doLogout")
        public RespBean doLogout(){
            return loginService.doLogout();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    1.2、业务层

    Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象

    1.2.1、LoginService
    public interface LoginService {
        RespBean doLogin(LoginDTO loginDTO);
        RespBean doLogout();
    }
    
    • 1
    • 2
    • 3
    • 4
    1.2.2、LoginServiceImpl

    这里的 AuthenticationManager 需要在 SpringSecurity 中使用 authenticationManagerBean() 方法才能调用AuthenticationManagerauthenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法

    这里把生成的Token和查询到的用户信息存到Redis中,方便后续使用

    @Service
    public class LoginServiceImpl implements LoginService {
    
        private static final Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class);
    
        @Value("${jwt.tokenHead}")
        private String tokenHead;
    
        @Autowired
        private TUserService userService;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private BCryptPasswordEncoderUtil passwordEncoder;
    
        @Autowired
        private RedisCache redisCache;
    
    
        /**
         * @Author: LZDWTL
         * @param: [username, password]
         * @return: com.guet.APPshareimage.response.RespBean
         * @Description: 登陆
         */
        @Override
        public RespBean doLogin(LoginDTO loginDTO) {
    
            /**
             * 因为我使用了全局异常处理,GobalExceptionHandler会自动捕获controller层抛出的异常
             * authenticationManager.authenticate 这一句认证失败会抛出AuthenticationException异常
             * 我定义了认证失败处理器无法获取到 AuthenticationException 异常,因为全局异常处理已经捕获了
             * 然后 AuthenticationException 异常不属于 ServicesException,所以会返回500,服务器响应错误
             */
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
    
            if (Objects.isNull(authenticate)) {
                //用户名密码错误
                throw new ServicesException(RespBeanEnum.USERNAME_PASSWORD_ERROR);
            }
            AuthUser authUser = (AuthUser) authenticate.getPrincipal();
            String username = authUser.getTUser().getUsername();
            String token = JwtUtil.createJWT(username);
    
            //把token和用户信息存到redis中
            redisCache.setCacheObject("Token_" + username, token);
            redisCache.setCacheObject("UserDetails_" + username, authUser);
    
            //将用户存入上下文中
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    
            Map map = new HashMap<>();
            map.put("token", token);
            return new RespBean(RespBeanEnum.SUCCESS, map);
        }
    
        @Override
        public RespBean doLogout() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            AuthUser authUser = (AuthUser) authentication.getPrincipal();
            String username = authUser.getTUser().getUsername();
    
            //删除redis中存的信息
            redisCache.deleteObject("Token_" + username);
            redisCache.deleteObject("UserDetails_" + username);
            //清除上下文
            SecurityContextHolder.clearContext();
    
            return new RespBean(RespBeanEnum.SUCCESS);
        }
    }
    
    • 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

    图片描述

    1.2.3、TUserService
    public interface TUserService extends IService {
    
        TUser getUserByUserName(String username);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1.2.4、TUserServiceImpl

    TUserMapper需要继承BaseMapper才能使用selectOne()这个方法

    @Service
    public class TUserServiceImpl extends ServiceImpl implements TUserService {
    
        @Value("${role.roleid}")
        private Integer roleId;
    
        @Autowired
        private TUserMapper userMapper;
    
    
        /**
         * @Author: LZDWTL
         * @Date: 2021/12/28
         * @param: [username]
         * @return: com.guet.response.RespBean
         * @Description: 通过用户名获取用户
         */
        @Override
        public TUser getUserByUserName(String username) {
            LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
            //查询条件:全匹配账号名,和状态为1的账号
            lambdaQueryWrapper
                    .eq(TUser::getUsername, username);
    
            //用getOne查询一个对象出来
    //        TUser user = this.getOne(lambdaQueryWrapper);
    
            TUser user = userMapper.selectOne(lambdaQueryWrapper);   //这个与上面的getOne有无区别?
    
            return user;
        }
    }
    
    • 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

    1.3、实现 UserDetails 接口

    /**
     * @Author LZDWTL
     * @Date 2021-12-15 23:35
     * @ClassName AuthUser
     * @Description 实现UserDetails,仿写User的原因是 防止User类名和自己创建的实体类 User 重合(虽然我这里创建的不是User而是TUser)
     */
    @Data
    @AllArgsConstructor  //全参构造
    @NoArgsConstructor  //无参构造
    public class AuthUser implements UserDetails {
    
        private TUser tUser;
    
    //    @JSONField(serialize = false)
        private Collection authorities;
    
        @Override
        public Collection getAuthorities() {
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return tUser.getPassword();
        }
    
        @Override
        public String getUsername() {
            return tUser.getUsername();
        }
    
        // 账户是否未过期
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        // 账户是否未被锁
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    • 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

    1.4、实现UserDetailsService 接口

    重写UserDetailsService接口的 loadUserByUsername()方法,在loadUserByUsername()方法中,会查询用户和权限(这里没有权限表,所以查的是角色),然后返回UserDetails对象

    /**
     * 要实现UserDetailsService接口,这个接口是security提供的
     */
    @Service(value = "userDetailsService")
    public class AuthUserDetailsServiceImpl implements UserDetailsService {
    
        private static final Logger logger = LoggerFactory.getLogger(AuthUserDetailsServiceImpl.class);
    
        @Autowired
        private TUserService userService;
    
        @Autowired
        private TRoleService roleService;
    
        /**
         * 通过账号查找用户、角色的信息
         *
         * @param username
         * @return
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            TUser user = userService.getUserByUserName(username);
            if (user == null) {
                //用户名不存在
                throw new ServicesException(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
            } else {
                //查找角色,实际应该查询权限,但我数据库没有设计所以就查角色就好了
                List roles = roleService.getRolesByUserName(username);
                List authorities = new ArrayList<>();
                for (String role : roles) {
                    authorities.add(new SimpleGrantedAuthority(role));
                }
                System.out.println("AuthUserDetailsServiceImpl-loadUserByUsername......user ===> " + user);
                return new AuthUser(user, authorities);
            }
        }
    }
    
    • 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

    1.5、Mapper

    1.5.1、TUserMapper
    @Mapper
    public interface TUserMapper extends BaseMapper {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    2、Token 认证模块

    2.1、认证过滤器

    /**
     * @Author LZDWTL
     * @Date 2021-12-20 16:28
     * @ClassName ${MyOncePerRequestFilter}
     * @Description ${认证过滤器}
     */
    @Component
    public class MyOncePerRequestFilter extends OncePerRequestFilter {
    
        private static final Logger logger = LoggerFactory.getLogger(MyOncePerRequestFilter.class);
    
        @Value("${jwt.tokenHeader}")
        private String header;
    
        @Autowired
        private RedisCache redisCache;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain chain) throws ServletException, IOException {
    
            // header的值是在yml文件中定义的 “Authorization”
            String token = request.getHeader(header);
            System.out.println("MyOncePerRequestFilter-token = " + token);
            if (!StrUtil.isEmpty(token)) {
                String username = null;
                try {
                    Claims claims = JwtUtil.parseJWT(token);
                    username = claims.getSubject();
                } catch (Exception e) {
                    e.printStackTrace();
    //                throw new ServicesException("非法Token,请重新登陆", RespBeanEnum.ERROR);
                    WriteJSON(request,response,new RespBean(RespBeanEnum.ERROR,"非法Token,请重新登陆"));
                    return;
                }
                String redisToken = redisCache.getCacheObject("Token_" + username);
                System.out.println("MyOncePerRequestFilter-redisToken = " + redisToken);
                if (StrUtil.isEmpty(redisToken)) {
                    //token令牌验证失败
    //                throw new ServicesException(RespBeanEnum.TOKEN_VALIDATE_FAILED);
    
                    //输出JSON
                    WriteJSON(request,response,new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED));
                    return;
                }
    
                //对比前端发送请求携带的的token是否与redis中存储的一致
                if (!Objects.isNull(redisToken) && redisToken.equals(token)) {
                    AuthUser authUser = redisCache.getCacheObject("UserDetails_" + username);
                    System.out.println("MyOncePerRequestFilter-authUser = " + authUser);
                    if (Objects.isNull(authUser)) {
    //                    throw new ServicesException(RespBeanEnum.USER_NOT_LOGIN);
                        WriteJSON(request,response,new RespBean(RespBeanEnum.USER_NOT_LOGIN));
                        return;
                    }
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
            chain.doFilter(request, response);
        }
        private void WriteJSON(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object obj) throws IOException, ServletException {
            //这里很重要,否则页面获取不到正常的JSON数据集
            response.setContentType("application/json;charset=UTF-8");
    
            //跨域设置
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Method", "POST,GET");
            //输出JSON
            PrintWriter out = response.getWriter();
            out.write(JSON.toJSONString(obj));
            out.flush();
            out.close();
        }
    }
    
    • 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

    2.2、SpringSecuity配置类

    在配置类中使用addFilterBefore()方法让认证过滤器MyOncePerRequestFilter添加在UsernamePasswordAuthenticationFilter这个过滤器前面

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private MyOncePerRequestFilter myOncePerRequestFilter;
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            //1、关闭csrf,关闭Session
            http
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            //2、设置不需要认证的URL
            http
                    .authorizeRequests()
                    //允许未登录的用户进行访问
                    .antMatchers("/doLogin").anonymous()
                    //其余url都要认证才能访问
                    .anyRequest().authenticated();
    
            //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
            http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
    
        }
    }
    
    • 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

    图片描述

    六、鉴权

    下面列举了两种鉴权方式,分别是注解鉴权和动态鉴权

    1、注解鉴权

    使用 @PreAuthorize注解需要在SpringSecurity配置类中使用下面的语句才能开启方法级的安全

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    
    
    @RestController
    @RequestMapping("/user")
    public class TUserController {
    
        @RequestMapping("/hello")
        //对于hasRole这个方法来讲,ROLE_ 加不加都可以,它的方法会自动判断的
        @PreAuthorize("hasRole('ROLE_user')")
        public String test() {
            return "Hello Login Success!";
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这样就可以了,因为前面已经写好了一些关联的代码,所以在访问该URL的时候,会执行hasRole()这个方法,然后查询AuthUser类(AuthUser类就是实现了UserDetails接口的实现类)中的属性authorities,只要authorities中包含"ROLE_user",则该用户就可以访问这个URL,否则会报错,提示权限不足。

    注意访问一些需要认证后才能访问的URL时,记得带上token和content-type。

    我这里的token的key是Authorization,这个是在application.yml文件中定义的,可以自行修改

    图片描述

    2、动态鉴权

    这里写的动态鉴权需要数据库中新创建两个表,分别是菜单表t_menu和角色菜单关系表t_role_menu,菜单表中存放前端需要访问的url地址
    下面编写鉴权类

    @Component("rbacService")
    public class MyRBACService {
        public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
            Object principal = authentication.getPrincipal();
            if (principal instanceof UserDetails) {
                UserDetails userDetails=(UserDetails)principal;
    
                /**
                 * 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true
                 */
                //本次要访问的资源
                SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(request.getRequestURI());
    
                //用户拥有的权限中是否包含请求的url
                return userDetails.getAuthorities().contains(simpleGrantedAuthority);
            }
    
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在SpringSecurity配置类中设置鉴权规则

    @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            //1、关闭csrf,关闭Session
            http
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            //2、设置不需要认证的URL
            http
                    .authorizeRequests()
                    //允许未登录的用户进行访问
                    .antMatchers("/user/doLogin").permitAll()
    //                .antMatchers("/swagger-ui.html","/user/test").permitAll()
                    //其余url都要认证才能访问
    //                .anyRequest().authenticated()
                    //鉴权规则
                    .anyRequest().access("@rbacService.hasPermission(request,authentication)");
    
            //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
            http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
    
    
            //4、异常处理
            http
                    .exceptionHandling()
                    //认证失败处理器
                    .authenticationEntryPoint(myAuthenticationEntryPoint)
                    //权限不足处理器
                    .accessDeniedHandler(myAccessDeniedHandler);
    
            //5、允许跨域
            http.cors();
        }
    
    • 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

    七、自定义失败处理器

    1、认证失败处理器

    继承自定义的JSON格式输出类JSONAuthentication输出JSON格式,同时在里面判断是什么异常做针对性输出

    @Component
    public class MyAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint {
    
        private static final Logger logger = LoggerFactory.getLogger(MyAuthenticationEntryPoint.class);
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
            //用户未登录或者身份校验失败
    //        RespBean respBean = new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED);
    //        this.WriteJSON(request, response, respBean);
    
            RespBean respBean;
            if (authException instanceof AccountExpiredException) {
                //账号过期
                respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_EXPIRED);
            } else if (authException instanceof InternalAuthenticationServiceException) {
                //用户不存在
                respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
            } else if (authException instanceof BadCredentialsException) {
                //用户名或密码错误(也就是用户名匹配不上密码)
                respBean = new RespBean(RespBeanEnum.USERNAME_PASSWORD_ERROR);
            } else if (authException instanceof CredentialsExpiredException) {
                //密码过期
                respBean = new RespBean(RespBeanEnum.USER_PASSWORD_EXPIRED);
            } else if (authException instanceof DisabledException) {
                //账号不可用
                respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_DISABLE);
            } else if (authException instanceof LockedException) {
                //账号锁定
                respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_LOCKED);
            } else {
                //其他错误
                respBean = new RespBean(RespBeanEnum.USER_NOT_LOGIN);
            }
    
            //打印错误
            logger.error(String.valueOf(authException));
    
            //输出
            this.WriteJSON(request, response, respBean);
        }
    }
    
    • 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

    2、权限不足处理器

    @Component
    public class MyAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
            //用户权限不足
            RespBean respBean = new RespBean(RespBeanEnum.USER_NO_PERMISSIONS);
            //输出
            this.WriteJSON(request, response, respBean);
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3、SpringSecurity配置

    configure方法中配置失败处理器

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private MyOncePerRequestFilter myOncePerRequestFilter;
    
        @Autowired
        private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    
        @Autowired
        private MyAccessDeniedHandler myAccessDeniedHandler;
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            //1、关闭csrf,关闭Session
            http
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            //2、设置不需要认证的URL
            http
                    .authorizeRequests()
                    //允许未登录的用户进行访问
                    .antMatchers("/doLogin").anonymous()
                    //其余url都要认证才能访问
                    .anyRequest().authenticated();
    
            //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
            http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
    
    
            //4、异常处理
            http
                    .exceptionHandling()
                    //认证失败处理器
                    .authenticationEntryPoint(myAuthenticationEntryPoint)
                    //权限不足处理器
                    .accessDeniedHandler(myAccessDeniedHandler);
    
        }
    }
    
    • 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

    八、跨域

    1、编写配置类

    /**
     * 解决跨域问题
     */
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    //允许任何域名
                    .allowedOriginPatterns("*")
                    //允许任何方法
                    .allowedMethods("PUT", "DELETE", "GET", "POST", "OPTIONS")
                    //允许任何头
                    .allowedHeaders("*")
                    //暴露头
                    .exposedHeaders("access-control-allow-headers",
                            "access-control-allow-methods",
                            "access-control-allow-origin",
                            "access-control-max-age",
                            "X-Frame-Options")
                    // 是否允许证书(cookies)
                    .allowCredentials(true)
                    .maxAge(3600);
        }
    }
    
    • 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

    2、在SpringSecurity配置类中配置

    在配置类的configure()方法中开启允许跨域

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private MyOncePerRequestFilter myOncePerRequestFilter;
    
        @Autowired
        private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    
        @Autowired
        private MyAccessDeniedHandler myAccessDeniedHandler;
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            //1、关闭csrf,关闭Session
            http
                    .csrf().disable()
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    
            //2、设置不需要认证的URL
            http
                    .authorizeRequests()
                    //允许未登录的用户进行访问
                    .antMatchers("/doLogin").anonymous()
                    //其余url都要认证才能访问
                    .anyRequest().authenticated();
    
            //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
            http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
    
    
            //4、异常处理
            http
                    .exceptionHandling()
                    //认证失败处理器
                    .authenticationEntryPoint(myAuthenticationEntryPoint)
                    //权限不足处理器
                    .accessDeniedHandler(myAccessDeniedHandler);
    
            //5、允许跨域
            http.cors();
        }
    }
    
    • 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

    }

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    Echarts实现半圆形饼图,Echarts实现扇形图
    go语言学习-数据结构
    BrokerChain——基于“做市商账户”的区块链跨分片协议
    C/C++之(一)洛谷刷题及洛谷评测
    【数据库】详细讲解三大范式1NF、2NF、3NF
    Chiplet技术与汽车芯片(一)
    CMSC5707-高级人工智能之音频信号预处理操作
    计算 tensorflow 和 pytorch 模型的浮点运算数
    entos7主网卡ip不通
    【白话科普】从“熊猫烧香”聊聊计算机病毒
  • 原文地址:https://blog.csdn.net/m0_67393039/article/details/126116542