马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
认证服务
1. 环境搭建
创建gulimall-auth-server模块,导依赖,引入login.html和reg.html,并把静态资源放到nginx的static目次下
2. 注册功能
(1) 验证码倒计时
- //点击发送验证码按钮触发下面函数
- $("#sendCode").click(function () {
-
-
- //如果有disabled,说明最近已经点过,则什么都不做
- if($(this).hasClass("disabled")){
-
-
- }else {
-
-
- //调用函数使得当前的文本进行倒计时功能
- timeOutChangeStyle();
- //发送验证码
- var phone=$("#phoneNum").val();
- $.get("/sms/sendCode?phone="+phone,function (data){
-
-
- if (data.code!=0){
-
-
- alert(data.msg);
- }
- })
- }
- })
- let time = 60;
- function timeOutChangeStyle() {
-
-
- //开启倒计时后设置标志属性disable,使得该按钮不能再次被点击
- $("#sendCode").attr("class", "disabled");
- //当时间为0时,说明倒计时完成,则重置
- if(time==0){
-
-
- $("#sendCode").text("点击发送验证码");
- time=60;
- $("#sendCode").attr("class", "");
- }else {
-
-
- //每秒调用一次当前函数,使得time--
- $("#sendCode").text(time+"s后再次发送");
- time--;
- setTimeout("timeOutChangeStyle()", 1000);
- }
- }
复制代码 (2) 整合短佩服务
在阿里云网页购买试用的短佩服务
在gulimall-third-party中编写发送短信组件,此中host、path、appcode可以在设置文件中使用前缀spring.cloud.alicloud.sms举行设置
- @Data
- @ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
- @Controller
- public class SmsComponent {
-
-
- private String host;
- private String path;
- private String appcode;
- public void sendCode(String phone,String code) {
-
-
- // String host = "http://dingxin.market.alicloudapi.com";
- // String path = "/dx/sendSms";
- String method = "POST";
- // String appcode = "你自己的AppCode";
- Map<String, String> headers = new HashMap<String, String>();
- //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
- headers.put("Authorization", "APPCODE " + appcode);
- Map<String, String> querys = new HashMap<String, String>();
- querys.put("mobile",phone);
- querys.put("param", "code:"+code);
- querys.put("tpl_id", "TP1711063");
- Map<String, String> bodys = new HashMap<String, String>();
- try {
-
-
- /**
- * 重要提示如下:
- * HttpUtils请从
- * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
- * 下载
- *
- * 相应的依赖请参照
- * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
- */
- HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
- System.out.println(response.toString());
- //获取response的body
- //System.out.println(EntityUtils.toString(response.getEntity()));
- } catch (Exception e) {
-
-
- e.printStackTrace();
- }
- }
- }
复制代码 编写controller,给别的服务提供远程调用发送验证码的接口
- @Controller
- @RequestMapping(value = "/sms")
- public class SmsSendController {
-
-
- @Resource
- private SmsComponent smsComponent;
- /**
- * 提供给别的服务进行调用
- * @param phone 电话号码
- * @param code 验证码
- * @return
- */
- @ResponseBody
- @GetMapping(value = "/sendCode")
- public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) {
-
-
- //发送验证码
- smsComponent.sendCode(phone,code);
- System.out.println(phone+code);
- return R.ok();
- }
- }
复制代码 (3) 接口防刷
由于发送验证码的接口袒露,为了防止恶意攻击,我们不能随意让接口被调用。
- 在redis中以phone-code将电话号码和验证码举行存储并将当前时间与code一起存储
- 如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,分析60s内该号码已经调用过,返回错误信息
- 60s以后再次调用,必要删除之前存储的phone-code
- code存在一个逾期时间,我们设置为10min,10min内验证该验证码有效
- @GetMapping("/sms/sendCode")
- @ResponseBody
- public R sendCode(@RequestParam("phone")String phone) {
-
-
- //接口防刷,在redis中缓存phone-code
- ValueOperations<String, String> ops = redisTemplate.opsForValue();
- String prePhone = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone;
- String v = ops.get(prePhone);
- if (!StringUtils.isEmpty(v)) {
-
-
- long pre = Long.parseLong(v.split("_")[1]);
- //如果存储的时间小于60s,说明60s内发送过验证码
- if (System.currentTimeMillis() - pre < 60000) {
-
-
- return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
- }
- }
- //如果存在的话,删除之前的验证码
- redisTemplate.delete(prePhone);
- //获取到6位数字的验证码
- String code = String.valueOf((int)((Math.random() + 1) * 100000));
- //在redis中进行存储并设置过期时间
- ops.set(prePhone,code+"_"+System.currentTimeMillis(),10, TimeUnit.MINUTES);
- thirdPartFeignService.sendCode(phone, code);
- return R.ok();
- }
复制代码 (4) 注册接口编写
在gulimall-auth-server服务中编写注册的主体逻辑
- 若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
- 若通过JSR303校验,则必要从redis中取值判断验证码是否准确,准确的话通过会员服务注册
- 会员服务调用乐成则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面
注: RedirectAttributes可以通过session生存信息并在重定向的时间携带已往
- @PostMapping("/register")
- public String register(@Valid UserRegisterVo registerVo, BindingResult result, RedirectAttributes attributes) {
-
-
- //1.判断校验是否通过
- Map<String, String> errors = new HashMap<>();
- if (result.hasErrors()){
-
-
- //1.1 如果校验不通过,则封装校验结果
- result.getFieldErrors().forEach(item->{
-
-
- errors.put(item.getField(), item.getDefaultMessage());
- //1.2 将错误信息封装到session中
- attributes.addFlashAttribute("errors", errors);
- });
- //1.2 重定向到注册页
- return "redirect:http://auth.gulimall.com/reg.html";
- }else {
-
-
- //2.若JSR303校验通过
- //判断验证码是否正确
- String code = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + registerVo.getPhone());
- //2.1 如果对应手机的验证码不为空且与提交上的相等-》验证码正确
- if (!StringUtils.isEmpty(code) && registerVo.getCode().equals(code.split("_")[0])) {
-
-
- //2.1.1 使得验证后的验证码失效
- redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + registerVo.getPhone());
- //2.1.2 远程调用会员服务注册
- R r = memberFeignService.register(registerVo);
- if (r.getCode() == 0) {
-
-
- //调用成功,重定向登录页
- return "redirect:http://auth.gulimall.com/login.html";
- }else {
-
-
- //调用失败,返回注册页并显示错误信息
- String msg = (String) r.get("msg");
- errors.put("msg", msg);
- attributes.addFlashAttribute("errors", errors);
- return "redirect:http://auth.gulimall.com/reg.html";
- }
- }else {
-
-
- //2.2 验证码错误
- errors.put("code", "验证码错误");
- attributes.addFlashAttribute("errors", errors);
- return "redirect:http://auth.gulimall.com/reg.html";
- }
- }
- }
复制代码 通过gulimall-member会员服务注册逻辑
- 通过非常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自界说非常,并在返回时封装对应的错误信息
- 如果没有注册,则封装通报过来的会员信息,并设置默认的会员品级、创建时间
- @RequestMapping("/register")
- public R register(@RequestBody MemberRegisterVo registerVo) {
-
-
- try {
-
-
- memberService.register(registerVo);
- //异常机制:通过捕获对应的自定义异常判断出现何种错误并封装错误信息
- } catch (UserExistException userException) {
-
-
- return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(), BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
- } catch (PhoneNumExistException phoneException) {
-
-
- return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
- }
- return R.ok();
- }
复制代码- public void register(MemberRegisterVo registerVo) {
-
-
- //1 检查电话号是否唯一
- checkPhoneUnique(registerVo.getPhone());
- //2 检查用户名是否唯一
- checkUserNameUnique(registerVo.getUserName());
- //3 该用户信息唯一,进行插入
- MemberEntity entity = new MemberEntity();
- //3.1 保存基本信息
- entity.setUsername(registerVo.getUserName());
- entity.setMobile(registerVo.getPhone());
- entity.setCreateTime(new Date());
- //3.2 使用加密保存密码
- BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
- String encodePassword = passwordEncoder.encode(registerVo.getPassword());
- entity.setPassword(encodePassword);
- //3.3 设置会员默认等级
- //3.3.1 找到会员默认登记
- MemberLevelEntity defaultLevel = memberLevelService.getOne(new QueryWrapper<MemberLevelEntity>().eq("default_status", 1));
- //3.3.2 设置会员等级为默认
- entity.setLevelId(defaultLevel.getId());
- // 4 保存用户信息
- this.save(entity);
- }
- private void checkUserNameUnique(String userName) {
-
-
- Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
- if (count > 0) {
-
-
- throw new UserExistException();
- }
- }
- private void checkPhoneUnique(String phone) {
-
-
- Integer count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
- if (count > 0) {
-
-
- throw new PhoneNumExistException();
- }
- }
复制代码 3. 用户名暗码登录
在gulimall-auth-server模块中的主体逻辑
- 通过会员服务远程调用登录接口
- 如果调用乐成,重定向至首页
- 如果调用失败,则封装错误信息并携带错误信息重定向至登录页
- @RequestMapping("/login")
- public String login(UserLoginVo vo,RedirectAttributes attributes){
-
-
- R r = memberFeignService.login(vo);
- if (r.getCode() == 0) {
-
-
- return "redirect:http://gulimall.com/";
- }else {
-
-
- String msg = (String) r.get("msg");
- Map<String, String> errors = new HashMap<>();
- errors.put("msg", msg);
- attributes.addFlashAttribute("errors", errors);
- return "redirect:http://auth.gulimall.com/login.html";
- }
- }
复制代码 在gulimall-member模块中完成登录
- 当数据库中含有以当前登录名为用户名或电话号且暗码匹配时,验证通过,返回查询到的实体
- 否则返回null,并在controller返回用户名或暗码错误
- @RequestMapping("/login")
- public R login(@RequestBody MemberLoginVo loginVo) {
-
-
- MemberEntity entity=memberService.login(loginVo);
- if (entity!=null){
-
-
- return R.ok();
- }else {
-
-
- return R.error(BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getCode(), BizCodeEnum.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
- }
- }
- @Override
- public MemberEntity login(MemberLoginVo loginVo) {
-
-
- String loginAccount = loginVo.getLoginAccount();
- //以用户名或电话号登录的进行查询
- MemberEntity entity = this.getOne(new QueryWrapper<MemberEntity>().eq("username", loginAccount).or().eq("mobile", loginAccount));
- if (entity!=null){
-
-
- BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
- boolean matches = bCryptPasswordEncoder.matches(loginVo.getPassword(), entity.getPassword());
- if (matches){
-
-
- entity.setPassword("");
- return entity;
- }
- }
- return null;
- }
复制代码 4. 外交登录
(1) oauth2.0
(2) 在微博开放平台创建应用
(3) 在登录页引导用户至授权页
- GET
- https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
复制代码
- client_id: 创建网站应用时的app key
- YOUR_REGISTERED_REDIRECT_URI: 认证完成后的跳转链接(必要清静台高级设置划一)
如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
code是我们用来调换令牌的参数
(4) 调换token
- POST
- https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE
复制代码
- client_id: 创建网站应用时的app key
- client_secret: 创建网站应用时的app secret
- YOUR_REGISTERED_REDIRECT_URI: 认证完成后的跳转链接(必要清静台高级设置划一)
- code:调换令牌的认证码
返回数据如下
(5) 获取用户信息
结果返回json
(6) 代码编写
认证接口
- 通过HttpUtils发送哀求获取token,并将token等信息交给member服务举行外交登录
- 若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页
- @Controller
- public class OauthController {
-
-
- @Autowired
- private MemberFeignService memberFeignService;
- @RequestMapping("/oauth2.0/weibo/success")
- public String authorize(String code, RedirectAttributes attributes) throws Exception {
-
-
- //1. 使用code换取token,换取成功则继续2,否则重定向至登录页
- Map<String, String> query = new HashMap<>();
- query.put("client_id", "2144***074");
- query.put("client_secret", "ff63a0d8d5*****29a19492817316ab");
- query.put("grant_type", "authorization_code");
- query.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
- query.put("code", code);
- //发送post请求换取token
- HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
- Map<String, String> errors = new HashMap<>();
- if (response.getStatusLine().getStatusCode() == 200) {
-
-
- //2. 调用member远程接口进行oauth登录,登录成功则转发至首页并携带返回用户信息,否则转发至登录页
- String json = EntityUtils.toString(response.getEntity());
- SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() {
-
-
- });
- R login = memberFeignService.login(socialUser);
- //2.1 远程调用成功,返回首页并携带用户信息
- if (login.getCode() == 0) {
-
-
- String jsonString = JSON.toJSONString(login.get("memberEntity"));
- MemberResponseVo memberResponseVo = JSON.parseObject(jsonString, new TypeReference<MemberResponseVo>() {
-
-
- });
- attributes.addFlashAttribute("user", memberResponseVo);
- return "redirect:http://gulimall.com";
- }else {
-
-
- //2.2 否则返回登录页
- errors.put("msg", "登录失败,请重试");
- attributes.addFlashAttribute("errors", errors);
- return "redirect:http://auth.gulimall.com/login.html";
- }
- }else {
-
-
- errors.put("msg", "获得第三方授权失败,请重试");
- attributes.addFlashAttribute("errors", errors);
- return "redirect:http://auth.gulimall.com/login.html";
- }
- }
复制代码 登录接口
- 登录包罗两种流程,实际上包罗了注册和登录
- 如果之前未使用该外交账号登录,则使用token调用开放api获取外交账号相干信息,注册并将结果返回
- 如果之前已经使用该外交账号登录,则更新token并将结果返回
- @RequestMapping("/oauth2/login")
- public R login(@RequestBody SocialUser socialUser) {
-
-
- MemberEntity entity=memberService.login(socialUser);
- if (entity!=null){
-
-
- return R.ok().put("memberEntity",entity);
- }else {
-
-
- return R.error();
- }
- }
- @Override
- public MemberEntity login(SocialUser socialUser){
-
-
- MemberEntity uid = this.getOne(new QueryWrapper<MemberEntity>().eq("uid", socialUser.getUid()));
- //1 如果之前未登陆过,则查询其社交信息进行注册
- if (uid == null) {
-
-
- Map<String, String> query = new HashMap<>();
- query.put("access_token",socialUser.getAccess_token());
- query.put("uid", socialUser.getUid());
- //调用微博api接口获取用户信息
- String json = null;
- try {
-
-
- HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), query);
- json = EntityUtils.toString(response.getEntity());
- } catch (Exception e) {
-
-
- e.printStackTrace();
- }
- JSONObject jsonObject = JSON.parseObject(json);
- //获得昵称,性别,头像
- String name = jsonObject.getString("name");
- String gender = jsonObject.getString("gender");
- String profile_image_url = jsonObject.getString("profile_image_url");
- //封装用户信息并保存
- uid = new MemberEntity();
- MemberLevelEntity defaultLevel = memberLevelService.getOne(new QueryWrapper<MemberLevelEntity>().eq("default_status", 1));
- uid.setLevelId(defaultLevel.getId());
- uid.setNickname(name);
- uid.setGender("m".equals(gender)?0:1);
- uid.setHeader(profile_image_url);
- uid.setAccessToken(socialUser.getAccess_token());
- uid.setUid(socialUser.getUid());
- uid.setExpiresIn(socialUser.getExpires_in());
- this.save(uid);
- }else {
-
-
- //2 否则更新令牌等信息并返回
- uid.setAccessToken(socialUser.getAccess_token());
- uid.setUid(socialUser.getUid());
- uid.setExpiresIn(socialUser.getExpires_in());
- this.updateById(uid);
- }
- return uid;
- }
复制代码 5. SpringSession
(1) session 原理
jsessionid相称于银行卡,存在服务器的session相称于存储的现金,每次通过jsessionid取出生存的数据
标题:但是正常环境下session不可跨域,它有自己的作用范围
(2) 分布式下session共享标题
(3) 办理方案
1) session复制
2) 客户端存储
3) hash划一性
4) 同一存储
(4) SpringSession整合redis
通过SpringSession修改session的作用域
1) 环境搭建
导入依赖
- <dependency>
- <groupId>org.springframework.session</groupId>
- <artifactId>spring-session-data-redis</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
复制代码 修改设置
- spring:
- redis:
- host: 192.168.56.102
- session:
- store-type: redis
复制代码 添加注解
- @EnableRedisHttpSession
- public class GulimallAuthServerApplication {
-
-
复制代码 2) 自界说设置
- 由于默认使用jdk举行序列化,通过导入RedisSerializer修改为json序列化
- 而且通过修改CookieSerializer扩大session的作用域至**.gulimall.com
- @Configuration
- public class GulimallSessionConfig {
-
-
- @Bean
- public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
-
-
- return new GenericJackson2JsonRedisSerializer();
- }
- @Bean
- public CookieSerializer cookieSerializer() {
-
-
- DefaultCookieSerializer serializer = new DefaultCookieSerializer();
- serializer.setCookieName("GULISESSIONID");
- serializer.setDomainName("gulimall.com");
- return serializer;
- }
- }
复制代码 (5) SpringSession焦点原理 - 装饰者模式
- 原生的获取session时是通过HttpServletRequest获取的
- 这里对request举行包装,而且重写了包装request的getSession()方法
- @Override
- protected void doFilterInternal(HttpServletRequest request,
- HttpServletResponse response, FilterChain filterChain)
- throws ServletException, IOException {
-
-
- request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
- //对原生的request、response进行包装
- SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
- request, response, this.servletContext);
- SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
- wrappedRequest, response);
- try {
-
-
- filterChain.doFilter(wrappedRequest, wrappedResponse);
- }
- finally {
-
-
- wrappedRequest.commitSession();
- }
- }
复制代码 购物车
1. 数据模子分析
(1) 数据存储
购物车是一个读多写多的场景,因此放入数据库并不符合,但购物车又是必要恒久化,因此这里我们选用redis存储购物车数据。
(2) 数据布局
一个购物车是由各个购物项构成的,但是我们用List举行存储并不符合,由于使用List查找某个购物项时必要挨个遍历每个购物项,会造成大量时间斲丧,为包管查找速率,我们使用hash举行存储
(3) VO编写
购物项vo
- public class CartItemVo {
-
-
- private Long skuId;
-
- //是否选中
- private Boolean check = true;
- //标题
- private String title;
-
- //图片
- private String image;
- //商品套餐属性
- private List<String> skuAttrValues;
- //价格
- private BigDecimal price;
- //数量
- private Integer count;
- //总价
- private BigDecimal totalPrice;
-
- /**
- * 当前购物车项总价等于单价x数量
- * @return
- */
- public BigDecimal getTotalPrice() {
-
-
- return price.multiply(new BigDecimal(count));
- }
- public void setTotalPrice(BigDecimal totalPrice) {
-
-
- this.totalPrice = totalPrice;
- }
复制代码 购物车vo
|