zoukankan      html  css  js  c++  java
  • spring boot:spring security给用户登录增加自动登录及图形验证码功能(spring boot 2.3.1)

    一,图形验证码的用途?

    1,什么是图形验证码?

    验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,
    它是用来区分用户是人类还是计算机的公共全自动程序

    它可以防止对url的恶意刷量/频繁攻击/破解密码等

    2,如果有短信验证码,还需要图形验证码吗?

    当然需要,很多发送短信验证码的url就是因为没有图形验证码才遭受到攻击    

    3,我们在这里使用了kaptcha这个图形验证码库,

        官方代码站:    

    https://github.com/penggle/kaptcha

    说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

             对应的源码可以访问这里获取: https://github.com/liuhongdi/

    说明:作者:刘宏缔 邮箱: 371125307@qq.com

    二,演示项目的相关信息

    1,项目地址 

    https://github.com/liuhongdi/securityloginadv

    2,项目功能说明:

             基于数据库实现登录和权限管理,

             记住登录(自动登录)

             用kaptcha实现图形验证码

    3,项目结构:如图:

    三,配置文件说明

    1,pom.xml

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--kaptcha begin-->
            <dependency>
                <groupId>com.github.penggle</groupId>
                <artifactId>kaptcha</artifactId>
                <version>2.3.2</version>
            </dependency>
            <!--security begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <!--thymeleaf begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
            <!--validation begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
            <!--mysql mybatis begin-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.3</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <!-- JSON解析fastjson begin-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.72</version>
            </dependency>

    2,application.properties

    #thymeleaf
    spring.thymeleaf.cache=false
    spring.thymeleaf.encoding=UTF-8
    spring.thymeleaf.mode=HTML
    spring.thymeleaf.prefix=classpath:/templates/
    spring.thymeleaf.suffix=.html
    #mysql
    spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=lhddemo
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    #mybatis
    mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
    mybatis.type-aliases-package=com.example.demo.mapper
    #error
    server.error.include-stacktrace=always
    #log
    logging.level.org.springframework.web=trace
    #session
    server.servlet.session.timeout=120

    3,数据库

    表结构:

    CREATE TABLE `sys_user` (
     `userId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
     `userName` varchar(100) NOT NULL DEFAULT '' COMMENT '用户名',
     `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码',
     `nickName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称',
     PRIMARY KEY (`userId`),
     UNIQUE KEY `userName` (`userName`)
    ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表'

    添加数据 :

    INSERT INTO `sys_user` (`userId`, `userName`, `password`, `nickName`) VALUES
    (1, 'lhd', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '老刘'),
    (2, 'admin', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '管理员'),
    (3, 'merchant', '$2a$10$yGcOz3ekNI6Ya67tqQueS.raxyTOedGsv5jh2BwtRrI5/K9QEIPGq', '商户老张');

    说明:3个密码都是111111,仅供演示使用,大家在生产环境中一定不要这样设置

    CREATE TABLE `sys_user_role` (
     `urId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
     `userId` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
     `roleName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色id',
     PRIMARY KEY (`urId`),
     UNIQUE KEY `userId` (`userId`,`roleName`)
    ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表'

    插入数据:

    INSERT INTO `sys_user_role` (`urId`, `userId`, `roleName`) VALUES
    (1, 2, 'ADMIN'),
    (2, 3, 'MERCHANT');

     用来保存记住登录信息的persistent_logins数据表:

    CREATE TABLE `persistent_logins` (
     `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
     `series` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
     `token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
     `last_used` timestamp NOT NULL,
     PRIMARY KEY (`series`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

    四,java代码说明:

    1,WebSecurityConfig.java

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        private final static BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder();
        private static final String SECRET = "lhd@2020";
        @Resource
        private UserLoginFailureHandler userLoginFailureHandler;//验证失败的处理类
        @Resource
        private UserLoginSuccessHandler userLoginSuccessHandler;//验证成功的处理类
        @Resource
        private UserLogoutSuccessHandler userLogoutSuccessHandler;
        @Resource
        private UserAccessDeniedHandler userAccessDeniedHandler;
        @Resource
        private SecUserDetailService secUserDetailService;
        //rememberme
        @Resource
        private DataSource dataSource;
    
        //rememberme repository
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            // 设置数据源
            tokenRepository.setDataSource(dataSource);
            return tokenRepository;
        }
    
        //指定加密的方式,避免出现:There is no PasswordEncoder mapped for the id "null"
        @Bean
        public PasswordEncoder passwordEncoder(){//密码加密类
            return  new BCryptPasswordEncoder();
        }
        //配置规则
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //static
            http.authorizeRequests()
                    .antMatchers("/css/**","/js/**","/img/**")//静态资源等不需要验证
                    .permitAll();
            //permitall
            http.authorizeRequests()
                    .antMatchers("/home/**","/image/defaultkaptcha**")//permitall
                    .permitAll();
            //login
            http.formLogin()
                    .loginPage("/login/login")
                    .loginProcessingUrl("/login/logined")//发送Ajax请求的路径
                    .usernameParameter("username")//请求验证参数
                    .passwordParameter("password")//请求验证参数
                    .failureHandler(userLoginFailureHandler)//验证失败处理
                    .successHandler(userLoginSuccessHandler)//验证成功处理
                    .permitAll(); //登录页面用户任意访问
            //logout
            http.logout()
                    .logoutUrl("/login/logout")
                    .logoutSuccessUrl("/login/logout")
                    .logoutSuccessHandler(userLogoutSuccessHandler)//登出处理
                    .deleteCookies("JSESSIONID")
                    .clearAuthentication(true)
                    .invalidateHttpSession(true)
                    .permitAll();
             //有角色的用户才能访问
             http.authorizeRequests()
                     .antMatchers("/admin/**").hasRole("ADMIN")
                     .antMatchers("/merchant/**").hasAnyRole("MERCHANT","ADMIN");
            //其他任何请求,登录后可以访问
            http.authorizeRequests().anyRequest().authenticated();
            //rememberme
            http.rememberMe()
                    .rememberMeCookieName("remember-me")
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(300)   //Token过期时间为1minutes,一个小时
                    .userDetailsService(secUserDetailService);
            //图形验证码
            http.addFilterBefore(new KaptchaFilter("/login/logined", "/login?error"), UsernamePasswordAuthenticationFilter.class);
            //logout时有可能session已过期
            http.csrf().ignoringAntMatchers("/login/logout");
            //accessdenied
            http.exceptionHandling().accessDeniedHandler(userAccessDeniedHandler);//无权限时的处理
        }
        @Resource
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(secUserDetailService).passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence charSequence) {
                    return ENCODER.encode(charSequence);
                }
                //密码匹配,看输入的密码经过加密与数据库中存放的是否一样
                @Override
                public boolean matches(CharSequence charSequence, String s) {
                    return ENCODER.matches(charSequence,s);
                }
            });
        }
    }

    访问规则和remeberme的配置

    2,SecUser.java

    public class SecUser extends User {
        //用户id
        private int userid;
        //昵称
        private String nickname;
    
        public SecUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, authorities);
        }
    
        public SecUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
            super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        }
    
        public String getNickname() {
            return nickname;
        }
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
    
        public int getUserid() {
            return userid;
        }
        public void setUserid(int userid) {
            this.userid = userid;
        }
    }

    继承自spring security中的User类,增加了用户id和昵称

    3,SecUserDetailService.java

    @Component("SecUserDetailService")
    public class SecUserDetailService implements UserDetailsService{
        @Resource
        private SysUserService sysUserService;
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            //得到用户信息
            SysUser oneUser = sysUserService.getOneUserByUsername(s);//数据库查询 看用户是否存在
            String encodedPassword = oneUser.getPassword();
            Collection<GrantedAuthority> collection = new ArrayList<>();//权限集合
            //用户角色role前面要添加ROLE_
            List<String> roles = oneUser.getRoles();
            System.out.println(roles);
            for (String roleone : roles) {
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+roleone);
                collection.add(grantedAuthority);
           }
            //给用户增加用户id和昵称
            SecUser user = new SecUser(s,encodedPassword,collection);
            user.setUserid(oneUser.getUserId());
            user.setNickname(oneUser.getNickName());
            return user;
        }
    }

    从数据库查询用户信息

    4,UserAccessDeniedHandler.java

    @Component("UserAccessDeniedHandler")
    public class UserAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                           AccessDeniedException e) throws IOException, ServletException {
            boolean isAjax = ServletUtil.isAjax();if (isAjax == true) {
                ServletUtil.printRestResult(RestResult.error(ResponseCode.ACCESS_DENIED));
            } else {
                ServletUtil.printString(ResponseCode.ACCESS_DENIED.getMsg());
            }
        }
    }

    处理访问被拒绝

    5,UserLoginFailureHandler.java

    @Component("UserLoginFailureHandler")
    public class UserLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                            AuthenticationException exception) throws IOException, ServletException {
            //System.out.println("UserLoginFailureHandler");
            ServletUtil.printRestResult(RestResult.error(ResponseCode.LOGIN_FAIL));
        }
    }

    处理登录失败

    6,UserLoginSuccessHandler.java

    @Component("UserLoginSuccessHandler")
    public class UserLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            ServletUtil.printRestResult(RestResult.success(0,"登录成功"));
        }
    }

    处理登录成功

    7,UserLogoutSuccessHandler.java

    @Component("UserLogoutSuccessHandler")
    public class UserLogoutSuccessHandler implements LogoutSuccessHandler{
        @Override
        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            httpServletRequest.getSession().invalidate();
            ServletUtil.printRestResult(RestResult.success(0,"退出成功"));
        }
    }

    处理退出成功

    8,KaptchaFilter.java

    public class KaptchaFilter extends AbstractAuthenticationProcessingFilter {
    
        // parameter name
        private static final String VRIFYCODE ="vrifyCode";
    
        // 拦截请求地址
        private String servletPath;
    
        public KaptchaFilter(String servletPath, String failureUrl) {
            super(servletPath);
            this.servletPath = servletPath;
            setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(failureUrl));
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse res = (HttpServletResponse) response;
            if ("POST".equalsIgnoreCase(req.getMethod()) && servletPath.equals(req.getServletPath())) {
                String expect = (String) req.getSession().getAttribute(VRIFYCODE);
    
                if (expect != null && !expect.equalsIgnoreCase(req.getParameter(VRIFYCODE))) {
                    System.out.println("kaptchafilter: vrifycode is not right");
                    ServletUtil.printRestResult(RestResult.error(ResponseCode.AUTHCODE_INVALID));
                    return;
                } else {
                    System.out.println("kaptchafilter: vrifycode is right");
                }
            } else {
                System.out.println("kaptchafilter:not post");
            }
            chain.doFilter(req, res);
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException, IOException, ServletException {
            return null;
        }
    }

    过滤器,检查图形验证码是否正确

    9,KaptchaSingle.java

    public class KaptchaSingle {
        private static KaptchaSingle instance;
    
        private KaptchaSingle() {
        };
    
        public static KaptchaSingle getInstance() {
            if (instance == null) {
                instance = new KaptchaSingle();
            }
            return instance;
        }
    
        /**
         * 生成DefaultKaptcha 默认配置
         * @return
         */
        public DefaultKaptcha produce() {
            Properties properties = new Properties();
            properties.put("kaptcha.border", "no");
            properties.put("kaptcha.border.color", "105,179,90");
            properties.put("kaptcha.textproducer.font.color", "blue");
            properties.put("kaptcha.image.width", "199");
            properties.put("kaptcha.image.height", "50");
            properties.put("kaptcha.textproducer.font.size", "37");
            properties.put("kaptcha.session.key", "code");
            properties.put("kaptcha.textproducer.char.length", "4");
            properties.put("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
            properties.put("kaptcha.textproducer.char.string", "0123456789ABCEFGHIJKLMNOPQRSTUVWXYZ");
            properties.put("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
            properties.put("kaptcha.noise.color", "black");
            properties.put("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
            properties.put("kaptcha.background.clear.from", "185,56,213");
            properties.put("kaptcha.background.clear.to", "white");
            properties.put("kaptcha.textproducer.char.space", "3");
    
            Config config = new Config(properties);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    }

    配置Kaptcha

    10,ImageController.java

    @Controller
    @RequestMapping("/image")
    public class ImageController {
        //生成图形验证码
        @RequestMapping("/defaultkaptcha")
        public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
                throws Exception {
            byte[] captchaChallengeAsJpeg = null;
            ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
            try {
                // 代码方式创建:DefaultKaptcha
                KaptchaSingle single = KaptchaSingle.getInstance();
                DefaultKaptcha defaultKaptcha = single.produce();
                // 生产验证码字符串并保存到session中
                String createText = defaultKaptcha.createText();
                httpServletRequest.getSession().setAttribute("vrifyCode", createText);
                // 使用生产的验证码字符串返回一个BufferedImage对象并转为byte写入到byte数组中
                BufferedImage challenge = defaultKaptcha.createImage(createText);
                ImageIO.write(challenge, "jpg", jpegOutputStream);
            } catch (IllegalArgumentException e) {
                httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
            // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
            captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
            httpServletResponse.setHeader("Cache-Control", "no-store");
            httpServletResponse.setHeader("Pragma", "no-cache");
            httpServletResponse.setDateHeader("Expires", 0);
            httpServletResponse.setContentType("image/jpeg");
            ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
            responseOutputStream.write(captchaChallengeAsJpeg);
            responseOutputStream.flush();
            responseOutputStream.close();
        }
    }

    生成图形验证码

    11,login.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta content="text/html;charset=UTF-8"/>
        <title>登录页面</title>
        <script type="text/javascript" language="JavaScript" src="/js/jquery-1.6.2.min.js"></script>
        <style type="text/css">
            body {
                padding-top: 50px;
            }
            .starter-template {
                padding: 40px 15px;
                text-align: center;
            }
        </style>
        <!-- CSRF -->
        <meta name="_csrf" th:content="${_csrf.token}"/>
        <!-- default header name is X-CSRF-TOKEN -->
        <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    </head>
    <body>
    <nav class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div id="navbar" class="collapse navbar-collapse">
                <ul class="nav navbar-nav">
                    <li><a href="/home/home"> 首页 </a></li>
                </ul>
            </div><!--/.nav-collapse -->
        </div>
    </nav>
    <div class="container">
        <div class="starter-template">
            <h2>使用账号密码登录</h2>
                <div class="form-group">
                    <label for="username">账号</label>
                    <input type="text" class="form-control" id="username" name="username" value="" placeholder="账号" />
                </div>
                <div class="form-group">
                    <label for="password">密码</label>
                    <input type="password" class="form-control" id="password" name="password" placeholder="密码" />
                </div>
            <div class="form-group">
                <label for="password">记住登录</label>
                <input type="checkbox" name="is_remember_me"  id="is_remember_me" value="true" />
            </div>
            <div class="form-group">
                <label for="password">验证码</label>
                <img id="kaptcha" alt="验证码" onclick = "refresh_kaptcha()" src="/image/defaultkaptcha" /><br/>
                <input type="text" id="vrifyCode" name="vrifyCode" placeholder="验证码" />
            </div>
                <button name="formsubmit" value="登录" onclick="go_login()" >登录</button>
        </div>
    </div>
    <script>
        //刷新图形验证码
        function refresh_kaptcha() {
            document.getElementById("kaptcha").src='/image/defaultkaptcha?d='+new Date();
        }
        //登录
        function go_login(){
            if ($("#username").val() == "") {
                alert('用户名不可为空');
                $("#username").focus();
                return false;
            }
            if ($("#password").val() == "") {
                alert('密码不可为空');
                $("#password").focus();
                return false;
            }
            if ($("#vrifyCode").val() == "") {
                alert('验证码不可为空');
                $("#vrifyCode").focus();
                return false;
            }
            var rememberme_val = false;
            if (document.getElementById('is_remember_me').checked == true) {
                rememberme_val = true;
            }
            var postdata = {
                username:$("#username").val(),
                password:$("#password").val(),
                vrifyCode:$("#vrifyCode").val(),
                'remember-me':rememberme_val
            }
            var csrfToken = $("meta[name='_csrf']").attr("content");
            var csrfHeader = $("meta[name='_csrf_header']").attr("content");
            $.ajax({
                type:"POST",
                //type:"GET",
                url:"/login/logined",
                data:postdata,
                //返回数据的格式
                datatype: "json",//"xml", "html", "script", "json", "jsonp", "text".
                beforeSend: function(request) {
                    request.setRequestHeader(csrfHeader, csrfToken); // 添加  CSRF Token
                },
                success:function(data){
                    if (data.code == 0) {
                        //
                        alert('login success:'+data.msg);
                        window.location.href="/home/home";
                    } else {
                        alert("failed:"+data.msg);
                        //window.location.href="/login/login";
                    }
                },
                //调用执行后调用的函数
                complete: function(XMLHttpRequest, textStatus){
                },
                //调用出错执行的函数
                error: function(){
                    //请求出错处理
                    alert('error');
                }
            });
        }
    </script>
    </body>
    </html>

    12,其他相关代码,可以访问github

    五,测试效果

    1,访问登录页面:

    http://127.0.0.1:8080/login/login

    如果输入错误的图形验证码时,会报错:

    2,登录时选中记住登录:

    查看cookie:

     可以看到cookie中增加了remember-me这个cookie

    查看数据库:

     persistent_logins数据表中也生成了记住登录信息的记录

    3,登录后记住当前的session id的值:

     因为我们配置了session的时长是120秒,

    所以在120秒后再回来刷新页面 ,因为rememberme的cookie的时长是5分钟(300秒)

    则刷新页面后应该会生成一个新的session id:

    可以见到虽然仍然处于登录状态,但原session已过期,

    remember-me功能为当前会话生成了新的session

    六,查看spring boot版本:

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.3.1.RELEASE)
  • 相关阅读:
    基于协程实现并发的套接字通信
    基于tcp协议的套接字通信:远程执行命令
    Java开发中的23种设计模式详解(转)
    SonarLint实践总结
    Java代码规范与质量检测插件SonarLint
    ES的基本介绍和使用
    ES基本介绍(简介)
    弗洛伊德追悼会 事发地市长跪在灵柩前大哭
    阿里云部署Web项目
    SpringBoot上传图片无法走复制流
  • 原文地址:https://www.cnblogs.com/architectforest/p/13577913.html
Copyright © 2011-2022 走看看