天天看點

Day421&422.認證服務 -谷粒商城認證服務

認證服務

一、初始化

  • 建立認證子產品
Day421&422.認證服務 -谷粒商城認證服務
Day421&422.認證服務 -谷粒商城認證服務
  • 統一springboot版本

    2.2.1.RELEASE

    ,并引入Common服務依賴,因為不操作資料庫,是以排除mybaitsplus依賴
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
</properties>
<dependency>
    <groupId>com.achang.achangmall</groupId>
    <artifactId>achangmall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <exclusions>
        <exclusion>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
           
  • application.properties
spring.application.name=achang-auth-server
spring.cloud.nacos.server-addr=127.0.0.1:8848
server.port=20000
spring.thymeleaf.cache=false
           
  • com.achang.achangmall.auth.AchangAuthServerApplication
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class AchangAuthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AchangAuthServerApplication.class, args);
    }
}
           
  • 啟動服務
Day421&amp;422.認證服務 -谷粒商城認證服務
  • 發現服務注冊進 Nacos
Day421&amp;422.認證服務 -谷粒商城認證服務
  • 拉入登入頁面,将資料進階篇登入頁面和注冊頁面放到 templates 下,并改名為login、reg.html
  • 為了測試直接通路登入頁,把login.html改名為index.html
Day421&amp;422.認證服務 -谷粒商城認證服務
  • C:\Windows\System32\drivers\etc\hosts,添加本地域名映射
192.168.109.101 auth.achangmall.com
           
Day421&amp;422.認證服務 -谷粒商城認證服務
  • 靜态檔案可以選擇 Nginx 動靜分離配置
Day421&amp;422.認證服務 -谷粒商城認證服務
  • 修改reg.html、login.html裡面的路徑引用
  • 添加網關轉發配置,achangmall-gateway/src/main/resources/application.yml
- id: auth_route
          uri: lb://achang-auth-server
          predicates:
            - Host=auth.achangmall.com
           
  • 啟動網關服務AchangmallGatewayApplication +AchangAuthServerApplication 測試轉發效果

通路

http://auth.achangmall.com/

,通路成功!!!

Day421&amp;422.認證服務 -谷粒商城認證服務
  • 修改product服務的注冊和登入的uri
Day421&amp;422.認證服務 -谷粒商城認證服務
  • com.achang.achangmall.auth.controller.LoginController
@Controller
public class LoginController {
    @GetMapping("/login.html")
    public String loginPage(){
        return "login";
    }

    @GetMapping("/reg.html")
    public String register(){
        return "reg";
    }
}
           
  • achang-auth-server/src/main/resources/templates/login.html
    Day421&amp;422.認證服務 -谷粒商城認證服務
  • achang-auth-server/src/main/resources/templates/reg.html
Day421&amp;422.認證服務 -谷粒商城認證服務

二、短信驗證碼

  • 前端驗證碼代碼
$(function (){
    $("#sendCode").click(function (){
        if ($(this).hasClass("disabled")){
            //todo 發送手機驗證碼業務
        }{
            timeoutChangeSytle()
        }
    });
})
var num = 60;
function timeoutChangeSytle(){
    $("#sendCode").attr("class","disabled")
    if (num==0){
        $("#sendCode").text("發送驗證碼")
        num = 60;
        $("#sendCode").attr("class","")
    }else {
        var str = num+"後再次發送"
        $("#sendCode").text(str)
        setTimeout("timeoutChangeSytle()",1000)
    }
    num--;
}
           
  • 直接通過mvc做

    視圖映射

com.achang.achangmall.auth.conf.AchangWebConfig

@Configuration
public class AchangWebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

           
  • 購買短信第三方服務api

    https://market.aliyun.com/products/57126001/cmapi00040066.html

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-IIijPFAa-1634478751848)(C:/Users/PePe/AppData/Roaming/Typora/typora-user-images/image-20211017162832535.png)]

  • 工具類httputils

https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java

  • 解耦配置檔案
spring:
  application:
    name: achangmall-third-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    alicloud:
      sms:
        host: https://intlsms.market.alicloudapi.com
        path: /comms/sms/sendmsgall
        method: POST
        appcode: 你的appcode
        channel: 0
        templateID: '0000000'
           
  • 封裝發送驗證碼元件
@Component
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data
public class SmsComponent {
    private String host;
    private String path;
    private String method;
    private String appcode;
    private String channel;
    private String templateID;

    public void sendCode(String phone,String code){
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Authorization", "APPCODE " + appcode);
        //根據API的要求,定義相對應的Content-Type
        headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
        Map<String, String> querys = new HashMap<String, String>();
        Map<String, String> bodys = new HashMap<String, String>();
        bodys.put("callbackUrl", "http://test.dev.esandcloud.com");
        bodys.put("channel", channel);
        bodys.put("mobile", "+86"+phone);
        bodys.put("templateID", templateID);
        bodys.put("templateParamSet", code+", 1");
        
        try {
            HttpResponse  response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            System.out.println(response.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           
  • 測試
@Test
public void test(){
    smsComponent.sendCode("13567790741","6379");
}
           
  • 發送短信接口

com.achang.achangmall.controller.SmsSendController

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

    @Resource
    private SmsComponent smsComponent;

    /**
     * 提供給别的服務進行調用
     * @param phone
     * @param code
     * @return
     */
    @GetMapping(value = "/sendCode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) {
        //發送驗證碼
        smsComponent.sendCode(phone,code);
        return R.ok();
    }

}
           
  • com.achang.achangmall.auth.feign.ThirdPartFeignService
@FeignClient("achangmall-third-service")
public interface ThirdPartFeignService {
    @GetMapping(value = "/sms/sendCode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
           
  • achang-auth-server/src/main/resources/templates/reg.html
$(function (){
    $("#sendCode").click(function (){
        if ($(this).hasClass("disabled")){
        }{
            var phone = $("#phoneNum").val();
            $.get("/sms/sendCode?phone="+phone);
            timeoutChangeSytle()
        }
    });
})
           
  • 引入redis依賴

achang-auth-server/pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
           
  • redis配置
spring.redis.port=6379
spring.redis.host=192.168.109.101
           
  • com.achang.achangmall.auth.controller.LoginController
@Controller
public class LoginController {
    @Autowired
    private ThirdPartFeignService thirdPartFeignService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


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

        //防止接口幂等性操作
        String phoneRedisStr = stringRedisTemplate.opsForValue().get("sms:code:"+phone);
        if (!StringUtils.isEmpty(phoneRedisStr)){
            //活動存入redis的時間,用目前時間減去存入redis的時間,判斷使用者手機号是否在60s内發送驗證碼
            long currentTime = Long.parseLong(phoneRedisStr.split("_")[1]);
            if (System.currentTimeMillis() - currentTime < 60000) {
                //60s内不能再發
                return R.error("發送頻率過多,請稍後重試");
            }
        }

        String code = UUID.randomUUID().toString().substring(0, 5);
        String redisStorage = code + "_" + System.currentTimeMillis();

        //存入redis,防止同一個手機号在60秒内再次發送驗證碼
        stringRedisTemplate.opsForValue().set("sms:code:"+ phone,
                redisStorage, 10, TimeUnit.MINUTES);

        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }
}
           

三、登入注冊功能

  • com.achang.achangmall.auth.vo.UserRegisterVo

通過注解可以給前端傳遞過來的值進行校驗,例如:

@Data
public class UserRegisterVo {

    @NotEmpty(message = "使用者名不能為空")
    @Length(min = 6, max = 19, message="使用者名長度在6-18字元")
    private String userName;

    @NotEmpty(message = "密碼必須填寫")
    @Length(min = 6,max = 18,message = "密碼必須是6—18位字元")
    private String password;

    @NotEmpty(message = "手機号不能為空")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手機号格式不正确")
    private String phone;

    @NotEmpty(message = "驗證碼不能為空")
    private String code;

}
           

但是這個注解必須配合

@Valid

使用,完成對參數的校驗:

Day421&amp;422.認證服務 -谷粒商城認證服務
  • com.achang.achangmall.auth.controller.LoginController
/**
     *
     * TODO: 重定向攜帶資料:利用session原理,将資料放在session中。
     * TODO:隻要跳轉到下一個頁面取出這個資料以後,session裡面的資料就會删掉
     * TODO:分布下session問題
     * RedirectAttributes:重定向也可以保留資料,不會丢失
     * 使用者注冊
     * @return
     */
@PostMapping(value = "/register")
public String register(@Valid UserRegisterVo vos, BindingResult result,
                       RedirectAttributes attributes) {

    //如果有錯誤回到注冊頁面
    if (result.hasErrors()) {
        Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        attributes.addFlashAttribute("errors",errors);

        //效驗出錯回到注冊頁面
        return "redirect:http://auth.achangmall.com/reg.html";
    }

    //1、效驗驗證碼
    String code = vos.getCode();

    //擷取存入Redis裡的驗證碼
    String redisCode = stringRedisTemplate.opsForValue().get("sms:code:" + vos.getPhone());
    if (!StringUtils.isEmpty(redisCode)) {
        //截取字元串
        if (code.equals(redisCode.split("_")[0])) {
            //删除驗證碼;令牌機制
            stringRedisTemplate.delete("sms:code:"+vos.getPhone());
            //驗證碼通過,真正注冊,調用遠端服務進行注冊
            R register = memberFeignService.register(vos);
            if (register.getCode() == 0) {
                //成功
                return "redirect:http://auth.achangmall.com/login.html";
            } else {
                //失敗
                Map<String, String> errors = new HashMap<>();
                errors.put("msg", register.getData("msg",new TypeReference<String>(){}));
                attributes.addFlashAttribute("errors",errors);
                return "redirect:http://auth.achangmall.com/reg.html";
            }


        } else {
            //效驗出錯回到注冊頁面
            Map<String, String> errors = new HashMap<>();
            errors.put("code","驗證碼錯誤");
            attributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.achangmall.com/reg.html";
        }
    } else {
        //效驗出錯回到注冊頁面
        Map<String, String> errors = new HashMap<>();
        errors.put("code","驗證碼錯誤");
        attributes.addFlashAttribute("errors",errors);
        return "redirect:http://auth.achangmall.com/reg.html";
    }
}
           
  • com.achang.achangmall.auth.feign.MemberFeignService
@FeignClient("achangmall-member")
public interface MemberFeignService {

    @PostMapping(value = "/member/member/register")
    R register(@RequestBody UserRegisterVo vo);
}
           
  • com.achang.achangmall.member.service.impl.MemberServiceImpl
@Override
public void register(MemberUserRegisterVo vo) {

    MemberEntity memberEntity = new MemberEntity();

    //設定預設等級
    MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
    memberEntity.setLevelId(levelEntity.getId());

    //設定其它的預設資訊
    //檢查使用者名和手機号是否唯一。感覺異常,異常機制
    checkPhoneUnique(vo.getPhone());
    checkUserNameUnique(vo.getUserName());

    memberEntity.setNickname(vo.getUserName());
    memberEntity.setUsername(vo.getUserName());
    //密碼進行MD5加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode = bCryptPasswordEncoder.encode(vo.getPassword());
    memberEntity.setPassword(encode);
    memberEntity.setMobile(vo.getPhone());
    memberEntity.setGender(0);
    memberEntity.setCreateTime(new Date());

    //儲存資料
    this.baseMapper.insert(memberEntity);
}
public void checkPhoneUnique(String phone) throws Exception {

    Integer phoneCount = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));

    if (phoneCount > 0) {
        throw new Exception();
    }

}

public void checkUserNameUnique(String userName) throws Exception {

    Integer usernameCount = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));

    if (usernameCount > 0) {
        throw new Exception();
    }
}
           
  • achangmall-member/src/main/resources/mapper/member/MemberLevelDao.xml
<select id="getDefaultLevel" resultType="com.achang.achangmall.member.entity.MemberLevelEntity">
    SELECT * FROM ums_member_level WHERE default_status = 1
</select>
           

四、MD5&MD5鹽值加密

Message Digest algorithm 5,資訊摘要算法

  • 壓縮性:任意長度的資料,算出的 MD5 值長度都是固定的;
  • 容易計算:從原資料計算出 MD5 值很容易;
  • 抗修改性:對原資料進行任何改動,哪怕隻修改 1 個位元組,所得到的 MD5 值都有很大差別;
  • 強抗碰撞:想找到兩個不同的資料,使它們具有相同的 MD5 值是非常困難的;
  • 不可逆
@Test
void contextLoads() {
    String s = DigestUtils.md5Hex("123456");
    System.out.println(s);
    // 鹽值加密
    System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));
    System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$qqqqqqqq"));

    // Spring 鹽值加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    //$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S
    //$2a$10$cR3lis5HQQsQSSh8/c3L3ujIILXkVYmlw28vLA39xz4mHDN/NBVUi
    String encode = bCryptPasswordEncoder.encode("123456");
    boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");

    System.out.println(encode + "==>" + matches);
}
           

五、登入功能

  • com.achang.achangmall.auth.vo.UserLoginVo
@Data
public class UserLoginVo {
    private String loginacct;
    private String password;
}
           
  • com.achang.common.vo.MemberResponseVo
@ToString
@Data
public class MemberResponseVo implements Serializable {

    private static final long serialVersionUID = 5573669251256409786L;

    private Long id;
    /**
     * 會員等級id
     */
    private Long levelId;
    /**
     * 使用者名
     */
    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 昵稱
     */
    private String nickname;
    /**
     * 手機号碼
     */
    private String mobile;
    /**
     * 郵箱
     */
    private String email;
    /**
     * 頭像
     */
    private String header;
    /**
     * 性别
     */
    private Integer gender;
    /**
     * 生日
     */
    private Date birth;
    /**
     * 所在城市
     */
    private String city;
    /**
     * 職業
     */
    private String job;
    /**
     * 個性簽名
     */
    private String sign;
    /**
     * 使用者來源
     */
    private Integer sourceType;
    /**
     * 積分
     */
    private Integer integration;
    /**
     * 成長值
     */
    private Integer growth;
    /**
     * 啟用狀态
     */
    private Integer status;
    /**
     * 注冊時間
     */
    private Date createTime;

    /**
     * 社交登入UID
     */
    private String socialUid;

    /**
     * 社交登入TOKEN
     */
    private String accessToken;

    /**
     * 社交登入過期時間
     */
    private long expiresIn;

}
           
  • com.achang.achangmall.auth.controller.LoginController
@GetMapping(value = "/login.html")
public String loginPage(HttpSession session) {

    //從session先取出來使用者的資訊,判斷使用者是否已經登入過了
    Object attribute = session.getAttribute("loginUser");
    //如果使用者沒登入那就跳轉到登入頁面
    if (attribute == null) {
        return "login";
    } else {
        return "redirect:http://achangmall.com";
    }
}

@PostMapping(value = "/login")
public String login(UserLoginVo vo, RedirectAttributes attributes, HttpSession session) {
    //遠端登入
    R login = memberFeignService.login(vo);

    if (login.getCode() == 0) {
        MemberResponseVo data = login.getData("data", new TypeReference<MemberResponseVo>() {});
        session.setAttribute("loginUser",data);
        return "redirect:http://gulimall.com";
    } else {
        Map<String,String> errors = new HashMap<>();
        errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
        attributes.addFlashAttribute("errors",errors);
        return "redirect:http://auth.achangmall.com/login.html";
    }
}
           
  • com.achang.achangmall.member.controller.MemberController
@PostMapping(value = "/login")
public R login(@RequestBody MemberUserLoginVo vo) {
    MemberEntity memberEntity = memberService.login(vo);
    if (memberEntity != null) {
        return R.ok().setData(memberEntity);
    } else {
        return R.error();
    }
}
           
  • com.achang.achangmall.auth.feign.MemberFeignService
@PostMapping(value = "/member/member/login")
R login(@RequestBody UserLoginVo vo);
           
  • com.xunqi.gulimall.member.service.impl.MemberServiceImpl
@Override
public MemberEntity login(MemberUserLoginVo vo) {
    String loginacct = vo.getLoginacct();
    String password = vo.getPassword();

    //1、去資料庫查詢 SELECT * FROM ums_member WHERE username = ? OR mobile = ?
    MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>()
                                                          .eq("username", loginacct).or().eq("mobile", loginacct));

    if (memberEntity == null) {
        //登入失敗
        return null;
    } else {
        //擷取到資料庫裡的password
        String password1 = memberEntity.getPassword();
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //進行密碼比對
        boolean matches = passwordEncoder.matches(password, password1);
        if (matches) {
            //登入成功
            return memberEntity;
        }
    }
    return null;
}
           

繼續閱讀