谷粒商城–认证中心–高级篇笔记八
上面没选好直接复制下面的pom文件,记得排除gulimall-common包的mybatis-plus
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.2.RELEASEversion>
<relativePath/>
parent>
<groupId>com.atguigu.gulimallgroupId>
<artifactId>gulimall-auth-serverartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gulimall-auth-servername>
<description>认证中心(社交登录、OAuth2.0、单点登录)description>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR6spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>com.zhourui.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
//可以远程调用,使服务能够被nacos发现
@EnableFeignClients
@EnableDiscoveryClient
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-auth-server
server:
port: 20000
192.168.157.128 auth.gulimall.com
对应nginx上的静态地址就行
gulimall-gateway/src/main/resources/application.yml
#认证服务
- id: gulimall_auth_host
uri: lb://gulimall-auth-server
predicates:
- Host=auth.gulimall.com
之前在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");
}
}
http://auth.gulimall.com/login.html
http://auth.gulimall.com/reg.html
验证码倒计时
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
购买地址:
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
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.15version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.2.1version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpcoreartifactId>
<version>4.2.1version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
<dependency>
<groupId>org.eclipse.jettygroupId>
<artifactId>jetty-utilartifactId>
<version>9.3.7.v20160115version>
dependency>
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);
}
}
}
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();
}
}
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();
}
}
导入配置提示依赖
gulimall-third-party/pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
新增配置
gulimall-third-party/src/main/resources/application.yml
sms:
host: http://fesms.market.alicloudapi.com
path: /sms/
skin: 1
sign: 1
appcode: 004c4072d4ed40b489d77b987ad3404d
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();
}
}
测试:localhost:30000/sms/sendCode?phone=17748781568&code=8888
测试结果
成功同时收到短信
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);
}
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();
}
}
发送验证码短信成功
接口写在前端js代码里,仍然可以被其他人拿来盗刷
由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。
phone-code
将电话号码和验证码进行存储并将当前时间与code一起存储
phone
取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息phone-code
加了接口防刷功能:
1)先查询redis,是否超过60s,否则不允许发送短信
2)存入redis,过期时间10min,并且存入当前系统时间
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
redis:
host: 192.168.157.128
port: 6379
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秒之内再次发送该号码的验证码
在gulimall-auth-server
服务中编写注册的主体逻辑
BindingResult
封装错误信息,并重定向至注册页面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;
}
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;
}
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();
}
和其他加密方式相比,BCryptPasswordEncoder有着它自己的优势所在,首先加密的hash值每次都不同,就像md5的盐值加密一样,只不过盐值加密用到了随机数,前者用到的是其内置的算法规则,毕竟随机数没有设合适的话还是有一定几率被攻破的。其次BCryptPasswordEncoder的生成加密存储串也有60位之多。最重要的一点是,md5的加密不是spring security所推崇的加密方式了,所以我们还是要多了解点新的加密方式。
BCryptPasswordEncoder每次加密相同的值,都会得到不同的密文
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);
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();
}
}
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";
}
}
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;
}
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";
}
}
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;
}
gulimall-auth-server/src/main/java/site/zhourui/gulimall/auth/feign/MemberFeignService.java
@PostMapping(value = "/member/member/login")
R login(@RequestBody UserLoginVo vo);
QQ、 微博、 github 等网站的用户量非常大, 别的网站为了简化自我网站的登陆与注册逻辑, 引入社交登陆功能;
步骤:
1) 、 用户点击 QQ 按钮
2) 、 引导跳转到 QQ 授权页
3) 、 用户主动点击授权, 跳回之前网页。
( A) 用户打开客户端以后, 客户端要求用户给予授权。
( B) 用户同意给予客户端授权。
( C) 客户端使用上一步获得的授权, 向认证服务器申请令牌。
( D) 认证服务器对客户端进行认证以后, 确认无误, 同意发放令牌。
( E) 客户端使用令牌, 向资源服务器申请获取资源。
( F) 资源服务器确认令牌无误, 同意向客户端开放资源。
注意点:
- 使用Code换取AccessToken,Code只能用一次
- 同一个用户的accessToken一段时间是不会变化的,即使多次获取
使用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
逻辑:
微博开放平台实名认证审了我8天还没通过,我放弃了,选用gitee,不需要实名认证审批
此处的回调地址必须与后台的一致
此步骤可以写在后端,防止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"/>
a>
li>
新增码云认证相关常量
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";
}
}
}
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 query = new HashMap<>();
// query.put("access_token", socialUser.getAccessToken());
// HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap(), 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());
}
}
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;
能够获取到一下信息就说明成功了,并且数据库中也会创建一个对应用户
我们在auth.gulimall.com中将用户信息存入session,但我们登录成功后重定向到gulimall.com
导致登录成功在首页依然不能获取到用户信息
解决方案:扩大session作用域
session也是一种记录浏览器状态的机制,但与cookie不同的是,session是保存在服务器中。
由于http是无状态协议,当服务器存储了多个用户的session数据时,如何确认http请求对应服务器上哪一条session,相当关键。这也是session原理的核心内容。
根据session原理可知,session信息是保存在服务器的,虽然是相同服务但是在不同服务器,也不能做到session共享
因为获取session对象,是根据cookie中JSESSIONID来作为key获取session的,不同会话session ID不同,获取的session对象就会不同
优点
缺点
优点
缺点
都是缺点,这只是一种思路。
具体如下:
这种方式不会使用
方式一:利用用户ip地址来做负载均衡,使某一用户永远都访问的是同一台服务器
方式二:利用用户id来做负载均衡,使某一用户永远都访问的是同一台服务器
在存入session时jsessionid的作用域提升至最大.比如auth.gulimall.com->.gulimall.com,那么gulimall.com及其下面的所有子域名都可以拿到这个jsessionid,然后再去redis中查询对应的session信息,可以实现不同服务之间的session共享
相同服务之间的session共享使用,session存入redis即可解决问题,相同服务的域名是相同的jsessionid也是相同的
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
spring:
redis:
host: 192.168.157.128
port: 6379
#使用redis存储session
session:
store-type: redis
server:
port: 20000
servlet:
#配置session过期时间
session:
timeout: 30m
将该注解配置在主启动类上或者配置类上
@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
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
哪个模块需要共享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后,进入登录页面
【社交登录、SpringSession+扩大子域 只是单系统分布式集群的登录】
多系统-单点登录
1、一处登录处处登录
2、一处退出处处退出
框架效果演示地址:https://gitee.com/xuxueli0323/xxl-sso
最重要的:中央认证服务器
核心:三个系统即使域名不一样,想办法给三个系统同步同一个用户的票据;
1)、中央认证服务器;ssoserver.com
2)、其他系统,想要登录去ssoserver.com登录,登录成功跳转回来
3)、只要有一个登录,其他都不用登录
4)、全系统统——个sso-sessionid;
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.2.RELEASEversion>
<relativePath/>
parent>
<groupId>site.zhouruigroupId>
<artifactId>gulimall-test-sso-serverartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gulimall-test-sso-servername>
<description>单点登录的中央认证服务器description>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
server.port=8080
#虚拟机地址,默认端口6379,不用配置
spring.redis.host=192.168.157.128
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";
}
}
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>
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>
server.port=8081
spring.redis.host=192.168.157.128
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";
}
}
}
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>
gulimall-testsso-client改为gulimall-testsso-client2
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>
server.port=8082
spring.redis.host=192.168.157.128
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";
}
}
}
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>
#单点登录
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com
将三个服务启动
访问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
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)根据令牌去中央服务器请求用户信息
实现:使用cookie,浏览器缓存中央服务器的cookie,所以每次跳转中央服务器会给浏览器记录sso_token【用于多系统间sso】,然后当客户端请求跳转到中央服务器会查看cookie然后实现了免登陆,并且将cookie值返回给客户端作为token【客户端获得了token就可以免登陆了】
然后将数据保存到自己的session中。【其实spring session也可以在redis查完数据后往session放一份0.0】
不同点:增加了认证服务,解决了Cookie不可跨域的问题,都是用同一个域名的cookie