文章目录
九、认证服务
新建一个认证服务模块,并注册到nacos
-
添加默认页面到templates/,添加静态资源到nginx/html/static
-
配置本机域名映射
-
配置网关路由
-
#将主机地址为auth.gulimall.com转发至gulimall-auth-server - id: gulimall_auth_host uri: lb://gulimall-auth-server predicates: - Host=auth.gulimall.com
-
编写各个html之间的跳转逻辑
-
利用SpringMVC的视图控制器完成页面跳转
-
@Configuration public class MyWebConfig implements WebMvcConfigurer { /** * 视图映射,控制页面跳转 * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login.html").setViewName("login"); registry.addViewController("/reg.html").setViewName("reg"); } }
1.短信验证码功能
前端点击自动倒计时六十秒、发送验证码按钮
<div class="register-box">
<label class="other_label">验 证 码
<input name=code maxlength="20" type="text" placeholder="请输入验证码" class="caa">
</label>
<a id="sendCode" class="">点击发送验证码</a>
<div class="tips" style="color:red" th:text="${errors!=null?(#maps.containsKey(errors,'code')?errors.code:''):''}">
</div>
</div>
$("#sendCode").click(function () {
//如果有disabled,说明最近已经点过
if($(this).hasClass("disabled")){
}else {
timeOutChangeStyle();
//发送验证码
var phone=$("#phoneNum").val();
$.get("/sms/sendCode?phone="+phone,function (data){
if (data.code!=0){
alert(data.msg);
}
})
}
})
let time = 60;
function timeOutChangeStyle() {
//开启倒计时后设置标志属性disable的
$("#sendCode").attr("class", "disabled");
if(time==0){
$("#sendCode").text("点击发送验证码");
time=60;
$("#sendCode").attr("class", "");
}else {
$("#sendCode").text(time+"s后再次发送");
time--;
setTimeout("timeOutChangeStyle()", 1000);
}
}
2.短信验证模仿
没有备案的服务器,参考各种云api文档。这里简单写一下流程。
从api-gateway-demo-sign-java/HttpUtils.java at master · aliyun/api-gateway-demo-sign-java (github.com)下载HttpUtils放到公共微服务模块
新建一个短信发送组件,可以配置短信发送的参数
package com.henu.soft.merist.thirdparty.component;
import com.henu.soft.merist.common.utils.HttpUtils;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Controller;
import java.util.HashMap;
import java.util.Map;
@Data
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Controller
public class SmsComponent {
private String host;
private String path;
private String appcode;
public void sendCode(String phone,String code) {
// String host = "http://dingxin.market.alicloudapi.com";
// String path = "/dx/sendSms";
String method = "POST";
// String appcode = "你自己的AppCode";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile",phone);
querys.put("param", "code:"+code);
querys.put("tpl_id", "TP1711063");
Map<String, String> bodys = new HashMap<String, String>();
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
配置文件:
spring:
cloud:
alicloud:
sms:
host: http://dingxin.market.alicloudapi.com
path: /dx/sendSms
appcode: #
controller:
package com.henu.soft.merist.thirdparty.controller;
import com.henu.soft.merist.common.utils.R;
import com.henu.soft.merist.thirdparty.component.SmsComponent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/sms")
public class SmsSendController {
@Autowired
SmsComponent smsComponent;
/**
* 提供给别的服务进行调用
* @param phone
* @param code
* @return
*/
@ResponseBody
@GetMapping(value = "/sendCode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) {
//发送验证码
// smsComponent.sendCode(phone,code);
System.out.println(phone+code);
return R.ok();
}
}
3.验证码生成远程调用发送短信
由于验证码生成在认证模块,而短信发送验证码在第三方模块,所以需要feign远程调用
-
导入open-feign的依赖,加上@EnableFeignClients 开启feign服务
-
编写接口远程调用
-
package com.henu.soft.merist.auth.feign; import com.henu.soft.merist.common.utils.R; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @FeignClient("gulimall-third-party") public interface ThreadPartyFeignService { @ResponseBody @GetMapping("/sms/sendCode") public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code); }
由于向手机发送验证码服务是假的 所以这里在控制台输出
验证码生成+远程调用:
package com.henu.soft.merist.auth.controller;
import com.henu.soft.merist.auth.feign.ThreadPartyFeignService;
import com.henu.soft.merist.common.utils.R;
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 java.util.UUID;
@Controller
public class LoginController {
@Autowired
ThreadPartyFeignService threadPartyFeignService;
/** 发送一个请求直接跳转到另一个页面
* SpringMVC viewcontroller:将请求和页面映射过来
*/
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){
String code = UUID.randomUUID().toString().substring(0, 5);
//由于向手机发送验证码服务是假的 所以这里在控制台输出
System.out.println("=====================code:"+code+"=====================");
threadPartyFeignService.sendCode(phone, code);
return R.ok();
}
}
获取到验证码
4.验证码防刷检验
这里要解决三个问题:
- 验证码的过期时间设置,同时完善注册时验证码的校验
- 验证码的路径是暴露的,会受到恶意人员不停地调用耗费资源
- 尽管发送验证码之后有60s的倒计时,但是刷新页面,重新输入手机号又可以发送验证码
4.1 验证码的校验
这里由于验证码肯定不是永久有效的,所以将验证码存储到 redis 中。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis 配置
spring.redis.host=192.168.137.128
spring.redis.port=6379
设置验证码十分钟内有效
@RestController
public class LoginController {
@Autowired
ThreadPartyFeignService threadPartyFeignService;
@Autowired
StringRedisTemplate stringRedisTemplate;
/** 发送一个请求直接跳转到另一个页面
* SpringMVC viewcontroller:将请求和页面映射过来
*/
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){
String code = UUID.randomUUID().toString().substring(0, 5);
//由于向手机发送验证码服务是假的 所以这里在控制台输出
System.out.println("=====================code:"+code+"=====================");
//存储 手机号k + 验证码v 到redis //public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
//同时设置 在redis 中的过期时间 设置十分钟内有效
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10,TimeUnit.MINUTES);
threadPartyFeignService.sendCode(phone, code);
return R.ok();
}
}
4.2、4.3 验证码防刷
- 为了防止验证码一直被刷,在 redis 存储的时候,加上时间
- 每次获取验证码之前都要判断是否在 redis 中存在对应的key
- 如果存在,判断是否过了60s,
- 没过60s,提示60s后才能发送
- 过了60s,发送验证码
- 如果不存在,发送验证码
- 如果存在,判断是否过了60s,
修改代码:
@GetMapping("/sms/sendCode")
public R sendCode(@RequestParam("phone") String phone){
String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone);
if (!StringUtils.isEmpty(redisCode)){
long time = Long.parseLong(redisCode.split("_")[1]);
if (System.currentTimeMillis() - time < 60000){
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
String code = UUID.randomUUID().toString().substring(0, 5) + "_" + System.currentTimeMillis();
//由于向手机发送验证码服务是假的 所以这里在控制台输出
System.out.println("=====================code:"+code+"=====================");
//存储 手机号k + 验证码v 到redis //public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
//同时设置 在redis 中的过期时间 设置十分钟内有效
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX+phone,code,10,TimeUnit.MINUTES);
threadPartyFeignService.sendCode(phone, code);
return R.ok();
}
另外就是,这里可以大量伪造手机号,进行访问,应该在获取验证码的时候加上图形验证。
5.注册功能
5.1 封装表单vo
新建一个 UserRegisterVo 封装提交表单的数据,同时进行参数校验。
使用@Long 校验长度,引入 hibernate.validator 参数校验。
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.13.Final</version>
</dependency>
UserRegisterVo:
package com.henu.soft.merist.auth.vo;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Data
public class UserRegisterVo {
@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 phoneNum;
@NotEmpty(message = "用户名必须填写")
private String code;
}
5.2 认证服务调用会员服务
校验验证码,并且远程调用会员服务进行注册(在数据库中插入数据)
@PostMapping("/register")
public String register(@Valid UserRegisterVo vo, BindingResult result, RedirectAttributes redirectAttributes){
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);
//校验出错,转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
//1.输入的验证码
String code = vo.getCode();
//获取存入redis中的验证码
String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(code)){
if (code.equals(redisCode.split("_")[0])){
//验证通过
//删除验证码,令牌机制
stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//进行真正的注册
R register = memberFeignService.register(vo);
if (register.getCode() == 0){
//成功
return "redirect:http://auth.gulimall.com/login.html";
}else {
//失败
Map<String,String> errors = new HashMap<>();
TypeReference<String> typeReference = new TypeReference<String>(){};
String msg = register.getData(typeReference);
errors.put("msg",msg);
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
}
}
//输入验证码为空 or 校验验证码失败 走到这里
//校验出错回到注册页面
Map<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/register")
public R register(@RequestBody UserRegisterVo userRegisterVo);
}
5.3 会员服务 注册
这一步包括 检验手机号、用户名是否被占用,密码MD5盐值加密,向数据库插入数据。
MD5&MD5盐值加密
- MD5:
- Message Digest algorithm 5,信息摘要算法
- 压缩性:任意长度的数据,算出的MD5值长度都是固定的
- 容易计算:从原数据计算出MD5值很容易
- 抗修改性:对原数据进行任何改动,哪怕只修改一个字节,所得到的MD5值有很大区别
- 强碰撞性:想找到两个不同的数据,使它们具有相同的MD5,是非常困难的
- Message Digest algorithm 5,信息摘要算法
- 加盐:
- 通过生成随机数与 MD5 生化字符串进行组合
- 数据库同时存储 MD5 值与 salt 值,验证正确性时用 salt 进行 MD5 即可
@Override
public void register(MemberRegisterVo vo) {
//1.检查号码是否唯一
checkPhoneUnique(vo.getPhone());
//2.检查用户名是否唯一
checkUserNameUnique(vo.getUserName());
//3.信息唯一 进行插入
MemberEntity memberEntity = new MemberEntity();
//3.1 保存基本信息
memberEntity.setUsername(vo.getUserName());
memberEntity.setMobile(vo.getPhone());
memberEntity.setPassword(vo.getPassword());
//3.2 加密保存密码
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodePassword = passwordEncoder.encode(vo.getPassword());
memberEntity.setPassword(encodePassword);
//3.3 设置会员默认等级
//3.3.1 找到会员默认等级
MemberLevelEntity defaultLevel = memberLevelService.getOne(
new QueryWrapper<MemberLevelEntity>().eq("default_status", 1));
//3.3.2 设置会员等级为默认
memberEntity.setLevelId(defaultLevel.getId());
//4. 保存用户信息
this.save(memberEntity);
}
private void checkPhoneUnique(String phone) throws PhoneNumExistException{
Long count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (count > 0){
throw new PhoneNumExistException();
}
}
private void checkUserNameUnique(String username) throws UserNameExistException{
Long count = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
if (count > 0){
throw new UserNameExistException();
}
}
6.登录功能
6.1 普通账号密码登录
认证模块:
controller:
@PostMapping("/login")
public String login(UserLoginVo loginVo, RedirectAttributes redirectAttributes, HttpSession session){
R login = memberFeignService.login(loginVo);
if (login.getCode() == 0){
String jsonString = JSON.toJSONString(login.get("memberEntity"));
MemberResponseTo memberResponseTo = JSON.parseObject(jsonString, new TypeReference<MemberResponseTo>() {
});
//public static final String LOGIN_USER = "loginUser";
session.setAttribute(AuthServerConstant.LOGIN_USER,memberResponseTo);
return "redirect:http://gulimall.com";
}else {
String msg = (String) login.get("msg");
Map<String,String> errors = new HashMap<>();
errors.put("msg",msg);
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
feign:
@FeignClient("gulimall-member")
public interface MemberFeignService {
@PostMapping("/member/member/register")
public R register(@RequestBody UserRegisterVo userRegisterVo);
@PostMapping("/member/member//login")
public R login(@RequestBody UserLoginVo userLoginVo);
}
member 会员模块:
controller:
/**
* 登录
*/
@PostMapping("/login")
public R login(@RequestBody MemberLoginVo memberLoginVo){
MemberEntity entity = new MemberEntity();
try {
entity = memberService.login(memberLoginVo);
}catch (PasswordWrongException e){
// PASSWORD_WRONG_EXCEPTION(15004,"密码错误");
return R.error(BizCodeEnume.PASSWORD_WRONG_EXCEPTION.getCode(), BizCodeEnume.PASSWORD_WRONG_EXCEPTION.getMsg());
}
return entity == null ?
// USER_NOT_EXIST_EXCEPTION(15003,"用户不存在"),
R.error(BizCodeEnume.USER_NOT_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_NOT_EXIST_EXCEPTION.getMsg())
: R.ok().put("memberEntity",entity);
}
serviceImpl:
@Override
public MemberEntity login(MemberLoginVo vo) {
//账号:手机号 or 用户名
String account = vo.getAccount();
MemberEntity memberEntity = this.getOne(new QueryWrapper<MemberEntity>().eq("username", account).or().eq("mobile", account));
if (memberEntity != null){
checkPassword(memberEntity,vo.getPassword());
return memberEntity;
}
return null;
}
@Override
public void checkPassword(MemberEntity memberEntity, String password) throws PasswordWrongException {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
if (!passwordEncoder.matches(password,memberEntity.getPassword())){
throw new PasswordWrongException();
}
}
6.2 OAuth2.0-社交登录
参考博客:OAuth2.0 社交登录-Gitee springboot项目整合(微服务分布式)完整代码包括 数据库、前、后端_HotRabbit.的博客-CSDN博客
6.3 分布式Session -Spring Sesion原理、实战
参考博客:分布式session ——Spring Session原理、实战解决 子域之间的 session 共享问题、不同服务器 session 共享解决方案_HotRabbit.的博客-CSDN博客
7.单点登录 SSO
核心
- 三个系统即使域名不同,也要给三个系统同步同一认证状态
- 中央认证服务器、其他系统想要登录去中央服务器登录,登录成功之后跳转回来