day14【前台】用户登录注册

day14【前台】用户登录注册

1、会员登录框架

image-20200930221912821

2、发送短信(测试)

2.1、去哪儿找API

  • 上阿里云:直接搜索【短信接口】

image-20200627095654082

  • 随便找一个就行,往下翻有API接口的使用方法

image-20200627100128522

image-20200627100538631

2.2、测试API

2.2.1、main方法
  • API文档中的示例代码直接拿过来

image-20200627101623669

public class ShortMessageTest {

	public static void main(String[] args) {

		// 短信调用接接口的URL地址
		String host = "https://fesms.market.alicloudapi.com";

		// 具体发送短信功能的地址
		String path = "/sms/";

		// 请求方式
		String method = "GET";

		// 登录到阿里云,进入控制台,找到自己的短信接口AppCode
		String appcode = "你自己的AppCode";

		// 最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
		Map<String, String> headers = new HashMap<String, String>();
		headers.put("Authorization", "APPCODE " + appcode);

		// 封装其他参数
		Map<String, String> querys = new HashMap<String, String>();
		querys.put("code", "123456"); 		// 要发送的验证码
		querys.put("phone", "13262792031"); // 收短信人的手机号
		querys.put("sign", "1"); 			// 签名编号
		querys.put("skin", "1"); 			// 模板编号

		// JDK 1.8示例代码请在这里下载: http://code.fegine.com/Tools.zip
		try {
			/**
			 * 
			 * https:/ /github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
			 * 或者直接下载: http://code.fegine.com/HttpUtils.zip 下载
			 *
			 * 相应的依赖请参照
			 * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
			 * 相关jar包(非pom)直接下载: http://code.fegine.com/aliyun-jar.zip
			 */
			HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
			// System.out.println(response.toString());如不输出json, 请打开这行代码,打印调试头部状态码。
			// 状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
			// 获取response的body
			System.out.println(EntityUtils.toString(response.getEntity()));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}
2.2.2、你自己的AppCode
  • 阿里云控制台获取自己的AppCode

image-20200627104444850

2.2.3、创建HttpUtils类
  • 创建HttpUtils

image-20200627102002773

public class HttpUtils {

	/**
	 * get
	 * 
	 * @param host
	 * @param path
	 * @param method
	 * @param headers
	 * @param querys
	 * @return
	 * @throws Exception
	 */
	public static HttpResponse doGet(String host, String path, String method, Map<String, String> headers,
			Map<String, String> querys) throws Exception {
		HttpClient httpClient = wrapClient(host);

		HttpGet request = new HttpGet(buildUrl(host, path, querys));
		for (Map.Entry<String, String> e : headers.entrySet()) {
			request.addHeader(e.getKey(), e.getValue());
		}

		return httpClient.execute(request);
	}

	/**
	 * post form
	 * 
	 * @param host
	 * @param path
	 * @param method
	 * @param headers
	 * @param querys
	 * @param bodys
	 * @return
	 * @throws Exception
	 */
	public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers,
			Map<String, String> querys, Map<String, String> bodys) throws Exception {
		HttpClient httpClient = wrapClient(host);

		HttpPost request = new HttpPost(buildUrl(host, path, querys));
		for (Map.Entry<String, String> e : headers.entrySet()) {
			request.addHeader(e.getKey(), e.getValue());
		}

		if (bodys != null) {
			List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();

			for (String key : bodys.keySet()) {
				nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
			}
			UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
			formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
			request.setEntity(formEntity);
		}

		return httpClient.execute(request);
	}

	/**
	 * Post String
	 * 
	 * @param host
	 * @param path
	 * @param method
	 * @param headers
	 * @param querys
	 * @param body
	 * @return
	 * @throws Exception
	 */
	public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers,
			Map<String, String> querys, String body) throws Exception {
		HttpClient httpClient = wrapClient(host);

		HttpPost request = new HttpPost(buildUrl(host, path, querys));
		for (Map.Entry<String, String> e : headers.entrySet()) {
			request.addHeader(e.getKey(), e.getValue());
		}

		if (StringUtils.isNotBlank(body)) {
			request.setEntity(new StringEntity(body, "utf-8"));
		}

		return httpClient.execute(request);
	}

	/**
	 * Post stream
	 * 
	 * @param host
	 * @param path
	 * @param method
	 * @param headers
	 * @param querys
	 * @param body
	 * @return
	 * @throws Exception
	 */
	public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers,
			Map<String, String> querys, byte[] body) throws Exception {
		HttpClient httpClient = wrapClient(host);

		HttpPost request = new HttpPost(buildUrl(host, path, querys));
		for (Map.Entry<String, String> e : headers.entrySet()) {
			request.addHeader(e.getKey(), e.getValue());
		}

		if (body != null) {
			request.setEntity(new ByteArrayEntity(body));
		}

		return httpClient.execute(request);
	}

	/**
	 * Put String
	 * 
	 * @param host
	 * @param path
	 * @param method
	 * @param headers
	 * @param querys
	 * @param body
	 * @return
	 * @throws Exception
	 */
	public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers,
			Map<String, String> querys, String body) throws Exception {
		HttpClient httpClient = wrapClient(host);

		HttpPut request = new HttpPut(buildUrl(host, path, querys));
		for (Map.Entry<String, String> e : headers.entrySet()) {
			request.addHeader(e.getKey(), e.getValue());
		}

		if (StringUtils.isNotBlank(body)) {
			request.setEntity(new StringEntity(body, "utf-8"));
		}

		return httpClient.execute(request);
	}

	/**
	 * Put stream
	 * 
	 * @param host
	 * @param path
	 * @param method
	 * @param headers
	 * @param querys
	 * @param body
	 * @return
	 * @throws Exception
	 */
	public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers,
			Map<String, String> querys, byte[] body) throws Exception {
		HttpClient httpClient = wrapClient(host);

		HttpPut request = new HttpPut(buildUrl(host, path, querys));
		for (Map.Entry<String, String> e : headers.entrySet()) {
			request.addHeader(e.getKey(), e.getValue());
		}

		if (body != null) {
			request.setEntity(new ByteArrayEntity(body));
		}

		return httpClient.execute(request);
	}

	/**
	 * Delete
	 * 
	 * @param host
	 * @param path
	 * @param method
	 * @param headers
	 * @param querys
	 * @return
	 * @throws Exception
	 */
	public static HttpResponse doDelete(String host, String path, String method, Map<String, String> headers,
			Map<String, String> querys) throws Exception {
		HttpClient httpClient = wrapClient(host);

		HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
		for (Map.Entry<String, String> e : headers.entrySet()) {
			request.addHeader(e.getKey(), e.getValue());
		}

		return httpClient.execute(request);
	}

	private static String buildUrl(String host, String path, Map<String, String> querys)
			throws UnsupportedEncodingException {
		StringBuilder sbUrl = new StringBuilder();
		sbUrl.append(host);
		if (!StringUtils.isBlank(path)) {
			sbUrl.append(path);
		}
		if (null != querys) {
			StringBuilder sbQuery = new StringBuilder();
			for (Map.Entry<String, String> query : querys.entrySet()) {
				if (0 < sbQuery.length()) {
					sbQuery.append("&");
				}
				if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
					sbQuery.append(query.getValue());
				}
				if (!StringUtils.isBlank(query.getKey())) {
					sbQuery.append(query.getKey());
					if (!StringUtils.isBlank(query.getValue())) {
						sbQuery.append("=");
						sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
					}
				}
			}
			if (0 < sbQuery.length()) {
				sbUrl.append("?").append(sbQuery);
			}
		}

		return sbUrl.toString();
	}

	private static HttpClient wrapClient(String host) {
		HttpClient httpClient = new DefaultHttpClient();
		if (host.startsWith("https://")) {
			sslClient(httpClient);
		}

		return httpClient;
	}

	private static void sslClient(HttpClient httpClient) {
		try {
			SSLContext ctx = SSLContext.getInstance("TLS");
			X509TrustManager tm = new X509TrustManager() {
				public X509Certificate[] getAcceptedIssuers() {
					return null;
				}

				public void checkClientTrusted(X509Certificate[] xcs, String str) {

				}

				public void checkServerTrusted(X509Certificate[] xcs, String str) {

				}
			};
			ctx.init(null, new TrustManager[] { tm }, null);
			SSLSocketFactory ssf = new SSLSocketFactory(ctx);
			ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
			ClientConnectionManager ccm = httpClient.getConnectionManager();
			SchemeRegistry registry = ccm.getSchemeRegistry();
			registry.register(new Scheme("https", 443, ssf));
		} catch (KeyManagementException ex) {
			throw new RuntimeException(ex);
		} catch (NoSuchAlgorithmException ex) {
			throw new RuntimeException(ex);
		}
	}
}
2.2.4、引入其他依赖
  • 引入依赖后,不再报错

image-20200627102157221

<dependencies>
    <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.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpcore</artifactId>
        <version>4.2.1</version>
    </dependency>
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-util</artifactId>
        <version>9.3.7.v20160115</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.5</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.3、测试

  • 使用阿里云自带的调试工具和Java程序的运行结果如下

image-20200627110336279

image-20200627110153938

image-20200627110133280

3、发送短信(项目)

3.1、引入依赖

  • 由于工具类需要放在util工程中,所以我们需要在util工程下添加短信API所需的依赖;注意依赖的版本号需要与auth工程中的版本号相对应(因为父工程中已经有了短信API所需的依赖,但是因为util工程并不依赖于父工程,所以只好单独写版本号咯)

image-20200627143642718

<!-- 以下是发送短信时调用第三方API所需依赖 -->
<!-- JSON工具 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.15</version>
</dependency>

<!-- httpclient依赖了httpcore,所以不需要再额外添加httpcore的依赖  -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.9</version>
</dependency>

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-util</artifactId>
    <version>9.4.19.v20190610</version>
</dependency>

<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>

3.2、创建HttpUtils类

  • util工程下创建HttpUtils类,代码和上一节一样

image-20200627144119454

3.3、测试短信API

3.3.1、引入依赖
  • 引入SpringBoot测试的依赖

image-20200627144324487

<!-- SpringBoot Test -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
3.3.2、创建测试类
  • 测试一波API接口

image-20200627144526152

@RunWith(SpringRunner.class)
@SpringBootTest
public class CrowdTest {
	
	private Logger logger = LoggerFactory.getLogger(CrowdTest.class);

	@Test
	public void testSendMessage() {
		
		// 短信调用接接口的URL地址
		String host = "https://fesms.market.alicloudapi.com";

		// 具体发送短信功能的地址
		String path = "/sms/";

		// 请求方式
		String method = "GET";

		// 登录到阿里云,进入控制台,找到自己的短信接口AppCode
		String appcode = "9844f3f479cf41ea92ccbea03c70db58";

		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("param", "123456");

		// 收短信的手机号
		querys.put("phone", "13262792031");

		// 签名编号
		querys.put("sign", "1");

		// 模板编号
		querys.put("skin", "1");
		// JDK 1.8示例代码请在这里下载: http://code.fegine.com/Tools.zip

		try {
			/**
			 * 重要提示如下: HttpUtils请从
			 * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
			 * 或者直接下载: http://code.fegine.com/HttpUtils.zip 下载
			 *
			 * 相应的依赖请参照
			 * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
			 * 相关jar包(非pom)直接下载: http://code.fegine.com/aliyun-jar.zip
			 */
			HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);

			StatusLine statusLine = response.getStatusLine();

			// 状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
			int statusCode = statusLine.getStatusCode();
			logger.info("code=" + statusCode);

			String reasonPhrase = statusLine.getReasonPhrase();
			logger.info("reason=" + reasonPhrase);

			// System.out.println(response.toString());如不输出json, 请打开这行代码,打印调试头部状态码。
			// 获取response的body
			logger.info(EntityUtils.toString(response.getEntity()));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

3.4、创建工具类

  • util工程下,向工具类中添加如下方法

image-20200627144800905

/**
 * 给远程第三方短信接口发送请求把验证码发送到用户手机上
 * @param host		短信接口调用的URL地址
 * @param path		具体发送短信功能的地址
 * @param method	请求方式
 * @param phoneNum	接收短信的手机号
 * @param appCode	用来调用第三方短信API的AppCode
 * @param sign		签名编号
 * @param skin		模板编号
 * @return 返回调用结果是否成功
 * 	成功:返回验证码
 * 	失败:返回失败消息
 * 	状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
 */
public static ResultEntity<String> sendCodeByShortMessage(
		
		String host,
		
		String path,
		
		String method,
		
		String phoneNum, 
		
		String appCode, 
		
		String sign, 
		
		String skin) {

	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>();
	
	// 生成验证码
	StringBuilder builder = new StringBuilder();
	for(int i = 0; i < 4; i++) {
		int random = (int) (Math.random() * 10);
		builder.append(random);
	}
	
	String code = builder.toString();

	// 要发送的验证码,也就是模板中会变化的部分
	querys.put("code", code);

	// 收短信的手机号
	querys.put("phone", phoneNum);

	// 签名编号
	querys.put("sign", sign);

	// 模板编号
	querys.put("skin", skin);
	
	// JDK 1.8示例代码请在这里下载: http://code.fegine.com/Tools.zip
	try {
		HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
		
		StatusLine statusLine = response.getStatusLine();
		
		// 状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误
		int statusCode = statusLine.getStatusCode();
		
		String reasonPhrase = statusLine.getReasonPhrase();
		
		if(statusCode == 200) {
			
			// 操作成功,把生成的验证码返回
			return ResultEntity.successWithData(code);
		}
		
		return ResultEntity.failed(reasonPhrase);
		
	} catch (Exception e) {
		e.printStackTrace();
		return ResultEntity.failed(e.getMessage());
	}
	
}

3.5、测试工具类

  • 创建测试方法

image-20200627151516483

@Test
public void testSendMessageUtil() {

    // 短信调用接接口的URL地址
    String host = "https://fesms.market.alicloudapi.com";

    // 具体发送短信功能的地址
    String path = "/sms/";

    // 请求方式
    String method = "GET";

    // 收件人电话号码
    String phoneNum = "13262792031";

    // 登录到阿里云,进入控制台,找到自己的短信接口AppCode
    String appCode = "9844f3f479cf41ea92ccbea03c70db58";

    // 签名编号
    String sign = "1";

    // 模板编号
    String skin = "1";

    // 发送短信,获取响应信息
    ResultEntity<String> resultEntity = CrowdUtil.sendCodeByShortMessage(host, path, method, phoneNum, appCode, sign, skin);

    logger.info(resultEntity.toString());
}
  • 测试结果:生成的验证码为data=3906
2020-06-27 15:13:41.178  INFO 5548 --- [           main] com.atguigu.crowd.test.CrowdTest         : ResultEntity [result=SUCCESS, message=null, data=3906]

4、常量定义

public class CrowdConstant {
	
	public static final String MESSAGE_LOGIN_FAILED = "抱歉!账号密码错误!请重新输入!";
	public static final String MESSAGE_LOGIN_ACCT_ALREADY_IN_USE = "抱歉!这个账号已经被使用了!";
	public static final String MESSAGE_ACCESS_FORBIDEN = "请登录以后再访问!";
	public static final String MESSAGE_STRING_INVALIDATE = "字符串不合法!请不要传入空字符串!";
	public static final String MESSAGE_SYSTEM_ERROR_LOGIN_NOT_UNIQUE = "系统错误:登录账号不唯一!";
	public static final String MESSAGE_ACCESS_DENIED = "抱歉!您不能访问这个资源!";
	public static final String MESSAGE_CODE_NOT_EXISTS = "验证码已过期!请检查手机号是否正确或重新发送!";
	public static final String MESSAGE_CODE_INVALID = "验证码不正确!";
	
	public static final String ATTR_NAME_EXCEPTION = "exception";
	public static final String ATTR_NAME_LOGIN_ADMIN = "loginAdmin";
	public static final String ATTR_NAME_LOGIN_MEMBER = "loginMember";
	public static final String ATTR_NAME_PAGE_INFO = "pageInfo";
	public static final String ATTR_NAME_MESSAGE = "message";
	
	public static final String REDIS_CODE_PREFIX = "REDIS_CODE_PREFIX_";

}

5、会员注册

5.1、添加view-controller

  • 点击【注册】按钮直接转发至注册页面,添加view-controller即可,无需编写Handler方法

image-20200627214340334

@Configuration
public class CrowdWebMvcConfig implements WebMvcConfigurer {
	
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		
		// 添加view-controller
		// 浏览器访问的地址	
		// 目标视图的名称,将来拼接“prefix: classpath:/templates/”、“suffix: .html”前后缀
		
		registry.addViewController("/auth/member/to/reg/page").setViewName("member-reg");
    
    }

}

5.2、修改注册超链接

  • 修改首页中【注册】按钮的超链接

image-20200627214653801

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

5.3、添加layer弹框组件

  • 引入layer弹框组件的资源

image-20200627214739956

5.4、新建注册页面

  • 导入Thymeleaf名称空间
  • 添加base标签
  • 导入layer弹框组件

image-20200627215001272

image-20200627215038214

<!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>

5.5、发送短信

5.5.1、绑定单击响应函数
  • 为【发送验证码】按钮绑定单击响应函数

image-20200627215239975

$(function(){
	$("#sendBtn").click(function(){
		
		// 1.获取接收短信的手机号
		var phoneNum = $.trim($("[name=phoneNum]").val());
		
		// 2.发送请求
		$.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("发送成功!");
				}
				
				if(result == "FAILED") {
					layer.msg("发送失败!请再试一次!");
				}
			},
			"error":function(response){
				layer.msg(response.status + " " + response.statusText);
			}
		});
		
	});
});
5.5.2、创建短信的配置类
  • 创建配置类

image-20200627215606822

@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
@ConfigurationProperties(prefix = "short.message")
public class ShortMessageProperties {
	
	private String host;
	private String path;
	private String method;
	private String appCode;
	private String sign;
	private String skin;

}
  • 遇到这个问题,fix即可,这样编写yml时才会有代码提示

image-20200627215433869

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
5.5.3、编写短信相关配置
  • yml配置文件中编写配置即可

image-20200627215812199

short:
  message:
    app-code: 9844f3f479cf41ea92ccbea03c70db58
    host: https://fesms.market.alicloudapi.com
    method: GET
    path: /sms/
    sign: 1
    skin: 1
5.5.4、启用feign-client
  • auth工程需要调用各个provider工程,provider工程作为Feign服务器端,则auth工程Feign客户端,在auth工程主启动类上添加@EnableFeignClients注解:启用Feign客户端功能
//启用Feign客户端功能
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class CrowdMainClass {
	
	public static void main(String[] args) {
		SpringApplication.run(CrowdMainClass.class, args);
	}

}
5.5.5、auth-consumer Handler
  • 接受Ajax请求
  • 调用短信API接口
  • 返回消息给浏览器

image-20200627220441500

// CrowdConstant类中的常量定义
public static final String REDIS_CODE_PREFIX = "REDIS_CODE_PREFIX_";

@Controller
public class MemberHandler {
	
	@Autowired
	private ShortMessageProperties shortMessageProperties;
	
	@Autowired
	private RedisRemoteService redisRemoteService;
	
	@ResponseBody
	@RequestMapping("/auth/member/send/short/message.json")
	public ResultEntity<String> sendMessage(@RequestParam("phoneNum") String phoneNum) {
		
		// 1.发送验证码到phoneNum手机
		ResultEntity<String> sendMessageResultEntity = CrowdUtil.sendCodeByShortMessage(
				shortMessageProperties.getHost(), 
				shortMessageProperties.getPath(), 
				shortMessageProperties.getMethod(), phoneNum, 
				shortMessageProperties.getAppCode(), 
				shortMessageProperties.getSign(), 
				shortMessageProperties.getSkin());
		
		// 2.判断短信发送结果
		if(ResultEntity.SUCCESS.equals(sendMessageResultEntity.getResult())) {
			// 3.如果发送成功,则将验证码存入Redis
			// ①从上一步操作的结果中获取随机生成的验证码
			String code = sendMessageResultEntity.getData();
			
			// ②拼接一个用于在Redis中存储数据的key
			String key = CrowdConstant.REDIS_CODE_PREFIX + phoneNum;
			
			// ③调用远程接口存入Redis
			ResultEntity<String> saveCodeResultEntity = redisRemoteService.setRedisKeyValueRemoteWithTimeout(key, code, 15, TimeUnit.MINUTES);
			
			// ④判断结果
			if(ResultEntity.SUCCESS.equals(saveCodeResultEntity.getResult())) {
				
				return ResultEntity.successWithoutData();
			}else {
				return saveCodeResultEntity;
			}
		} else {
			return sendMessageResultEntity;
		}
		
	}

}
5.5.6、发送短信测试
  • 反正我收到了验证码

image-20200627221725827

5.6、mysql-provider

5.6.1、loginacct唯一约束
  • t_member表中,loginacct列添加唯一约束

image-20200628111051415

ALTER TABLE `project_crowd`.`t_member` ADD UNIQUE INDEX (`loginacct`); 
5.6.2、Handler代码
  • 保存成功:返回成功消息
  • 保存失败:返回失败消息
  • 注意:@RequestBody是必须的,因为auth-consumer调用mysql-provider时,请求参数是通过RequestBody发送过来滴

image-20200628111349628

Logger logger = LoggerFactory.getLogger(MemberProviderHandler.class);

@RequestMapping("/save/member/remote")
public ResultEntity<String> saveMember(@RequestBody MemberPO memberPO) {

    logger.info(memberPO.toString());

    try {

        memberService.saveMember(memberPO);

        return ResultEntity.successWithoutData();

    }catch(Exception e) {

        if(e instanceof DuplicateKeyException) {
            return ResultEntity.failed(CrowdConstant.MESSAGE_LOGIN_ACCT_ALREADY_IN_USE);
        }

        return ResultEntity.failed(e.getMessage());
    }

}
5.6.3、Service代码
  • 事务传播行为:开启新事务
  • 遇到Exception类型的异常便回滚
  • 该方法为写操作

image-20200628111555088

@Transactional(
    propagation = Propagation.REQUIRES_NEW, 
    rollbackFor = Exception.class, 
    readOnly = false)
@Override
public void saveMember(MemberPO memberPO) {

    memberPOMapper.insertSelective(memberPO);

}
5.6.4、声明api远程调用接口
  • api工程中,声明远程调用接口

image-20200628111833408

@RequestMapping("/save/member/remote")
public ResultEntity<String> saveMember(@RequestBody MemberPO memberPO);
5.6.5、测试
  • 使用Postman进行测试

    • 请求方式:POST
    • 请求地址:http://localhost:2000/save/member/remote
    • 请求头:Content-Type:application/json;charset=UTF-8
    • 请求体
    {
    	"loginacct":"Heygo",
    	"userpswd":123123
    }
    

image-20200628112828569

image-20200628112844236

  • 保存成功

image-20200628112736291

5.7、auth-consumer

5.7.1、调整表单
  • 调整注页面的表单
    • 表单提交地址
    • 表单项的name属性值
    • 错误信息回显
<div class="container">
	<form action="/auth/do/member/register" 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>
5.7.2、创建MemberVO
  • MemberVO对应着浏览器表单数据

image-20200628113531874

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberVO {
	
	private String loginacct;
	
	private String userpswd;

    private String username;
	
	private String email;
	
	private String phoneNum;
	
	private String code;

}
5.7.3、Handler代码
  • 通过用户手机号(key)去Redis里面查询对应的value
  • 如果验证码一致,则将用户密码进行加密,执行保存操作

image-20200628113718904

@Autowired
private ShortMessageProperties shortMessageProperties;

@Autowired
private RedisRemoteService redisRemoteService;

@Autowired
private MySQLRemoteService mySQLRemoteService;

@RequestMapping("/auth/do/member/register")
public String register(MemberVO memberVO, ModelMap modelMap) {
	
	// 1.获取用户输入的手机号
	String phoneNum = memberVO.getPhoneNum();
	
	// 2.拼Redis中存储验证码的Key
	String key = CrowdConstant.REDIS_CODE_PREFIX + phoneNum;
	
	// 3.从Redis读取Key对应的value
	ResultEntity<String> resultEntity = redisRemoteService.getRedisStringValueByKeyRemote(key);
	
	// 4.检查查询操作是否有效
	String result = resultEntity.getResult();
	if(ResultEntity.FAILED.equals(result)) {
		
		modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, resultEntity.getMessage());
		
		return "member-reg";
	}
	
	String redisCode = resultEntity.getData();
	
	if(redisCode == null) {
		
		modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_CODE_NOT_EXISTS);
		
		return "member-reg";
	}
	
	// 5.如果从Redis能够查询到value则比较表单验证码和Redis验证码
	String formCode = memberVO.getCode();
	
	if(!Objects.equals(formCode, redisCode)) {
		
		modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_CODE_INVALID);
		
		return "member-reg";
	}
	
	// 6.如果验证码一致,则从Redis删除
	redisRemoteService.removeRedisKeyRemote(key);
	
	// 7.执行密码加密
	BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
	String userpswdBeforeEncode = memberVO.getUserpswd();
	
	String userpswdAfterEncode = passwordEncoder.encode(userpswdBeforeEncode);
	
	memberVO.setUserpswd(userpswdAfterEncode);
	
	// 8.执行保存
	// ①创建空的MemberPO对象
	MemberPO memberPO = new MemberPO();
	
	// ②复制属性
	BeanUtils.copyProperties(memberVO, memberPO);
	
	// ③调用远程方法
	ResultEntity<String> saveMemberResultEntity = mySQLRemoteService.saveMember(memberPO);
	
	if(ResultEntity.FAILED.equals(saveMemberResultEntity.getResult())) {
		
		modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, saveMemberResultEntity.getMessage());
		
		return "member-reg";
	}
	
	// 使用重定向避免刷新浏览器导致重新执行注册流程
	return "redirect:/auth/member/to/login/page";
}
5.7.4、ribbon超时时间
  • 由于在第一次请求中需要建立缓存、建立连接,操作较多,所以比较耗时。
  • 如果按照默认的ribbon超时时间来工作,第一次请求会超过这个时间导致超时报错。我就说有次我第一次报错504(这个好像是Redis超时错误),调了半天程序也没问题。。。
  • 为了避免这个问题,把ribbon的超时时间延长,在yml配置文件中添加如下配置

image-20200628114139419

ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000
  • 奇怪,说属性找不到,这个配置会生效吗?

image-20200628114214159

5.7.5、测试
  • 成功发送短信

image-20200628114906908

  • 账号已经被使用

image-20200628114804878

  • 验证码错误

image-20200628114937021

  • 成功注册

image-20200628115034126

  • 问题:表单并没有回显。。。

  • 解决办法:出错将转发至注册页面,

    • 解决办法一:我们只需将信息放到Request域中,然后回显即可

    • 解决办法二:通过Ajax请求的返回值将要回显的数据返回给浏览器端

6、会员登录

6.1、创建会员登录页面

  • 老规矩:
    • 引入Thymeleaf名称空间
    • 修改字符编码
    • 添加base标签
    • 调整登录表单
    • 添加登录错误信息的回显

image-20200630132000579

<!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 src="jquery/jquery-2.1.1.min.js"></script>
<script src="bootstrap/js/bootstrap.min.js"></script>
<style>
</style>
</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/login" 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="checkbox" style="text-align: right;">
				<a href="reg.html" th:href="@{/auth/member/to/reg/page}">我要注册</a>
			</div>
			<button class="btn btn-lg btn-success btn-block" type="submit">登录</button>
		</form>
	</div>
</body>
</html>

6.2、添加view-controller

  • 工程路径下的/auth/member/to/login/page请求,对应着templates/member-login.html页面

image-20200630130359287

@Configuration
public class CrowdWebMvcConfig implements WebMvcConfigurer {
	
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		
		// 添加view-controller
		// 浏览器访问的地址	
		// 目标视图的名称,将来拼接“prefix: classpath:/templates/”、“suffix: .html”前后缀
		
		registry.addViewController("/auth/member/to/reg/page").setViewName("member-reg");
		registry.addViewController("/auth/member/to/login/page").setViewName("member-login");
		
	}

}

6.3、跳转至登录页面

  • 修改首页中【登陆】按钮的超链接

image-20200630133636864

<li><a href="login.html" th:href="@{/auth/member/to/login/page}">登录</a></li>

6.4、创建MemberLoginVO类

  • 用户登陆成功之后,将MemberLoginVO类的对象,存入session域中

image-20200630132129326

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

6.5、Handler代码

  • 编写用户登录的逻辑
    • 调用远程mysql-provider,根据loginacct查询用户
    • 如果用户不存在,或者用户密码错误则返回相应的提示消息
    • 如果用户存在且密码正确,则将用户信息封装为MemberLoginVO对象,存入session域中

image-20200630132319634

@RequestMapping("/auth/member/do/login")
public String login(
		@RequestParam("loginacct") String loginacct, 
		@RequestParam("userpswd") String userpswd,
		ModelMap modelMap,
		HttpSession session) {
	
	// 1.调用远程接口根据登录账号查询MemberPO对象
	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 = resultEntity.getData();
	
	if(memberPO == null) {
		modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_LOGIN_FAILED);
		
		return "member-login";
	}
	
	// 2.比较密码
	String userpswdDataBase = memberPO.getUserpswd();
	
	BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
	
	boolean matcheResult = passwordEncoder.matches(userpswd, userpswdDataBase);
	
	if(!matcheResult) {
		modelMap.addAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_LOGIN_FAILED);
		
		return "member-login";
	}
	
	// 3.创建MemberLoginVO对象存入Session域
	MemberLoginVO memberLoginVO = new MemberLoginVO(memberPO.getId(), memberPO.getUsername(), memberPO.getEmail());
	session.setAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER, memberLoginVO);
	
	return "redirect:/auth/member/to/center/page";
}

6.6、创建会员中心页面

  • 老规矩:
    • 引入Thymeleaf名称空间
    • 修改字符编码
    • 添加base标签
    • session域中取出用户昵称,在页面上显示

image-20200630132812709

<!-- 上方导航栏显示用户昵称 -->
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="glyphicon glyphicon-user"></i> [[${session.loginMember.username}]]<span class="caret"></span></a>

<!-- 左侧导航栏显示用户昵称 -->
<div class="caption" style="text-align:center;">
	<h3>
		[[${session.loginMember.username}]]
	</h3>
	<span class="label label-danger" style="cursor:pointer;" onclick="window.location.href='accttype.html'">未实名认证</span>
</div>

6.7、添加view-controller

  • 工程路径下的/auth/member/to/center/page请求,对应着templates/member-center.html页面

image-20200628113531874

@Configuration
public class CrowdWebMvcConfig implements WebMvcConfigurer {
	
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		
		// 添加view-controller
		// 浏览器访问的地址	
		// 目标视图的名称,将来拼接“prefix: classpath:/templates/”、“suffix: .html”前后缀
		
		registry.addViewController("/auth/member/to/reg/page").setViewName("member-reg");
		registry.addViewController("/auth/member/to/login/page").setViewName("member-login");
		registry.addViewController("/auth/member/to/center/page").setViewName("member-center");
	}

}

6.8、测试

  • 用户名或用户密码不正确

image-20200630133827287

  • 成功登陆

image-20200630142248232

  • 我佛了,通过zuul访问为啥就是500错误?

image-20200630142424359

image-20200630142353652

  • 问题:仍然是没有回显表单
    • 解决办法一:存入request
    • 解决办法二:通过Ajax带回浏览器

7、会员退出登录

7.1、修改超链接

  • 修改【退出】按钮的超链接
<li><a href="index.html" th:href="@{/auth/member/logout}"><i class="glyphicon glyphicon-off"></i> 退出系统</a></li>

7.2、Handler代码

  • 清除session

image-20200630132319634

@RequestMapping("/auth/member/logout")
public String logout(HttpSession session) {	
    session.invalidate();		
    return "redirect:/";
}

8、@RequestBody

8.1、去掉@RequestBody

  • 去掉api远程调用接口和mysql-provider工程中的@RequestBody注解,会发生什么?

image-20200630212742800

image-20200630212834981

  • 日志打印MemberPO对象的属性值均为null,即MemberPO对象的属性值无法注入

8.2、分析原因

  • MemberPO对象的属性值无法注入,为什么?
    • 如果不写@RequestBodySpringMVC默认发送过来的请求参数为Query Strings的形式,即key1=value1&key2=value2
    • auth-consumer调用远程mysql-provider工程,传参数时,会将参数序列化为json字符串进行传输
    • 如果mysql-provider中的Handler方法不写@RequestBody注解,那么SpringMVC按照Query Strings的形式去解析json字符串,这样当然无法解析,所以无法为MemberPO对象的属性注入值
    • 所以必须加上@RequestBody注解,让mysql-provider中的Handler方法以json字符串的形式去解析请求参数,这样才能为MemberPO对象成功注入值

9、session共享

9.1、提出问题

  • 各个consumer之间的session域并不互通,auth-consumer微服务的session和其他consumer微服务的session并不是同一个session

9.2、SpringSession的使用

9.2.1、创建工程
  • 创建两个Maven Projectpro14-spring-session-apro14-spring-session-b

image-20200701195947814

9.2.2、引入依赖
  • 两个工程的依赖一样
<!-- SpringBoot父工程 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
</parent>

<dependencies>
    <!-- 引入SpringMVC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 引入springboot&redis整合场景 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- 引入springboot&springsession整合场景 -->
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
</dependencies>
9.2.3、创建配置文件
  • pro14-spring-session-a工程

image-20200701200547389

# Redis Host
spring.redis.host=192.168.152.129

# SpringSession use Redis
spring.session.store-type=redis
  • pro14-spring-session-b工程

image-20200701200656157

# Redis Host
spring.redis.host=192.168.152.129

# SpringSession use Redis
spring.session.store-type=redis

# Server Port
server.port=8181
9.2.4、创建主启动类
  • 两个工程主启动类一样

image-20200701200902456

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

}
9.2.5、创建Handler
  • pro14-spring-session-a工程

image-20200701201052577

@RestController
public class HelloHandler {
	
	@RequestMapping("/test/spring/session/save")
	public String testSession(HttpSession session) {
		
		session.setAttribute("king", "hello-king");
		
		return "数据存入Session域!";
	}

}
  • pro14-spring-session-b工程

image-20200701201117041

@RestController
public class HelloHandler {
	
	@RequestMapping("/test/spring/session/retrieve")
	public String testSession(HttpSession session) {
		
		String value = (String) session.getAttribute("king");
		
		return value;
	}

}
9.2.6、测试
  • 存入session

image-20200701202108302

image-20200701202306658

  • 取出session

image-20200701202344334

  • Session名称不是原来的JSESSIONID,变成了SESSION

image-20200701203738669

  • SpringSession是真他妈的牛逼,原有代码都不需要改动,这种非侵入式的框架,牛批!

10、SpringSession原理

10.1、SpringSession配置类

  • 熟悉的XxxProperties配置类
@ConfigurationProperties(prefix = "spring.session")
public class SessionProperties {

	/**
	 * Session store type.
	 */
	private StoreType storeType;

	/**
	 * Session timeout. If a duration suffix is not specified, seconds will be used.
	 */
	@DurationUnit(ChronoUnit.SECONDS)
	private Duration timeout;

	private Servlet servlet = new Servlet();

	private final ServerProperties serverProperties;
  • 熟悉的XxxAutoConfiguration自动配置类

    @EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class })

@Configuration
@ConditionalOnClass(Session.class)
@ConditionalOnWebApplication
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class })
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, HazelcastAutoConfiguration.class,
		JdbcTemplateAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class,
		RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class })
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
public class SessionAutoConfiguration {

10.2、SpringSession接管Session原理

image-20200701203911802

10.3、SessionRepositoryFilter

  • SessionRepositoryFilter 类中doFilterInternal方法大致流程

    • 接收包装前的HttpServletRequestHttpServletResponse

    • HttpServletRequestHttpServletResponse包装为我们自定义的SessionRepositoryRequestWrapperSessionRepositoryResponseWrapper

    • 将我们包装后的RequestResponse传入filterChain中,在之后的流程中,程序便会使用我们自定义的SessionRepositoryRequestWrapperSessionRepositoryResponseWrapper,执行我们重写之后的方法

      filterChain.doFilter(wrappedRequest, wrappedResponse);

image-20200701205145595

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
    request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
        request, response, this.servletContext);
    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
        wrappedRequest, response);

    try {
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    }
    finally {
        wrappedRequest.commitSession();
    }
}

10.4、SessionRepositoryRequestWrapper

  • SessionRepositoryRequestWrapper类中重写了有关Session部分的方法

image-20200701205718789

private S getRequestedSession() {
    if (!this.requestedSessionCached) {
        List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
            .resolveSessionIds(this);
        for (String sessionId : sessionIds) {
            if (this.requestedSessionId == null) {
                this.requestedSessionId = sessionId;
            }
            S session = SessionRepositoryFilter.this.sessionRepository
                .findById(sessionId);
            if (session != null) {
                this.requestedSession = session;
                this.requestedSessionId = sessionId;
                break;
            }
        }
        this.requestedSessionCached = true;
    }
    return this.requestedSession;
}
  • S是啥?S代表Session的子类

image-20200701205850117

@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {

10.5、HttpServletRequestWrapper

  • SessionRepositoryRequestWrapper继承于HttpServletRequestWrapper类,
  • 为什么SessionRepositoryRequestWrapper类只需要重写几个方法就能完成对Session 掌控?那得归功于HttpServletRequestWrapper
  • HttpServletRequestWrapper类中有默认的方法实现,如无重写需求,可直接调用,所以只需要重写我们需要重写的方法即可
public class HttpServletRequestWrapper extends ServletRequestWrapper implements
        HttpServletRequest {

    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     *
     * @throws java.lang.IllegalArgumentException
     *             if the request is null
     */
    public HttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    private HttpServletRequest _getHttpServletRequest() {
        return (HttpServletRequest) super.getRequest();
    }

    /**
     * The default behavior of this method is to return getAuthType() on the
     * wrapped request object.
     */
    @Override
    public String getAuthType() {
        return this._getHttpServletRequest().getAuthType();
    }

    /**
     * The default behavior of this method is to return getCookies() on the
     * wrapped request object.
     */
    @Override
    public Cookie[] getCookies() {
        return this._getHttpServletRequest().getCookies();
    }

    /**
     * The default behavior of this method is to return getDateHeader(String
     * name) on the wrapped request object.
     */
    @Override
    public long getDateHeader(String name) {
        return this._getHttpServletRequest().getDateHeader(name);
    }

    // ...

10.6、RedisOperationsSessionRepository

  • 继承树

image-20200701211329140

  • 顶级接口SessionRepository
public interface SessionRepository<S extends Session> {

	S createSession();
    
	void save(S session);

	S findById(String id);

	void deleteById(String id);
}
  • FindByIndexNameSessionRepository接口继承于SessionRepository接口
public interface FindByIndexNameSessionRepository<S extends Session>
		extends SessionRepository<S> {

	String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()
			.concat(".PRINCIPAL_NAME_INDEX_NAME");

	Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);

	default Map<String, S> findByPrincipalName(String principalName) {

		return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);

	}

}
  • RedisOperationsSessionRepository类实现了FindByIndexNameSessionRepository接口
public class RedisOperationsSessionRepository implements
		FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>,
		MessageListener {

RedisOperationsSessionRepository方法:使用Redissession保存维护起来

image-20200701211718111

public RedisOperationsSessionRepository(
    RedisOperations<Object, Object> sessionRedisOperations) {
    Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
    this.sessionRedisOperations = sessionRedisOperations;
    this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations,
                                                             this::getExpirationsKey, this::getSessionKey);
    configureSessionChannels();
}

@Override
public void save(RedisSession session) {
    session.save();
    if (session.isNew()) {
        String sessionCreatedKey = getSessionCreatedChannel(session.getId());
        this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
        session.setNew(false);
    }
}

public void cleanupExpiredSessions() {
    this.expirationPolicy.cleanExpiredSessions();
}

11、星图

image-20200701212332739

12、登录状态检查

12.1、思路

image-20200930222232310

12.2、Session共享

12.2.1、引入依赖
  • zuul工程和auth-consumer工程的pom文件中引入SpringSession整合Redi的依赖
<!-- 引入springboot&redis整合场景 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入springboot&springsession整合场景 -->
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>
12.2.2、修改配置文件
  • zuul工程:配置Redis主机地址,配置SpringSession使用Redis作为Session缓存

image-20200701221711944

server:
  port: 80
spring:
  application:
    name: atguigu-crowd-zuul
  redis:
    host: 192.168.152.129
  session:
    store-type: redis
  • auth-consumer工程:配置Redis主机地址,配置SpringSession使用Redis作为Session缓存

image-20200701221813362

server:
  port: 4000
spring:
  application:
    name: atguigu-crowd-auth
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
  redis:
    host: 192.168.152.129
  session:
    store-type: redis

12.3、需要放行的资源

  • 创建常量类,存储放行资源的地址(无需登录就能访问):
    • 访问特定请求地址无需登录
    • 访问静态资源无需登录
  • 静态方法judgeCurrentServletPathWetherStaticResource:检查当前请求是否为需要放行的资源

image-20200701221916810

public class AccessPassResources {
	
	public static final Set<String> PASS_RES_SET = new HashSet<>();
	
	static {
		PASS_RES_SET.add("/");
		PASS_RES_SET.add("/auth/member/to/reg/page");
		PASS_RES_SET.add("/auth/member/to/login/page");
		PASS_RES_SET.add("/auth/member/logout");
		PASS_RES_SET.add("/auth/member/do/login");
		PASS_RES_SET.add("/auth/do/member/register");
		PASS_RES_SET.add("/auth/member/send/short/message.json");
	}
	
	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");
	}

	/**
	 * 用于判断某个ServletPath值是否对应一个静态资源
	 * @param servletPath
	 * @return
	 * 		true:是静态资源
	 * 		false:不是静态资源
	 */
	public static boolean judgeCurrentServletPathWetherStaticResource(String servletPath) {
		
		// 1.排除字符串无效的情况
		if(servletPath == null || servletPath.length() == 0) {
			throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);
		}
		
		// 2.根据“/”拆分ServletPath字符串
		String[] split = servletPath.split("/");
		
		// 3.考虑到第一个斜杠左边经过拆分后得到一个空字符串是数组的第一个元素,所以需要使用下标1取第二个元素
		String firstLevelPath = split[1];
		
		// 4.判断是否在集合中
		return STATIC_RES_SET.contains(firstLevelPath);
	}

}

12.4、登录检查

12.4.1、引入依赖
  • Zuul工程中引入util工具类工程的依赖
<!-- 使用工具类 -->
<dependency>
    <groupId>com.atguigu.crowd</groupId>
    <artifactId>atcrowdfunding05-common-util</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
12.4.2、创建Filter
  • shouldFilter方法:判断当前请求路径是否为无需登录就可以访问的资源
    • 是:直接return false,放行资源
    • 否:return true,执行run方法,检查用户是否登录
  • run方法:从共享的session域中,检查用户是否已经登录
    • 已经登录:放行资源
    • 未登录:
      • 将错误信息CrowdConstant.MESSAGE_ACCESS_FORBIDEN放入共享session域中
      • 重定向至登录页面
  • filterType方法:pre表示在目标微服务前执行过滤

image-20200701222602851

@Component
public class CrowdAccessFilter extends ZuulFilter {

	@Override
	public boolean shouldFilter() {

		// 1.获取RequestContext对象
		RequestContext requestContext = RequestContext.getCurrentContext();

		// 2.通过RequestContext对象获取当前请求对象(框架底层是借助ThreadLocal从当前线程上获取事先绑定的Request对象)
		HttpServletRequest request = requestContext.getRequest();

		// 3.获取servletPath值
		String servletPath = request.getServletPath();

		// 4.根据servletPath判断当前请求是否对应可以直接放行的特定功能
		boolean containsResult = AccessPassResources.PASS_RES_SET.contains(servletPath);

		if (containsResult) {

			// 5.如果当前请求是可以直接放行的特定功能请求则返回false放行
			return false;
		}

		// 5.判断当前请求是否为静态资源
		// 工具方法返回true:说明当前请求是静态资源请求,取反为false表示放行不做登录检查
		// 工具方法返回false:说明当前请求不是可以放行的特定请求也不是静态资源,取反为true表示需要做登录检查
		return !AccessPassResources.judgeCurrentServletPathWetherStaticResource(servletPath);
	}

	@Override
	public Object run() throws ZuulException {
		
		// 1.获取当前请求对象
		RequestContext requestContext = RequestContext.getCurrentContext();
		HttpServletRequest request = requestContext.getRequest();
		
		// 2.获取当前Session对象
		HttpSession session = request.getSession();
		
		// 3.尝试从Session对象中获取已登录的用户
		Object loginMember = session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_MEMBER);
		
		// 4.判断loginMember是否为空
		if(loginMember == null) {
			
			// 5.从requestContext对象中获取Response对象
			HttpServletResponse response = requestContext.getResponse();
			
			// 6.将提示消息存入Session域
			session.setAttribute(CrowdConstant.ATTR_NAME_MESSAGE, CrowdConstant.MESSAGE_ACCESS_FORBIDEN);
			
			// 7.重定向到auth-consumer工程中的登录页面
			try {
				response.sendRedirect("/auth/member/to/login/page");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		return null;
	}

	@Override
	public String filterType() {
		
		// 这里返回“pre”意思是在目标微服务前执行过滤
		return "pre";
	}

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

}
12.4.3、Zuul的特殊配置
  • sensitive-headers: "*":在Zuul向其他微服务重定向时保持原本头信息(请求头、响应头)

image-20200701223113627

zuul:
  ignored-services: "*"
  sensitive-headers: "*"  # 在Zuul向其他微服务重定向时保持原本头信息(请求头、响应头)
  routes:
    crowd-portal:
      service-id: atguigu-crowd-auth
      path: /**           # 这里一定要使用两个“*”号,不然“/”路径后面的多层路径将无法访问
12.4.4、页面信息回显
  • 添加错误信息回显

image-20200704142912297

<p th:text="${session.message}">检查登录状态拦截后的提示消息</p>

12.5、测试

  • 静态资源无需登录就能访问

image-20200701223553277

  • 强行访问受保护的地址
    • 靠,我为啥就不能回显session中的数据呢???
    • 但编写测试方法测试,共享session确实是存在的啊。。。
    • 详细解决办法见day15
      • 通过zuul网关访问,走的是localhost:80这条线
      • 直接通过auth-consumer访问,走的是localhost:4000这条线
      • 域名不同,cookie不同,进而导致session不同

image-20200701231821229

image-20200701231853161

  • 解决分布式session问题之后,拦截成功,返回登录页面并显示提示消息

image-20200704143016176

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值