1.会员注册
1.目标
2.实现
1、在阿里云市场购买短信验证码
这里选择的版本不需要引入额外的依赖,通过JDK1.8直接可以使用
2.创建发短信验证码工具方法
项目结构:
代码:
/**
*
* @param host 请求的地址
* @param path 请求的后缀
* @param appCode 购入的api的appCode
* @param phoneNum 发送验证码的目的号码
* @param sign 签名编号
* @param skin 模板编号
* @return 发送成功则返回发送的验证码,放在ResultEntity中,失败则返回失败的ResultEntity
*/
public static ResultEntity<String> sendCodeByShortMessage(
String host,
String path,
String appCode,
String phoneNum,
String sign,
String skin
){
// 生成验证码
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 4; i++){
int random = (int)(Math.random()*10);
builder.append(random);
}
String param = builder.toString();
String urlSend = host + path + "?param=" + param + "&phone=" + phoneNum + "&sign=" + sign + "&skin=" + skin;
try {
URL url = new URL(urlSend);
HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
httpURLCon.setRequestProperty("Authorization", "APPCODE " + appCode);// 格式Authorization:APPCODE (中间是英文空格)
int httpCode = httpURLCon.getResponseCode();
if (httpCode == 200) {
String json = read(httpURLCon.getInputStream());
System.out.println("正常请求计费(其他均不计费)");
System.out.println("获取返回的json:");
System.out.print(json);
return ResultEntity.successWithData(param);
} else {
Map<String, List<String>> map = httpURLCon.getHeaderFields();
String error = map.get("X-Ca-Error-Message").get(0);
if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
return ResultEntity.failed("AppCode错误 ");
} else if (httpCode == 400 && error.equals("Invalid Url")) {
return ResultEntity.failed("请求的 Method、Path 或者环境错误");
} else if (httpCode == 400 && error.equals("Invalid Param Location")) {
return ResultEntity.failed("参数错误");
} else if (httpCode == 403 && error.equals("Unauthorized")) {
return ResultEntity.failed("服务未被授权(或URL和Path不正确)");
} else if (httpCode == 403 && error.equals("Quota Exhausted")) {
return ResultEntity.failed("套餐包次数用完 ");
} else {
return ResultEntity.failed("参数名错误 或 其他错误" + error);
}
}
} catch (MalformedURLException e) {
return ResultEntity.failed("URL格式错误");
} catch (UnknownHostException e) {
return ResultEntity.failed("URL地址错误");
} catch (Exception e) {
e.printStackTrace();
return ResultEntity.failed("套餐包次数用完 ");
}
}
/*
* 读取返回结果
*/
private static String read(InputStream is) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line = null;
while ((line = br.readLine()) != null) {
line = new String(line.getBytes(), StandardCharsets.UTF_8);
sb.append(line);
}
br.close();
return sb.toString();
}
3.发送验证码流程
1.目标
目标1:将验证码发送到用户手机上
目标2:将验证码存入Redis中
2.思路
3.代码
代码:创建view-controller
项目结构:
代码:
@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);
}
}
代码:修改注册超链接
项目结构:
代码:
<li><a href="reg.html" th:href="@{/auth/to/member/reg/page.html}">注册</a></li>
代码:准备注册页面
项目结构:
代码:
<!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>
代码:点击按钮发送验证码
项目结构:
修改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配置文件中调用发送短信接口时的参数
创建一个参数属性类:
项目结构:
代码:
@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配置文件:
项目结构:
代码:
short:
message:
host: https://gyytz.market.alicloudapi.com
path: /sms/smsSend
appcode: b2c3e981da14403ab548291c755ff12b # 这里就是购买得到的appCode
minute: 5
method: POST
smsSignId: 2e65b1bb3d054466b82f0c9d125465e2
templateId: 908e94ccf08b4476ba6c876d13f084ad
代码:启用Feign功能
项目结构:
代码:
// 开启feign客户端功能
@EnableFeignClients
@SpringBootApplication
public class CrowdMainAuthApp {
public static void main(String[] args) {
SpringApplication.run(CrowdMainAuthApp.class, args);
}
}
代码:获取验证码的工具方法
1.引入依赖
项目结构:
依赖代码:
<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.工具类
项目结构:
代码:
/**\
* 获取验证码的工具方法
* @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方法
项目结构:
代码:
@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.思路
3.代码
代码:给t_member表loginacct字段增加唯一约束
ALTER TABLE t_member ADD UNIQUE INDEX (login_acct);
代码:在member-api项目中创建远程接口
项目结构:
代码:
远程方法调用记得保持API模块中的方法名和@RequestMapping中的参数 与 MySQL模块中的保持一致。
@FeignClient("crowd-mysql")
public interface MySQLRemoteService {
@RequestMapping("/save/member/remote")
ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO);
}
代码:在mysql-provider项目中创建Handler方法
项目结构:
代码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类
项目结构:
代码:
@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标签,用于显示注册出错时的信息:
项目结构:
代码:
<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方法
项目结构:
代码:
@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
2.用户登录
1.目标
检查用户登录信息正确后将用户信息存入Session,表示用户已登录。
2.思路
3.代码
代码:登录页面
项目结构:
代码:
<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的必须地址)
代码:创建MemberLoginVO类
项目结构:
代码:
@NoArgsConstructor
@AllArgsConstructor
@Data
public class MemberLoginVO {
private Integer id;
private String userName;
private String email;
}
代码:handler方法
项目结构:
代码:
// 登录操作
@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域取出,通过 “[[${}]]“ ):
项目结构:
代码:
3.退出登陆
1.前端修改
项目结构:
代码:
<li>
<a href="index.html" th:href="@{/auth/do/member/logout.html}">
<i class="glyphicon glyphicon-off"></i> 退出系统
</a>
</li>
2.Handler方法
项目结构:
代码:
// 退出登录
@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.思路
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的相关依赖
项目结构:
依赖:
<!-- 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文件中都配置:
项目结构:
配置内容:
spring:
# 设置redis服务器的ip
redis:
host: 192.168.0.101
# 设置spring-session的存储方式为存入redis中
session:
store-type: redis
2.不需要登录检查的资源
特定请求地址与静态资源
项目结构:
代码:
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类
项目结构:
引入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的信息了 :
项目结构:
配置内容:
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
抛找不到类异常。
项目结构:
代码:
<dependency>
<groupId>com.zh.crowd</groupId>
<artifactId>crowdfunding09-member-entity</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
6.MemberLoginVO类需要序列化
项目结构:
代码:
public class MemberLoginVO implements Serializable {
//private static final long serialVersionUID = 1L;
private Integer id;
private String userName;
private String email;
}
7.从个人中心跳转到发起项目的页面
项目结构:
代码:
<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
项目结构:
代码:
@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");
}
}