一、环境配置
1、创建新模块
创建管理用户登录注册的新模块auth-server
2、导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</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>登录认证</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.7</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3、相关配置
nacos配置 redis配置 相关端口
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-auth-server
redis:
host: 127.0.0.1
port: 6379
password: 123456
server:
port: 20000
session配置
spring.thymeleaf.cache=false
spring.session.store-type=redis
server.servlet.session.timeout=30m
4、前端配置
修改host
导入页面
nginx添加静态资源
修改引用静态资源的地址
如下:全部替换
5、网关配置
6、新建配置类,映射请求和页面,实现WebMvcConfigurer接口
gulimall-auth-server/src/main/java/site/xx/gulimall/auth/config/GulimallWebConfig.java
这样就不需要写controller处理请求页面映射了:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
/**
* 视图映射
*
*/
@Override
public void addViewControllers(ViewControllerRegistry registry){
//请求跳转页面
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
7、测试
二、短信验证码功能
1、短信服务
阿里云购买短信验证码服务
购买完后记住自己的两个code和Secret
每个短信服务,我们可以在下面可以看到他的短信接口,以及相应的java代码
点击调试按钮,去查看接口功能是否完整,能否发送验证码
1.短信配置
阅读使用手册后,我们知道需要导入一些固定配置才能在java开发中使用:
package com.atguigu.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.新增短信服务组件SmsComponent
com.atguigu.gulimall.thirdparty.component.SmsComponent
复制自己短信业务下的java语法,创建smscomponent类
import com.atguigu.gulimall.thirdparty.util.HttpUtils;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Data
@Component
public class SmsComponent {
public void sendSmsCode(String phone, String code) {
String host = "输入自己的接口地址";
String path = "/data/send_sms";
String method = "POST";
String appcode = "输入自己的appcode";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
//根据API的要求,定义相对应的Content-Type
headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
Map<String, String> querys = new HashMap<String, String>();
Map<String, String> bodys = new HashMap<String, String>();
bodys.put("content", "code:"+code);
bodys.put("template_id", "CST_ptdie100");
bodys.put("phone_number", phone);
try {
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.yml添加自定义的短信属性配置
<!--自定义配置的提示 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
4.新增发验证码Controller
用于控制短信发送服务,便于其他类调用当前功能
com.atguigu.gulimall.thirdparty.controller.SmsSendController
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.thirdparty.component.SmsComponent;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
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;
@RestController
@RequestMapping("/sms")
public class SmsSendController {
@Autowired
private SmsComponent smsComponent;
/**
* 提供给别的服务进行调用
*
*/
@GetMapping("/sendCode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code){
smsComponent.sendSmsCode(phone, code);
return R.ok();
}
}
2、认证服务调用短信服务
1.“认证服务”调用“第三方服务”发送短信(feign远程调用)
auth模块远程调用third模块中的发送短信功能
创建feign包,用于控制远程调用功能
ThirdPartFeignService接口用于调用第三方模块中的短信功能
com.atguigu.gulimall.auth.feign.ThirdPartFeignService
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
2.登录controller发送验证码
auth模块下,创建logincontroller控制登录功能
com.atguigu.gulimall.auth.controller.LoginController
(简单实现,不考虑接口防刷)
package site.xxx.gulimall.auth.controller;
@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、使用redis实现接口防刷
接口写在前端js代码里,仍然可以被其他人拿来盗刷
由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。
在redis中以phone-code将电话号码和验证码进行存储并将当前时间与code一起存储
如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息
60s以后再次调用,需要删除之前存储的phone-code
code存在一个过期时间,我们设置为10min,10min内验证该验证码有效
接口防刷过程:
1)先查询redis,是否超过60s,否则不允许发送短信
2)存入redis,key为“sms:code:手机号”,value为“六位数字+当前系统时间”,通过比较redis存的时间和现在时间是否超过一分钟,防止一分钟内不断刷验证码,构造参数存入过期时间10min,并且存入当前系统时间
1.引入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.yml配置redis地址信息
redis:
host: 127.0.0.1
port: 6379
3.LoginController发送验证码(考虑接口防刷)
@ResponseBody
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){
//1.接口放刷
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
if(!StringUtils.isEmpty(redisCode)){
long l = Long.parseLong(redisCode.split("_")[1]);
if(System.currentTimeMillis() - l < 60000){
//60秒内不能再发
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
//2.验证码的再次校验:redis
// String code = UUID.randomUUID().toString().substring(0, 5);
Random rand = new Random();
int temp = rand.nextInt(1000000);
String code = String.format("%06d", temp);
String substring = code+"_"+System.currentTimeMillis();
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,substring,10, TimeUnit.MINUTES);
thirdPartFeignService.sendCode(phone, code);
return R.ok();
}
认证模块相关的常量类:
com.atguigu.common.constant.AuthServerConstant
public class AuthServerConstant {
public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
public static final String LOGIN_USER = "loginUser";
}
common模块的错误码枚举类:
com.atguigu.common.exception.BizCodeEnume
三、注册页面功能实现
1、基本配置准备
1.抽取注册模型类
com.atguigu.gulimall.auth.vo.UserRegistVo
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Data
public class UserRegistVo {
@NotEmpty(message = "用户名必须提交")
@Length(min = 6, max = 18, 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;
}
2. 业务流程
注册的主体逻辑:
1.若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
2.若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员模块的feign注册(检查验证码、用户名、手机号 唯一),校验通过后,调用会员服务添加会员信息
3.会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面
3.controller参数注解@Valid
注: RedirectAttributes可以通过session保存信息并在重定向的时候携带过去。这里用于存错误消息
//BindingResult参数获取校验结果
//RedirectAttributes可以通过session保存信息并在重定向的时候携带过去。这里用于存错误消息
@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));
// flash是一闪而过,此数据只取一次
attributes.addFlashAttribute("errors",errors);
//效验出错,重定向到注册页面。不用转发是为了防止刷新时重复提交表单
// 不用return reg是因为本来就在注册页面点击发送了这个注册请求,要重定向清空表单
return "redirect:http://auth.gulimall.com/reg.html";
}
//1、效验验证码
return null
}
2、【会员模块】存储会员信息
1.抽取模型类,接收用户信息
member模块下
com.atguigu.gulimall.member.vo.MemberRegistVo
import lombok.Data;
@Data
public class MemberRegistVo {
private String userName;
private String password;
private String phone;
}
2.自定义“用户名与手机号重复”的异常类
package com.atguigu.gulimall.member.exception;
public class UsernameExistException extends RuntimeException{
public UsernameExistException() {
super("存在相同的用户名");
}
}
package com.atguigu.gulimall.member.exception;
public class PhoneExistException extends RuntimeException{
public PhoneExistException() {
super("存在相同的手机号");
}
}
3.注册controller实现
业务流程:
1.通过异常机制判断当前注册会员名和电话号码是否已经注册
2.如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
3.如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间
member/controller/MemberController.java
@PostMapping(value = "/regist")
public R regist(@RequestBody MemberRegistVo vo) {
try {
memberService.regist(vo);
//异常机制:通过捕获对应的自定义异常判断出现何种错误并封装错误信息
} catch (PhoneExistException e) {
return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
} catch (UsernameExistException e) {
return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
4. 使用BCrypt实现密码加密解密
BCrypt加密: 一种加盐的单向Hash,不可逆的加密算法,同一种明文(plaintext),每次加密后的密文都不一样,而且不可反向破解生成明文,破解难度很大。
和其他加密方式相比,BCryptPasswordEncoder有着它自己的优势所在,首先加密的hash值每次都不同,就像md5的盐值加密一样,只不过盐值加密用到了随机数,前者用到的是其内置的算法规则,毕竟随机数没有设合适的话还是有一定几率被攻破的。其次BCryptPasswordEncoder的生成加密存储串也有60位之多。最重要的一点是,md5的加密不是spring security所推崇的加密方式了,所以我们还是要多了解点新的加密方式。
BCryptPasswordEncoder每次加密相同的值,都会得到不同的密文
BCryptPasswordEncoder加密(encode)解密(matches)
5.注册功能方法实现
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;
member/service/impl/MemberServiceImpl.java
@Override
public void regist(MemberRegistVo 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 PhoneExistException {
Integer phoneCount = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (phoneCount > 0) {
throw new PhoneExistException();
}
}
//检查用户名是否已存在
@Override
public void checkUserNameUnique(String userName) throws UsernameExistException {
Integer usernameCount = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
if (usernameCount > 0) {
throw new UsernameExistException();
}
}
3、【认证模块】注册业务实现
业务流程
1.若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
2.若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员模块的feign注册(检查验证码、用户名、手机号 唯一),校验通过后,调用会员服务添加会员信息
3.会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面
1.远程调用服务
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.auth.vo.SocialUser;
import com.atguigu.gulimall.auth.vo.UserLoginVo;
import com.atguigu.gulimall.auth.vo.UserRegistVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
R regist(@RequestBody UserRegistVo vo);
}
2.controller类创建注册方法
com.atguigu.gulimall.auth.controller.LoginController
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes, HttpSession session){
if(result.hasErrors()) {
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
// model.addAttribute("errors", errors);
redirectAttributes.addFlashAttribute("errors",errors);
//校验出错,转发到出错页
//用户注册-》regist[post]------>>转发/reg.html(路径映射默认都是get)
return "redirect:http://auth.yjlmall.com/reg.html";
}
// 真正的注册
//1. 检验验证码
String code = vo.getCode();
String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if(!StringUtils.isEmpty(s)){
if(code.equals(s.split("_")[0])){
//删除验证码,令牌机制
redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//验证码通过 // 真正注册 调用远程服务进行注册
R r = memberFeignService.regist(vo);
if (r.getCode() == 0) {
//成功,重定向到登录页面
return "redirect:http://auth.yjlmall.com/login.html";
} else {
//失败
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData("msg", new TypeReference<String>() {}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.yjlmall.com/reg.html";
}
}else {
//验证码错误
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.yjlmall.com/reg/html";
}
}else {
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.yjlmall.com/reg/html";
}
}
四、用户名密码登录
1、【认证模块】登录业务
1.模型类,接收用户名密码
com.atguigu.gulimall.auth.vo.UserLoginVo
import lombok.Data;
@Data
public class UserLoginVo {
private String loginacct; //登录账号名
private String password;
}
2.feign客户端新增登录功能
com.atguigu.gulimall.auth.feign.MemberFeignService
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
R regist(@RequestBody UserRegistVo vo);
@PostMapping("/member/member/login")
R login(@RequestBody UserLoginVo vo);
@PostMapping("/member/member/oauth2/login")
R oauthlogin(@RequestBody SocialUser socialUser) throws Exception;
}
3.LoginController处理登录请求
com.atguigu.gulimall.auth.controller.LoginController
@PostMapping(value = "/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session) {
//远程登录
R login = memberFeignService.login(vo);
if (login.getCode() == 0) {
//登陆成功,将登录者信息放到session中,重定向到首页。
MemberResponseVo data = login.getData("data", new TypeReference<MemberResponseVo>() {});
session.setAttribute(AuthServerConstant.LOGIN_USER, data);
return "redirect:http://yjlmall.com";
} else {
//登陆失败,将错误消息添加到attributes,重定向到登录页。
Map<String,String> errors = new HashMap<>();
errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.yjlmall.com/login.html";
}
}
检测是否已登录:
/**
* 判断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";
}
}
2、【用户模块】 登录业务
1.模型类
com.atguigu.gulimall.member.vo.MemberLoginVo
import lombok.Data;
@Data
public class MemberLoginVo {
private String loginacct;
private String password;
}
2.controller 登录验证
com.atguigu.gulimall.member.controller.MemberController
/**
* 登录接口
*/
@PostMapping(value = "/login")
public R login(@RequestBody MemberLoginVo vo) {
MemberEntity memberEntity = memberService.login(vo);
if (memberEntity != null) {
return R.ok().setData(memberEntity);
} else {
return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
}
}
3.service and Impl
com.atguigu.gulimall.member.service.MemberService
MemberEntity login(MemberLoginVo vo);
@Override
public MemberEntity login(MemberLoginVo 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;
}else {
return null;
}
}
}
五、第三方社交登录
1、OAuth3.0授权协议介绍
OAuth1.0: OAuth(开放授权) 是一个开放标准, 允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息, 而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
OAuth2.0: 对于用户相关的 开放OpenAPI(例如获取用户信息, 昵称、头像、动态同步, 照片, 日志, 分享等) , 为了保护用户数据的安全和隐私, 第三方网站访问用户数据前都需要显式的向用户征求授权。
( A) 用户打开客户端以后, 客户端要求用户给予授权。
( B) 用户同意给予客户端授权。
( C) 客户端使用上一步获得的授权, 向认证服务器申请令牌。
( D) 认证服务器对客户端进行认证以后, 确认无误, 同意发放令牌。
( E) 客户端使用令牌, 向资源服务器申请获取资源。
( F) 资源服务器确认令牌无误, 同意向客户端开放资源。
注意点
1.使用Code换取AccessToken,Code只能用一次
2.同一个用户的accessToken一段时间是不会变化的,即使多次获取:
2、gitee社交登录
1.gitee基本设置
进入gitee官网第三方应用,创建新应用
设置回调地址,记住自己的id,secret,还可以赋予权限
进入gitee apit文档,可以查看Oauth相关api
2.业务实现
3.修改授权回调页
前端将api地址复制到目标位置,并修改回调地址,以及自己的client——id
4.抽取社交用户信息模型类
com.atguigu.gulimall.auth.vo.SocialUser
import lombok.Data;
@Data
public class SocialUser {
private String access_token;
private String token_type;
private Long expires_in;
private String refresh_token;
private String scope;
private Long created_at;
}
3、【会员模块】通过社交实体类登录业务
1.数据库表“ums_member”添加字段
2.MemberEntity添加字段
com.atguigu.gulimall.member.entity.MemberEntity
@Data
@TableName("ums_member")
public class MemberEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* 会员等级id
*/
private Long levelId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String nickname;
/**
* 手机号码
*/
private String mobile;
/**
* 邮箱
*/
private String email;
/**
* 头像
*/
private String header;
/**
* 性别
*/
private Integer gender;
/**
* 生日
*/
private Date birth;
/**
* 所在城市
*/
private String city;
/**
* 职业
*/
private String job;
/**
* 个性签名
*/
private String sign;
/**
* 用户来源
*/
private Integer sourceType;
/**
* 积分
*/
private Integer integration;
/**
* 成长值
*/
private Integer growth;
/**
* 启用状态
*/
private Integer status;
/**
* 注册时间
*/
private Date createTime;
private String socialUid;
private String accessToken;
private Long expiresIn;
}
3.controller
新建controller类控制社交登录
4.service,传入社交用户uid、token等信息,返回会员实体类
1.如果注册过,就更新令牌、令牌过期时间
2.如果第一次登录,就通过社交账号的昵称、头像等信息新创建用户存入member数据库。
@Override
public MemberEntity login(SocialUser socialUser) throws Exception {
//具有登录和注册逻辑
String uid = Long.toString(socialUser.getCreated_at());
//1、判断当前社交用户是否已经登录过系统
MemberEntity memberEntity = this.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.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
this.baseMapper.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else {
//2、没有查到当前社交用户对应的记录我们就需要注册一个
MemberEntity register = new MemberEntity();
//3、查询当前社交用户的社交账号信息(昵称、性别等)
Map<String,String> query = new HashMap<>();
query.put("access_token",socialUser.getAccess_token());
query.put("uid",uid);
HttpResponse response = HttpUtils.doGet("https://gitee.com", "/api/v5/user", "get", new HashMap<String, String>(), query);
if (response.getStatusLine().getStatusCode() == 200) {
//查询成功
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
//昵称
String name = jsonObject.getString("login");
// String gender = jsonObject.getString("gender");
// String profileImageUrl = jsonObject.getString("profile_image_url");
register.setNickname(name);
// register.setGender("m".equals(gender)?1:0);
// register.setHeader(profileImageUrl);
register.setCreateTime(new Date());
register.setSocialUid(uid);
register.setAccessToken(socialUser.getAccess_token());
register.setExpiresIn(socialUser.getExpires_in());
//把用户信息插入到数据库中
this.baseMapper.insert(register);
}
return register;
}
}
4、【认证模块】远程调用会员模块社交登录
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
R regist(@RequestBody UserRegistVo vo);
@PostMapping("/member/member/login")
R login(@RequestBody UserLoginVo vo);
@PostMapping("/member/member/oauth2/login")
R oauthlogin(@RequestBody SocialUser socialUser) throws Exception;
}
5、【认证模块】社交登录实现,处理社交登录授权后的请求
/**
* 处理社交登录请求
*
*/
@Slf4j
@Controller
public class OAuth2Controller {
@Autowired
MemberFeignService memberFeignService;
@GetMapping("/oauth2.0/gitee/success")
public String gitee(@RequestParam("code") String code, HttpSession session) throws Exception {
Map<String, String> map = new HashMap<String, String>();
map.put("grant_type", "authorization_code");
map.put("code", code);
map.put("client_id", "27c7bd0cd3fcc7c58d2fbda1bbc6b6cb94afcfd36ea0eb8a74722db03216af52");
map.put("redirect_uri", "http://auth.yjlmall.com/oauth2.0/gitee/success");
map.put("client_secret", "91b53b055f3ec7a22d9f066c3e09363e24ecb5ee961767a8f7bb6355c4162d16");
//1.根据code换取accesstoken
HttpResponse response = HttpUtils.doPost("https://gitee.com", "/oauth/token", "post", new HashMap<>(), map,new HashMap<>());
//2、处理
if (response.getStatusLine().getStatusCode() == 200) {
//获取到了access_token,转为通用社交登录对象
String json = EntityUtils.toString(response.getEntity());
//String json = JSON.toJSONString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);
//知道了哪个社交用户
//1)、memberFeignService.oauthLogin当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息,以后这个社交账号就对应指定的会员)
//登录或者注册这个社交用户
System.out.println(socialUser.getAccess_token());
//调用远程服务
R oauthLogin = memberFeignService.oauthlogin(socialUser);
if (oauthLogin.getCode() == 0) {
MemberResponseVo data = oauthLogin.getData("data", new TypeReference<MemberResponseVo>() {
});
log.info("登录成功:用户信息:{}", data.toString());
//1、第一次使用session,命令浏览器保存卡号,JSESSIONID这个cookie
//以后浏览器访问哪个网站就会带上这个网站的cookie
//TODO 1、默认发的令牌。当前域(解决子域session共享问题)
//TODO 2、使用JSON的序列化方式来序列化对象到Redis中
session.setAttribute(AuthServerConstant.LOGIN_USER, data);
//2、登录成功跳回首页
return "redirect:http://yjlmall.com";
} else {
return "redirect:http://auth.yjlmall.com/login.html";
}
} else {
return "redirect:http://auth.yjlmall.com/login.html";
}
}
}
六、session
1、session原理
session也是一种记录浏览器状态的机制,但与cookie不同的是,session是保存在服务器中。
由于http是无状态协议,当服务器存储了多个用户的session数据时,如何确认http请求对应服务器上哪一条session,相当关键。这也是session原理的核心内容。
2、分布式下session共享问题
3、 Session共享问题的几种解决方案
1.Session复制(同步),不推荐
优点:
web-server(Tomcat)原生支持,只需要修改配置文件
缺点:
session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力
任意一台web-server保存的数据都是所有webserver的session总和,受到内存限制无法水平扩展更多的web-server
大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。
个人总结:使用方便,但是每台服务器都需要保存全量session数据,占用网络带宽(适合小型分布式)
2. 客户端存储,不推荐
优点
服务器不需存储session,用户保存自己的session信息到cookie中。节省服务端资源
缺点
都是缺点,这只是一种思路。
具体如下:
每次http请求,携带用户在cookie中的完整信息,浪费网络带宽
session数据放在cookie中,cookie有长度限制4K,不能保存大量信息
session数据放在cookie中,存在泄漏、篡改、窃取等安全隐患
3.hash一致性(某一用户永远都访问的是同一台服务器)
方式一:利用用户ip地址来做负载均衡,使某一用户永远都访问的是同一台服务器
方式二:利用用户id来做负载均衡,使某一用户永远都访问的是同一台服务器
4.redis统一存储
5.提升作用域到父域名,子域session共享(推荐,使用)
在存入session时jsessionid的作用域提升至最大.比如auth.gulimall.com->.gulimall.com,那么gulimall.com及其下面的所有子域名都可以拿到这个jsessionid,然后再去redis中查询对应的session信息,可以实现不同服务之间的session共享
相同服务之间的session共享使用,session存入redis即可解决问题,相同服务的域名是相同的jsessionid也是相同的
4、SpringSession解决session共享问题
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>
2.yml配置session存储方式和过期时间
spring:
redis:
host: 127.0.0.1
port: 6379
#使用redis存储session
session:
store-type: redis
server:
port: 20000
servlet:
#配置session过期时间
session:
timeout: 30m
3.启动类注解开启springsession
将该注解配置在认证模块主启动类GulimallAuthServerApplication 上或者配置类上
@EnableRedisHttpSession //整合Redis作为session存储
给响应模型类实现序列化接口,并把它移到common模块,以便于商品模块也能获取到:
4.配置类设置session使用json序列化,并放大作用域(自定义)
session默认使用jdk进行序列化,不方便阅读,建议修改为json
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域到父域名
cookieSerializer.setDomainName("yjlmall.com");
cookieSerializer.setCookieName("GULISESSION");
cookieSerializer.setCookieMaxAge(60 * 60 * 24 * 7);
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
5.用户名密码登录成功时也存储session
此时我们手动输入http://auth.yjlmall.com/login.html仍然可以进入到登录页面再次进行登录,就需要在进入登录页面时进行判断,用户是否登录,如果已经用户登录直接重定向到首页,用户未登录才允许用户登录
用户的登录页面之前设置了视图映射这个必须要注释起来,视图映射是没有任何逻辑的,只要是这个请求就会跳到指定的视图(html),但是我们现在的登录页面是有逻辑判断的,需要在controller中新增
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://yjlmall.com";
}
}
6.测试
登录回显前端修改
同理把检索等页面一样修改回显
详情页面也能看到回显消息
七、单点登录
1、单点登录业务介绍
早期单一服务器,用户认证。
缺点:单点性能压力,无法扩展
分布式,SSO(single sign on)模式
解决 :
用户身份信息独立管理,更好的分布式管理。
可以自己扩展安全策略
跨域不是问题
缺点:
认证服务器访问压力较大