• 【web开发】spring security配合验证码,java获取地理位置(绘制百度地图)、天气


    SpringBoot


    项目碎碎念 ---- 验证码、地图、IP、天气等零碎服务


    不积跬步无以至千里,一个完美的项目总是不断增加功能,升级迭代,优化设计产生的,目前的基础的SpringBoot开发是很easy的,但是很多其他的细节就是后期需要优化的部分,诸如缓存、持久化存储、高并发,多线程、异常等 【Nginx负载均衡、CDN缓存(js等静态)、限流(IP,变灰)、削峰、redis缓存【分布式🔒】、亿级数据Mycat分库分表】

    后面cfeng将走上分布式和性能优化之路,fighting… 这之前先记录一些常规操纵【使用了还是需要记录下来以防忘记】: 结合security和redis进行图形验证码登录、IP的锁定、地图等

    important: filter level在controller level之前,如果security过滤器配置了认证,controller层面不能permittAll

    验证码放在什么位置?验证码是有过期时间的,按照knowledge来说应该存储在redis中,因为其expire完美的适应这个功能,之前在Security部分提过,分布式的服务中,为了保证nginx分发后各机器的状态一致,保证其中一台服务器宕机后用户保持登录状态,就需要使用spring Session, spirng session就是利用外部存储(redis等)作为session存储用户信息,可以存储token,权限和token 验证码等信息

    要使用Spring Seesion,需要引入Redis的依赖,同时在security配置文件中注册外部的session告知framework使用

    redis:
        port: 6379
        host: localhost  #连接远程redis
        database: 1
        timeout: 1000
        password:
        jedis:
          pool:
            max-active: 10
            max-idle: 8
            min-idle: 1
            max-wait: 1
      ##配置spring session,自动配置的,注入属性后创建filter过滤器,将HttpSessin转换为Spring session
      session:
        timeout: 300  #会话超时时间,默认后缀为秒
        store-type: redis  #设置session的外部容器为redis
        redis:
          flush-mode: on_save  #ON SAVE 还是IMMEDIATE  刷新策略
          namespace: spring:session  #存储session的命名空间
         
    ------- config---
    .and()
                    .sessionManagement()
                    .maximumSessions(1) //并发上限1
                    .maxSessionsPreventsLogin(false)  //true为阻止,false为提出旧的
                    .expiredSessionStrategy(new SessionInfoExiredListener(objectMapper)) //session失效监听的处理程序
                    .sessionRegistry(sessionRegistry()); //添加注册器
    
    • 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

    这里的Session配置可能出现不生效的情况? 解决办法

    并发的管理就是查找该用户的所有的session,超出配置的数量就会踢出,但是在同一个浏览器是可以开多个页面登录的,不同的浏览器不能同时登录

    可能的原因是没有设置expireUrl或者相关的处理strategy

    Session共享就是在新的机器上面部署redis,同时要开启EnableSessionRedis的注解,同时就可以实现session共享

    集成Kaptcha实现验证码

    为了防止机器的恶意注册,现在的验证方式已经越来越丰富,比如滑块验证码等等,we可以借助Kaptcha这个验证码生成工具

    可以配置多样化的验证码,并且以图片形式显示,不能进行复制和粘贴,进一步保证安全

    引入kaptcha依赖

            <dependency>
                <groupId>com.github.pengglegroupId>
                <artifactId>kaptchaartifactId>
                <version>2.3.2version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    编写一个常量类,定义验证码存储的key等信息

    public interface KaptchaConstant {
    
        //定义captcha存储session中的key
        String CAPTCHA_SESSION_KEY = "captcha_key";
    
        String SMS_SESSION_KEY = "sms_key";
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    验证码放在session中管理,过期时间封装在code中,使用LocalDateTime进行判断

     * 验证码是作为一个info放在用户session中,可以直接放在Redis中,但是这里直接设置手动存储
     */
    
    public class CaptchaCodeVo {
    
        //验证码
        private String code;
    
        //过期时间s
        private LocalDateTime expireTime;
    
        public CaptchaCodeVo(String code,int expireAfterSeconds) {
            this.code = code;
            //过期时间为创建之后60s
            this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
        }
    
        //是否过期,晚于过期时间旧过期
        public boolean isExpired() {
            return LocalDateTime.now().isAfter(expireTime);
        }
    
        public String getCode() {
            return this.code;
        }
    }
    
    • 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

    编写主要的处理器类和过滤器类装载

     * 主要就是借助Kaptcha工具类生成动态的图片验证码,配合security进行验证登录,验证码放在redis的session中
     */
    
    @RestController
    @RequiredArgsConstructor
    public class KaptchaController {
        //配置的kaptcha工具类
        private final DefaultKaptcha defaultKaptcha;
    
        /**
         * 前台请求验证码,返回资源,使用httpResponse返回
         */
        @GetMapping("/kaptcha")
        public void getKaptchaImage(HttpSession httpSession, HttpServletResponse response) throws IOException {
            //设置相应的格式和类型
            response.setDateHeader("Expires",0);
            response.setHeader("Cache-control","no-store, no-cache, must-revalidate");
            response.addHeader("Cache-Control", "post-check=0, pre-check=0");
            response.setHeader("Pragma", "no-cache");
            response.setContentType("image/jpeg");
            //创建验证码
            String capText = defaultKaptcha.createText();
            //将验证码放入session,会自动由Spring Session受理
            httpSession.setAttribute(KaptchaConstant.CAPTCHA_SESSION_KEY,new CaptchaCodeVo(capText,60)); //60s
            //返回响应
            BufferedImage bufferedImage = defaultKaptcha.createImage(capText);
            ServletOutputStream out = response.getOutputStream();
            ImageIO.write(bufferedImage,"jpg",out);
            try {
                out.flush();
            } finally {
                out.close();
            }
        }
    }
    
    
    @Component //这里不使用普通的过滤器注解,直接component即可
    public class KaptchaCodeFilter extends OncePerRequestFilter {
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
            String uri = request.getServletPath();
            //post登录提交的位置
            if("/authentication/form".equals(uri) && request.getMethod().equalsIgnoreCase("post")) {
                //用户输入的验证码
                String captchaInRequest = request.getParameter("captchaCode").trim();
                //session中的
                CaptchaCodeVo captchaInSession = (CaptchaCodeVo)request.getSession().getAttribute(KaptchaConstant.CAPTCHA_SESSION_KEY);
                if(StringUtils.isEmpty(captchaInRequest)) {
                    throw new SessionAuthenticationException("验证码不能为空");
                }
                if(captchaInSession == null) {
                    throw  new SessionAuthenticationException("验证码不存在");
                }
                if(captchaInSession.isExpired()) {
                    //从用户session中删除
                    request.getSession().removeAttribute(KaptchaConstant.CAPTCHA_SESSION_KEY);
                    throw new SessionAuthenticationException("验证码已过期");
                }
                if(! captchaInRequest.equalsIgnoreCase(captchaInSession.getCode())) {
                    throw new SessionAuthenticationException("验证码错误");
                }
            }
            filterChain.doFilter(request,response);
        }
    }
    
    • 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

    之后就是简单配置一下config,和前端的代码, 放行此路径

     http
                    //添加过滤器
                    .addFilterBefore(kaptchaCodeFilter, UsernamePasswordAuthenticationFilter.class)
         
    public WebSecurityCustomizer webSecurityCustomizer() {
            return (web) -> web.ignoring().antMatchers("/js/**", "/img/**","/css/**","/editorMd-master/**","/login.html","/register.html","/login/**","/layui/**","/kaptcha");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在前台表单处添加一个img和验证码提交的input即可

    //js中给出点击刷新 --- 重新获取验证码的方法
    <div class="item">
                <input type="text" name="captchaCode" id="captchaCode"/>
                <img src="/kaptcha" id="kaptcha" width="110px" height="40px"/>
                <label>验证码</label>
            </div>
            
        
     //点击刷新
        window.onload = function () {
            var kaptchaImg = document.getElementById("kaptcha");
    
            kaptchaImg.onclick = function () {
                kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
            }
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    前端audio音乐播放 ⚠

    这里提一下前端的播放,使用的audio标签,首页的背景音乐 【 autoplay自动 loop循环 controls按钮控制 muted静音 preload预加载】

    需要注意一般采用外网播放,这里的src如果直接链接static下面的MP3失败,因为编译的时候会破环文件不能播放; 比如在static下面同时放置img和music,访问img成功,但是music失败,这是因为项目都是访问的编译后的资源, 在编译到target之后,音频文件遭到破环,导致访问失败, 解决办法 :

    将原来的音乐视频资源文件替换target或者打包后的其中的文件, 这样就可以访问成功

    JS控制音乐的播放和暂停直接获取到audio元素,调用其pause()和play()方法即可

    其余的解决办法就是替换target中的文件,或者后台进行流转换再使用

    集成ip2region根据IP获取位置

    在cfeng的项目构建中,需要根据前台用户访问的ip获取用户的具体的位置,为了不占用网络资源【在线访问会耗费】,所以就集成ip2fregion工具,安装其离线ip对照包进行位置的获取

        <!-- ip to region -->
        <dependency>
          <groupId>org.lionsoul</groupId>
          <artifactId>ip2region</artifactId>
          <version>1.7.2</version>
        </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    离线数据db保存,ip2region将已知的ip和城市信息的对应关系保存在数据库中,文件为ip2region.db,能够满足基本的项目需求,将其添加到项目的static文件夹下面以供使用【下载去github上面拉取data即可】

    IP查询基本的工具类RegionUtil

    需要注意jar读取文件,创建一个临时目录,读取不到时加上临时目录,借助apache的commons-io完成输入流到文件的转化

    package com.Cfeng.XiaohuanChat.util;
    
    import com.mysql.cj.util.StringUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.io.FileUtils;
    import org.lionsoul.ip2region.DataBlock;
    import org.lionsoul.ip2region.DbConfig;
    import org.lionsoul.ip2region.DbSearcher;
    import org.lionsoul.ip2region.Util;
    import org.springframework.core.io.ClassPathResource;
    
    import java.io.File;
    import java.io.IOException;
    import java.io.InputStream;
    import java.lang.reflect.Method;
    import java.util.Objects;
    
    /**
     * @author Cfeng
     * @date 2022/8/21
     *
     * 根据IP离线查询地址, 调用IpRegion2Util进行查询
     */
    
    @Slf4j
    public class RegionUtil {
    
        //jar临时文件夹
        private static final String JAVA_TEMP_DIR = "java.io.tmpdir";
    
        static DbConfig config = null;
    
        static DbSearcher searcher = null;
    
        //使用一个静态代码块让第一次加载的时候就读取资源,使用class.getResouce
        static {
            //项目中直接在static下面 即/ip2region.db
            //jar无法读取文件,需要复制创建临时文件,绝对path
            try {
                String path = RegionUtil.class.getResource("/static/ip2region.db").getPath();
                File file = new File(path);
                //文件不存在就加上jar的临时目录重新读取到file中
                if(!file.exists()) {
                    String tmpDir = System.getProperties().getProperty(JAVA_TEMP_DIR);
                    path = tmpDir + "ip2region.db";
                    file = new File(path);
                    ClassPathResource classPathResource = new ClassPathResource("static" + File.separator + "ip2region.db");
                    //通过流获取File
                    InputStream resourceStream = classPathResource.getInputStream();
                    if(resourceStream != null) {
                        FileUtils.copyInputStreamToFile(resourceStream,file);
                    }
                }
                config = new DbConfig(); //region的config
                searcher = new DbSearcher(config,path); //创建seacher
                log.info("bean create success, {},{}",config,searcher);
            } catch (Exception e) {
                log.error("init ip region error {}",e);
            }
        }
    
        /**
         * 解析IP地址
         */
        public static String getRegion(String ip) {
            try {
    
                if(Objects.isNull(searcher) || StringUtils.isNullOrEmpty(ip)) {
                    log.error("Dbsearcher is null");
                    return "";
                }
                long startTime = System.currentTimeMillis();
                // 查询算法,使用反射动态选择方法
                int algorithm = DbSearcher.MEMORY_ALGORITYM;
                Method method = null;
                switch (algorithm)
                {
                    case DbSearcher.BTREE_ALGORITHM:
                        method = searcher.getClass().getMethod("btreeSearch", String.class);
                        break;
                    case DbSearcher.BINARY_ALGORITHM:
                        method = searcher.getClass().getMethod("binarySearch", String.class);
                        break;
                    case DbSearcher.MEMORY_ALGORITYM:
                        method = searcher.getClass().getMethod("memorySearch", String.class);
                        break;
                }
    
                DataBlock dataBlock = null;
                if (Util.isIpAddress(ip) == false)
                {
                    log.warn("warning: Invalid ip address");
                }
                dataBlock = (DataBlock) method.invoke(searcher, ip);
                String result = dataBlock.getRegion();
                long endTime = System.currentTimeMillis();
                log.debug("regionSearch use time[{}] result[{}]", endTime - startTime, result);
                return result;
            } catch (Exception e) {
                log.error("error:{}",e);
            }
            return "";
        }
    }
    
    • 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

    注意文件的获取方法: XXX.class.getResource(); 这里是从编译后的target目录开始寻找,/代表target一级,所以这里就是/static/xxx

    使用这个工具类就可以查询出最原始的数据了: 0|0|0|内网IP|内网IP; 比如这样子

    接下来为了更加方便的操作地址,对操作的地址进一步封装,创建AddressUtils

    package com.Cfeng.XiaohuanChat.util;
    
    import com.mysql.cj.util.StringUtils;
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * @author Cfeng
     * @date 2022/8/21
     *  地址查询类, 内网地址不详细,外网地址进一步处理
     */
    
    @Slf4j
    public class AddressUtil {
    
        //未知地址
        public static final String UNKNOWN = "XX XX";
    
        /**
         * 根据IP获取真实地址
         */
        public static String getRealAddressByIP(String ip)
        {
            String address = UNKNOWN;
            // 内网不查询
            if (IpUtils.internalIp(ip))
            {
                return "内网IP";
            }else {
                try
                {
                    String rspStr = RegionUtil.getRegion(ip);
                    if (StringUtils.isNullOrEmpty(rspStr))
                    {
                        log.error("获取地理位置异常 {}", ip);
                        return UNKNOWN;
                    }
                    String[] obj = rspStr.split("\\|");
                    String region = obj[2];
                    String city = obj[3];
                    //地区  城市
                    return String.format("%s %s", region, city);
                }
                catch (Exception e)
                {
                    log.error("获取地理位置异常 {}", e);
                }
            }
            return address;
        }
    }
    
    • 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

    这里面还是使用到了更具体的IpUtil,这里简单给一部分,包括获取ip,判断是否内网IP

     * 该工具类可以从请求中提取ip,
     */
    
    public class IpUtils {
    
        /**
         * 获取客户端IP
         * 从请求头中提取IP
         * @param request 请求对象
         * @return IP地址
         */
        public static String getIpAddr(HttpServletRequest request)
        {
            if (request == null)
            {
                return "unknown";
            }
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getHeader("X-Forwarded-For");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getHeader("X-Real-IP");
            }
    
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            {
                ip = request.getRemoteAddr();
            }
    
            return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
        }
    
        /**
         * 检查是否为内部IP地址
         *
         * @param ip IP地址
         * @return 结果
         */
        public static boolean internalIp(String ip)
        {
            byte[] addr = textToNumericFormatV4(ip);
            return internalIp(addr) || "127.0.0.1".equals(ip);
        }
    
    • 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

    使用最上面的RegionUtil就可以查询出最原始的数据,通过addressUtil就可以查询到国内的 身份 + 城市

    IpUtil主要提供的就是对于IP的操作

    上面就是后台的操作,获取IP就是提取请求头中的参数,如果没有对应的参数,那就直接getRemoteAdr即可

    前端的地址获取

    之前cfeng介绍了很多前端js,包括editormd、stomp、websocket、socketJs、live2d等

    而前端的地理位置的获取使用的是navgitor.geolocation插件

    • 使用其getCurrentPosition就可以获取用户当前的地理位置信息 (successhandler,errorXX,options); success中包括两个属性coords和timestamp ; coords包括精确的accuracy,经度longitude,纬度latitude,海拔altitude,海报高度精确度altitudeAccuracy,朝向heading、速度speed等; 失败的回调包括错误码和错误信息,而positionOptions为JSON格式的餐宿,设置maximumAge【重新获取位置的间隔时间】 timeout 超时时间,enableHighcuracy启用高精度
    • 除此之外还可以使用watchCurrentPostion和clearWatch配合使用; 新版本的浏览器一般都是支持geolocation的【胖客户端】
    //获取当前经纬度 if(navigator.geolocation)就可以判断是否支持
    showLocation = function(position) {
                //成功的回调函数
                var longitude = position.coords.longitude;
                var latitude = position.coords.latitude;
                alert(longitude + ":" + latitude);
    
                $.get("/sys/getAddress",{latitude:latitude,longitude:longitude},function (response) {
                    if(response.code == 0) {
                        var address = response.data;
                        $("#position").text(address);
                    }
                });
            }
    
            errorHandler = function(error) {
                if(error.code == 1) {
                    alert("access denied");
                } else if(error.code == 2) {
                    alert("position is not avaliable");
                }
            }
    
            getLocation = function() {
                //navigator 最新插件 geolocation是否可以使用,可以才自动定位
                if(navigator.geolocation) {
                    var options = {timeout: "6000"};
                    //使用插件获取位置
                    navigator.geolocation.getCurrentPosition(showLocation,errorHandler,options);
                } else {
                    aler("对不起,你的浏览器不支持geolocation插件")
                }
            }
    
    • 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

    geolocation是保护用户隐私的,只有用户允许才会获取位置信息

    获取经纬度传递给后台由后台的百度的进行解析即可获得数据,处理只是将其转为String类型

    使用百度API将经纬度转为地理位置

    获取到经纬度之后,可以直接将经纬度提交到后台,之后由后台将借助百度地图进行快捷的经纬度查询

    首先我们需要在百度地图注册成为开发者,创建个人应用,通过应用的ak来进行服务的调用,申请入口: 百度地图开放平台 | 百度地图API SDK | 地图开发 (baidu.com) ; 创建之后就可以按照规范对该地址进行请求, 主要就是ak、location(经纬度)和经纬度类型

    @Slf4j
    public class BaiduMapGeoUtil {
        /**
         * 百度地图调用
         */
        public final static String BAIDU_MAP_AK = "9XjkjlhhgkajgF7VmYyGMjjgkajgkahgsawvhaC0hGHt"; //这里就是申请的ak
    
        /**
         * 根据经纬度得出位置
         * @param longitude 经度
         * @param latitude  纬度
         * @return 位置 String, 封装的json字符串addressInfo
         */
        public static String getAddressInfoByLngAndLat(String longitude, String latitude) {
            String position = ""; //返回的结果对象JSon字符串
            String location = latitude + "," + longitude;
            //百度URL 相关的参数coordtype :bd09ll(百度经纬度坐标)、bd09mc(百度米制坐标)、gcj02ll(国测局经纬度坐标,仅限中国)、wgs84ll( GPS经纬度); ak就是访问
            String url ="http://api.map.baidu.com/reverse_geocoding/v3/?ak="+BAIDU_MAP_AK+"&output=json&coordtype=wgs84ll&location="+location;
            //访问baidu的服务,传入ak、coordtype、location
            try {
                String result =  loadUrl(url);
                //使用fastJSON进行对象转化JSON
                JSONObject res = JSONObject.parseObject(result);
                log.info("具体的位置信息: {}" ,res.toString());
                //该对象的status属性为状态码,0为成功,result为数据
                if(Objects.equals("0",String.valueOf(res.get("status")))) {
                    //数据对象
                    JSONObject data = JSONObject.parseObject(String.valueOf(res.get("result")));
                    //AddressComponent对象就是最终封装的位置信息
                    position = String.valueOf(data.get("addressComponent"));
                }
            } catch (Exception e) {
                log.error("未能找到相匹配的经纬度,请检查");
            }
            return position;
        }
    
        //通过url获取结果String
      private static String loadUrl(String url) {
            StringBuilder stringBuilder = new StringBuilder();
    
          try {
              //创建链接地址
              URL region = new URL(url);
              //访问链接URL地址
              URLConnection connection = region.openConnection();
              //读取url缓冲字符流
              BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(),"UTF-8"));
              //读取流写入字符串
              String inputLine = null;
              //读取到末尾
              while((inputLine = reader.readLine()) != null) {
                  stringBuilder.append(inputLine);
              }
              reader.close();
          } catch (Exception e) {
              log.error("出错了:{}",e.getMessage());
          }
          return stringBuilder.toString();
      }
    }
    
    • 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

    loadURL的作用就是加载URL访问之后将结果封装为一个String对象,借用缓冲的字符流来进行结果的读取,保存在StringBuilder中,最后再变为String; StringBuilder线程不安全,但是构建迅速

    之后就可以配合前台传入的经纬度进行地理位置的较为精确的定位【IP定位不够精确】

    需要注意,开发时对于不同的端需要不同的应用,服务端的就是创建server供后台的接口进行访问,而前端浏览器需要创建浏览器应用进行访问; 创建浏览器后也要设置白名单同时将ak引入script

    前台调用百度地图实现地图展示【google退出,需要代理】

    简单提一下谷歌地图的使用:

    如果调用谷歌地图,需要使用其API,首先也是需要注册一个API密钥ak,但是需要注意:

    • 加入API页面没有发布,本地使用,可以不使用密钥,随便使用字符串代替即可【上面百度】
    • API密钥只是对当前网站目录或者域有效,不同的网站需要不同的ak

    其次就是页面利用script引入谷歌地图: http://ditu.google.com/maps?file=api&hl=zh-CN&v=2&key=abcdefg

    • ditu.google.com: .cn也可以; 需要在地图上显示中国之外区域,使用.com
    • file=api: 请求js的固定格式
    • hl = zh-CN: 设定地图除了地图图片之外的各种声明的文本的语言版本language,默认是英语
    • v=2: 引入的类库的版本号,由.s 和.x 还有缺省和具体四种,.s最慢最稳定, 这里2介于.s和.x之间【当前主版本】
    • key=abcdefg: 注册的ak, 这里开发环境随意

    设置地图类型的方法:

    • enableDragging(): 设置地图可以拖动 disalbeDragging draggingEnabled—返回是否
    • enableInfoWindow: 设置地图信息的窗口可弹出 ~~
    • enableDoubleClickZoom: 设置可以双击缩放地图 ~~
    • enableContinuousZoom: 设置可以连续平滑缩放
    • enableScrollWheelZoom: 鼠标滚轮控制缩放
    • isLoaded 如果已经被setCenter()初始化,,返回true

    显示个人位置

    • 首先在页面创建一个map的div用来防止地图
    • 将Google Maps API添加到项目中,如果想要gps定位,那么就要sensor为true,http://maps.google.com/maps/api/js?sensor=false获取js
    • 加载之后创建一个google.maps.LatLng实例,保存在postion中,传入经纬度,当前的位置
    • 设置缩放级别zoom,地图中心位置【LatLng】,地图显示方式google.maps.MapTypeId,包括ROAD公路路线,TERRAIN公路名称和地势,HYBRID:卫星地图和公路路线叠加,SATELLITE卫星地图

    百度地图,首先就是申请密钥,上面已经提过,不再赘述

    • 首先引入百度地图的script
    <script type="text/javascript" src="http://api.map.bai
    du.com/api?v=3.0&ak=xxxxxxx">script>
    
    • 1
    • 2
    • 创建map的div容器,作为展现
    • 创建地图实例
    //使用的对象为BMap【goole为google.maps.Map】
    var map = new BMap('container'); //容器id
    
    new BMap.Point() 设置中心点
    
    map.centerAndZoom(point,15); 初始化地图并且设置展示的级别3-19
    
    map.enableScrollWheelZoom(true);  鼠标滚轮缩放  其他的参数上面google提过,类似
    
    //设置参数,包括平移缩放组件,缩略地图,比例尺,地图类型,都是通过addControl添加
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • Marker: 地图添加标记点
    var marker = new Bmap.Marker(point); //创建标注
    map.addOverlay(marker);
    
    //点击marker事件
    marker.addEventListener('click',funtion() {
                            //点击marker时,进入地图移动了页面,那么信息窗口的中央就不是当前位置了
                            map.openInfoWindow(infoWindow,point);
                            })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 信息窗口 infoWindow
    //窗口配置信息options
    var options = {
        width: 250,
        height: 100,
        title: "标题"
    }
    //信息窗口的数据
    var content = "
    你好
    "
    //创建窗口 var infoWindow = new BMap.InfoWindow(content,options); //默认进入打开信息窗口 map.openInfoWindow(infoWindow,map.getCenter); //地图中心打开
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 地址解析和逆地址解析
    //地址解析服务 new BMap.Geocoder()
    利用Geocoder的getPoint方法可以解析一个具体的位置,解析成功进入回调函数success
    
    var myGeo = new BMap.Geocoder();
    myGeo.getPoitn("北京市海淀区上地10街3号",function(point) {
        if(point) {
            //point为解析出的经纬度
        }
    })
    
    //逆地址解析
    使用Geocoder的getLocation服务, new BMap.Point(), 解析成功后进入回调
    
    myGeo.getLocation(new BMap.point(sfsj,sfs),function(result) {
        if(result) {
            alert(result)
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上面的利用经纬度获取地址就是逆地址解析,只是将服务放在了后台,在胖客户端下,可以将该服务直接放前台【当时放后台更加安全】

    综合demo

    showLocation = function(position) {
    			//当前经纬度
    			var longtitude = position.coords.longitude;
    			var latitude = position.coords.latitude;
    			alert(longtitude + ":" + latitude);
    			//构建地图
    			map = new BMap.Map("map");
    			//创建中心点
    			var point = new BMap.Point(longtitude,latitude);
    			//地图初始化
    			map.centerAndZoom(point,15);
    			//鼠标滚轮缩放
    			map.enableScrollWheelZoom(true);
    			//平移缩放控件
    			map.addControl(new BMap.NavigationControl());
    			//缩略地图
    			map.addControl(new BMap.OverviewMapControl());
    			//比列尺
    			map.addControl(new BMap.ScaleControl());
    			//地图类型
    			map.addControl(new BMap.MapTypeControl());
    			//控件的位置
    			// var options = {anchor:BMAP_ANCHAOR_BOTTOM_RIGHT};
    			// map.addControl(new BMap.NavigationControl(options));
    			
    			//为我的位置创建标注
    			var marker = new BMap.Marker(point);
    			//将标注放入地图
    			map.addOverlay(marker);
    			
    			var options = {
    				width:250,
    				height:100,
    				title: '我的位置'
    			}
    			var content = "
    我的家在这里
    "
    var infoWindow = new BMap.InfoWindow(content,options); //监听标注,点击后显示信息窗口 marker.addEventListener('click',function() { //在标记点打开窗口,如果是中央map.getCenter() map.openInfoWindow(infoWindow,point); }); } errorHandler = function(error) { if(error.code == 1) { alert("access denied"); } else if(error.code == 2) { alert("position is not avaliable"); } } getLocation = function() { //navigator 最新插件 geolocation是否可以使用,可以才自动定位 if(navigator.geolocation) { var options = {timeout: "6000"}; //使用插件获取位置 navigator.geolocation.getCurrentPosition(showLocation,errorHandler,options); } else { aler("对不起,你的浏览器不支持geolocation插件") } }
    • 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

    在项目中引用时可能出现报错:

    百度地图引用时 报出A Parser-blocking, cross site (i.e. different eTLD+1) script

    因为页面渲染完成之后使用document.wirte(),不被允许,所以可以将url中的api改为getscript

    <script type="text/javascript" src="http://api.map.baidu.com/getscript?v=3.0&ak=NUvQmHyvEMWTO53
    
    • 1

    同时需要注意的是script标签应该放在body后,这样页面加载完成才能够正确获取容器进行创建

     <div id="map" style="width: 800px;height: 800px;margin-top: 100px">
    
        div>
    
        <script src="/js/JQuerym.js">script>
        <script type="text/javascript" src="http://api.map.baidu.com/getscript?v=3.0&ak=NUvQmHyvEMWTO534b5GcCGUM4eGuGXwT">script>
        <script type="text/javascript">
            $(function() {
                //发起请求获取错误信息
                $.get("/sys/getErrorMessage",{},function(response) {
                    if(response.code == 0) {
                        var error = response.data;
                        alert(error.message);
                    }
                })
    
                //发起请求,后台根据ip分析出大概位置
                $.get("/sys/getRegion",{},function(response) {
                    if(response.code == -1) {
                        $("#ipMsg").text(response.message);
                    } else {
                        $("#ipMsg").text(response.data);
                    }
                })
            })
    
            //根据现在的经纬度添加Map
            addMap = function(longitude,latitude) {
                //根据百度地图绘制位置
                //构建地图 在某个具体的div中
                map = new BMap.Map("map");
                //创建中心点
                var point = new BMap.Point(longitude,latitude);
                //地图初始化
                map.centerAndZoom(point,15);
                //鼠标滚轮缩放
                map.enableScrollWheelZoom(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

    这样就可以按照百度地图的规范进行地图的绘制显示

    显示天气信息http://wthrcdn.etouch.cn/weather_mini?city= X

    要显示天气信息,去网站直接爬取是不推荐的,可以访问几个免费的天气信息的提供方,这里cfeng采用http://wthrcdn.etouch.cn/weather_mini?city= X; 请求该接口就可以返回当地的数据

    java后台访问并进行数据处理

    之前的baidu的后天的经纬度转化也就是访问URL,之后使用缓冲流读入字符串,显示,这里也是类似的

    需要注意这里读取到的是压缩流,需要使用GZIPInputStream进行读取

    package com.Cfeng.XiaohuanChat.util;
    
    import com.alibaba.fastjson.JSONObject;
    import lombok.extern.slf4j.Slf4j;
    
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.net.URL;
    import java.net.URLConnection;
    import java.util.zip.GZIPInputStream;
    
    /**
     * @author Cfeng
     * @date 2022/8/22
     * 访问http://wthrcdn.etouch.cn/获取天气信息
     */
    @Slf4j
    public class WeatherUtil {
    
        //获取天气信息,返回JSON字符串
        public static String getWheatherByCity(String city) {
            String url = "http://wthrcdn.etouch.cn/weather_mini?city="+city;
            //解析获得数据
            String res = loadUrl(url);
    //        JSONObject res =  JSONObject.parseObject(result);
            log.info("天气信息:{}",res.toString());
            return res.toString();
        }
    
        //将url返回的数据写入字符串
        private static String loadUrl(String url) {
            StringBuilder stringBuilder = new StringBuilder();
    
            try {
                //创建链接地址
                URL region = new URL(url);
                //访问链接URL地址
                URLConnection connection = region.openConnection();
                //读取url缓冲,这里需要使用GZIPInputStream压缩输入流读取连接的流
                BufferedReader reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(connection.getInputStream()),"utf-8"));
                //读取流写入字符串
                String inputLine = null;
                //读取到末尾
                while((inputLine = reader.readLine()) != null) {
                    stringBuilder.append(inputLine);
                }
                reader.close();
            } catch (Exception e) {
                log.error("出错了:{}",e.getMessage());
            }
            return stringBuilder.toString();
        }
    }
    
    • 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

    这里直接将封装的数据返回到前台,由前台进行读取【也可直接在前台访问】

    //对返回的数据进行处理
    if(response.code == 0) {
                        var address = JSON.parse(response.data);
                        $("#position").text(address.data.city);
                        //处理返回的JSON数据
                        var yesterday = address.data.yesterday;
    
                        var forecast = address.data.forecast;
                        //主体
                        var str = "

    " + address.data.wendu; str +=" ℃

    " + yesterday.low + "~" + yesterday.high ; str += "

    " + yesterday.type; str += "

    " + yesterday.fx + yesterday.fl; str += "

    "
    ; //预报 for(var i = 0; i < forecast.length; i++) { var day = forecast[i]; str += "

    " + day.date; str += "

    " + day.low + "~" + day.high + "

    " str += "

    " + day.type; str += "

    " + day.fengxiang + day.fengli; str += "

    "
    ; } $("#today").html("    " + yesterday.date); $("#weatherArea").append(str); }
    • 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

    最终可以在前台看到效果:

    在这里插入图片描述

    当然在具体的业务需求中,需要设计更加精美的样式,这里只是提供设计思路🖤

  • 相关阅读:
    MySQL 枚举类型如何定义比较好 tinyint?enum?varchar?
    推荐一个.Net Core开发的Websocket群聊、私聊的开源项目
    赛码网的输入规则(Jsv8)
    Java基础用Date类编写万年历
    这4个网站太厉害了, 每一个都能帮你打开新世界大门
    Harmony | 超好用的单细胞测序数据合并(3‘和5‘数据合并)(二)
    【无标题】
    Week 7 Learning Representation with Auto-Encoder(无监督学习)
    第2部分 路由器基本配置
    【进阶篇】MySQL的MVCC实现机制详解
  • 原文地址:https://blog.csdn.net/a23452/article/details/126476678