谷粒商城十六认证服务之注册和普通登录

环境搭建

mvn依赖在文章最下面

application.properties

spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server.port=20000

spring.thymeleaf.cache=false

spring.redis.host=192.168.56.10
spring.redis.port=6379

启动类

package com.atlinxi.gulimall.authserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallAuthServerApplication {
    
    

	public static void main(String[] args) {
    
    
		SpringApplication.run(GulimallAuthServerApplication.class, args);
	}

}

hosts和nginx

192.168.56.10	gulimall.com
192.168.56.10	search.gulimall.com
192.168.56.10	item.gulimall.com
192.168.56.10	auth.gulimall.com

nginx下的html文件夹创建login和reg两个文件夹,放登录和注册的静态资源。

验证码

config

package com.atlinxi.gulimall.authserver.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


/**
 * 发送一个请求直接跳转到一个页面,我们又不想controller中写这些空方法
 * SpringMvc viewcontroller:将请求和页面映射过来
 * @return
 */
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    
    



    /**
     * 直接跳转(渲染一个页面),无需写自定义的controller
     *
     *
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
    
    
        /**
         * * @GetMapping("/login.html")
         *      *     public String loginPage(){
         *      *
         *      *         return "login";
         *      *     }
         */
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

html

<a id="sendCode">发送验证码</a>
<input class="phone" maxlength="20" type="text" placeholder="建议使用常用手机">

function timeOutChangeStyle() {
    
    
				var  str = '10s后再次发送验证码';
				var num = 60
				countDown = setInterval(function () {
    
    
					$("#sendCode").attr("class","disabled");
					num--;
					if (num==0){
    
    
						$("#sendCode").text('发送验证码');
						clearInterval(countDown);
						$("#sendCode").attr("class","");
					}else {
    
    

						$("#sendCode").text(num + 's后再次发送')
					}

				},1000);
			}

			$("#sendCode").click(function () {
    
    


				// 2. 倒计时
				if (!$(this).hasClass("disabled")){
    
    
					phoneNum = $(".phone").val()
					// 1. 给指定手机号发送验证码

					// / 代表当前项目路径
					$.get({
    
    
						url: "/sms/sendCode",
						data:{
    
    "phone": phoneNum},
						dataType: "json",
						success:function(data){
    
    
							console.log("-------------------")
							console.log(data)
							if (data.code != 0){
    
    
								alert(data.msg);
							}
						}
					})
					timeOutChangeStyle();
				}





			});

阿里云发送短信 - 这段代码是第三方服务的

该接口不直接提供页面调用,而是提供各微服务调用。

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atlinxi.gulimall</groupId>
    <artifactId>gulimall-third-party</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-third-party</name>
    <description>第三方服务</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.atlinxi.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--        这个版本和springcloudalibaba的版本不统一,不知道以后会不会出什么问题-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <!--
            spring元数据处理器,加了就有提示了,不加也没任何问题

            optional 依赖不会被传递
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dysmsapi20170525</artifactId>
            <version>2.0.23</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea-openapi</artifactId>
            <version>0.2.8</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea-console</artifactId>
            <version>0.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea-util</artifactId>
            <version>0.2.16</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea</artifactId>
            <version>1.1.14</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <!--                之前用的一直是这个版本,springcloudoss依赖就是无法导入,换成下面那个就好了-->
                <version>2021.1</version>
                <!--                这是老师用的版本,导入服务就启动不了 了-->
                <!--                <version>2.1.0.RELEASE</version>-->
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

server:
  port: 30000
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    alicloud:
      access-key: 阿里云找自己的
      secret-key: 阿里云找自己的
      oss:
        endpoint: oss-cn-beijing.aliyuncs.com
        bucket: gulimall-linxi
  application:
    name: gulimall-third-party


sms:
  endPoint: dysmsapi.aliyuncs.com
  signName: 阿里云找自己的
  templateCode: 阿里云找自己的
  accessKeyId: 阿里云找自己的
  accessKeySecret: 阿里云找自己的


component

package com.atlinxi.gulimall.thirdparty.component;

import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import lombok.Data;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


/**
 * 下面的方式调用的话,不安全,我感觉阿里官方的意思是
 * 需要我们加一个自己的凭证(token),而且需要我们自己维护
 * 不知道老师会不会做这一步,会做的话再说吧,
 *
 */
@Data
@Component
@ConfigurationProperties(prefix = "sms")
public class SmsComponent implements InitializingBean {
    
    

    private static String endpoint;

    private String endPoint;

    private String signName;

    private String templateCode;

    private String accessKeyId;

    private String accessKeySecret;


    public static Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
    
    
        Config config = new Config()
                // 必填,您的 AccessKey ID
                .setAccessKeyId(accessKeyId)
                // 必填,您的 AccessKey Secret
                .setAccessKeySecret(accessKeySecret);
        // 访问的域名,下面的也不清楚是不是固定的,
        // 反正换成hangzhou这个是报 InvalidVersion
//        config.endpoint = "ecs-cn-hangzhou.aliyuncs.com";
//        config.endpoint = "dysmsapi.aliyuncs.com";
        config.endpoint = endpoint;
        return new Client(config);

    }





    public void sendSmsCode(String phone,String code) throws Exception {
    
    
        // 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html
        Client client = SmsComponent.createClient(accessKeyId, accessKeySecret);
        SendSmsRequest sendSmsRequest = new SendSmsRequest()
                .setPhoneNumbers(phone)
                .setSignName(signName)
                .setTemplateCode(templateCode)
                .setTemplateParam("{\"code\":" + code + "}");

        RuntimeOptions runtimeOptions = new RuntimeOptions();
        // 复制代码运行请自行打印 API 的返回值
        SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtimeOptions);
        SendSmsResponseBody respBody = sendSmsResponse.getBody();
        String respMessage = respBody.getMessage();
        String respCode = respBody.getCode();
        String respBizId = respBody.getBizId();
        String respRequestId = respBody.getRequestId();
        System.out.println(respMessage + "=" + respCode + "=" + respBizId + "=" + respRequestId);
//        com.aliyun.teaconsole.Client.log(Common.toJSONString(sendSmsResponse));

//        try {
    
    
//            RuntimeOptions runtimeOptions = new RuntimeOptions();
//            // 复制代码运行请自行打印 API 的返回值
//            SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtimeOptions);
//
//            System.out.println(sendSmsResponse);
//
//        } catch (TeaException error) {
    
    
//            // 如有需要,请打印 error
//            Common.assertAsString(error.message);
//        } catch (Exception _error) {
    
    
//            TeaException error = new TeaException(_error.getMessage(), _error);
//            // 如有需要,请打印 error
//            Common.assertAsString(error.message);
//        }
    }


    /**
     *
     * @ConfigurationProperties 不能为static成员变量注入配置文件中的值
     *
     * InitializingBean是Spring提供的拓展性接口,InitializingBean接口为bean提供了属性初始化后的处理方法,
     * 它只有一个afterPropertiesSet方法,凡是继承该接口的类,在bean的属性初始化后都会执行该方法。
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
    
    
        SmsComponent.endpoint = this.endPoint;
    }
}

controller

package com.atlinxi.gulimall.thirdparty.controller;

import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.thirdparty.component.SmsComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/sms")
public class SmsSendController {
    
    


    @Autowired
    SmsComponent smsComponent;


    /**
     * 提供给别的服务进行调用
     * @param phone
     * @param code
     * @return
     */
    @GetMapping("/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) throws Exception {
    
    

        smsComponent.sendSmsCode(phone,code);

        return R.ok();
    }
}

认证服务

controller

package com.atlinxi.gulimall.authserver.controller;

import com.alibaba.fastjson.TypeReference;
import com.atlinxi.common.constant.AuthServerConstant;
import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.authserver.feign.MemberFeignService;
import com.atlinxi.gulimall.authserver.feign.ThirdPartFeignService;
import com.atlinxi.gulimall.authserver.vo.UserRegistVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Controller
public class LoginController {
    
    

    @Autowired
    ThirdPartFeignService thirdPartFeignService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private MemberFeignService memberFeignService;


    @ResponseBody
    @GetMapping("/sms/sendCode")
    public R sendCode(@RequestParam("phone") String phone) throws Exception {
    
    


        // todo 1. 接口防刷(浏览器f12可以看到后端请求地址)
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if (!StringUtils.isEmpty(redisCode)) {
    
    
            long l = Long.parseLong(redisCode.split("_")[1]);
            if (System.currentTimeMillis() - l < 60 * 1000) {
    
    
                // 60s内不能再发
                return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMessage());
            }
        }


        String code = UUID.randomUUID().toString().substring(0, 5) + "_" + System.currentTimeMillis();
        // 2. 验证码的再次校验,存到redis,因为不是永久的
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, code, 10, TimeUnit.MINUTES);
        thirdPartFeignService.sendCode(phone, code.split("_")[0]);
        return R.ok();
    }




    /**
     * 这里跳转是reg.html点击跳转的,不是浏览器直接访问的,所以需要重定向
     * <p>
     * BindingResult 校验的结果
     *
     * @param vo
     * @param result
     * @return
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result, /**Model model*/RedirectAttributes redirectAttributes) {
    
    

        // 校验出错,转发到注册页
        if (result.hasErrors()) {
    
    


//         result.getFieldErrors().stream().map(fieldError -> {
    
    
//                          String field = fieldError.getField();
//                          String defaultMessage = fieldError.getDefaultMessage();
//                          errors.put(field,defaultMessage);
            Map<String, String> errors = new HashMap<>();

            try {
    
    

                errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            }catch (IllegalStateException e){
    
    

                List<FieldError> fieldErrors = result.getFieldErrors();

                for (FieldError fieldError : fieldErrors) {
    
    
                    String field = fieldError.getField();
                    String defaultMessage = fieldError.getDefaultMessage();
                    errors.put(field,defaultMessage);
                }

            }

            // 例如 code 验证码必须提交
//            model.addAttribute("errors", errors);
            // addFlashAttribute 代表里面的属性只需要取一次
            redirectAttributes.addFlashAttribute("errors",errors);
            // Request method 'POST' not supported
            // 用户注册 -> /regist(post) -> 数据校验发现有错误
            //  -> 转发 /reg.html(GulimallWebConfig路径映射默认都是GET方式访问的)

            // 转发就是将原请求原封不动转给下一个人,然而下一个人要求的请求方式是GET方式,就不支持POST了



            // reg 是拼接 templates .html
            // forward和 redirect是视图解析器不拼接,通过GulimallWebConfig映射找到页面
//            return "forward:/reg.html";
//            return "reg"; 是一个转发的方式,浏览器的地址不会改变,再刷新一次,表单又会被提交一次
            // 所以我们选择重定向的方式
            // 请求转发时,model是在请求域中的,我们可以获取到
            // 重定向获取不到,使用RedirectAttributes,作用就是重定向时携带参数

            // RedirectAttributes 利用session原理,将数据放在session中,
            // 只要跳到下一个页面取出这个数据以后,session里面的数据就会删掉
            // 例如错误信息跳转过去后会显示,但是再刷新一次浏览器就不会显示了

			
            // todo 分布式下的session问题
            // 这个和上面那些注释没关系,不要误会

            return "redirect:http://auth.gulimall.com/reg.html";
        }



        // 真正注册,调用远程服务进行注册
        // 1. 校验验证码
        String code = vo.getCode();
        String phone = vo.getPhone();
        String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);

        if (!StringUtils.isEmpty(s)){
    
    
            if (code.equals(s.split("_")[0])){
    
    
                // 验证码通过
                // 删除验证码
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);

                // 真正注册。远程调用会员服务进行注册
                R r = memberFeignService.regist(vo);

                // code为0是成功
                if (r.getCode() == 0){
    
    
                    // 成功
                    return "redirect:http://auth.gulimall.com/login.html";
                }else {
    
    
                    Map<String,String> errors = new HashMap<>();
                    errors.put("msg",r.getData(new TypeReference<String>(){
    
    }));
                    redirectAttributes.addFlashAttribute("errors",errors);
                    return "redirect:http://auth.gulimall.com/reg.html";
                }



            }else {
    
    
                Map<String,String> errors = new HashMap<>();
                errors.put("code","验证码错误");
                redirectAttributes.addFlashAttribute("errors",errors);

                return "redirect:http://auth.gulimall.com/reg.html";
            }
        }else {
    
    

            Map<String,String> errors = new HashMap<>();
            errors.put("code","验证码错误");
            redirectAttributes.addFlashAttribute("errors",errors);

            return "redirect:http://auth.gulimall.com/reg.html";
        }


        // 注册成功返回到首页

        // return "redirect:http://auth.gulimall.com/login.html";
        // 因为我们在GulimallWebConfig映射了登录地址
        // 所以可以这么写,/ 代表项目路径
        // 但是这么写的话,它就是一个不知道什么ip+请求路径了,不是我们的域名
//        return "redirect:/login.html";
    }

}

feign

package com.atlinxi.gulimall.authserver.feign;

import com.atlinxi.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
    
    

    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) throws Exception;
}

vo

package com.atlinxi.gulimall.authserver.vo;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

@Data
public class UserRegistVo {
    
    

    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6,max = 18,message = "用户名必须是6-18位字符")
    private String userName;

    @NotEmpty(message = "密码必须提交")
    @Length(min = 6,max = 18,message = "密码必须是6-18位字符")
    private String password;

    // 以1开始,第二位3-9,剩下的9位0-9
    @NotEmpty(message = "手机号必须提交")
    @Pattern(regexp = "^[1][3-9][0-9]{9}$",message = "手机号格式不正确")
    private String phone;


    @NotEmpty(message = "验证码必须提交")
    private String code;

}

common服务

package com.atlinxi.common.exception;

/* * 错误码和错误信息定义类 *
 * 1. 错误码定义规则为 5 为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知 异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式 *
 *
 * 错误码列表:
 *      10: 通用
 *          001:参数格式校验
 *          002: 短信验证码频率太高
 *      11: 商品
 *      12: 订单
 *      13: 购物车
 *      14: 物流
 *      15:用户(会员)
 *
 *
 **/
public enum BizCodeEnume {
    
    

    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002,"短信验证码获取频率太高,稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已存在"),
    LOGINACCT_PASSWORD_INVALID_EXCEPTION(15003,"账号或密码错误");

    private int code;
    private String message;

    BizCodeEnume(int code, String message) {
    
    
        this.code = code;
        this.message = message;
    }

    public int getCode() {
    
    
        return code;
    }

    public String getMessage() {
    
    
        return message;
    }
}







package com.atlinxi.common.constant;

public class AuthServerConstant {
    
    

    public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";



}

异常机制

之前service在完成业务的处理后,总是会返回正确或者错误的信息给到controller,
用户注册,我们需要校验用户名或者手机号是否唯一,只要有一个不唯一,则不允许注册。

在这里我们使用异常机制来处理这个问题,一旦校验失败则直接向controller抛出异常。

自定义异常

package com.atlinxi.gulimall.member.exception;

public class PhoneExistException extends RuntimeException {
    
    

    public PhoneExistException() {
    
    
        super("手机号存在");
    }
}



package com.atlinxi.gulimall.member.exception;

public class UsernameExistException extends RuntimeException {
    
    

    public UsernameExistException() {
    
    
        super("用户名存在");
    }
}


service

package com.atlinxi.gulimall.member.service.impl;

import com.atlinxi.gulimall.member.dao.MemberLevelDao;
import com.atlinxi.gulimall.member.entity.MemberLevelEntity;
import com.atlinxi.gulimall.member.exception.PhoneExistException;
import com.atlinxi.gulimall.member.exception.UsernameExistException;
import com.atlinxi.gulimall.member.vo.MemberRegistVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;

import com.atlinxi.gulimall.member.dao.MemberDao;
import com.atlinxi.gulimall.member.entity.MemberEntity;
import com.atlinxi.gulimall.member.service.MemberService;


@Override
    public void regist(MemberRegistVo memberRegistVo) {
    
    
	
		MemberEntity entity = new MemberEntity();
		// 检查用户名或手机号是否唯一。为了让controller能感知异常,异常机制。
        checkPhoneUnique(memberRegistVo.getPhone());
        checkUsernameUnique(memberRegistVo.getUserName());
}


// 接口也同样需要声明异常
@Override
    public void checkPhoneUnique(String phone) throws PhoneExistException{
    
    
        Integer mobile = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (mobile>0){
    
    
            throw new PhoneExistException();
        }

    }


    @Override
    public void checkUsernameUnique(String username) throws UsernameExistException {
    
    
        Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));

        if (count>0){
    
    
            throw  new UsernameExistException();
        }


    }

controller

@PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo memberRegistVo){
    
    


        try {
    
    
            memberService.regist(memberRegistVo);
        } catch (PhoneExistException e) {
    
    

            return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMessage());

        }catch(UsernameExistException e){
    
    
            return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(), BizCodeEnume.USER_EXIST_EXCEPTION.getMessage());
        }

        return R.ok();
    }

common枚举

package com.atlinxi.common.exception;

/* * 错误码和错误信息定义类 *
 * 1. 错误码定义规则为 5 为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知 异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式 *
 *
 * 错误码列表:
 *      10: 通用
 *          001:参数格式校验
 *          002: 短信验证码频率太高
 *      11: 商品
 *      12: 订单
 *      13: 购物车
 *      14: 物流
 *      15:用户(会员)
 *
 *
 **/
public enum BizCodeEnume {
    
    

    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002,"短信验证码获取频率太高,稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已存在");

    private int code;
    private String message;

    BizCodeEnume(int code, String message) {
    
    
        this.code = code;
        this.message = message;
    }

    public int getCode() {
    
    
        return code;
    }

    public String getMessage() {
    
    
        return message;
    }
}

密码加密

如果非法人员通过爆破我们的数据库,得到了我们的账号密码,所以肯定不能存明文

密文分两种,可逆和不可逆

  • 可逆:通过密文可以推算出它原来的明文,假设我知道它的加密算法
  • 不可逆:我即使知道它的算法和密文,也推断不出明文

密码存为不可逆的才合理。

MD5盐值加密

严格意义上讲,MD5不算一个加密算法,是一个信息摘要算法

例如有一大段文本,MD5可以根据文本特征值得出一个固定长度的MD5值,而且这个文本长度里面只要有一个字节发生了变化,MD5就会发生变化,所以它只是一个信息摘要算法。

  • MD5(原始数据一样,算出的MD5值一定一样)
    • Message Digest algorithm 5,信息摘要算法
      • 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
      • 容易计算:从原数据计算出MD5值很容易。
      • 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
      • 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
      • 不可逆
  • 加盐:
    • 通过生成随机数与MD5生成字符串进行组合
    • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

MD5为什么不可逆?

因为MD5是一个消息摘要,它会损失部分原数据,所以我们不能通过打散的原数据计算得到的MD5值推断出原数据。

为什么不能直接使用MD5来加密?
基于抗修改性得出一样的原始数据加密的MD5值一定一样,就可以做一张彩虹表(把加密好的MD5值和原始数据做映射),进行暴力破解,为了防止暴力破解,不能直接使用MD5来对密码进行加密。

网上有很多MD5解密的网站,但是只能解密一些简单的,例如123456,稍微复杂一些就不行了,例如asdsdgfdgdf,所以MD5解密是伪概念
如果非抬杠,就要把世界上所有的数据都用MD5加密一遍存在数据库里用于解密,百度说的理论时间是几万年。。。。。。

MD5的应用场景

百度网盘的秒传功能,上传的时候先计算文件的MD5值,在整个百度网盘的数据库里进行匹配,如果有相同的MD5值,就可以直接拿过来用了。

简单测试

package com.atlinxi.gulimall.member;

import org.apache.commons.codec.digest.Md5Crypt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

//@SpringBootTest
class GulimallMemberApplicationTests {
    
    

    @Test
    void contextLoads() {
    
    
        // MD5加密
        // e10adc3949ba59abbe56e057f20f883e
        String s = DigestUtils.md5Hex("123456");
        System.out.println(s);

        // MD5 盐值加密
        // $1$12345678$a4ge4d5iJ5vwvbFS88TEN0
        // 如果不给盐的话,md5Crypt会自动生成
        // $1$ + 8位字符,固定写法
        String s2 = Md5Crypt.md5Crypt("123456".getBytes(),"$1$12345678");
        System.out.println(s2);


        // 我们使用spring提供的密码加密器,来对密码进行加密和验证
        // 对盐值的操作进行了封装,无需我们在数据库专门定义一个盐值字段
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        // 两次加密的格式都不一样
        // $2a$10$15iQb8noKWeEEBld0M6J5O7Ovjr.IsIGrVKm4ucZLGEPW5/E15Pky
        // $2a$10$l/GX4cIoDVl9S2SC5z.YmuvaBfbRBtLgNiRysh.7Zkz.J36LxlGPu
        String encode = bCryptPasswordEncoder.encode("123456");
        // true
        boolean matches = bCryptPasswordEncoder
                .matches("123456",
                        "$2a$10$15iQb8noKWeEEBld0M6J5O7Ovjr.IsIGrVKm4ucZLGEPW5/E15Pky");

        // true
        boolean matches2 = bCryptPasswordEncoder
                .matches("123456",
                        "$2a$10$l/GX4cIoDVl9S2SC5z.YmuvaBfbRBtLgNiRysh.7Zkz.J36LxlGPu");

        System.out.println(encode);
        System.out.println(matches);
        System.out.println(matches2);
    }




}

普通登录

普通登录是相较于OAuth2.0和单点登录来说的

vo

package com.atlinxi.gulimall.authserver.vo;

import lombok.Data;

@Data
public class UserLoginVo {
    
    

    private String loginacct;
    private String password;

}






package com.atlinxi.gulimall.member.vo;

import lombok.Data;

@Data
public class MemberLoginVo {
    
    


    private String loginacct;
    private String password;
}

controller

package com.atlinxi.gulimall.authserver.controller;

import com.alibaba.fastjson.TypeReference;
import com.atlinxi.common.constant.AuthServerConstant;
import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.authserver.feign.MemberFeignService;
import com.atlinxi.gulimall.authserver.feign.ThirdPartFeignService;
import com.atlinxi.gulimall.authserver.vo.UserLoginVo;
import com.atlinxi.gulimall.authserver.vo.UserRegistVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Controller
public class LoginController {
    
    

@PostMapping("/login")
    public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){
    
    

        R login = memberFeignService.login(vo);

        if (login.getCode()==0){
    
    

            // 成功
            return "redirect:http://gulimall.com";
        }else {
    
    
            Map<String,String> errors = new HashMap<>();
            errors.put("msg",login.getData("msg",new TypeReference<String>(){
    
    }));
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }

    }

}










package com.atlinxi.gulimall.member.controller;

import java.util.Arrays;
import java.util.Map;

import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.gulimall.member.exception.PhoneExistException;
import com.atlinxi.gulimall.member.exception.UsernameExistException;
import com.atlinxi.gulimall.member.feign.CouponFeignService;
import com.atlinxi.gulimall.member.vo.MemberLoginVo;
import com.atlinxi.gulimall.member.vo.MemberRegistVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;

import com.atlinxi.gulimall.member.entity.MemberEntity;
import com.atlinxi.gulimall.member.service.MemberService;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.R;



/**
 * 会员
 *
 * @author linxi
 * @email [email protected]
 * @date 2022-10-13 17:20:57
 */
@RefreshScope
@RestController
@RequestMapping("member/member")
public class MemberController {
    
    

@PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo){
    
    

        MemberEntity memberEntity = memberService.login(vo);

        if (memberEntity!=null){
    
    
            return R.ok();
        }else {
    
    
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getCode(),
                    BizCodeEnume.LOGINACCT_PASSWORD_INVALID_EXCEPTION.getMessage());
        }


    }

service

package com.atlinxi.gulimall.member.service.impl;

import com.atlinxi.gulimall.member.dao.MemberLevelDao;
import com.atlinxi.gulimall.member.entity.MemberLevelEntity;
import com.atlinxi.gulimall.member.exception.PhoneExistException;
import com.atlinxi.gulimall.member.exception.UsernameExistException;
import com.atlinxi.gulimall.member.vo.MemberLoginVo;
import com.atlinxi.gulimall.member.vo.MemberRegistVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;

import com.atlinxi.gulimall.member.dao.MemberDao;
import com.atlinxi.gulimall.member.entity.MemberEntity;
import com.atlinxi.gulimall.member.service.MemberService;


@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {
    
    

@Override
    public MemberEntity login(MemberLoginVo vo) {
    
    
        String loginacct = vo.getLoginacct();
        String password = vo.getPassword();
        MemberEntity entity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct)
                .or().eq("mobile", loginacct));

        if (entity==null){
    
    
            // 登录失败
            return null;
        }else {
    
    
            String passwordDb = entity.getPassword();

            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            boolean matches = passwordEncoder.matches(password, passwordDb);
            if (matches){
    
    
                return entity;
            }else {
    
    
                return null;
            }
        }

    }

html

<form action="/login" method="post">
						<div style="color: red" th:text="${errors!=null && (#maps.containsKey(errors,'msg'))?errors.msg:''}"></div>
						<ul>
							<li class="top_1">
								<img src="/static/login/JD_img/user_03.png" class="err_img1" />
								<input type="text" name="loginacct" placeholder=" 邮箱/用户名/已验证手机" class="user" />
							</li>
							<li>
								<img src="/static/login/JD_img/user_06.png" class="err_img2" />
								<input type="password" name="password" placeholder=" 密码" class="password" />
							</li>
							<li class="bri">
								<a href="/static/login/index.html">忘记密码</a>
							</li>
							<li class="ent"><button type="submit" class="btn2">&nbsp; &nbsp;</a></button></li>
						</ul>
					</form>

mvn依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atlinxi.gulimall</groupId>
    <artifactId>gulimall-third-party</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-third-party</name>
    <description>第三方服务</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.atlinxi.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--        这个版本和springcloudalibaba的版本不统一,不知道以后会不会出什么问题-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>



        <!--
            spring元数据处理器,加了就有提示了,不加也没任何问题

            optional 依赖不会被传递
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dysmsapi20170525</artifactId>
            <version>2.0.23</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea-openapi</artifactId>
            <version>0.2.8</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea-console</artifactId>
            <version>0.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea-util</artifactId>
            <version>0.2.16</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>tea</artifactId>
            <version>1.1.14</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <!--                之前用的一直是这个版本,springcloudoss依赖就是无法导入,换成下面那个就好了-->
                <version>2021.1</version>
                <!--                这是老师用的版本,导入服务就启动不了 了-->
                <!--                <version>2.1.0.RELEASE</version>-->
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>








<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atlinxi.gulimall</groupId>
    <artifactId>gulimall-member</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-member</name>
    <description>谷粒商城-会员服务</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.atlinxi.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
<!--        由于SpringCloud Feign高版本不使用Ribbon而是使用spring-cloud-loadbalancer,
            所以需要引用spring-cloud-loadbalancer或者降版本-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>









<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.atlinxi.gulimall</groupId>
	<artifactId>gulimall-auth-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>gulimall-auth-server</name>
	<description>认证中心(社交登录、OAuth2.0、单点登录)</description>
	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>2020.0.4</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
		<!--        由于SpringCloud Feign高版本不使用Ribbon而是使用spring-cloud-loadbalancer,
            所以需要引用spring-cloud-loadbalancer或者降版本-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-loadbalancer</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.atlinxi.gulimall</groupId>
			<artifactId>gulimall-common</artifactId>
			<version>0.0.1-SNAPSHOT</version>
			<exclusions>
				<exclusion>
					<groupId>com.baomidou</groupId>
					<artifactId>mybatis-plus-boot-starter</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

怡婷很悲愤,她知道 的比世界上任何一个小孩都来得多,但是她永远不能得知一个自知貌美的女子走在路上低眉敛首的心情。

房思琪的初恋乐园
林奕含

Guess you like

Origin blog.csdn.net/weixin_44431371/article/details/129333839