1.会员注册

1.目标

实现通过手机验证码来完成会员账号的注册。

2.实现

1、在阿里云市场购买短信验证码

这里选择的版本不需要引入额外的依赖,通过JDK1.8直接可以使用

2.创建发短信验证码工具方法

项目结构:
image.png
代码:

  1. /**
  2. *
  3. * @param host 请求的地址
  4. * @param path 请求的后缀
  5. * @param appCode 购入的api的appCode
  6. * @param phoneNum 发送验证码的目的号码
  7. * @param sign 签名编号
  8. * @param skin 模板编号
  9. * @return 发送成功则返回发送的验证码,放在ResultEntity中,失败则返回失败的ResultEntity
  10. */
  11. public static ResultEntity<String> sendCodeByShortMessage(
  12. String host,
  13. String path,
  14. String appCode,
  15. String phoneNum,
  16. String sign,
  17. String skin
  18. ){
  19. // 生成验证码
  20. StringBuilder builder = new StringBuilder();
  21. for (int i = 0; i < 4; i++){
  22. int random = (int)(Math.random()*10);
  23. builder.append(random);
  24. }
  25. String param = builder.toString();
  26. String urlSend = host + path + "?param=" + param + "&phone=" + phoneNum + "&sign=" + sign + "&skin=" + skin;
  27. try {
  28. URL url = new URL(urlSend);
  29. HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
  30. httpURLCon.setRequestProperty("Authorization", "APPCODE " + appCode);// 格式Authorization:APPCODE (中间是英文空格)
  31. int httpCode = httpURLCon.getResponseCode();
  32. if (httpCode == 200) {
  33. String json = read(httpURLCon.getInputStream());
  34. System.out.println("正常请求计费(其他均不计费)");
  35. System.out.println("获取返回的json:");
  36. System.out.print(json);
  37. return ResultEntity.successWithData(param);
  38. } else {
  39. Map<String, List<String>> map = httpURLCon.getHeaderFields();
  40. String error = map.get("X-Ca-Error-Message").get(0);
  41. if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
  42. return ResultEntity.failed("AppCode错误 ");
  43. } else if (httpCode == 400 && error.equals("Invalid Url")) {
  44. return ResultEntity.failed("请求的 Method、Path 或者环境错误");
  45. } else if (httpCode == 400 && error.equals("Invalid Param Location")) {
  46. return ResultEntity.failed("参数错误");
  47. } else if (httpCode == 403 && error.equals("Unauthorized")) {
  48. return ResultEntity.failed("服务未被授权(或URL和Path不正确)");
  49. } else if (httpCode == 403 && error.equals("Quota Exhausted")) {
  50. return ResultEntity.failed("套餐包次数用完 ");
  51. } else {
  52. return ResultEntity.failed("参数名错误 或 其他错误" + error);
  53. }
  54. }
  55. } catch (MalformedURLException e) {
  56. return ResultEntity.failed("URL格式错误");
  57. } catch (UnknownHostException e) {
  58. return ResultEntity.failed("URL地址错误");
  59. } catch (Exception e) {
  60. e.printStackTrace();
  61. return ResultEntity.failed("套餐包次数用完 ");
  62. }
  63. }
  64. /*
  65. * 读取返回结果
  66. */
  67. private static String read(InputStream is) throws IOException {
  68. StringBuilder sb = new StringBuilder();
  69. BufferedReader br = new BufferedReader(new InputStreamReader(is));
  70. String line = null;
  71. while ((line = br.readLine()) != null) {
  72. line = new String(line.getBytes(), StandardCharsets.UTF_8);
  73. sb.append(line);
  74. }
  75. br.close();
  76. return sb.toString();
  77. }

3.发送验证码流程

1.目标

目标1:将验证码发送到用户手机上
目标2:将验证码存入Redis中

2.思路

image.png

3.代码

代码:创建view-controller

项目结构:
image.png
代码:

@Configuration
public class CrowdWebMvcConfig implements WebMvcConfigurer {


    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 前端请求的url地址
        String urlPath = "/auth/to/member/reg/page.html";

        // 实际后端跳转页面(会自动拼上前后缀)
        String viewName = "member-reg";

        // 前往用户注册页面
        registry.addViewController(urlPath).setViewName(viewName);
    }
}

代码:修改注册超链接

项目结构:
image.png
代码:

<li><a href="reg.html" th:href="@{/auth/to/member/reg/page.html}">注册</a></li>

代码:准备注册页面

项目结构:
image.png
代码:

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="keys" content="">
    <meta name="author" content="">
    <base th:href="@{/}">
    <link rel="stylesheet" href="bootstrap/css/bootstrap.min.css">
    <link rel="stylesheet" href="css/font-awesome.min.css">
    <link rel="stylesheet" href="css/login.css">
    <script type="text/javascript" src="jquery/jquery-2.1.1.min.js" ></script>
    <script type="text/javascript" src="bootstrap/js/bootstrap.min.js"></script>
    <script type="text/javascript" src="layer/layer.js"></script>
    <script type="text/javascript">
        $(function () {
            $("#sendBtn").click(function () {
                var phoneNum = $.trim($("[name=phoneNum]").val());
                $.ajax({
                    url: "/auth/member/send/short/message.json",
                    type: "post",
                    data: {
                        "phoneNum":phoneNum
                    },
                    dataType: "json",
                    success: function (response) {
                        var result = response.result;
                        if (result == "SUCCESS"){
                            layer.msg("发送成功!");
                        } else {
                            layer.msg("发送失败 请重试!");
                        }
                    },
                    error: function (response) {
                        layer.msg(response.status + " " + response.statusText);
                    }
                });
            });
        });
    </script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <div><a class="navbar-brand" href="index.html" style="font-size:32px;">尚筹网-创意产品众筹平台</a></div>
        </div>
    </div>
</nav>

<div class="container">

    <form action="/auth/member/do/register.html"  method="post" class="form-signin" role="form">
        <h2 class="form-signin-heading"><i class="glyphicon glyphicon-log-in"></i> 用户注册</h2>
        <p th:text="${message}"></p>
        <div class="form-group has-success has-feedback">
            <input type="text" name="loginAcct" class="form-control" id="inputSuccess4" placeholder="请输入登录账号" autofocus>
            <span class="glyphicon glyphicon-user form-control-feedback"></span>
        </div>
        <div class="form-group has-success has-feedback">
            <input type="text" name="userPswd" class="form-control" id="inputSuccess4" placeholder="请输入登录密码" style="margin-top:10px;">
            <span class="glyphicon glyphicon-lock form-control-feedback"></span>
        </div>
        <div class="form-group has-success has-feedback">
            <input type="text" name="userName" class="form-control" id="inputSuccess4" placeholder="请输入用户昵称" style="margin-top:10px;">
            <span class="glyphicon glyphicon-lock form-control-feedback"></span>
        </div>
        <div class="form-group has-success has-feedback">
            <input type="text" name="email" class="form-control" id="inputSuccess4" placeholder="请输入邮箱地址" style="margin-top:10px;">
            <span class="glyphicon glyphicon glyphicon-envelope form-control-feedback"></span>
        </div>
        <div class="form-group has-success has-feedback">
            <input type="text" name="phoneNum" class="form-control" id="inputSuccess4" placeholder="请输入手机号" style="margin-top:10px;">
            <span class="glyphicon glyphicon glyphicon-earphone form-control-feedback"></span>
        </div>
        <div class="form-group has-success has-feedback">
            <input type="text" name="code" class="form-control" id="inputSuccess4" placeholder="请输入验证码" style="margin-top:10px;">
            <span class="glyphicon glyphicon glyphicon-comment form-control-feedback"></span>
        </div>
        <button type="button" id="sendBtn" class="btn btn-lg btn-success btn-block"> 获取验证码</button>
        <button type="submit" class="btn btn-lg btn-success btn-block">注册</button>
    </form>
</div>

</body>
</html>

代码:点击按钮发送验证码

项目结构:
image.png
修改HTML代码:

<button type="button" id="sendBtn" class="btn btn-lg btn-success btn-block"> 获取验证码</button>

JS代码:

<script type="text/javascript">
        $(function () {
            $("#sendBtn").click(function () {
                var phoneNum = $.trim($("[name=phoneNum]").val());
                $.ajax({
                    url: "/auth/member/send/short/message.json",
                    type: "post",
                    data: {
                        "phoneNum":phoneNum
                    },
                    dataType: "json",
                    success: function (response) {
                        var result = response.result;
                        if (result == "SUCCESS"){
                            layer.msg("发送成功!");
                        } else {
                            layer.msg("发送失败 请重试!");
                        }
                    },
                    error: function (response) {
                        layer.msg(response.status + " " + response.statusText);
                    }
                });
            });
        });
    </script>

代码:在yml配置文件中调用发送短信接口时的参数

创建一个参数属性类:
项目结构:
image.png
代码:

@AllArgsConstructor
@NoArgsConstructor
@Data
// 加入ioc容器
@Component
// 给类设置在配置文件中设置时的前缀为“short.message”
@ConfigurationProperties(prefix = "short.message")
public class ShortMessageProperties {
    private String host;
    private String path;
    private String appcode;
    private String minute;
    private String method;
    private String smsSignId;
    private String templateId;

编写yml配置文件:
项目结构:
image.png
代码:

short:
  message:
    host: https://gyytz.market.alicloudapi.com
    path: /sms/smsSend
    appcode: b2c3e981da14403ab548291c755ff12b # 这里就是购买得到的appCode
    minute: 5
    method: POST
    smsSignId: 2e65b1bb3d054466b82f0c9d125465e2
    templateId: 908e94ccf08b4476ba6c876d13f084ad

代码:启用Feign功能

项目结构:
image.png
代码:

// 开启feign客户端功能
@EnableFeignClients
@SpringBootApplication
public class CrowdMainAuthApp {
    public static void main(String[] args) {
        SpringApplication.run(CrowdMainAuthApp.class, args);
    }
}

代码:获取验证码的工具方法

1.引入依赖
项目结构:
image.png
依赖代码:

 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.12</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-util</artifactId>
            <version>9.4.31.v20200723</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

2.工具类
项目结构:
image.png
代码:

 /**\
     * 获取验证码的工具方法
     * @param host
     * @param path
     * @param mobile
     * @param method
     * @param appcode
     * @param minute
     * @param smsSignId
     * @param templateId
     * @return
     */
    public static ResultEntity<String> sendCodeByShortMessage(
            String host,
            String path,
            String mobile,
            String method,
            String appcode,
            String minute,
            String smsSignId,
            String templateId
    ) {
        // 生成验证码
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 4; i++) {
            int random = (int) (Math.random() * 10);
            builder.append(random);
        }
        String code = builder.toString();
        Map<String, String> headers = new HashMap<String, String>();
        //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> queries = new HashMap<String, String>();
        queries.put("mobile", mobile);
        queries.put("param", "**code**:" + code + ",**minute**:" + minute);
        queries.put("smsSignId", smsSignId);
        queries.put("templateId", templateId);
        Map<String, String> bodys = new HashMap<String, String>();

        try {
            HttpResponse response =  HttpUtils.doPost(host, path, method, headers, queries, bodys);
            StatusLine statusLine = response.getStatusLine();
            int statusCode = statusLine.getStatusCode();
            // System.out.println("***************************");
            // System.out.println(statusCode);
            // System.out.println("***************************");
             String reasonPhrase = statusLine.getReasonPhrase();
            // System.out.println(reasonPhrase);
            // System.out.println("***************************");
            // System.out.println(response.toString());
            // System.out.println("***************************");
            // System.out.println(EntityUtils.toString(response.getEntity()));
            // HttpEntity entity = response.getEntity();
            // String s = EntityUtils.toString(response.getEntity());         
            if (statusCode == 200){
                return ResultEntity.successWithData(code);
            }
            return ResultEntity.failed(reasonPhrase);
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    }

代码:编写Handler方法

项目结构:
image.png
代码:

@Controller
public class MemberHandler {

    @Autowired
    RedisRemoteService redisRemoteService;

    // 自动注入对象,对象的属性从application.yml文件中获取
    @Autowired
    ShortMessageProperties shortMessageProperties;

    // 发送验证码
    @ResponseBody
    @RequestMapping("/auth/member/send/short/message.json")
// 从前端获取手机号
    public ResultEntity<String> sendShortMessage(@RequestParam("phoneNum") String phoneNum){

        // 调用工具类中的发送验证码的方法,可以从配置文件中读取配置的接口信息
        ResultEntity<String> sendResultEntity = CrowdUtil.sendCodeByShortMessage(
                // 通过一个properties类+application.yml的配置,装配API需要的参数
                shortMessageProperties.getHost(),
                shortMessageProperties.getPath(),
                phoneNum,
                shortMessageProperties.getMethod(),
                shortMessageProperties.getAppcode(),
                shortMessageProperties.getMinute(),
                shortMessageProperties.getSmsSignId(),
                shortMessageProperties.getTemplateId());

        // 判断-发送成功
        if (ResultEntity.SUCCESS.equals(sendResultEntity.getResult())){

            // 得到ResultEntity中的验证码
            String code = sendResultEntity.getData();

            // 将验证码存入到redis中(验证码会过期,因此需要设置TTL,这里设置为5分钟)
            ResultEntity<String> redisResultEntity = redisRemoteService.setRedisKeyValueWithTimeoutRemote(
                    CrowdConstant.REDIS_CODE_PREFIX + phoneNum, code, 5, TimeUnit.MINUTES);

            // 判断存入redis是否成功
            if (ResultEntity.SUCCESS.equals(redisResultEntity.getResult())){
                // 存入成功,返回成功
                return ResultEntity.successWithoutData();
            } else {
                // 存入失败,返回redis返回的ResultEntity
                return redisResultEntity;
            }
        } else {
            // 发送验证码失败,返回发送验证码的ResultEntity
            return sendResultEntity;
        }
    }
}

4.执行注册流程

1.目标

如果针对注册操作所做的各项验证能够通过,则将Member信息存入数据库。

2.思路

image.png

3.代码

代码:给t_member表loginacct字段增加唯一约束

ALTER TABLE t_member ADD UNIQUE INDEX (login_acct);

代码:在member-api项目中创建远程接口

项目结构:
image.png
代码:
远程方法调用记得保持API模块中的方法名和@RequestMapping中的参数 与 MySQL模块中的保持一致。

@FeignClient("crowd-mysql")
public interface MySQLRemoteService {

    @RequestMapping("/save/member/remote")
    ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO);
}

代码:在mysql-provider项目中创建Handler方法

项目结构:
image.png
代码1:Handler方法

@RestController
public class MemberProviderHandler {

    @Autowired
    MemberService memberService;

    @RequestMapping("/save/member/remote")
    public ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO){
        try {
            memberService.saveMember(memberPO);
            return ResultEntity.successWithoutData();
        } catch (Exception e){
            if (e instanceof DuplicateKeyException){
                return ResultEntity.failed(CrowdConstant.MESSAGE_SYSTEM_ERROR_LOGIN_NOT_UNIQUE);
            }
            return ResultEntity.failed(e.getMessage());
        }
    }
    }

代码2:Service接口

public interface MemberService {
    void saveMember(MemberPO memberPO);
}

代码3:Service实现

@Transactional(readOnly = true)
@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    MemberPOMapper memberPOMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    @Override
    public void saveMember(MemberPO memberPO) {
        memberPOMapper.insertSelective(memberPO);
    }
}

代码:创建MemberVO类

项目结构:
image.png
代码:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class MemberVO {
    private String loginAcct;

    private String userPswd;

    private String userName;

    private String email;

    private String phoneNum;

    private String code;

}

代码:修改前端注册form表单

前端的表单,添加action、method,给所有input标签设置对应的name,并且添加一个p标签,用于显示注册出错时的信息:
项目结构:
image.png
代码:

<form action="/auth/member/do/register.html"  method="post" class="form-signin" role="form">
    <h2 class="form-signin-heading"><i class="glyphicon glyphicon-log-in"></i> 用户注册</h2>
    <p th:text="${message}"></p>
    <div class="form-group has-success has-feedback">
        <input type="text" name="loginAcct" class="form-control" id="inputSuccess4" placeholder="请输入登录账号" autofocus>
        <span class="glyphicon glyphicon-user form-control-feedback"></span>
    </div>
    <div class="form-group has-success has-feedback">
        <input type="text" name="userPswd" class="form-control" id="inputSuccess4" placeholder="请输入登录密码" style="margin-top:10px;">
        <span class="glyphicon glyphicon-lock form-control-feedback"></span>
    </div>
    <div class="form-group has-success has-feedback">
        <input type="text" name="userName" class="form-control" id="inputSuccess4" placeholder="请输入用户昵称" style="margin-top:10px;">
        <span class="glyphicon glyphicon-lock form-control-feedback"></span>
    </div>
    <div class="form-group has-success has-feedback">
        <input type="text" name="email" class="form-control" id="inputSuccess4" placeholder="请输入邮箱地址" style="margin-top:10px;">
        <span class="glyphicon glyphicon glyphicon-envelope form-control-feedback"></span>
    </div>
    <div class="form-group has-success has-feedback">
        <input type="text" name="phoneNum" class="form-control" id="inputSuccess4" placeholder="请输入手机号" style="margin-top:10px;">
        <span class="glyphicon glyphicon glyphicon-earphone form-control-feedback"></span>
    </div>
    <div class="form-group has-success has-feedback">
        <input type="text" name="code" class="form-control" id="inputSuccess4" placeholder="请输入验证码" style="margin-top:10px;">
        <span class="glyphicon glyphicon glyphicon-comment form-control-feedback"></span>
    </div>
    <button type="button" id="sendBtn" class="btn btn-lg btn-success btn-block"> 获取验证码</button>
    <button type="submit" class="btn btn-lg btn-success btn-block">注册</button>
</form>

代码:后端Handler方法

项目结构:
image.png
代码:

@Autowired
private RedisRemoteService redisRemoteService;

@Autowired
private MySQLRemoteService mySQLRemoteService;
// 进行用户注册操作
@RequestMapping("/auth/member/do/register.html")
public String doMemberRegister(MemberVO memberVO, ModelMap modelMap){
    // 获取手机号
    String phoneNum = memberVO.getPhoneNum();

    // 拼接为redis存放的key
    String key = CrowdConstant.REDIS_CODE_PREFIX + phoneNum;

    // 通过key寻找value(验证码)
    ResultEntity<String> resultEntity = redisRemoteService.getRedisValueByKeyRemote(key);

    String result = resultEntity.getResult();

    // 判断获取redis中的验证码是否成功
    if (ResultEntity.FAILED.equals(result)){
        // 失败,返回主页页面
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, resultEntity.getMessage());
        return "member-reg";
    }

    // 获取redis中的验证码的值
    String redisCode = resultEntity.getData();
    if (redisCode == null){
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE,CrowdConstant.MESSAGE_CODE_NOT_EXIST);
        return "member-reg";
    }

    // 获取表单提交的验证码
    String formCode = memberVO.getCode();

    // 如果redis中的验证码与表单提交的验证码不同
    if (!Objects.equals(formCode,redisCode)){
        // request域存入不匹配的message
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE,CrowdConstant.MESSAGE_CODE_INVALID);
        // 返回注册页面
        return "member-reg";
    }

    // 验证码比对一致,删除redis中的验证码数据
    redisRemoteService.removeRedisKeyByKeyRemote(key);

    // 进行注册操作

    // 1、加密
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String formPwd = memberVO.getUserPswd();
    String encode = bCryptPasswordEncoder.encode(formPwd);

    // 2、将加密后的密码放入MemberVO对象
    memberVO.setUserPswd(encode);

    // 3、执行保存
    MemberPO memberPO = new MemberPO();
    BeanUtils.copyProperties(memberVO,memberPO);
    ResultEntity<String> saveResultEntity = mySQLRemoteService.saveMemberRemote(memberPO);

    // 4、判断保存是否成功
    String saveResult = saveResultEntity.getResult();
    if (ResultEntity.FAILED.equals(saveResult)){
        // 保存失败,则返回保存操作的ResultEntity中的message,存入request域的message
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, saveResultEntity.getMessage());
        // 回到注册页面
        return "member-reg";
    }

    // 全部判断成功,跳转到登录页面
    return "redirect:http://localhost/auth/to/member/login/page.html";
}

一个问题

测试时发现第一次注册时会发生错误,这是因为第一次注册时,连接redis等时间长,让ribbon以为超时而报错,在application.yml中配置:

# 由于项目刚启动第一次进行redis操作时会比较慢,可能被ribbon认为是超时报错,因此通过下面的配置延长ribbon超时的时间
ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000

注意@RequestBody

image.png

2.用户登录

1.目标

检查用户登录信息正确后将用户信息存入Session,表示用户已登录。

2.思路

image.png

3.代码

代码:登录页面

项目结构:
image.png
代码:

 <form action="/auth/do/member/login.html" method="post" class="form-signin" role="form">
        <h2 class="form-signin-heading"><i class="glyphicon glyphicon-log-in"></i> 用户登录</h2>
        <p th:text="${message}">登陆失败时显示的提示</p>
        <p th:text="${session.message}">未登录时访问受限现实的提示</p>
        <div class="form-group has-success has-feedback">
            <input type="text" name="loginAcct" class="form-control" id="inputSuccess4" placeholder="请输入登录账号" autofocus>
            <span class="glyphicon glyphicon-user form-control-feedback"></span>
        </div>
        <div class="form-group has-success has-feedback">
            <input type="text" name="loginPswd" class="form-control" id="inputSuccess4" placeholder="请输入登录密码" style="margin-top:10px;">
            <span class="glyphicon glyphicon-lock form-control-feedback"></span>
        </div>
        <div class="checkbox" style="text-align:right;"><a href="reg.html" th:href="@{/auth/to/member/reg/page.html}">我要注册</a></div>
        <button type="submit" class="btn btn-lg btn-success btn-block" href="member.html" > 登录</button>
    </form>

注:下面的代码在这里必须有(thymeleaf的名称空间、base标签给出url的必须地址)
thymeleaf页面头部.png

代码:创建MemberLoginVO类

项目结构:
image.png
代码:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class MemberLoginVO {
    private Integer id;
    private String userName;
    private String email;
}

代码:handler方法

项目结构:
image.png
代码:

// 登录操作
@RequestMapping("/auth/do/member/login.html")
public String doMemberLogin(
        @RequestParam("loginAcct") String loginAcct,
        @RequestParam("loginPswd") String loginPswd,
        ModelMap modelMap,
        HttpSession session) {

    // 远程方法调用,通过loinAcct,得到数据库中的对应Member
    ResultEntity<MemberPO> resultEntity = mySQLRemoteService.getMemberPOByLoginAcctRemote(loginAcct);

    // 判断-查询操作是否成功
    if (ResultEntity.FAILED.equals(resultEntity.getResult())){
        // 查询失败,返回登陆页面
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, resultEntity.getMessage());
        return "member-login";
    }

    // 查询操作成功,则取出MemberPO对象
    MemberPO memberPO = resultEntity.getData();

    // 判断得到的MemberPO是否为空
    if (memberPO == null){
        // 为空则返回登陆页面
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_LOGIN_FAILED);
        return "member-login";
    }

    // 返回的MemberPO非空,取出数据库中的密码(已经加密的)
    String userPswd = memberPO.getUserPswd();

    // 使用BCryptPasswordEncoder,比对表单的密码与数据库中的密码是否匹配
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    boolean matches = passwordEncoder.matches(loginPswd, userPswd);

    // 判断-密码不同
    if (!matches){
        // 返回登陆页面,存入相应的提示信息
        modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_LOGIN_FAILED);
        return "member-login";
    }

    // 密码匹配,则通过一个LoginMemberVO对象,存入需要在session域通信的用户信息(这样只在session域放一些相对不私秘的信息,保护用户隐私)
    LoginMemberVO loginMember = new LoginMemberVO(memberPO.getId(), memberPO.getUserName(), memberPO.getEmail());

    // 将LoginMemberVO对象存入session域(因为session会放入redis,因此LoginMemberVO必须实现序列化)
    session.setAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER,loginMember);

    // 重定向到登陆成功后的主页面
    return "redirect:http://localhost/auth/to/member/center/page.html";
}

==因为session会放入redis,因此LoginMemberVO必须实现序列化==
跳转入登录成功后的页面:

代码:登录成功后页面

创建member-center.html
主要有在前端显示登录后的用户昵称(从session域取出,通过 “[[${}]]“ ):
项目结构:
image.png
代码:
thymeleaf页面头部.png登录成功页面显示member1.png

3.退出登陆

1.前端修改

项目结构:
image.png
代码:

<li>
    <a href="index.html" th:href="@{/auth/do/member/logout.html}">
        <i class="glyphicon glyphicon-off"></i> 退出系统
    </a>
</li>

2.Handler方法

项目结构:
image.png
代码:

// 退出登录
@RequestMapping("/auth/do/member/logout.html")
public String doLogout(HttpSession session){
    // 清除session域数据
    session.invalidate();

    // 重定向到首页
    return "redirect:http://localhost/";
}

4.Session与Cookie

Cookie 的工作机制
服务器端返回 Cookie 信息给浏览器
Java 代码:response.addCookie[(cookie 对象);
HTTP 响应消息头:Set-Cookie: Cookie 的名字=Cookie 的值
浏览器接收到服务器端返回的 Cookie,以后的每一次请求都会把 Cookie 带上
HTTP 请求消息头:Cookie:Cookie 的名字=Cookie 的值
Session 的工作机制
获取 Session 对象:request.getSession()
检查当前请求是否携带了 JSESSIONID,这个 Cookie
带了:根据这个 JSESSIONID,在服务器端查找对应的 Session 对象
能找到:就把找到的 Session 对象返回
没找到:新建 Session 对象返回,同时返回 JSESSIONID,的 Cookie
没带:新建 Session 对象返回,同时返回 JSESSIONID的 Cookie

5.登录检查

1.目标

把项目中必须登录才能访问的功能保护起来,如果没有登录就跳转到登录页面。

2.思路

image.png

3.代码

1.设置Session共享

在分布式和集群的环境下,每一个模块运行在各自的单独的Tomcat服务器上,而Session被不同的Tomcat隔离,因此无法互通,导致程序运行时会发生数据不互通的情况。
针对这个问题,这边采用后端统一存储Session数据的方法——将Session数据存入到Redis中(这样速度比MySQL更快)
Spring也提供了此工具:spring-session
spring-session的依赖与使用redis存储session,也需要引入redis的依赖。
给crowdfunding12-member-authentication-consumer与crowdfunding16-member-zuul模块加上了spring-session的相关依赖
项目结构:
image.png
依赖:

<!-- spring-session的依赖 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- redis的依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

两处的application.yml文件中都配置:
项目结构:
image.png
配置内容:

spring:
  # 设置redis服务器的ip
  redis:
    host: 192.168.0.101
  # 设置spring-session的存储方式为存入redis中
  session:
    store-type: redis

2.不需要登录检查的资源

特定请求地址与静态资源
项目结构:
image.png
代码:

public class AccessPassResources {

    // 保存不被过滤的请求
    public static final Set<String> PASS_RES_SET = new HashSet<>();

    // 静态代码块中加入不被过滤的内容
    static {
        PASS_RES_SET.add("/");
        PASS_RES_SET.add("/auth/to/member/reg/page.html");
        PASS_RES_SET.add("/auth/to/member/login/page.html");
        PASS_RES_SET.add("/auth/member/send/short/message.json");
        PASS_RES_SET.add("/auth/member/do/register.html");
        PASS_RES_SET.add("/auth/do/member/login.html");
        PASS_RES_SET.add("/auth/do/member/logout.html");
        PASS_RES_SET.add("/error");
        PASS_RES_SET.add("/favicon.ico");
    }

    // 保存不被过滤的静态资源
    public static final Set<String> STATIC_RES_SET = new HashSet<>();

    // 静态代码块中加入不被过滤的内容
    static {
        STATIC_RES_SET.add("bootstrap");
        STATIC_RES_SET.add("css");
        STATIC_RES_SET.add("fonts");
        STATIC_RES_SET.add("img");
        STATIC_RES_SET.add("jquery");
        STATIC_RES_SET.add("layer");
        STATIC_RES_SET.add("script");
        STATIC_RES_SET.add("ztree");
    }

    /**
     * @param servletPath 当前请求的路径  就是localhost:8080/aaa/bbb/ccc中的 “aaa/bbb/ccc”
     * @return true: 表示该资源是静态资源; false: 表示该资源不是静态资源
     */
    public static boolean judgeIsStaticResource(String servletPath){

        // 先判断字符串是否为空
        if (servletPath == null || servletPath.length() == 0){
            throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);
        }

        // 通过“/”来分割得到的请求路径
        String[] split = servletPath.split("/");

        // split[0]是一个空字符串,因此取split[1],相当于/aaa/bbb/ccc的“aaa”
        String path = split[1];

        // 判断是否包含得到的请求的第一个部分
        return STATIC_RES_SET.contains(path);
    }


}

3.创建ZuulFilter类

项目结构:
image.png
引入util工程依赖:

<dependency>
            <groupId>com.zh.crowd</groupId>
            <artifactId>zhcrowdfunding01-admin-util</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

代码:

// 加入ioc容器
@Component
public class CrowdAccessFilter extends ZuulFilter {

    // return "pre" 表示在请求发生其前进行过滤
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     *
     * @return true:表示被拦截; false: 表示放行
     */
    @Override
    public boolean shouldFilter() {

        // 通过getCurrentContext得到当前的RequestContext
        RequestContext requestContext = RequestContext.getCurrentContext();

        // 通过RequestContext得到HttpServletRequest
        HttpServletRequest request = requestContext.getRequest();

        // 获得当前请求的路径
        String servletPath = request.getServletPath();

        // 判断当前请求路径是否包含在不被过滤的请求的set集合中
        boolean isPassContains = AccessPassResources.PASS_RES_SET.contains(servletPath);

        // 如果包含在set中,返回false,表示不被过滤
        if (isPassContains){
            return false;
        }

        // 判断是否是静态资源
        boolean isStaticResource = AccessPassResources.judgeIsStaticResource(servletPath);

        // 是静态资源则工具方法返回true,因为应该放行,所以取反,返回false
        return !isStaticResource;
    }

    @Override
    public Object run() throws ZuulException {

        // 通过getCurrentContext得到当前的RequestContext
        RequestContext requestContext = RequestContext.getCurrentContext();

        // 通过RequestContext得到HttpServletRequest
        HttpServletRequest request = requestContext.getRequest();

        // 得到session
        HttpSession session = request.getSession();

        // 从session中得到“loginMember”
        Object loginMember = session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER);

        // 判断得到的loginMember是否为空
        if (loginMember == null){
            // 为空 取得response,为了后面进行重定向
            HttpServletResponse response = requestContext.getResponse();

            // 向session域中存放"message":"还未登录,禁止访问受保护资源!",为了在重定向后能够在前台显示错误信息
            session.setAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_ACCESS_FORBIDDEN);

            try {
                // 重定向到登录页面
                response.sendRedirect("/auth/to/member/login/page.html");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 返回null就是不操作
        return null;
    }
}

这里Zuul模块中的application.yml一定要设置 sensitive-headers: “*”,保持原有的头信息,否则重定向后重新创建了request、response对象,就无法携带session的信息了 :
项目结构:
image.png
配置内容:

zuul:
  ignored-services: "*"       # 表示忽视直接通过application-name访问微服务,必须通过route
  sensitive-headers: "*"      # 在Zuul向其他微服务重定向时,保持原本的头信息(请求头、响应头)
  routes:                     # 指定网关路由
    crowd-protal:
      service-id: crowd-auth  # 对应application-name
      path: /**               # 表示直接通过根路径访问,必须加上**,否则多层路径无法访问

4.今后项目中重定向的问题

在两个不同的网站,浏览器工作时不会使用相同的cookie,也就使不同微服务之间无法很好地同步数据。因此分布式项目中重定向时,需要带上前缀:这里使用Zuul,且端口号为80,因此可以看到上面crowdfunding12-member-authentication-consumer模块的代码中的重定向都是转发到localhost上,类似:
以后重定向的地址都按照通过Zuul访问的方式写地址。
方式:return “redirect:http://localhost/xxx/xxx“;
例如:return “redirect:http://localhost/auth/to/member/center/page.html“;

5.Zuul工程需要依赖entity工程

通过 Zuul访问所有工程,在成功登录之后,要前往会员中心页面。
这时,在 ZuulFilter中需要从 Session 域读取 MemberLoginVo对象。SpringSession
会从 Redis中加载相关信息。
相关信息中包含了 MemberLoginvo,的全类名。
需要根据这个全类名找到 MemberLoginvo.类,用来反序列化。
可是我们之前没有让 Zuul,工程依赖 entity 工程,所以找不到MemberLoginvo.类。I
抛找不到类异常。
项目结构:
image.png
代码:

<dependency>
            <groupId>com.zh.crowd</groupId>
            <artifactId>crowdfunding09-member-entity</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

6.MemberLoginVO类需要序列化

项目结构:
image.png
代码:

public class MemberLoginVO implements Serializable {
    //private static final long serialVersionUID = 1L;
    private Integer id;
    private String userName;
    private String email;
}

7.从个人中心跳转到发起项目的页面

项目结构:
image.png
代码:

<div class="list-group-item " style="cursor:pointer;">
    <a th:href="@{/auth/to/member/crowd/page.html}" style="text-decoration: none">我的众筹</a>
    <span class="badge"><i class="glyphicon glyphicon-chevron-right"></i></span>
</div>

8.登录与注册功能的view-controller

项目结构:
image.png
代码:

@Configuration
public class CrowdWebMvcConfig implements WebMvcConfigurer {


    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 前端请求的url地址
        String urlPath = "/auth/to/member/reg/page.html";

        // 实际后端跳转页面(会自动拼上前后缀)
        String viewName = "member-reg";

        // 前往用户注册页面
        registry.addViewController(urlPath).setViewName(viewName);

        // 前往登录页面
        registry.addViewController("/auth/to/member/login/page.html").setViewName("member-login");

        // 前往登录完成后的用户主页面
        registry.addViewController("/auth/to/member/center/page.html").setViewName("member-center");

        // 前往“我的众筹”页面
        registry.addViewController("/auth/to/member/crowd/page.html").setViewName("member-crowd");
    }
}