谷粒商城篇章7 ---- P211-P235 ---- 认证服务【分布式高级篇四】

发布时间:2023年12月28日

目录

1 环境搭建

1.1 新建认证服务模块gulimall-auth-server

1.2 认证服务模块基础配置

1.2.1 pom.xml

1.2.2 yml配置

1.2.2.1 application.yml配置

1.2.2.2 bootstrap.yml配置

1.2.3 主类

1.3 SwitchHosts增加配置

1.4 认证页面搭建

1.5 网关配置?

1.6 新增视图映射

2 验证码倒计时

2.1 注册页面

3?整合短信验证码

3.1 购买阿里云短信接口

3.2 PostMan测试短信接口

3.3 整合短信服务

3.3.1 新增HttpUtils.java

3.3.2 测试验证码发送

3.3.3 整合短信服务

3.3.3.1 抽取短信服务组件

3.3.3.2 抽取组件配置到application.yml配置文件

3.3.3.3 调用短信组件进行测试

4 验证码防刷校验

4.1 新增SmsSendController提供给其他服务调用

4.2 认证服务远程调用短信发送服务

4.2.1 远程调用短信发送接口

4.2.2 认证服务发送短信接口

4.2.3 前端调用发送验证码接口

?4.2.4 目前短信验证码接口的缺陷

4.2.5 验证码防刷

4.2.5.1 引入redis依赖

4.2.5.2 yml配置redis相关信息

4.2.5.3 认证服务短信验证码功能实现

4.2.5.4 测试

5 注册页面相关功能实现

5.1 页面参数JSR303校验

5.2 会员服务,存储注册用户信息

5.2.1 接收注册用户信息vo

5.2.2 定义用户名和手机号重复异常

5.2.3 用户注册controller

5.2.4 MD5&盐值&BCrypt

5.2.5 用户服务注册接口service

5.3 认证服务注册接口

6 账号密码登录

6.1 接收前端传的登录信息对象

6.2 用户服务的登录接口

6.2.1 接收登录信息的vo

6.2.2 controller层

6.2.3 service层

6.3 认证服务的登录接口

6.3.1 Feign接口,远程调用用户服务

6.3.2 controller层

6.3.3 登录页修改

7 社交登录(OAuth2.0)

7.1 OAuth2.0简介

7.2 微博社交登录(身份认证耗时略过)

7.3 Gitee社交登录

7.3.1 在Gitee创建第三方应用

7.3.2 修改登录页面,获取Gitee授权码

7.3.3 授权成功后回调接口编写

8 分布式Session共享问题

8.1 session原理

8.2 分布式下session共享问题

8.3 分布式session解决方案原理

8.3.1 Session共享问题解决-session复制

8.3.2?Session共享问题解决-客户端存储

8.3.3 Session共享问题解决-hash一致性

8.3.4?Session共享问题解决-统一存储

8.3.5?Session共享问题解决-不同服务,子域session共享?

8.4 SpringSession整合

8.4.1 引入依赖

8.4.2 配置session存储方式以及过期时间

8.4.3 开启SpringSession

8.4.4 测试?

8.5 自定义SpringSession完成子域session共享

8.6 SpringSession原理

8.6.1 原理

8.6.2??页面效果完成

9 单点登录SSO

9.1 单点登录简介

9.2 许雪里单点登录效果演示

9.3 单点登录实现

9.3.1 单点登录服务端gulimall-test-sso-server

9.3.1.1 创建单点登录服务模块

9.3.1.2 pom.xml中相关依赖

9.3.1.3 application.properties配置

9.3.1.4 LoginController登录控制层

9.3.1.5 login.html登录页

9.3.2 客户端 gulimall-test-sso-client

9.3.2.1 创建客户端(client1)模块

9.3.2.2 pom.xml中相关依赖

9.3.2.3 application.properties配置

9.2.3.4 HelloController测试控制层

9.2.3.5 list.html员工列表页

9.3.3 客户端gulimall-test-sso-client2

9.3.3.1 创建客户端(client2)

9.3.4 host配置

9.3.5 测试???????

9.3.6 总结


1 环境搭建

1.1 新建认证服务模块gulimall-auth-server

这里SpingBoot的版本与其他模块保持一致用2.7.8。

1.2 认证服务模块基础配置

1.2.1 pom.xml

引入gulimall-common模块依赖,并排除mybatis-plus-boot-starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.8</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.wen.gulimall</groupId>
	<artifactId>gulimall-auth-server</artifactId>
	<version>1.0</version>
	<name>gulimall-auth-server</name>
	<description>认证中心(社交登录、OAuth2.0、单点登录)</description>
	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>2021.0.5</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>com.wen.gulimall</groupId>
			<artifactId>gulimall-common</artifactId>
			<version>1.0</version>
			<exclusions>
				<exclusion>
					<groupId>com.baomidou</groupId>
					<artifactId>mybatis-plus-boot-starter</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

1.2.2 yml配置

1.2.2.1 application.yml配置

注册中心、服务名、端口。

spring:
  cloud:
    nacos:
      discovery:
        server-addr: xxx.xxx.xxx.xxx:8848
  application:
    name: gulimall-auth-server
server:
  port: 20000
1.2.2.2 bootstrap.yml配置

配置中心、命名空间、服务名。

spring:
  cloud:
    nacos:
      config:
        server-addr: xxx.xxx.xxx.xxx:8848
        namespace: 8664cc43-affa-4545-b82b-5a5d248ab872
  application:
    name: gulimall-auth-server

1.2.3 主类

开启服务发现和远程调用。

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {

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

}

1.3 SwitchHosts增加配置

添加认证服务的域名与ip映射:xxx.xxx.11.10 auth.gulimall.com

1.4 认证页面搭建

1. 将登录页面index.html复制到gulimall-auth-server/src/main/resources/templates/下,更名为login.html;
2. 将注册页面index.html复制到gulimall-auth-server/src/main/resources/templates/下,更名为reg.html;
3. 将登录页面和注册页面的静态资源放到nginx的/root/docker/nginx/html/static/目录下login文件夹和reg文件夹下;
4. 登录页面的src和href以/static/login/开头,注册页面的src和href以/static/reg/开头。

1.5 网关配置?

- id: gulimall_auth_route
  uri: lb://gulimall-auth-server
  predicates:
    # 由以下的主机域名访问转发到商品服务
    - Host=auth.gulimall.com

1.6 新增视图映射

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java

@Controller
public class LoginController {

    @GetMapping("/login.html")
    public String loginPage(){
        return "login";
    }

    @GetMapping("/reg.html")
    public String regPage(){
        return "reg";
    }
}

????????如果在controller中请求不做任何处理直接返回对应的视图是可以的,但是会出现很多空方法。这里就可以使用SpringMVC viewcontroller,将请求和页面进行映射,如下:

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/config/GulimallWebConfig.java

/**
 * @author W
 * @createDate 2023/9/14 14:24
 * @description 请求直接跳转页面没有其他逻辑可以使用以下视图映射
 */
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

    /**
     * 视图映射
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("login.html").setViewName("login");
        registry.addViewController("reg.html").setViewName("reg");
    }
}

测试:

调整商城首页、登录页、注册页面的跳转。

2 验证码倒计时

2.1 注册页面

1. 使用setTimeout()方法完成60s倒计时,可以参照以下网址:
https://www.w3school.com.cn/jsref/met_win_settimeout.asp

2. reg.html验证码相关代码:
    (1)将请发送验证码下面第一个<span>标签改为<a>标签,改动行5050行,如下
       <input maxlength="20" type="text" placeholder="请输入验证码" class="caa">
		</label>
				<a id="sendCode">发送验证码</a>
		   </div>

    (2)验证码倒计时相关js,如下:
         $(function (){
				$('#sendCode').click(function (){
					// 2. 倒计时
					if($(this).hasClass("disabled")){
						// 正在倒计时
					}else {
						// 1. 给指定手机号发送验证码
						timeoutChangeStyle();
					}
				});
			});
			var num = 60;
			function timeoutChangeStyle(){
				$('#sendCode').attr("class","disabled");
				if(num == 0){
					$('#sendCode').text("发送验证码");
					num = 60;
					$('#sendCode').attr("class","");
				}else {
					var str = num + "s后再次发送";
					$('#sendCode').text(str);
					// 计时器,注意多次点击,可能会有多个定时器,时间变快
					setTimeout("timeoutChangeStyle()",1000);
				}
				num --;
			}

3?整合短信验证码

3.1 购买阿里云短信接口

1. 登录阿里云-》点击云市场-》了解更多-》搜索短信
2. 视频中老师购买短信的地址,如下:
https://market.aliyun.com/products/57126001/cmapi024822.html?spm=5176.21213303.J_6704733920.11.49fb3edaM8bteY&scm=20140722.S_market%40%40API%E5%B8%82%E5%9C%BA%40%40cmapi024822..ID_market%40%40API%E5%B8%82%E5%9C%BA%40%40cmapi024822-RL%E4%B8%89%E5%90%88%E4%B8%80%E7%9F%AD%E4%BF%A1-OR_main-V_2-P0_2#sku=yuncode18822000012
因为以上购买地址需要企业认证,我这里使用个人认证的阿里云账户,使用如下购买地址:
https://market.aliyun.com/products/57126001/cmapi00037415.html?spm=5176.730005.result.8.370a3524CZKeDx&innerSource=search_%E7%9F%AD%E4%BF%A1#sku=yuncode31415000020
3. 购买后可以在云市场首页-》买家中心-》进入管理控制台-》已购买的服务中看到自己购买的短信接口

3.2 PostMan测试短信接口

1. 请求url:http://gyytz.market.alicloudapi.com/sms/smsSend
2. 请求方式:POST
2. 请求参数:
    1)mobile 手机号
    2)templateId 短信模板ID
    3)smsSignId 短信前缀ID(签名ID)
    4)param 短信模板变量 字符串格式:**key**:value,**key**:value。例如:**code**:12345,**minute**:5。
3. 简单身份认证,将AppCode放在Header中请求Header中添加的Authorization字段;配置Authorization字段的值为“APPCODE + 半角空格 +APPCODE值”。
格式如下:
        Authorization:APPCODE AppCode值

测试结果:

?手机收到短信:

3.3 整合短信服务

不应该前端直接请求阿里云短信接口,容易泄露APPCODE,造成损失。

3.3.1 新增HttpUtils.java

gulimall-third-party/src/main/java/com/wen/gulimall/thirdparty/util/HttpUtils.java

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() {
                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
                @Override
                public void checkClientTrusted(X509Certificate[] xcs, String str) {

                }
                @Override
                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);
        }
    }
}

3.3.2 测试验证码发送

@SpringBootTest
class GulimallThirdPartyApplicationTests {

	...

	@Test
	void sendSms(){
		String host = "https://gyytz.market.alicloudapi.com";
		String path = "/sms/smsSend";
		String method = "POST";
		String appcode = "804dd153e2824f12a4045331b19xxxxx";
		Map<String, String> headers = new HashMap<String, String>();
		//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
		headers.put("Authorization", "APPCODE " + appcode);
		Map<String, String> querys = new HashMap<String, String>();
		querys.put("mobile", "1xxxxxxxxx5");
		querys.put("param", "**code**:6789");

//smsSignId(短信前缀)和templateId(短信模板),可登录国阳云控制台自助申请。参考文档:http://help.guoyangyun.com/Problem/Qm.html

		querys.put("smsSignId", "2e65b1bb3d054466b82f0c9d125465e2");
		querys.put("templateId", "63698e3463bd490dbc3edc46a20c55f5");
		Map<String, String> bodys = new HashMap<String, String>();


		try {
			/**
			 * 重要提示如下:
			 * HttpUtils请从\r\n\t    \t* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java\r\n\t    \t* 下载
			 *
			 * 相应的依赖请参照
			 * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
			 */
			HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
			System.out.println(response.toString());
			//获取response的body
			System.out.println(EntityUtils.toString(response.getEntity()));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

3.3.3 整合短信服务

3.3.3.1 抽取短信服务组件

gulimall-third-party/src/main/java/com/wen/gulimall/thirdparty/component/SmsComponent.java

注意:host、path、smsSignId、templateId、appcode这些属性值从yml配置文件中获取。

@ConfigurationProperties(prefix = "spring.alicloud.sms")
@Data
@Component
public class SmsComponent {

    private String host;
    private String path;
    private String smsSignId;
    private String templateId;
    private String appcode;


    public void sendSms(String phone, String code){
        String method = "POST";
        Map<String, String> headers = new HashMap<String, String>();
        //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> querys = new HashMap<String, String>();
        querys.put("mobile", phone);
        querys.put("param", "**code**:"+code);

//smsSignId(短信前缀)和templateId(短信模板),可登录国阳云控制台自助申请。参考文档:http://help.guoyangyun.com/Problem/Qm.html

        querys.put("smsSignId", smsSignId);
        querys.put("templateId", templateId);
        Map<String, String> bodys = new HashMap<String, String>();


        try {
            /**
             * 重要提示如下:
             * HttpUtils请从\r\n\t    \t* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java\r\n\t    \t* 下载
             *
             * 相应的依赖请参照
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
             */
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            System.out.println(response.toString());
            //获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
3.3.3.2 抽取组件配置到application.yml配置文件

1. gulimall-third-party模块,引入配置提示依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

2. 新增配置

gulimall-third-party/src/main/resources/application.yml

spring:
  ...
  alicloud:
    sms:
      host: https://gyytz.market.alicloudapi.com
      path: /sms/smsSend
      sms-sign-id: 2e65b1bb3d054466b82f0c9d125465e2
      template-id: 63698e3463bd490dbc3edc46a20c55f5
      appcode: 804dd153e2824f12a4045331b1976e20
3.3.3.3 调用短信组件进行测试
@SpringBootTest
class GulimallThirdPartyApplicationTests {

	...

	@Resource
	private SmsComponent smsComponent;

	@Test
	void testSendCode(){
		smsComponent.sendSms("18xxxxxxx25","789456123");
	}
}

?验证码发送成功,手机收到验证码。

4 验证码防刷校验

4.1 新增SmsSendController提供给其他服务调用

前端不直接调用第三方服务,短信接口提供给别的服务进行调用。

gulimall-third-party/src/main/java/com/wen/gulimall/thirdparty/controller/SmsSendController.java

@RestController
@RequestMapping("/sms")
public class SmsSendController {
    @Resource
    private SmsComponent smsComponent;

    /**
     * 前端不直接调用第三方服务,提供给别的服务进行调用
     * @param phone
     * @param code
     * @return
     */
    @GetMapping("/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code){
        smsComponent.sendSms(phone,code);
        return R.ok();
    }
}

4.2 认证服务远程调用短信发送服务

4.2.1 远程调用短信发送接口

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/feign/ThirdPartyFeignService.java

@FeignClient("gulimall-third-party")
public interface ThirdPartyFeignService {
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}

4.2.2 认证服务发送短信接口

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java

@Controller
public class LoginController {
    @Resource
    private ThirdPartyFeignService thirdPartyFeignService;

   @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        String code = generateCode(5);
        // 验证码只能是数字
        thirdPartyFeignService.sendCode(phone,code);
        return R.ok();
    }
    // 生成纯数字随机验证码
    public static String generateCode(int length) {
        StringBuilder sbr = new StringBuilder();
        if(length <= 0) {
            throw new RuntimeException("验证码长度不能为负数");
        }
        Random random = new Random();
        for (int i = 0; i < length; i++) {
            //随机生成一个 0-9 的正整数
            int index = random.nextInt(10) ;
            sbr.append(index);
        }
        return sbr.toString();
    }
}

注意:我使用的这个阿里云短信接口只支持纯数字验证码,这里自定义了验证码生成方法。

4.2.3 前端调用发送验证码接口

发送短信验证码成功:

?4.2.4 目前短信验证码接口的缺陷

问题:
1. 暴漏了验证码发送的请求,可能会被恶意攻击,照成损失
2. 发送验证码虽然有60s的倒计时,但是刷新页面可以重新发送,没有限制
要求:
1. 验证码必须要有严格意义上的60s重新发送;
2. 验证码必须要有有效期

4.2.5 验证码防刷

? ? ? ? 1. 发送验证码之前先去redis查询,是否间隔超过60s,否提示错误信息不允许发送验证码;

? ? ? ? 2. 将验证码存放到redis设置过期时间10min,并存入当前时间。

4.2.5.1 引入redis依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
4.2.5.2 yml配置redis相关信息
spring:
  ...
  redis:
  host: 1xx.x.x.10
  port: 6379
4.2.5.3 认证服务短信验证码功能实现

redis中短信验证码key前缀

gulimall-common/src/main/java/com/wen/common/constant/AuthServerConstant.java

public class AuthServerConstant {
    /**
     * 短信验证码在redis中的前缀
     */
    public static final String SMS_CODE_CACHE_PREFIX="sms:code:";
}

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java?

@Controller
public class LoginController {
    @Resource
    private ThirdPartyFeignService thirdPartyFeignService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        // 1. 接口防刷
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
        if(StringUtils.isNotEmpty(redisCode)){
            long l = Long.parseLong(redisCode.split("_")[1]);
            if(System.currentTimeMillis()-l<60000){
                // 60秒内不能在发送
                return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
            }
        }
        // 2. 验证码的再次校验,验证码是暂时存储无需持久化,所以存放在redis。key-phone,value-code
        String code = generateCode(5);
        // redis中缓存验证码,防止同一个手机号在60秒内再次发送验证码
        stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code+"_"+System.currentTimeMillis(),10, TimeUnit.MINUTES);
        // 验证码只能是数字
        thirdPartyFeignService.sendCode(phone,code);
        return R.ok();
    }

    public static String generateCode(int length) {
        StringBuilder sbr = new StringBuilder();
        if(length <= 0) {
            throw new RuntimeException("验证码长度不能为负数");
        }
        Random random = new Random();
        for (int i = 0; i < length; i++) {
            //随机生成一个 0-9 的正整数
            int index = random.nextInt(10) ;
            sbr.append(index);
        }
        return sbr.toString();
    }
}

异常码和异常信息

gulimall-common/src/main/java/com/wen/common/exception/BizCodeEnum.java?

?注册页面

gulimall-auth-server/src/main/resources/templates/reg.html

4.2.5.4 测试

点击注册页面发送验证码

60s内再次发送同一个手机号的验证码

5 注册页面相关功能实现

5.1 页面参数JSR303校验

认证服务引入JSR303数据校验相关依赖

<!-- 数据校验 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

注册页面接收参数对象UserRegistVo,进行JSR303数据校验

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/vo/UserRegistVo.java

@Data
public class UserRegistVo {

    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6,max = 18,message = "用户名必须是6-8位字符")
    private String userName;

    @NotEmpty(message = "密码必须填写")
    @Length(min = 6,max = 18,message = "密码必须是6-8位字符")
    private String password;

    @NotEmpty(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手机号格式不正确")
    private String phone;

    @NotEmpty(message = "验证码必须填写")
    private String code;
}

注册页面表单提交控制器

@Valid注解用于表示校验的对象,BindingResult用来接收校验异常信息

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java

@Controller
public class LoginController {

   ...

    /**
     * 1.可以使用BindingResult接收校验异常信息,也可以使用全局异常处理来捕获处理
     * 2. RedirectAttributes redirectAttributes :模拟重定向携带数据
     * // todo:重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面取出这个数据后,session里面的数据就会删掉。(分布式情况下有问题)
     * // todo:分布式下的session问题。
     * @param vo
     * @param result
     * @return
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes){
        if(result.hasErrors()){
            // 会出现重复key错误
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            redirectAttributes.addFlashAttribute("errors",errors);
            // 校验出错,转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }
        // 调用远程服务注册

        return "redirect:/login.html";
    }
}

注册表单代码调整,需要注释掉表单提交按钮单击事件的代码

gulimall-auth-server/src/main/resources/templates/reg.html

<form action="/regist" method="post" class="one">
	<div class="register-box">
		<label  class="username_label">用 户 名
			<input name="userName" maxlength="20" type="text" placeholder="您的用户名和登录名">
		</label>
		<div class="tips" style="color: red"
			 th:text="${errors!=null?(#maps.containsKey(errors, 'userName')?errors.userName:''):''}">

		</div>
	</div>
	<div class="register-box">
		<label class="other_label">设 置 密 码
			<input name="password" maxlength="20" type="password" placeholder="建议至少使用两种字符组合">
		</label>
		<div class="tips" style="color: red"
			 th:text="${errors!=null?(#maps.containsKey(errors, 'password')?errors.password:''):''}">

		</div>
	</div>
	<div class="register-box">
		<label class="other_label">确 认 密 码
			<input maxlength="20" type="password" placeholder="请再次输入密码">
		</label>
		<div class="tips">

		</div>
	</div>
	<div class="register-box">
		<label class="other_label">
			<span>中国 0086∨</span>
			<input name="phone" class="phone" id="phoneNum" maxlength="20" type="text" placeholder="建议使用常用手机">
		</label>
		<div class="tips" style="color: red"
			 th:text="${errors!=null?(#maps.containsKey(errors, 'phone')?errors.phone:''):''}">

		</div>
	</div>
	<div class="register-box">
		<label class="other_label">验 证 码
			<input name="code" maxlength="20" type="text" placeholder="请输入验证码" class="caa">
		</label>
		<a id="sendCode">发送验证码</a>
		<div class="tips" style="color: red"
			 th:text="${errors!=null?(#maps.containsKey(errors, 'code')?errors.code:''):''}">

		</div>
	</div>
	<div class="arguement">
		<input type="checkbox" id="xieyi"> 阅读并同意
		<a href="/static/reg/#">《谷粒商城用户注册协议》</a>
		<a href="/static/reg/#">《隐私政策》</a>
		<div class="tips">

		</div>
		<br/>
		<div class="submit_btn">
			<button type="submit" id="submit_btn">立 即 注 册</button>
		</div>
	</div>

</form>

5.2 会员服务,存储注册用户信息

5.2.1 接收注册用户信息vo

gulimall-member/src/main/java/com/wen/gulimall/member/vo/MemberRegistVo.java

@Data
public class MemberRegistVo {
    private String userName;
    private String password;
    private String phone;
}

5.2.2 定义用户名和手机号重复异常

gulimall-member会员服务,用户注册逻辑:
(1)如果当前注册会员名或手机号重复,业务层抛出相应的自定义异常,控制层捕获异常并封装返回相应的错误信息;
(2)如果没有被注册过,保存传递过来的注册信息,设置默认会员等级和创建时间。

gulimall-member/src/main/java/com/wen/gulimall/member/exception/PhoneExistException.java

public class PhoneExistException extends RuntimeException{
    public PhoneExistException() {
        super("手机号已存在");
    }
}

gulimall-member/src/main/java/com/wen/gulimall/member/exception/UsernameExistException.java

public class UsernameExistException extends RuntimeException{
    public UsernameExistException() {
        super("用户名已存在");
    }
}

5.2.3 用户注册controller

gulimall-member/src/main/java/com/wen/gulimall/member/controller/MemberController.java

@RestController
@RequestMapping("member/member")
public class MemberController {
    @Autowired
    private MemberService memberService;

   ...

    @PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo vo){
        try {
            memberService.regist(vo);
        } catch (PhoneExistException e) {
            return R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
        } catch (UsernameExistException e){
            return R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(), BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
        }
        return R.ok();
    }

}

定义异常编码和异常信息

gulimall-common/src/main/java/com/wen/common/exception/BizCodeEnum.java?

public enum BizCodeEnum {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验异常"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已存在");
    ...
}

5.2.4 MD5&盐值&BCrypt

????????加密算法有可逆和不可逆之分,密码加密应该使用不可逆加密算法安全性更高。

1. MD5&MD5盐值加密

  • MD5
    • Message Digest algorithm 5,信息摘要算法
      • 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
      • 容易计算:从原数据计算出MD5值很容易。
      • 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
      • 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
      • 不可逆
  • 加盐
    • 通过生成随机数与MD5生成字符串进行组合。
    • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可。

2. BCryptPasswordEncoder

????????spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。

优势:

  1. 每次编码都会随机生成不一样的salt值去编码,所以相同密码加密后值不同
  2. 可控制代价因子(也就是循环加密次数的控制)让加密速度慢,破解代价增大。
  • 加密encode
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 加密
String encode = passwordEncoder.encode("123456");
  • 匹配matches

? ? ? ? 尽管同一密码每次加密后的密文不一样,但可以通过matches进行匹配。它的原理是把需要配对的密码经过同一个hash函数计算,把计算得到的hash值到数据库中匹配,相同的hash值则说明是同一个密码。

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 加密
String encode = passwordEncoder.encode("123456");
// 使用明文和密文进行比较
boolean matches = passwordEncoder.matches("123456", encode);

5.2.5 用户服务注册接口service

gulimall-member/src/main/java/com/wen/gulimall/member/service/MemberService.java

public interface MemberService extends IService<MemberEntity> {

    ...

    void regist(MemberRegistVo vo);


    void checkUsernameUnique(String username) throws UsernameExistException;

    void checkPhoneUnique(String phone) throws PhoneExistException;
}

?gulimall-member/src/main/java/com/wen/gulimall/member/service/impl/MemberServiceImpl.java

@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    @Resource
    private MemberLevelDao memberLevelDao;

    ...

    @Override
    public void regist(MemberRegistVo vo) {
        MemberEntity memberEntity = new MemberEntity();

        // 设置默认等级
        MemberLevelEntity memberLevelEntity = memberLevelDao.getDefaultLevel();
        memberEntity.setLevelId(memberLevelEntity.getId());

        checkPhoneUnique(vo.getPhone());
        checkUsernameUnique(vo.getUserName());
        // 检查用户名和邮箱是否唯一
        memberEntity.setUsername(vo.getUserName());
        memberEntity.setMobile(vo.getPhone());

        // 设置密码加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        memberEntity.setPassword(encode);
        // 其他的默认信息
        // 保存
        this.baseMapper.insert(memberEntity);

    }

    @Override
    public void checkUsernameUnique(String username) throws UsernameExistException{
        Long count = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
        if(count>0L){
            throw new UsernameExistException();
        }
    }

    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException{
        Long count = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if(count>0L){
            throw new PhoneExistException();
        }
    }

}

5.3 认证服务注册接口

Feign接口

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/feign/MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @PostMapping("/member/member/regist")
    R regist(@RequestBody UserRegistVo vo);
}

注册接口

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java

@Controller
public class LoginController {
    @Resource
    private ThirdPartyFeignService thirdPartyFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private MemberFeignService memberFeignService;
 
    ...

    /**
     * 1.可以使用BindingResult接收校验异常信息,也可以使用全局异常处理来捕获处理
     * 2. RedirectAttributes redirectAttributes :模拟重定向携带数据
     * // todo:重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面取出这个数据后,session里面的数据就会删掉。(分布式情况下有问题)
     * // todo:分布式下的session问题。
     * @param vo
     * @param result
     * @return
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes){
        if(result.hasErrors()){
            // todo 会出现重复key错误
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            redirectAttributes.addFlashAttribute("errors",errors);
            // 校验出错,转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }
        // 调用远程服务注册
        // 1. 校验验证码
        String code = vo.getCode();
        String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if(!StringUtils.isEmpty(s)){
            if(code.equals(s.split("_")[0])){
                // 删除验证码,只能用一次;令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
                // 调用远程服务注册
                R r = memberFeignService.regist(vo);
                if(r.getCode() == 0){
                    // 成功
                    return "redirect:http://auth.gulimall.com/login.html";
                } else {
                    Map<String, String> errors = new HashMap<>();
                    errors.put("msg",r.getData("msg",new TypeReference<String>(){}));
                    redirectAttributes.addFlashAttribute("errors",errors);
                    return "redirect:http://auth.gulimall.com/reg.html";
                }
            }else {
                Map<String, String> errors = new HashMap<>();
                errors.put("code","验证码错误");
                redirectAttributes.addFlashAttribute("errors",errors);
                // 校验出错,转发到注册页
                return "redirect:http://auth.gulimall.com/reg.html";
            }

        }else {
            Map<String, String> errors = new HashMap<>();
            errors.put("code","验证码已失效,请重新发送");
            redirectAttributes.addFlashAttribute("errors",errors);
            // 校验出错,转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }

        //return "redirect:/login.html";
    }

注册页面,添加远程会员服务注册异常信息提示,用户名重复、手机号重复

gulimall-auth-server/src/main/resources/templates/reg.html

<form action="/regist" method="post" class="one">
	<div class="tips" style="color: red"
		 th:text="${errors!=null?(#maps.containsKey(errors, 'msg')?errors.msg:''):''}">

	</div>
    ...
</from>

6 账号密码登录

6.1 接收前端传的登录信息对象

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/vo/UserLoginVo.java

@Data
public class UserLoginVo {
    private String loginAccount;
    private String password;
}

6.2 用户服务的登录接口

? ? ? ? 提供给认证服务远程调用。

6.2.1 接收登录信息的vo

会员服务的接收登录信息的vo属性要和认证服务接收前端登录信息vo的属性一致

gulimall-member/src/main/java/com/wen/gulimall/member/vo/MemberLoginVo.java

@Data
public class MemberLoginVo {
    private String loginAccount;
    private String password;
}

6.2.2 controller层

gulimall-member/src/main/java/com/wen/gulimall/member/controller/MemberController.java

@RestController
@RequestMapping("member/member")
public class MemberController {
    @Autowired
    private MemberService memberService;

    ...

    @PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo){
        MemberEntity memberEntity = memberService.login(vo);
        if(memberEntity!=null){
            return R.ok();
        }else {
            return R.error(BizCodeEnum.LOGINACCOUNT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnum.LOGINACCOUNT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }
}

异常信息

gulimall-common/src/main/java/com/wen/common/exception/BizCodeEnum.java

public enum BizCodeEnum {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验异常"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已存在"),
    LOGINACCOUNT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误");

    ...
}

6.2.3 service层

gulimall-member/src/main/java/com/wen/gulimall/member/service/MemberService.java

public interface MemberService extends IService<MemberEntity> {

    ...

    MemberEntity login(MemberLoginVo vo);
}

gulimall-member/src/main/java/com/wen/gulimall/member/service/impl/MemberServiceImpl.java

@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    @Resource
    private MemberLevelDao memberLevelDao;
    
    ...

    @Override
    public MemberEntity login(MemberLoginVo vo) {
        String loginAccount = vo.getLoginAccount();
        String password = vo.getPassword();

        // 用户名或手机号登录
        MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginAccount)
                .or().eq("mobile", loginAccount));
        if(memberEntity == null){
            // 登录失败
            return null;
        }else {
            String passwordDb = memberEntity.getPassword();
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            // 匹配密码
            boolean matches = passwordEncoder.matches(password, passwordDb);
            if (matches){
                return memberEntity;
            }else {
                return null;
            }
        }
    }
}

6.3 认证服务的登录接口

6.3.1 Feign接口,远程调用用户服务

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/feign/MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {

    ...

    @PostMapping("/member/member/login")
    R login(@RequestBody UserLoginVo vo);
}

6.3.2 controller层

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java

@Controller
public class LoginController {
    @Resource
    private ThirdPartyFeignService thirdPartyFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private MemberFeignService memberFeignService;
   
    ...

    @PostMapping("/login")
    public String login(UserLoginVo vo,RedirectAttributes redirectAttributes){

        // 远程登录
        R login = memberFeignService.login(vo);
        if(login.getCode() == 0){
            // 成功
            return "redirect:http://gulimall.com";
        }else {
            Map<String, String> errors = new HashMap<>();
            errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }
}

6.3.3 登录页修改

登录以表单的方式提交

gulimall-auth-server/src/main/resources/templates/login.html

<form action="/login" method="post">
	<div style="color: red"
		 th:text="${errors!=null?(#maps.containsKey(errors, 'msg')?errors.msg:''):''}">
	</div>
	<ul>
		<li class="top_1">
			<img src="/static/login/JD_img/user_03.png" class="err_img1"/>
			<input type="text" name="loginAccount" placeholder=" 邮箱/用户名/已验证手机" class="user"/>
		</li>
		<li>
			<img src="/static/login/JD_img/user_06.png" class="err_img2"/>
			<input type="password" name="password" placeholder=" 密码" class="password"/>
		</li>
		<li class="bri">
			<a href="">忘记密码</a>
		</li>
		<li class="ent">
			<button class="btn2" type="submit"><a>登 &nbsp; &nbsp;录</a></button>
		</li>
	</ul>
</form>

7 社交登录(OAuth2.0)

? ? ? ? QQ、微博、github等网站的用户量非常大,别的网站为了简化自我网站的登录与注册逻辑,引入社交登录功能。

步骤:

1)用户点击QQ按钮;

2)引导跳转到QQ授权页;

3)用户主动点击授权,跳回之前的网页。

7.1 OAuth2.0简介

  • OAuth: OAuth(开放授权) 是一个开放标准, 允许用户授权第三方网站访问他们存储

在另外的服务提供者上的信息, 而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。

  • OAuth2.0: 对于用户相关的 OpenAPI(例如获取用户信息, 动态同步, 照片, 日志, 分

享等) , 为了保护用户数据的安全和隐私, 第三方网站访问用户数据前都需要显式的向
用户征求授权。

  • 官方版流程:

( A) 用户打开客户端以后, 客户端要求用户给予授权。
( B) 用户同意给予客户端授权。
( C) 客户端使用上一步获得的授权, 向认证服务器申请令牌。
( D) 认证服务器对客户端进行认证以后, 确认无误, 同意发放令牌。
( E) 客户端使用令牌, 向资源服务器申请获取资源。
( F) 资源服务器确认令牌无误, 同意向客户端开放资源。

以登录CSDN为例:

7.2 微博社交登录(身份认证耗时略过)

1. 微博开放平台配置
    (1)https://open.weibo.com/-》登录微博-》微连接-》网站接入-》立即接入-》创建应用
    (2)高级信息-》OAuth2.0授权设置
        1)授权回调页:http://auth.gulimall.com//oauth2.0/weibo/success
        2)取消授权回调页:http://gulimall.com/fail
    (3)OAuth2.0授权认证文档:https://open.weibo.com/wiki/授权机制说明
        1)修改微博登录的a标签跳转地址:https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
        修改client_id为创建应用的基本信息中App Key,redirect_uri为授权回调页地址。
        2)用户同意微博授权登录后跳转到回调页并带上授权码(code=xxxxx)
        3)根据授权码code换取Access Token,注意code只能使用一次。
        可以使用PostMan访问以下地址https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE
进行Access Token获取测试,注意获取Access Token的请求方式是POST。
        返回结果:
                {
                   "access_token": "SlAV32hkKG",
                    "remind_in": 3600,
                    "expires_in": 3600
                }
        4)可以根据access_token访问微博授权的接口,比如获取用户信息等,用户已有权限访问的微博接口可以访问地址https://open.weibo.com/apps/2129105835/privilege【access_token有存活时间,可重复使用】查看,即在我的应用-》接口管理-》已有权限中可以查看微博已授权通过access_token可以访问的接口。
    注意:换取access token需要的参数client_secret是必须保密的,同时通过access token获取用户相关信息等也需要对access token进行保密,所以换取access token应该通过后台进行。

换取Access_Token:

微博社交登录流程:

7.3 Gitee社交登录

????????微博开放平台身份认证麻烦,这里使用Gitee测试社交登录。

7.3.1 在Gitee创建第三方应用

1. https://gitee.com/explore登录Gitee
2. 点击设置-》第三方应用-》创建应用(填好必填信息提交)
3. 点击自己创建好的应用gulimall-shop可以查看应用详情(Client ID、Client Secret、应用回调地址等)

?

7.3.2 修改登录页面,获取Gitee授权码

获取Gitee授权码可以参考Gitee的OAuth文档:Gitee OAuth 文档

?注意:Gitee的图片可以访问地址https://e-assets.gitee.com/gitee-community-web/_next/static/media/logo-black.0c964084.svg获取,保存到nginx静态资源相关目录下(/nginx/html/static/login/JD_img/)。

<li>
	<a href="https://gitee.com/oauth/authorize?client_id=d12cc89d9c15e36a43edd0a79011a95e5764888f3538ea4eff9c5fc23afdbdb6&redirect_uri=http://auth.gulimall.com/oauth2.0/gitee/success&response_type=code">
		<img style="width: 50px;height: 18px" src="/static/login/JD_img/gitee.png" />

	</a>
</li>

7.3.3 授权成功后回调接口编写

新增社交登录用户对象GiteeSocialUser存放授权登录成功获取的accessToken相关信息。这个对象可以放到公共模块下便于远程调用。

@Data
public class GiteeSocialUser {
    private String access_token;
    private String token_type;
    private String expires_in;
    private String refresh_token;
    private String scope;
    private String created_at;
}

社交登录成功回调,需要远程调用member服务进行认证

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/OAuth2Controller.java

@Slf4j
@Controller
@RequestMapping("/oauth2.0")
public class OAuth2Controller {
    @Resource
    private MemberFeignService memberFeignService;

    /**
     * 社交登录成功回调
     * @param code
     * @return
     * @throws Exception
     */
    @GetMapping("/gitee/success")
    public String gitee(@RequestParam("code") String code, HttpSession session) throws Exception {
        Map<String, String> param = new HashMap<>();
        param.put("grant_type","authorization_code");
        param.put("client_id","d12cc89d9c15e36a43edd0a79011a95e5764888f3538ea4eff9c5fc23afdbdb6");
        param.put("client_secret","409692b27e5f16a14e6fe4e1cbb053efaea85d7952e11cbb5c43b83cbe6ffd10");
        param.put("redirect_uri","http://auth.gulimall.com/oauth2.0/gitee/success");
        param.put("code",code);
        // 1. 根据code换取accessToken
        HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", new HashMap<>(), new HashMap<>(), param);

        if(response.getStatusLine().getStatusCode() == 200){
            // 获取到accessToken
            String jsonStr = EntityUtils.toString(response.getEntity());
            GiteeSocialUser giteeSocialUser = JSON.parseObject(jsonStr, GiteeSocialUser.class);

            // 知道当前是哪个社交用户
            // 1)当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员)
            // 登录或注册这个社交用户
            R oauth2Login = memberFeignService.oauth2Login(giteeSocialUser);
            if(oauth2Login.getCode() == 0){
                MemberRespVo data = oauth2Login.getData("data", new TypeReference<MemberRespVo>() {
                });
                log.info("登录成功,用户:{}",data);
                // 1. 第一次使用session,命令浏览器保存卡号。JSESSIONID这个cookie;
                // 以后浏览器访问哪个网站都会带上这个网站的cookie;
                // 子域之间;gulimall.com auth.gulimall.com order.gulimall.com
                // 发卡时(指定域名为父域名),即使子域系统发的卡,也能让父域直接使用。
                session.setAttribute("loginUser",data);
                // 2. 登录成功就跳回首页
                return "redirect:http://gulimall.com";
            }else {
                return "redirect:http://auth.gulimall.com/login.html";
            }

        }else {
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }
}

远程调用member服务进行认证的openFeign接口oauth2Login

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/feign/MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
   
    ...

    @PostMapping("/member/member/oauth2/login")
    R oauth2Login(@RequestBody GiteeSocialUser socialUser) throws Exception;
}

member服务的社交登录接口controller层

gulimall-member/src/main/java/com/wen/gulimall/member/controller/MemberController.java

@RestController
@RequestMapping("member/member")
public class MemberController {
    @Autowired
    private MemberService memberService;

    ...

    @PostMapping("/oauth2/login")
    public R oauth2Login(@RequestBody GiteeSocialUser socialUser) throws Exception {
        MemberEntity entity = memberService.login(socialUser);
        if(entity!=null){
            return R.ok().setData(entity);
        }else {
            return R.error(BizCodeEnum.LOGINACCOUNT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnum.LOGINACCOUNT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }
}

?涉及的异常枚举信息,gulimall-common/src/main/java/com/wen/common/exception/BizCodeEnum.java

public enum BizCodeEnum {
    
    ...

    LOGINACCOUNT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误");
    
    ...
}

member服务的社交登录接口servicer层

gulimall-member/src/main/java/com/wen/gulimall/member/service/MemberService.java

public interface MemberService extends IService<MemberEntity> {

  ...

    MemberEntity login(GiteeSocialUser socialUser) throws Exception;
}

gulimall-member/src/main/java/com/wen/gulimall/member/service/impl/MemberServiceImpl.java

这里需要判断该用户是否使用该社交账号登录过,所以需要子啊数据库的ums_member表中保存这叫登录信息,这里需要增加三个字段:social_uid(社交用户的唯一id)、access_token(访问令牌)、expires_in(访问令牌的过期时间)。

这里的处理逻辑和老师视频中的不太一样,因为获取access_token返回的相关信息中没有id,所以这里要先根据access_token获取gitee用户id。

如下,获取的用户信息中没有用户性别,代码中暂且不设置性别信息。

具体代码如下:

@Service("memberService")
public class MemberServiceImpl extends ServiceImpl<MemberDao, MemberEntity> implements MemberService {

    ...

    @Override
    public MemberEntity login(GiteeSocialUser socialUser) throws Exception {
        // 注册和登录合并逻辑
        Map<String, String> query = new HashMap<>();
        query.put("access_token",socialUser.getAccess_token());
        // gitee获取accessToken没有返回登录用户id,获取社交登录id
        HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user","get", new HashMap<>(), query);
        if(response.getStatusLine().getStatusCode() == 200){
            String json = EntityUtils.toString(response.getEntity());
            JSONObject jsonObject = JSON.parseObject(json);
            String id = jsonObject.getString("id");
            String name = jsonObject.getString("name");
            // 判断当前社交用户是否已经登录过系统
            MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", id));
            if(memberEntity!=null){
                // 该用户已经注册,修改访问令牌、令牌过期时间
                MemberEntity update = new MemberEntity();
                update.setId(memberEntity.getId());
                update.setAccessToken(socialUser.getAccess_token());
                update.setExpiresIn(socialUser.getExpires_in());

                this.baseMapper.updateById(update);
                memberEntity.setAccessToken(socialUser.getAccess_token());
                memberEntity.setExpiresIn(socialUser.getExpires_in());
                return memberEntity;
            }else {
                // 没有查询到当前社交用户对应的记录我们需要注册一个
                MemberEntity regist = new MemberEntity();
                regist.setSocialUid(id);
                regist.setNickname(name);
                // Gitee返回的用户信息没有性别,所以这里不设置
                regist.setAccessToken(socialUser.getAccess_token());
                regist.setExpiresIn(socialUser.getExpires_in());
                // 插入用户
                this.baseMapper.insert(regist);
                return regist;
            }
        }
        return null;
    }
}

8 分布式Session共享问题

8.1 session原理

? ? ? ? Session是指在服务器端存储用户信息的一种技术。当用户第一次访问网站时,服务器会为该用户创建一个Session对象,并生成一个Session ID,浏览器将Session ID保存在Cookie中(jsessionid),下次访问网站时会自动将Cookie中的Session ID发送给服务器。服务器接收到Session ID后,会根据Session ID找到对应的Session对象,将用户信息保存在Session对象中。

8.2 分布式下session共享问题

分布式下session共享问题:

(1)相同服务:由Session原理,可知Session时保存在服务器端的一种技术,相同服务不同服务器,Session是不共享的。

(2)不同服务:获取Session对象是根据Cookie中保存的JSESSIONID去服务器获取的,不同的会话Session ID不同对应的Session对象也不同。

8.3 分布式session解决方案原理

8.3.1 Session共享问题解决-session复制

保证相同服务存储的session一致。

  • 优点
    • web-server(Tomcat)原生支持,只需要修改配置文件。
  • 缺点
    • session同步需要数据传输,占用大量的网络带宽,降低了服务器群的业务处理能力;
    • 任意一台web-server保存的数据都是所有web-server的session总和,受到内存限制无法水平扩展更多的web-server;
    • 大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。? ? ? ??

8.3.2?Session共享问题解决-客户端存储

  • 优点
    • 服务器不需存储session,用户保存自己的session信息到cookie中。节省服务端资源。
  • 缺点
    • 都是缺点,这只是一种思路

? ? ? ? ? ? 具体如下:

? ? ? ? ? ? 》每次http请求,携带用户在cookie中的完整信息,浪费网络带宽;

? ? ? ? ? ? 》session数据放在cookie中,cookie有长度限制4K,不能保存大量信息;

? ? ? ? ? ? 》session数据放在cookie中,存在泄漏、篡改、窃取等安全隐患。? ? ? ??

8.3.3 Session共享问题解决-hash一致性

????????使用用户ip地址或用户id来做负载均衡,使某一用户永远访问的是同一台服务器。

  • ?优点:
    • ?只需要改nginx配置,不需要修改应用代码;
    • 负载均衡,只要hash属性的值分布是均匀的,多台web-server的负载是均衡的;
    • 可以支持web-server水平扩展(session同步法是不行的,受内存限制)。
  • 缺点
    • session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录;
    • 如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session;
  • 但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用。

8.3.4?Session共享问题解决-统一存储

  • 优点:
    • ?没有安全隐患;
    • 可以水平扩展,数据库/缓存水平切分即可;
    • web-server重启或者扩容都不会有session丢失。
  • 不足
    • 增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替换为从Redis查数据的方式。redis获取数据比内存慢很多;
    • 上面缺点可以用SpringSession完美解决 。

8.3.5?Session共享问题解决-不同服务,子域session共享?

????????在存入session时,将jsessionid的作用域提升至最大,例如由auth.gulimall.com->.gulimall.com,那么gulimall.com以及其下面的所有子域都可以拿到这个jsessionid去服务器中获取session,可以实现不同服务之间的session共享。

解决方法:

? ? ? ? 前端放大域名,后端通过redis统一存储。

8.4 SpringSession整合

8.4.1 引入依赖

gulimall-auth-server/pom.xml

<!--  整合SpringSession完成session共享问题	-->
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>

8.4.2 配置session存储方式以及过期时间

gulimall-auth-server/src/main/resources/application.yml

spring:
  redis:
    host: 172.1.11.10
    port: 6379
  session:
    store-type: redis # session存储方式
server:
  servlet:
    session:
      timeout: 30m # session过期时间

8.4.3 开启SpringSession

????????在启动类上或配置类上添加以下注解:

@EnableRedisHttpSession // 整合redis作为session存储
@EnableRedisHttpSession // 整合redis作为session存储
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {

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

}

8.4.4 测试?

gulimall-product服务添加同上配置整合SpringSession,进行测试:

1.?gulimall-product/src/main/resources/templates/index.html中获取登录用户,如下:

<li>
  <a href="http://auth.gulimall.com/login.html">你好,请登录[[${session.loginUser.nickname}]]</a>
</li>

2. 手动扩大session作用域,auth.gulimall.com->.gulimall.com

?使用的jdk序列化不便于阅读,建议使用JSON序列化:

8.5 自定义SpringSession完成子域session共享

????????自定义session配置,修改为JSON序列化并放大session作用域。这里和老师视频中讲的有点不同,我把GulimallSessionConfig放到公共模块(gulimall-common)用于其他服务,不需要每个服务在进行session配置类编写。?

????????公共模块引入SpringSession相关依赖,依赖版本根据SpringBoot版本而定,我这里SpringBoot版本是2.7.8,SpringSession版本可以去gulimall-auth-server的Dependencies中查看。

gulimall-common/pom.xml?

<!--	整合SpringSession完成session共享问题	-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.7.0</version>
</dependency>

gulimall-common/src/main/java/com/wen/common/config/GulimallSessionConfig.java?

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSION");
        // 放大session作用域
        serializer.setDomainName("gulimall.com");
        return serializer;
    }

    // JSON序列化存储到redis
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

????????公共模块需要开启自动配置,才能将公共模块的配置类用于其他服务(其他服务需要引入公共模块gulimall-common)。

?????spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。

gulimall-common/src/main/resources/META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.wen.common.config.GulimallSessionConfig

?测试,登录后可以自动跳回首页并显示登录用户名。注意对首页的session获取登录用户进行非空判断。

8.6 SpringSession原理

8.6.1 原理

/**
 * 核心原理
 * @EnableRedisHttpSession 导入 RedisHttpSessionConfiguration配置
 * 	1.给容器中添加一个组件
 * 		SessionRepository=>RedisIndexedSessionRepository->redis操作session。session的增删改查
 * 	2.SessionRepositoryFilter-》Filter: session存储过滤器;每个请求过来都必须经过filter
 * 		(1)创建时从容器中自动获取到SessionRepository;
 * 		(2)原始的request和response都被包装。SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper
 * 		(3)以后获取session由request.getSession();
 * 		(4)wrappedRequest.getSession();->SessionRepository中获取到的。
 * 	装饰者模式: 参考教程:https://www.jianshu.com/p/04a3bec6220c
 * 	
 * 	自动延期:redis中的数据也是有过期时间的
 */

8.6.2??页面效果完成

1. 网关模块报错解决:

注意:由于SpringSession的配置GulimallSessionConfig.java放到了公共模块,网关(gulimall-gateway)也引入了公共模块,会报BeanCreationException异常,错误信息如下:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cookieSerializer' defined in class path resource 。所以在网关模块的启动类上排除SpringSession的配置,如下图:

2. 将使用用户密码登录成功后的用户信息放到session

使用用户密码登录成功后,当前登录的用户信息也要放到session中,gulimall-product在整合SpringSession后,就可以在首页拿到当前登录用户信息。session相关的key放到常量类中,gulimall-member(会员服务)的用户密码登录成功,返回登录用户信息。

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java?

?gulimall-common/src/main/java/com/wen/common/constant/AuthServerConstant.java

public class AuthServerConstant {
    /**
     * 短信验证码在redis中的前缀
     */
    public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";

    public static final String LOGIN_USER = "loginUser";
}

?gulimall-member/src/main/java/com/wen/gulimall/member/controller/MemberController.java

3.?gulimall-product商城首页优化?

4. 登录成功后再次访问登录页应该跳回首页

登录成功后,浏览器访问http://auth.gulimall.com/login.html仍然可以进入登录页再次登录。所以在用户访问登录页时,就要判断用户是否已经登录,如果用户已经登录就要重定向到首页,未登录才可进行登录。

由于用户的登录页之前设置了视图映射,这里注释,在controller层对登录页进行逻辑处理,如下:

gulimall-auth-server/src/main/java/com/wen/gulimall/auth/controller/LoginController.java

@Controller
public class LoginController {
    
    ...

    @GetMapping("/login.html")
    public String login(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute == null){
            return "login";
        }else {
            return "redirect:http://gulimall.com";
        }
    }
}

?5. 注册保存用户昵称

登陆成功后首页展示的是用户昵称,当时注册时没有保存昵称,这里补充一下。

6. 修改搜索页面和详情页面的登录状态,搜索服务整合springsession

搜索服务整合SpringSession,因为引入了gulimall-common,不需要在引入redis、SpringSession相关依赖,也不需要编写SpringSession配置类,这些都在公共模块配置好了,gulimall-search搜索服务yml中进行相关配置,如下:

修改搜索页面登录状态,如下:?

gulimall-search/src/main/resources/templates/list.html?

?

?修改详情页登录状态,如下:

gulimall-product/src/main/resources/templates/item.html?

9 单点登录SSO

9.1 单点登录简介

1.SpringSession+扩大子域 适用于单系统分布式集群的登录。

2.多系统单点登录,使用SpringSession时Session作用域最多只能放大到一级域名,不可能放大到.com让世界上所有系统通用。

3.多系统-单点登录的效果:

(1)一处登录处处登录;

(2)一处退出处处退出。

9.2 许雪里单点登录效果演示

许雪里单点登录开源框架地址:许雪里单点登录开源框架icon-default.png?t=N7T8https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search

核心:统一的认证服务器

实现效果:多个系统即使域名不一致,也可以获得同一个用户的票据。

(1)一个统一认证的服务器ssoserver.com;

(2)其他系统想登录去ssoserver.com登录,登录成功后跳转回来;

(3)一处登录处处登录,一处退出处处退出;

(4)全部系统登录成功后统一一个xxl_sso_sessionid。

# 打包命令
mvn clean package -Dmaven.skip.test=true

# jar包运行命令
java -jar xxl-sso-server-1.1.1-SNAPSHOT.jar
java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8081
java -jar xxl-sso-web-sample-springboot-1.1.1-SNAPSHOT.jar --server.port=8082

9.3 单点登录实现

单点登录流程如下:

9.3.1 单点登录服务端gulimall-test-sso-server

9.3.1.1 创建单点登录服务模块

注意:这里SpringBoot使用2.7.8版本,java使用1.8版本,和其他服务保持一致,创建好后修改pom.xml文件。

9.3.1.2 pom.xml中相关依赖

(thymleaf、redis)

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
9.3.1.3 application.properties配置
server.port=8080
# redis端口默认6379,不用配置
spring.redis.host=1xx.xxx.xxx.10
9.3.1.4 LoginController登录控制层

gulimall-test-sso-server/src/main/java/com/wen/gulimall/ssoserver/controller/LoginController.java

package com.wen.gulimall.ssoserver.controller;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Controller
public class LoginController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @ResponseBody
    @GetMapping("/userInfo")
    public String userInfo(String token){
        String s = stringRedisTemplate.opsForValue().get(token);
        return s;
    }

    @GetMapping("/login.html")
    public String loginPage(@RequestParam("redirect_url") String url, Model model, @CookieValue(name = "sso_token",required = false) String sso_token){
        if(!StringUtils.isEmpty(sso_token)){
            // 说明之前登录过,浏览器留下了痕迹
            return "redirect:"+url+"?token="+sso_token;
        }
        model.addAttribute("url",url);
        return "login";
    }

    @PostMapping("/doLogin")
    public String doLogin(String username, String password, String url, HttpServletResponse response){
        if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
            // 登录成功,跳转到之前访问路径
            String uuid = UUID.randomUUID().toString();
            uuid = uuid.replace("-","");
            // 存储登陆成功的用户
            stringRedisTemplate.opsForValue().set(uuid,username);
            Cookie cookie = new Cookie("sso_token",uuid);
            response.addCookie(cookie);
            return "redirect:"+url+"?token="+uuid;
        }
        return "login";
    }
}
9.3.1.5 login.html登录页

gulimall-test-sso-server/src/main/resources/templates/login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
    用户名:<input name="username" /> </br>
    密码:<input type="password" name="password" /> </br>
    <input type="hidden" name="url" th:value="${url}">
    <input type="submit" value="登录">
</form>
</body>
</html>

9.3.2 客户端 gulimall-test-sso-client

9.3.2.1 创建客户端(client1)模块

注意:这里SpringBoot使用2.7.8版本,java使用1.8版本,和其他服务保持一致,创建好后修改pom.xml文件。

9.3.2.2 pom.xml中相关依赖

(thymleaf、redis)

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
9.3.2.3 application.properties配置
server.port=8081
# 单点登录服务器地址
sso.server.url=http://ssoserver.com:8080/login.html
spring.redis.host=1xx.xxx.xxx.10
9.2.3.4 HelloController测试控制层

gulimall-test-sso-client/src/main/java/com/wen/gulimall/ssoclient/controller/HelloController.java

package com.wen.gulimall.ssoclient.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;


@Controller
public class HelloController {

    @Value("${sso.server.url}")
    private String ssoServerUrl;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 无需登录就可访问
     * @return
     */
    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    @GetMapping("/employees")
    public String employees(Model model, HttpSession session,@RequestParam(required = false) String token){
        if(!StringUtils.isEmpty(token)){
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
            String body = forEntity.getBody();
            session.setAttribute("loginUser",body);
        }
        Object loginUser = session.getAttribute("loginUser");
        if(loginUser==null){
            // 没登录,跳转到登录服务器进行登录
            return "redirect:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
        }else {
            List<String> emps = new ArrayList<>();
            emps.add("张三");
            emps.add("李四");
            model.addAttribute("emps", emps);
            return "list";
        }
    }
}
9.2.3.5 list.html员工列表页

gulimall-test-sso-client/src/main/resources/templates/list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>老板列表</title>
</head>
<body>
<h1>欢迎,[[${session.loginUser}]]</h1>
<ul>
    <li th:each="emp:${emps}">姓名:[[${emp}]]</li>
</ul>
</body>
</html>

9.3.3 客户端gulimall-test-sso-client2

9.3.3.1 创建客户端(client2)

复制gulimall-test-sso-client模块放到父模块(gulimall)下面=》修改模块名为gulimall-test-sso-client2 =》修改pom文件、application.properties、HelloController、list.html,区分客户端client1。

9.3.4 host配置

打开SwitchHosts软件,配置ip和域名映射

9.3.5 测试

1. 启动三个服务;

2. 浏览器访问客户端(client1)http://client1.com:8081/employees,未登录跳转到单点登录服务ssoserver.com的登录页login.html,如下:

3. 浏览器访问客户端(client2)http://client2.com:8082/boss?,未登录跳到单点登录服务ssoserver.com的登录页login.html,如下:????????

4. 在redirect_url=http://client1.com:8081/employees下登录,如下:

登录成功,如下:

?5. 刷新(client2)http://ssoserver.com:8080/login.html?redirect_url=http://client2.com:8082/boss或访问http://client2.com:8082/boss

?6. 查看登录服务的登录标识sso_token,如下:

9.3.6 总结

SSO流程:

1. 创建单点登录服务器和客户端
2. 浏览器访问客户端(client1)http://client1.com:8081/employees =》跳转到单点登录服务器 http://ssoserver.com:8080/login.html?redirect_url=http://client1.com:8081/employees
3. 登录页面将带来的redirect_url值放在隐藏的输入框
4. 输入用户名密码,点击登录,登录成功后跳转到redirect_url指定的地址并带上token:
1) 用户名密码正确,将用户信息存放在redis;
2)将登录标识sso_token存放在Cookie中,有这个标识别的客户端无需在登陆;
3)跳转到redirect_url指定的地址并将令牌token返回给客户端http://client1.com:8081/employees?token=uuid
5. 客户端判断是否返回token
1)判断是否返回token(是否登录);
2)根据token去单点登录服务器获取登录用户信息;
3)将从登录服务器获取的用户信息放到自己的session
7. 客户端(client2)无需登录可以直接访问http://client2.com:8082/boss,因为登录服务器Cookie中保存有登录标识。

文章来源:https://blog.csdn.net/weixin_48568302/article/details/132858542
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。