谷粒商城–认证中心–高级篇笔记八
1. 环境搭建
1.1 新建模块gulimall-auth-server
1.2 pom文件
上面没选好直接复制下面的pom文件,记得排除gulimall-common包的mybatis-plus
<?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.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-auth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-auth-server</name>
<description>认证中心(社交登录、OAuth2.0、单点登录)</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.zhourui.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<!--不需要数据库操作移除mybatis-plus,防止报错-->
<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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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>
</plugin>
</plugins>
</build>
</project>
1.3 修改主类
//可以远程调用,使服务能够被nacos发现
@EnableFeignClients
@EnableDiscoveryClient
1.4 配置服务名,端口及注册中心地址
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-auth-server
server:
port: 20000
1.5 host新增域名映射
192.168.157.128 auth.gulimall.com
1.6 静态资源上传
1.7 修改reg.html login.html的 地址
对应nginx上的静态地址就行
1.8 配置网关
gulimall-gateway/src/main/resources/application.yml
#认证服务
- id: gulimall_auth_host
uri: lb://gulimall-auth-server
predicates:
- Host=auth.gulimall.com
1.9 新增视图映射
之前在controller中新增请求不做任何处理只返回对应视图也可以做到,但是这回导致controller里有空方法如:
@GetMapping(value = "/login.html")
public String loginPage(HttpSession session) {
return "login";
}
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/config/GulimallWebConfig.java
package site.zhourui.gulimall.auth.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author zr
* @date 2021/11/29 16:33
*/
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
/**
* 视图映射:发送一个请求,直接跳转到一个页面
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
1.10 测试
http://auth.gulimall.com/login.html
http://auth.gulimall.com/reg.html
1.11 此部分前端代码
验证码倒计时
login.html :https://gitee.com/zhourui815/gulimall/blob/master/gulimall-auth-server/src/main/resources/templates/login.html
reg.html: https://gitee.com/zhourui815/gulimall/blob/master/gulimall-auth-server/src/main/resources/templates/reg.html
2. 整合短信验证码
2.1 购买阿里云短信服务
购买地址:
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
2.2 postMan测试
2.3 整合短信服务
2.3.1 导入HttpUtils需要的所有依赖
<!--HttpUtils需要的所有依赖-->
<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>
2.3.2 新增HttpUtils
gulimall-common/src/main/java/site/zhourui/common/utils/HttpUtils.java
package site.zhourui.common.utils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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.3.3 整合测试
gulimall-third-party/src/test/java/site/zhourui/gulimall/thirdparty/SMSTest.java
package site.zhourui.gulimall.thirdparty;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
public class SMSTest {
public static void main(String[] args) {
String host = "https://fesms.market.alicloudapi.com";// 【1】请求地址 支持http 和 https 及 WEBSOCKET
String path = "/sms/";// 【2】后缀
String appcode = "004c4072d4ed40b48xxx"; // 【3】开通服务后 买家中心-查看AppCode
String code = "123456"; // 【4】请求参数,详见文档描述
String phone = "17748781xxx"; // 【4】请求参数,详见文档描述
String sign = "1"; // 【4】请求参数,详见文档描述
String skin = "1"; // 【4】请求参数,详见文档描述
String urlSend = host + path + "?code=" + code + "&phone=" + phone + "&sign=" + sign + "&skin=" + skin ; // 【5】拼接请求链接
try {
URL url = new URL(urlSend);
HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
httpURLCon.setRequestProperty("Authorization", "APPCODE " + appcode);// 格式Authorization:APPCODE
// (中间是英文空格)
int httpCode = httpURLCon.getResponseCode();
if (httpCode == 200) {
String json = read(httpURLCon.getInputStream());
System.out.println("正常请求计费(其他均不计费)");
System.out.println("获取返回的json:");
System.out.print(json);
} else {
Map<String, List<String>> map = httpURLCon.getHeaderFields();
String error = map.get("X-Ca-Error-Message").get(0);
if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
System.out.println("AppCode错误 ");
} else if (httpCode == 400 && error.equals("Invalid Url")) {
System.out.println("请求的 Method、Path 或者环境错误");
} else if (httpCode == 400 && error.equals("Invalid Param Location")) {
System.out.println("参数错误");
} else if (httpCode == 403 && error.equals("Unauthorized")) {
System.out.println("服务未被授权(或URL和Path不正确)");
} else if (httpCode == 403 && error.equals("Quota Exhausted")) {
System.out.println("套餐包次数用完 ");
} else {
System.out.println("参数名错误 或 其他错误");
System.out.println(error);
}
}
} catch (MalformedURLException e) {
System.out.println("URL格式错误");
} catch (UnknownHostException e) {
System.out.println("URL地址错误");
} catch (Exception e) {
// 打开注释查看详细报错异常信息
// e.printStackTrace();
}
}
/*
* 读取返回结果
*/
private static String read(InputStream is) throws IOException {
StringBuffer sb = new StringBuffer();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line = null;
while ((line = br.readLine()) != null) {
line = new String(line.getBytes(), "utf-8");
sb.append(line);
}
br.close();
return sb.toString();
}
}
3.3.4 三方服务整合短信服务
3.3.4.1 新增短信服务组件
gulimall-third-party/src/main/java/site/zhourui/gulimall/thirdparty/component/SmsComponent.java
package site.zhourui.gulimall.thirdparty.component;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
/**
* @author zr
* @date 2021/11/29 18:15
*/
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data
@Component
public class SmsComponent {
private String host;
private String path;
private String skin;
private String sign;
private String appcode;
public void sendCode(String phone,String code) {
String urlSend = host + path + "?code=" + code + "&phone=" + phone + "&sign=" + sign + "&skin=" + skin ; // 【5】拼接请求链接
try {
URL url = new URL(urlSend);
HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
httpURLCon.setRequestProperty("Authorization", "APPCODE " + appcode);// 格式Authorization:APPCODE
// (中间是英文空格)
int httpCode = httpURLCon.getResponseCode();
if (httpCode == 200) {
String json = read(httpURLCon.getInputStream());
System.out.println("正常请求计费(其他均不计费)");
System.out.println("获取返回的json:");
System.out.print(json);
} else {
Map<String, List<String>> map = httpURLCon.getHeaderFields();
String error = map.get("X-Ca-Error-Message").get(0);
if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
System.out.println("AppCode错误 ");
} else if (httpCode == 400 && error.equals("Invalid Url")) {
System.out.println("请求的 Method、Path 或者环境错误");
} else if (httpCode == 400 && error.equals("Invalid Param Location")) {
System.out.println("参数错误");
} else if (httpCode == 403 && error.equals("Unauthorized")) {
System.out.println("服务未被授权(或URL和Path不正确)");
} else if (httpCode == 403 && error.equals("Quota Exhausted")) {
System.out.println("套餐包次数用完 ");
} else {
System.out.println("参数名错误 或 其他错误");
System.out.println(error);
}
}
} catch (MalformedURLException e) {
System.out.println("URL格式错误");
} catch (UnknownHostException e) {
System.out.println("URL地址错误");
} catch (Exception e) {
// 打开注释查看详细报错异常信息
// e.printStackTrace();
}
}
/*
* 读取返回结果
*/
private static String read(InputStream is) throws IOException {
StringBuffer sb = new StringBuffer();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line = null;
while ((line = br.readLine()) != null) {
line = new String(line.getBytes(), "utf-8");
sb.append(line);
}
br.close();
return sb.toString();
}
}
3.3.4.2 抽取组件相关配置到配置文件
导入配置提示依赖
gulimall-third-party/pom.xml
<!--自定义配置提示 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
新增配置
gulimall-third-party/src/main/resources/application.yml
sms:
host: http://fesms.market.alicloudapi.com
path: /sms/
skin: 1
sign: 1
appcode: 004c4072d4ed40b489d77b987ad3404d
3.3.4.3 新增SmsSendController 提供给别的服务进行调用
gulimall-third-party/src/main/java/site/zhourui/gulimall/thirdparty/controller/SmsSendController.java
package site.zhourui.gulimall.thirdparty.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.thirdparty.component.SmsComponent;
/**
* @author zr
* @date 2021/11/29 18:28
*/
@RestController
@RequestMapping(value = "/sms")
public class SmsSendController {
@Autowired
private SmsComponent smsComponent;
/**
* 提供给别的服务进行调用
* @param phone
* @param code
* @return
*/
@GetMapping(value = "/sendCode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) {
//发送验证码
smsComponent.sendCode(phone,code);
return R.ok();
}
}
3.3.4.4 测试
测试结果
成功同时收到短信
3. 认证服务调用短信服务
3.1 认证服务远程调用三方服务发送短信
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/feign/ThirdPartFeignService.java
package site.zhourui.gulimall.auth.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.utils.R;
/**
* @author zr
* @date 2021/11/29 22:13
*/
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
@GetMapping(value = "/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
3.2 认证服务发送短信接口
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/controller/LoginController.java
package site.zhourui.gulimall.auth.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.auth.feign.ThirdPartFeignService;
import java.util.UUID;
/**
* @author zr
* @date 2021/11/29 22:15
*/
@Controller
public class LoginController {
@Autowired
ThirdPartFeignService thirdPartFeignService;
@GetMapping(value = "/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone)
{
String code = UUID.randomUUID().toString().substring(0, 5);
thirdPartFeignService.sendCode(phone,code);
return R.ok();
}
}
3.3 前端调用接口
发送验证码短信成功
3.4 此时的缺陷
接口写在前端js代码里,仍然可以被其他人拿来盗刷
由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。
- 在redis中以
phone-code
将电话号码和验证码进行存储并将当前时间与code一起存储- 如果调用时以当前
phone
取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息 - 60s以后再次调用,需要删除之前存储的
phone-code
- code存在一个过期时间,我们设置为10min,10min内验证该验证码有效
- 如果调用时以当前
3.5 接口防刷功能
加了接口防刷功能:
1)先查询redis,是否超过60s,否则不允许发送短信
2)存入redis,过期时间10min,并且存入当前系统时间
3.5.1 引入redis相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.5.2 配置redis相关信息
redis:
host: 192.168.157.128
port: 6379
3.5.3 认证服务发送短信验证码功能实现
gulimall-common/src/main/java/site/zhourui/common/constant/AuthServerConstant.java
该常量用于redis短信验证码前缀
package site.zhourui.common.constant;
/**
* @author zr
* @date 2021/11/29 22:47
*/
public class AuthServerConstant {
public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
}
package site.zhourui.gulimall.auth.controller;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.exception.BizCodeEnume;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.auth.feign.ThirdPartFeignService;
import java.util.concurrent.TimeUnit;
/**
* @author zr
* @date 2021/11/29 22:15
*/
@Controller
public class LoginController {
@Autowired
private ThirdPartFeignService thirdPartFeignService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ResponseBody
@GetMapping(value = "/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone) {
//1、接口防刷
String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if (!StringUtils.isEmpty(redisCode)) {
//活动存入redis的时间,用当前时间减去存入redis的时间,判断用户手机号是否在60s内发送验证码
long currentTime = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() - currentTime < 60000) {
//60s内不能再发
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
//2、验证码的再次效验 redis.存key-phone,value-code
// String code = UUID.randomUUID().toString().substring(0, 5);
// String redisValue = code+"_"+System.currentTimeMillis();
int code = (int) ((Math.random() * 9 + 1) * 100000);// 验证码只可以是数字
String codeNum = String.valueOf(code);
String redisStorage = codeNum + "_" + System.currentTimeMillis();
//存入redis,防止同一个手机号在60秒内再次发送验证码
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone,
redisStorage,365, TimeUnit.DAYS);
thirdPartFeignService.sendCode(phone, codeNum);
return R.ok();
}
}
在60秒之内再次发送该号码的验证码
4. 注册页面相关功能实现
4.1 后台JSR 303校验
在gulimall-auth-server
服务中编写注册的主体逻辑
- 若JSR303校验未通过,则通过
BindingResult
封装错误信息,并重定向至注册页面 - 若通过JSR303校验,则需要从
redis
中取值判断验证码是否正确,正确的话通过会员服务注册(检查验证码、用户名、手机号 唯一),校验通过后,存储会员信息 - 会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面
注: RedirectAttributes
可以通过session保存信息并在重定向的时候携带过去
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/vo/UserRegisterVo.java
package site.zhourui.gulimall.auth.vo;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.Length;
/**
* @author zr
* @date 2021/11/30 10:39
*/
/**
* 注册使用的vo
*/
@Data
public class UserRegisterVo {
@NotEmpty(message = "用户名必须提交")
@Length(min = 6, max = 19, message="用户名长度必须是6-18字符")
private String userName;
@NotEmpty(message = "密码必须填写")
@Length(min = 6,max = 18,message = "密码长度必须是6—18位字符")
private String password;
@NotEmpty(message = "手机号必须填写")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手机号格式不正确")
private String phone;
@NotEmpty(message = "验证码必须填写")
private String code;
}
4.2 member服务,存储会员信息
4.2.1 接收前端传的注册对象
gulimall-member/src/main/java/site/zhourui/gulimall/member/vo/MemberUserRegisterVo.java
package site.zhourui.gulimall.member.vo;
/**
* @author zr
* @date 2021/11/30 11:01
*/
import lombok.Data;
/**
* 会员注册Vo
*/
@Data
public class MemberUserRegisterVo {
private String userName;
private String password;
private String phone;
}
4.2.2 用户名与手机号重复异常
gulimall-member/src/main/java/site/zhourui/gulimall/member/exception/PhoneException.java
package site.zhourui.gulimall.member.exception;
/**
* @author zr
* @date 2021/11/30 11:02
*/
public class PhoneException extends RuntimeException {
public PhoneException() {
super("存在相同的手机号");
}
}
gulimall-member/src/main/java/site/zhourui/gulimall/member/exception/UsernameException.java
package site.zhourui.gulimall.member.exception;
/**
* @author zr
* @date 2021/11/30 11:02
*/
public class UsernameException extends RuntimeException {
public UsernameException() {
super("存在相同的用户名");
}
}
gulimall-member/src/main/java/com/atguigu/gulimall/member/controller/MemberController.java
通过gulimall-member
会员服务注册逻辑
- 通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
- 如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间
@PostMapping(value = "/register")
public R register(@RequestBody MemberUserRegisterVo vo) {
try {
memberService.register(vo);
//异常机制:通过捕获对应的自定义异常判断出现何种错误并封装错误信息
} catch (PhoneException e) {
return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
} catch (UsernameException e) {
return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
4.2.3 MD5&盐值&BCrypt
4.2.3.1 MD5
- Message Digest algorithm 5,信息摘要算法
- 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
- 容易计算:从原数据计算出MD5值很容易。
- 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
- 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
- 不可逆
4.2.3.2 加盐
- 通过生成随机数与MD5生成字符串进行组合
- 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可
4.2.3.3 BCryptPasswordEncoder
和其他加密方式相比,BCryptPasswordEncoder有着它自己的优势所在,首先加密的hash值每次都不同,就像md5的盐值加密一样,只不过盐值加密用到了随机数,前者用到的是其内置的算法规则,毕竟随机数没有设合适的话还是有一定几率被攻破的。其次BCryptPasswordEncoder的生成加密存储串也有60位之多。最重要的一点是,md5的加密不是spring security所推崇的加密方式了,所以我们还是要多了解点新的加密方式。
BCryptPasswordEncoder每次加密相同的值,都会得到不同的密文
4.2.3.4 BCryptPasswordEncoder加密(encode)解密(matches)
4.2.3.4.1 加密encode
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode(vo.getPassword());
4.2.3.4.2 解密
尽管每次加密后的值都不同,但matches能够匹配
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//进行密码匹配,参数一是用户输入的明文密码,参数二是查询数据得到的对应用户的加密后的密码
boolean matches = passwordEncoder.matches(password, password1);
4.2.4 用户服务注册接口
gulimall-member/src/main/java/site/zhourui/gulimall/member/service/MemberService.java
/**
* 用户注册
* @param vo
*/
void register(MemberUserRegisterVo vo);
/**
* 判断邮箱是否重复
* @param phone
* @return
*/
void checkPhoneUnique(String phone) throws PhoneException;
/**
* 判断用户名是否重复
* @param userName
* @return
*/
void checkUserNameUnique(String userName) throws UsernameException;
gulimall-member/src/main/java/site/zhourui/gulimall/member/service/impl/MemberServiceImpl.java
@Override
public void register(MemberUserRegisterVo vo) {
MemberEntity memberEntity = new MemberEntity();
//设置默认等级【普通会员】
MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
memberEntity.setLevelId(levelEntity.getId());
//设置其它的默认信息
//检查用户名和手机号是否唯一。感知异常,异常机制
checkPhoneUnique(vo.getPhone());
checkUserNameUnique(vo.getUserName());
memberEntity.setNickname(vo.getUserName());
memberEntity.setUsername(vo.getUserName());
//密码进行MD5加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode(vo.getPassword());
memberEntity.setPassword(encode);
memberEntity.setMobile(vo.getPhone());
memberEntity.setGender(0);
memberEntity.setCreateTime(new Date());
//保存数据
baseMapper.insert(memberEntity);
}
@Override
public void checkPhoneUnique(String phone) throws PhoneException {
Integer phoneCount = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (phoneCount > 0) {
throw new PhoneException();
}
}
@Override
public void checkUserNameUnique(String userName) throws UsernameException {
Integer usernameCount = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
if (usernameCount > 0) {
throw new UsernameException();
}
}
4.3 认证服务注册接口
feign接口
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/feign/MemberFeignService.java
package site.zhourui.gulimall.auth.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.auth.vo.UserRegisterVo;
/**
* @author zr
* @date 2021/11/30 11:12
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping(value = "/member/member/register")
R register(@RequestBody UserRegisterVo vo);
}
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/controller/LoginController.java
JSR303启用要加
@Valid
/**
*
* TODO: 重定向携带数据:利用session原理,将数据放在session中。
* TODO:只要跳转到下一个页面取出这个数据以后,session里面的数据就会删掉
* TODO:分布下session问题
* RedirectAttributes:重定向也可以保留数据,不会丢失
* 用户注册
* @return
*/
@PostMapping(value = "/register")
public String register(@Valid UserRegisterVo vos, BindingResult result, RedirectAttributes attributes) {
//如果有错误回到注册页面
if (result.hasErrors()) {
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
attributes.addFlashAttribute("errors", errors);
//效验出错回到注册页面
return "redirect:http://auth.gulimall.com/reg.html";
// 1、return "reg"; 请求转发【使用Model共享数据】【异常:,405 POST not support】
// 2、"redirect:http:/reg.html"重定向【使用RedirectAttributes共享数据】【bug:会以ip+port来重定向】
// 3、redirect:http://auth.gulimall.com/reg.html重定向【使用RedirectAttributes共享数据】
}
//1、效验验证码
String code = vos.getCode();
//获取存入Redis里的验证码
String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vos.getPhone());
if (!StringUtils.isEmpty(redisCode)) {
// 判断验证码是否正确【有BUG,如果字符串存储有问题,没有解析出code,数据为空,导致验证码永远错误】
if (code.equals(redisCode.split("_")[0])) {
//删除验证码(不可重复使用);令牌机制
stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX+vos.getPhone());
//验证码通过,真正注册,调用远程服务进行注册【会员服务】
R register = memberFeignService.register(vos);
if (register.getCode() == 0) {
//成功
return "redirect:http://auth.gulimall.com/login.html";
} else {
//失败
Map<String, String> errors = new HashMap<>();
errors.put("msg", register.getData("msg",new TypeReference<String>(){}));
attributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
//验证码错误
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
attributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
// redis中验证码过期
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码过期");
attributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
}
5. 用户名密码登录
5.1 接收前端传的登录对象
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/vo/UserLoginVo.java
用户登录前端发送的vo
package site.zhourui.gulimall.auth.vo;
import lombok.Data;
/**
* @author zr
* @date 2021/11/30 13:51
*/
@Data
public class UserLoginVo {
private String loginacct;
private String password;
}
5.2 认证服务的登录接口,供前端调用
gulimall-auth-server/src/main/java/com/atguigu/gulimall/auth/controller/LoginController.java
@PostMapping(value = "/login")
public String login(UserLoginVo vo, RedirectAttributes attributes, HttpSession session) {
//远程登录
R login = memberFeignService.login(vo);
if (login.getCode() == 0) {
MemberResponseVo data = login.getData("data", new TypeReference<MemberResponseVo>() {});
session.setAttribute(AuthServerConstant.LOGIN_USER, data);
return "redirect:http://gulimall.com";
} else {
Map<String,String> errors = new HashMap<>();
errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
attributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
5.3 用户服务的登录接口,供其他服务远程调用
gulimall-member/src/main/java/site/zhourui/gulimall/member/vo/MemberUserLoginVo.java
package site.zhourui.gulimall.member.vo;
import lombok.Data;
/**
* @author zr
* @date 2021/11/30 13:42
*/
@Data
public class MemberUserLoginVo {
private String loginacct;
private String password;
}
gulimall-member/src/main/java/site/zhourui/gulimall/member/controller/MemberController.java
/**
* 登录接口
*/
@PostMapping(value = "/login")
public R login(@RequestBody MemberUserLoginVo vo) {
MemberEntity memberEntity = memberService.login(vo);
if (memberEntity != null) {
return R.ok().setData(memberEntity);
} else {
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
}
}
gulimall-member/src/main/java/site/zhourui/gulimall/member/service/MemberService.java
/**
* 用户登录
*/
MemberEntity login(MemberUserLoginVo vo);
/**
* 本地登录
*/
@Override
public MemberEntity login(MemberUserLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword();
//1、去数据库查询 SELECT * FROM ums_member WHERE username = ? OR mobile = ?
MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>()
.eq("username", loginacct).or().eq("mobile", loginacct));
if (memberEntity == null) {
//登录失败
return null;
} else {
//获取到数据库里的password
String password1 = memberEntity.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//进行密码匹配
boolean matches = passwordEncoder.matches(password, password1);
if (matches) {
//登录成功
return memberEntity;
}
}
return null;
}
5.4 feign接口新增
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/feign/MemberFeignService.java
@PostMapping(value = "/member/member/login")
R login(@RequestBody UserLoginVo vo);
6. 社交登录 (OAuth2.0)
QQ、 微博、 github 等网站的用户量非常大, 别的网站为了简化自我网站的登陆与注册逻辑, 引入社交登陆功能;
步骤:
1) 、 用户点击 QQ 按钮
2) 、 引导跳转到 QQ 授权页
3) 、 用户主动点击授权, 跳回之前网页。
6.1 OAuth2.0
- OAuth: OAuth(开放授权) 是一个开放标准, 允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息, 而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
- OAuth2.0: 对于用户相关的 OpenAPI(例如获取用户信息, 动态同步, 照片, 日志, 分享等) , 为了保护用户数据的安全和隐私, 第三方网站访问用户数据前都需要显式的向用户征求授权。
6.1.1 官方流程
( A) 用户打开客户端以后, 客户端要求用户给予授权。
( B) 用户同意给予客户端授权。
( C) 客户端使用上一步获得的授权, 向认证服务器申请令牌。
( D) 认证服务器对客户端进行认证以后, 确认无误, 同意发放令牌。
( E) 客户端使用令牌, 向资源服务器申请获取资源。
( F) 资源服务器确认令牌无误, 同意向客户端开放资源。
6.1.2 流程
注意点:
- 使用Code换取AccessToken,Code只能用一次
- 同一个用户的accessToken一段时间是不会变化的,即使多次获取
6.2 微博社交登录
6.2.1 进入微博开放平台 (审批时间过长放弃)
使用QQ、微信登录
步骤:
1)、用户点击QQ按钮
2)、引导跳转到QQ授权页
3)、用户主动点击授权,跳回之前网页。
1、OAuth2.0
OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
OAuth2.0:对于用户相关的OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
官方版流程:
Client客户端:CSDN
Resource Owner:用户本人
2、微博登录工作
1)https://open.weibo.com/=》网站接入=》创建新应用
2)高级信息OAuth2.0 授权设置
授权回调页:http://auth.gulimall.com/oauth2.0/weibo/success
取消授权回调页:http://gulimall.com/fall
3)doc: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
2、修改client_id和redirect_uri【自己创建的应用App Key和 授权回调页】
用户使用微博账号登录后跳转到回调页,并带上一个code=xxxx
3、根据code换取Access Token【code只能使用一次!】
发送请求: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": "SlAV32hkKG",
"remind_in": 3600,
"expires_in": 3600,
"uid": 134156431
}
4、可以使用access_token来使用微博的接口获取信息了:【例如用户接口访问用户微博账号信息】https://open.weibo.com/apps/2129105835/privilege【access_token有存活时间,可重复使用】
3、编写java controller请求:【具体自己看代码】
1)发送post请求使用code换区access_token
2)判断当前用户曾经是否登录过【登录或注册,绑定 uid与本系统账号】
1、有 直接登录【根据uid关联查询用户信息】
2、没有 自动创建
调用微博的 账户信息【昵称、性别、头像、】
3、返回用户信息
·gulimall-auth-server 接收/auth2.0/sucess 社交登录成功回调
·gulimall-auth-server发送post请求根据code获取access_token和uid
·向gulimall-member发送登录请求,查询member表,uid是否有关联账户【修改member表,增加三个字段】
社交登录UID socialUid
社交登录TOKEN accessToken
社交登录过期时间 expiresIn
换取Access_Token
逻辑:
6.3 gitee社交登录(不需要实名认证)
6.3.1 在gitee开通三方api权限
微博开放平台实名认证审了我8天还没通过,我放弃了,选用gitee,不需要实名认证审批
6.3.2 创建应用
此处的回调地址必须与后台的一致
6.3.3 发起认证请求
此步骤可以写在后端,防止client_id暴露,回调地址填写上一步回调地址
<li>
<a href="https://gitee.com/oauth/authorize?client_id=43966d65b7e3920a830bec212333df3a81e3fceb673520996db1d9265a0c26e6&redirect_uri=http://auth.gulimall.com/oauth2.0/gitee/success&response_type=code">
<img style="width: 50px;height: 18px;" src="/static/login/JD_img/weibo.png"/>
<!-- <span>weibo</span>-->
</a>
</li>
6.3.4 新增回调接口
新增码云认证相关常量
package site.zhourui.gulimall.auth.constant;
/**
* @author zr
* @date 2021/12/8 17:19
*/
public class GiteeConstant {
/**
* 第三方权限认证的步骤
* 1. 通过client_id 和回调方法获取指定的第三方授权页面
* 2. 第三方页面授权成功,通过回调方法 获取返回的通行code
* 3. 通过 code和回调等 从第三方服务换取登录成功票据 token
* 4. 通过第三方服务的票据 token 获取用户的基本信息返回到前端页面
*/
// 码云我的应用中对应的 客户端id
public static final String clientId = "xxxx";
// 码云我的应用中对应的
public static final String secret = "xxxx";
// 配置的回调接口地址
public static final String callback ="http://auth.gulimall.com/oauth2.0/gitee/success";
// 跳转码云的授权页面
public static final String GiteeURI = "https://gitee.com/oauth/authorize?client_id=" + clientId + "&redirect_uri="+ callback+"&response_type=code";
// 通过码云的code换取他的token凭据
public static final String address = "https://gitee.com";
public static final String path = "/oauth/token";
// 通过码云服务的token凭据 获取用户信息
public static final String userInfo = "https://gitee.com/api/v5/user";
}
新增SocialUser,用户接受获取access_token相关信息
该类最好放在common服务下,远程调用需要
package site.zhourui.gulimall.auth.vo;
import lombok.Data;
/**
* @author zr
* @date 2021/12/8 17:22
*/
@Data
public class GiteeSocialUser {
private String accessToken;
private String tokenType;
private Long expiresIn;
private String refreshToken;
private String scope;
private String createdAt;
}
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/controller/OAuth2Controller.java
此处需要调用远程服务社交登录接口oauthLogin
package site.zhourui.gulimall.auth.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.utils.HttpUtils;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.auth.constant.GiteeConstant;
import site.zhourui.gulimall.auth.feign.MemberFeignService;
import site.zhourui.gulimall.auth.vo.GiteeSocialUser;
import vo.MemberResponseVo;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
/**
* @author zr
* @date 2021/12/8 17:13
*/
@Slf4j
@Controller
public class OAuth2Controller {
@Autowired
MemberFeignService memberFeignService;
@GetMapping(value = "/oauth2.0/gitee/success")
public String gitee(@RequestParam("code") String code, HttpSession session) throws Exception {
//这个几个参数格式是强制性的 可以参考码云的官方api
Map<String,String> params = new HashMap<>();
params.put("grant_type","authorization_code");
params.put("code",code);
params.put("client_id", GiteeConstant.clientId);
params.put("redirect_uri",GiteeConstant.callback);
params.put("client_secret",GiteeConstant.secret);
//1、根据用户授权返回的code换取access_token
HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", new HashMap<>(), params, new HashMap<>());
//2、处理
if (response.getStatusLine().getStatusCode() == 200) {
//获取到了access_token
String json = EntityUtils.toString(response.getEntity());// 获取到json串
//String json = JSON.toJSONString(response.getEntity());
GiteeSocialUser socialUser = JSON.parseObject(json, GiteeSocialUser.class);
//知道了哪个社交用户
//1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息,以后这个社交账号就对应指定的会员)
//登录或者注册这个社交用户
System.out.println("登录后用code换取的token值:" + socialUser.getAccessToken());
//调用远程服务
R oauthLogin = memberFeignService.oauthLogin(socialUser);
if (oauthLogin.getCode() == 0) {
MemberResponseVo data = oauthLogin.getData("data", new TypeReference<MemberResponseVo>() {});
log.info("登录成功:用户信息:\n{}",data.toString());
//1、第一次使用session,命令浏览器保存卡号,JSESSIONID这个cookie
//以后浏览器访问哪个网站就会带上这个网站的cookie
//TODO 1、默认发的令牌。当前域(解决子域session共享问题)
//TODO 2、使用JSON的序列化方式来序列化对象到Redis中
session.setAttribute(AuthServerConstant.LOGIN_USER, 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";
}
}
}
6.3.5 新增远程调用接口oauthLogin
gulimall-member/src/main/java/site/zhourui/gulimall/member/service/MemberService.java
/**
* 社交用户的登录
* @param socialUser
* @return
*/
MemberEntity login(GiteeSocialUser socialUser) throws Exception;
gulimall-member/src/main/java/site/zhourui/gulimall/member/service/impl/MemberServiceImpl.java
此处需要判断该用户之前是否使用该社交账号登录过
所以需要在数据库中的ums_menber表中新增字段用户保存用户社交登录信息
此处的逻辑我与老师的不一样,我获取access_token信息是并没有返回该平台用户id,所以我把用户信息查询写在了前面
用户信息返回结果外码云也没有用户性别信息
@Override
public MemberEntity login(GiteeSocialUser socialUser) throws Exception {
Map<String, String> query = new HashMap<>();
query.put("access_token", socialUser.getAccessToken());
HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<String, String>(), query);
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String id = jsonObject.getString("id");
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
String profileImageUrl = jsonObject.getString("avatar_url");
//具有登录和注册逻辑
String uid = id;
//1、判断当前社交用户是否已经登录过系统
MemberEntity memberEntity = baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity != null) {
//这个用户已经注册过
//更新用户的访问令牌的时间和access_token
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccessToken());
update.setExpiresIn(socialUser.getExpiresIn());
baseMapper.updateById(update);
memberEntity.setAccessToken(socialUser.getAccessToken());
memberEntity.setExpiresIn(socialUser.getExpiresIn());
return memberEntity;
} else {
//2、没有查到当前社交用户对应的记录我们就需要注册一个
MemberEntity register = new MemberEntity();
//3、查询当前社交用户的社交账号信息(昵称、性别等)
// 远程调用,不影响结果
try {
// Map<String, String> query = new HashMap<>();
// query.put("access_token", socialUser.getAccessToken());
// HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<String, String>(), query);
if (response.getStatusLine().getStatusCode() == 200) {
//查询成功
// String gender = jsonObject.getString("gender");
register.setUsername(name);
register.setNickname(name);
register.setCreateTime(new Date());
register.setGender("m".equals(gender) ? 1 : 0);
register.setHeader(profileImageUrl);
}
}catch (Exception e){}
register.setCreateTime(new Date());
register.setSocialUid(uid);
register.setAccessToken(socialUser.getAccessToken());
register.setExpiresIn(socialUser.getExpiresIn());
//把用户信息插入到数据库中
baseMapper.insert(register);
return register;
}
}
gulimall-member/src/main/java/site/zhourui/gulimall/member/controller/MemberController.java
@PostMapping(value = "/oauth2/login")
public R oauthLogin(@RequestBody GiteeSocialUser socialUser) throws Exception {
MemberEntity memberEntity = memberService.login(socialUser);
if (memberEntity != null) {
return R.ok().setData(memberEntity);
} else {
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_EXCEPTION.getMsg());
}
}
6.3.6 新增远程feign接口
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/feign/MemberFeignService.java
@PostMapping(value = "/member/member/oauth2/login")
R oauthLogin(@RequestBody GiteeSocialUser socialUser) throws Exception;
6.3.7 测试
能够获取到一下信息就说明成功了,并且数据库中也会创建一个对应用户
6.3.8 现在的问题
我们在auth.gulimall.com中将用户信息存入session,但我们登录成功后重定向到gulimall.com
导致登录成功在首页依然不能获取到用户信息
解决方案:扩大session作用域
7.Session共享问题
7.1 session原理
session也是一种记录浏览器状态的机制,但与cookie不同的是,session是保存在服务器中。
由于http是无状态协议,当服务器存储了多个用户的session数据时,如何确认http请求对应服务器上哪一条session,相当关键。这也是session原理的核心内容。
7.2 分布式下session共享问题
7.2.1 分布式场景下相同服务
根据session原理可知,session信息是保存在服务器的,虽然是相同服务但是在不同服务器,也不能做到session共享
7.2.2 不同服务
因为获取session对象,是根据cookie中JSESSIONID来作为key获取session的,不同会话session ID不同,获取的session对象就会不同
7.3 Session共享问题解决–Session复制
优点
- web-server(Tomcat)原生支持,只需要修改配置文件
缺点
- session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力
- 任意一台web-server保存的数据都是所有webserver的session总和,受到内存限制无法水平扩展更多的web-server
- 大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。
- 个人总结:使用方便,但是每台服务器都需要保存全量session数据,占用网络带宽(适合小型分布式)
7.4 Session共享问题解决–客户端存储
优点
- 服务器不需存储session,用户保存自己的session信息到cookie中。节省服务端资源
缺点
-
都是缺点,这只是一种思路。
具体如下:
- 每次http请求,携带用户在cookie中的完整信息,浪费网络带宽
- session数据放在cookie中,cookie有长度限制4K,不能保存大量信息
- session数据放在cookie中,存在泄漏、篡改、窃取等安全隐患
这种方式不会使用
7.5 Session共享问题解决–hash一致性
方式一:利用用户ip地址来做负载均衡,使某一用户永远都访问的是同一台服务器
方式二:利用用户id来做负载均衡,使某一用户永远都访问的是同一台服务器
- 优点:
- 只需要改nginx配置,不需要修改应用代码
- 负载均衡,只要hash属性的值分布是均匀的,多台web-server的负载是均衡的
- 可以支持web-server水平扩展(session同步法是不行的,受内存限制)
- 缺点
- session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录
- 如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session
- 但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用
7.6 Session共享问题解决–统一存储
- 优点:
- 没有安全隐患
- 可以水平扩展,数据库/缓存水平切分即可
- web-server重启或者扩容都不会有session丢失
- 不足
- 增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替换为从Redis查数据的方式。redis获取数据比内存慢很多
- 上面缺点可以用SpringSession完美解决
7.7 Session共享问题解决–不同服务,子域session共享
在存入session时jsessionid的作用域提升至最大.比如auth.gulimall.com->.gulimall.com,那么gulimall.com及其下面的所有子域名都可以拿到这个jsessionid,然后再去redis中查询对应的session信息,可以实现不同服务之间的session共享
相同服务之间的session共享使用,session存入redis即可解决问题,相同服务的域名是相同的jsessionid也是相同的
7.8 SpringSession整合redis
7.8.1 导入所需依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
7.8.2 配置session存储方式
spring:
redis:
host: 192.168.157.128
port: 6379
#使用redis存储session
session:
store-type: redis
server:
port: 20000
servlet:
#配置session过期时间
session:
timeout: 30m
7.8.3 开启springsession
将该注解配置在主启动类上或者配置类上
@EnableRedisHttpSession //整合Redis作为session存储
package site.zhourui.gulimall.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* 核心原理
* 1)、@EnableRedisHttpSession导入RedisHttpSessionConfiguration配置
* 1、给容器中添加了一个组件
* RedisOperationsSessionRepository:Redis操作session,session的增删改查封装类
*
*/
@EnableRedisHttpSession //整合Redis作为session存储
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallAuthServerApplication.class, args);
}
}
此时执行登录操作进行测试
由于默认使用jdk进行序列化,不方便阅读,建议修改为json
7.8.4 修改为json序列化,并放大作用域(自定义)
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/config/GulimallSessionConfig.java
package site.zhourui.gulimall.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* @author zr
* @date 2021/12/12 10:29
*/
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
cookieSerializer.setCookieMaxAge(60*60*24*7);
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
首页可以拿到jsessionid
session序列化方式修改为json
7.8.5 其他模块需要获取session也需要整合(整合步骤与上面一致)
哪个模块需要共享session就在哪个模块整合spring session
gulimall-product整合后在用户登录后首页就可以拿到session,进而拿到用户信息进行展示
此时我们手动输入http://auth.gulimall.com/login.html仍然可以进入到登录页面再次进行登录,就需要在进入登录页面时进行判断,用户是否登录,如果已经用户登录直接重定向到首页,用户未登录才允许用户登录
用户的登录页面之前设置了视图映射这个必须要注释起来,视图映射是没有任何逻辑的,只要是这个请求就会跳到指定的视图(html),但是我们现在的登录页面是有逻辑判断的,需要在controller中新增
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/controller/LoginController.java
/**
* 判断session是否有loginUser,没有就跳转登录页面,有就跳转首页
*/
@GetMapping(value = "/login.html")
public String loginPage(HttpSession session) {
//从session先取出来用户的信息,判断用户是否已经登录过了
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
//如果用户没登录那就跳转到登录页面
if (attribute == null) {
return "login";
} else {
return "redirect:http://gulimall.com";
}
}
在登录成功后手动输入http://auth.gulimall.com/login.html,自动回重定向至首页,清除session后,进入登录页面
7. 单点登录 SSO
【社交登录、SpringSession+扩大子域 只是单系统分布式集群的登录】
多系统-单点登录
1、一处登录处处登录
2、一处退出处处退出
7.1 许雪里 开源项目
框架效果演示地址:https://gitee.com/xuxueli0323/xxl-sso
最重要的:*认证服务器
核心:三个系统即使域名不一样,想办法给三个系统同步同一个用户的票据;
1)、*认证服务器;ssoserver.com
2)、其他系统,想要登录去ssoserver.com登录,登录成功跳转回来
3)、只要有一个登录,其他都不用登录
4)、全系统统——个sso-sessionid;
7.2 单点登录实现
7.2.0 流程图
7.2.1 单点登录服务端gulimall-test-sso-server
7.2.1.1 创建步骤
7.2.1.2 pom文件(包含thymeleaf,redis)
<?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.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>site.zhourui</groupId>
<artifactId>gulimall-test-sso-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-test-sso-server</name>
<description>单点登录的*认证服务器</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<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>
7.2.1.3 配置application
server.port=8080
#虚拟机地址,默认端口6379,不用配置
spring.redis.host=192.168.157.128
7.2.1.4 新增LoginController
gulimall-test-sso-server/src/main/java/site/zhourui/gulimall/ssoserver/controller/LoginController.java
package site.zhourui.gulimall.ssoserver.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author zr
* @date 2021/12/4 16:48
*/
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/userinfo")
public String userinfo(@RequestParam(value = "token") String token) {
String s = redisTemplate.opsForValue().get(token);
return s;
}
@GetMapping("/login.html")
public String loginPage(@RequestParam(value = "redirect_url",required = false) String url, Model model, @CookieValue(value = "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(value = "/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("redirect_url") String url,
HttpServletResponse response) {
//登录成功跳转,跳回到登录页
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
String uuid = UUID.randomUUID().toString().replace("_", "");
redisTemplate.opsForValue().set(uuid, username);
Cookie sso_token = new Cookie("sso_token", uuid);
response.addCookie(sso_token);
return "redirect:" + url + "?token=" + uuid;
}
return "login";
}
}
7.2.1.4 新增模板login.html
gulimall-test-sso-server/src/main/resources/templates/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input type="text" name="username" /><br />
密码:<input type="password" name="password" /><br />
<input type="hidden" name="redirect_url" th:value="${url}" />
<input type="submit" value="登录">
</form>
</body>
</html>
7.2.2 单点登录客户端(clien1)
7.2.2.1 创建步骤
7.2.2.2 pom文件
client1pom文件
<?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.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>site.zhourui</groupId>
<artifactId>gulimall-test-sso-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-test-sso-client</name>
<description>客户端-测试sso</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
</dependencies>
<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>
7.2.2.3 新增application配置
server.port=8081
spring.redis.host=192.168.157.128
7.2.2.4 新增HelloController
gulimall-test-sso-client/src/main/java/site/zhourui/gulimall/ssoclient/controller/HelloController.java
package site.zhourui.gulimall.ssoclient.controller;
/**
* @author zr
* @date 2021/12/4 16:27
*/
import org.springframework.beans.factory.annotation.Autowired;
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.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
/**
* 测试单点登录
*/
@Controller
public class HelloController {
/**
* 无需登录就可访问
*
* @return
*/
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
return "hello";
}
@GetMapping(value = "/employees")
public String employees(Model model, HttpSession session, @RequestParam(value = "token", 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:" + "http://ssoserver.com:8080/login.html"+"?redirect_url=http://client1.com:8081/employees";
} else {
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
}
7.2.2.5 新增模板employees.html
gulimall-test-sso-client/src/main/resources/templates/employees.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>
7.2.3 单点登录客户端(clien1)
7.2.3.1 创建步骤
gulimall-testsso-client改为gulimall-testsso-client2
7.2.3.2 pom文件
client2pom文件
<?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.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>gulimall-test-sso-client2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-test-sso-client2</name>
<description>客户端-测试sso</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
</dependencies>
<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>
7.2.3.3 新增application配置
server.port=8082
spring.redis.host=192.168.157.128
7.2.3.4 新增HelloController
gulimall-test-sso-client2/src/main/java/com/example/gulimall/ssoclient2/controller/HelloController.java
package com.example.gulimall.ssoclient2.controller;
/**
* @author zr
* @date 2021/12/4 16:52
*/
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.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
/**
* 测试单点登录
*/
@Controller
public class HelloController {
/**
* 无需登录就可访问
* @return
*/
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
return "hello";
}
@GetMapping(value = "/boss")
public String employees(Model model, HttpSession session, @RequestParam(value = "token", 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:" + "http://ssoserver.com:8080/login.html"+"?redirect_url=http://client2.com:8082/boss";
} else {
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
}
7.2.3.5 新增模板employees.html
gulimall-test-sso-client2/src/main/resources/templates/employees.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>
7.2.4 配置host
#单点登录
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
7.2.5 测试
将三个服务启动
访问client1的/employees路径,未登录直接跳转带ssoserver.con的登录页面
http://client1.com:8081/employees
访问client2的/boss路径,未登录直接跳转带ssoserver.con的登录页面
http://client2.com:8082/boss
在http://ssoserver.com:8080/login.html?redirect_url=http://client1.com:8081/employees下登录
登录成功
刷新http://ssoserver.com:8080/login.html?redirect_url=http://client2.com:8082/boss
或者访问[员工列表 http://client2.com:8082/boss
7.2.6 总结
7.2.6.1实现一:在*服务器登录并返回token机制
1、创建*服务器
2、创建客户端
3、访问http://client1.com:8081/employees => 跳转 http://ssoserver.com:8080/login.html
4、带上了参数
http://ssoserver.com:8080/login.html?redirect_url=http://client1.com:8081/employess
5、登录页面隐藏域放上url的值,doLogin登录完之后跳转
1)先将用户信息存储起来 redis
2)在将令牌传回给客户端redirect:http://client1.com:8081/employess?token=uuid
6、判断是否返回token,如果登录成功,则获得令牌了
1)判断是否有令牌【是否登录】
2)根据令牌去*服务器请求用户信息
6.2.6.2 实现二:只要一个客户端在*服务器登录,其他服务器也是登录状态
实现:使用cookie,浏览器缓存*服务器的cookie,所以每次跳转*服务器会给浏览器记录sso_token【用于多系统间sso】,然后当客户端请求跳转到*服务器会查看cookie然后实现了免登陆,并且将cookie值返回给客户端作为token【客户端获得了token就可以免登陆了】
6.2.6.3 实现三:该接口由认证中心提供,远程调用传token查询数据【数据存储在redis中,跟Spring Session一样】
然后将数据保存到自己的session中。【其实spring session也可以在redis查完数据后往session放一份0.0】
不同点:增加了认证服务,解决了Cookie不可跨域的问题,都是用同一个域名的cookie