1.验证码注册防刷校验
package com.xdt.auth.controller;
import com.xdt.auth.feign.ThirdFeignService;
import com.xdt.common.constant.AuthServerConstant;
import com.xdt.common.exception.BizCodeEnum;
import com.xdt.common.utils.R;
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 org.springframework.util.StringUtils;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Controller
public class LoginController {
@Autowired
ThirdFeignService thirdFeignService;
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam String phone) {
// 1、接口防刷
String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_PREFIX + phone);
System.out.println(redisCode);
System.out.println(!StringUtils.isEmpty(redisCode));
if (!StringUtils.isEmpty(redisCode)) {
long l = Long.parseLong(redisCode.split("_")[1]);//取出第一次存入的时间
if (System.currentTimeMillis() - l < 60000) {
//60秒内不能再次发送
return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
}
}
//2、验证码的再次校验。redis
String code = UUID.randomUUID().toString().substring(0, 5);
String substring = code + "_" + System.currentTimeMillis();//杠后面接时间
//redis缓存验证码,防止同一个手机在六十秒内再次发送
redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_PREFIX + phone, substring, 5, TimeUnit.MINUTES);
thirdFeignService.sendCode(phone, code);
return R.ok();
}
@PostMapping("/regist")
public String regist(@Valid @RequestBody UserRegistVo userRegistVo, BindingResult result, RedirectAttributes attributes) {
//又有需要重定向携带数据,那么就不能直接使用Model,springboot中的RedirectAttributes
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.xdt.com/reg.html";
}
//校验验证码
String code = userRegistVo.getCode();
String RedisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_PREFIX + userRegistVo.getPhone());
if (!StringUtils.isEmpty(RedisCode)) {
if (code.equals(RedisCode.split("_")[0])) {
//删除验证码
redisTemplate.delete(AuthServerConstant.SMS_CODE_PREFIX + userRegistVo.getPhone());
//验证码通过 //真正注册,调用远程服务
R r = memberFeignService.regist(userRegistVo);
if (r.getCode() == 0) {
//成功
return "redirect:login.html";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData("msg", new TypeReference<String>() {
}));
attributes.addFlashAttribute("errors", errors);
return "redirect:reg.html";//重定向
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
attributes.addFlashAttribute("errors", errors);
return "redirect:reg.html";//重定向
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
attributes.addFlashAttribute("errors", errors);
return "redirect:reg.html";//重定向
}
}
}
注册业务流程
/**
* 用户注册
* @param vo
* @return
*/
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
try {
memberService.regist(vo);
}catch (PhoneExistException e){
R.error(BizCodeEnum.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnum.PHONE_EXIST_EXCEPTION.getMsg());
}catch (UsernameExistException e){
R.error(BizCodeEnum.USER_EXIST_EXCEPTION.getCode(),BizCodeEnum.USER_EXIST_EXCEPTION.getMsg());
}
return R.ok();
}
@Override
public void regist(MemberRegistVo vo) {
MemberDao memberDao= this.baseMapper;
MemberEntity entity = new MemberEntity();
//设置会员默认等级
MemberLevelEntity levelEntity= memberLevelDao.getDefaultLevel();
entity.setLevelId(levelEntity.getId());//设置会员等级id
//检查用户名和手机号码是否唯一
checkPhoneUnique(vo.getPhone());
entity.setMobile(vo.getPhone());
checkUsernameUnique(vo.getUsername());
entity.setUsername(vo.getUsername());
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();//盐值加密
String encode = passwordEncoder.encode(vo.getPassword());
entity.setPassword(encode);
//todo 其他默认信息
//保存
memberDao.insert(entity);
}
@Override
public void checkPhoneUnique(String phone) throws PhoneExistException {
MemberDao memberDao= this.baseMapper;
Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));//查询是否存在该手机号
if (mobile>0) {
throw new PhoneExistException();
}
}
@Override
public void checkUsernameUnique(String username) throws UsernameExistException{
MemberDao memberDao= this.baseMapper;
Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));//查询是否存在该手机号
if (mobile>0) {
throw new UsernameExistException();
}
}
用户登录
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session) {
System.out.println(vo.getLoginacct());
System.out.println(vo.getPassword());
//远程登录
R login = memberFeignService.login(vo);
if (login.getCode() == 0) {
//TODO 登录成功后的处理
//成功
MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
});
session.setAttribute(AuthServerConstant.LOGIN_USER,data);
return "redirect:index";
} else {
//失败
Map<String, String> errors = new HashMap<>();
errors.put("msg", login.getData("msg", new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:login.html";
}
}
/*远程方法*/
@Override
public MemberEntity login(MemberLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword();
//去数据库查询
MemberDao memberDao= this.baseMapper;
MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile", loginacct));
if (entity==null){
//登录失败
return null;
}else {
//获取数据库的password
String passwordDb = entity.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//密码是否匹配
boolean matches = passwordEncoder.matches(password, passwordDb);
if(matches){
return entity;
}else {
return null;
}
}
}
微博登录
新建OAuth2Controller
输入回调地址
在网页上建立引导按钮跳转微博指定地址:https://api.weibo.com/oauth2/authorize?client_id=你刚才申请应用的id&response_type=code&redirect_uri=用户确认登录之后的回调地址
:
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
//例如回调地址是
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session) throws Exception {
System.out.println(code);
//根据code换取access_token
//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
Map<String, String> map = new HashMap<>();//用于存放参数
map.put("client_id", "基本信息中的Id");
map.put("client_secret", "基本信息中的secret");
map.put("grant_type", "authorization_code");
map.put("redirect_uri", "你的回调地址");
map.put("code", code);
Map<String, String> headers = new HashMap<String, String>();
//通过微博的地址获取用户的Access Token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", headers, null, map);
if (response.getStatusLine().getStatusCode() == 200) {
String json = EntityUtils.toString(response.getEntity());//转换成json字符串
SocialUser socialUser = JSON.parseObject(json, SocialUser.class);//json字符串转化成实体类
//获取到了access_token
//知道当前是哪个社交用户
// 1.如果用户是第一次进来 自动注册进来(为当前社交用户生成一个会员信息 以后这个账户就会关联这个账号)
R oauthlogin = memberFeignService.oauthlogin(socialUser);
if (oauthlogin.getCode() == 0) {
MemberRespVo data = oauthlogin.getData("data", new TypeReference<MemberRespVo>() {
});
log.info("登陆成功:用户信息:{}", data.toString());
session.setAttribute(AuthServerConstant.LOGIN_USER, data);
// TODO 1.默认发的当前域的session (需要解决子域session共享问题)
// TODO 2.使用JSON的方式序列化到redis
//登录成功跳回首页
return "redirect:index.html";
} else {
return "redirect:login.html";
}
} else {
return "redirect:login.html";
}
}
拿到tocent之后就可以到接口管理的地方获取微博官方提供的接口用来查询用户信息
//自己系统的注册逻辑
@Override
public MemberEntity login(SocialUser socialUser) {
//登录和注册合并逻辑
String uid=socialUser.getUid();
//判断当前社交用户是否登录过系统
MemberDao memberDao= this.baseMapper;
MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", uid));
if (memberEntity!=null){
//说明已经注册过了
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
memberDao.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
}else {
//没有查到当前社交账户对应的记录需要注册一个
MemberEntity regList = new MemberEntity();
try {
//查询当前社交用户的社交账号信息(昵称性别等等)
Map<String, String> querys = new HashMap<>();
querys.put("access_token",socialUser.getAccess_token());
querys.put("uid",socialUser.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<String,String>(), querys);
if (response.getStatusLine().getStatusCode()==200){
//查询成功
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String name = jsonObject.getString("name");//获取微博的名字
String gender = jsonObject.getString("gender");//性别
regList.setNickname(name);
regList.setGender("m".equals(gender)?1:0);
regList.setHeader(jsonObject.getString("profile_image_url"));//设置头像
}
}catch (Exception e){}
String uuid = UUID.randomUUID().toString().substring(0,5);
regList.setUsername("微博用户:"+uuid);
regList.setSocialUid(socialUser.getUid());
regList.setAccessToken(socialUser.getAccess_token());
regList.setExpiresIn(socialUser.getExpires_in());
memberDao.insert(regList);
return regList;
}
}
微服务情况下会出现session问题
1、同一个服务,复制多份,session不同步的问题。
2、不同服务,session不能共享问题。
1.1解决方法:
1、Session:复制(不推荐)
2、Nginx的IP Hash策略(可以使用)
3、Session共享,Session集中存储(推荐)
<!-- 使用springSession解决session问题 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
在application中添加:spring.session.store-type=redis
启动类开启注解:@EnableRedisHttpSession
接下来往session中添加东西就会存储到redis中session.setAttribute(AuthServerConstant.LOGIN_USER,data);
注意:session中的data必须实现序列化Serializable,因为需要将内存对象远程保存到redis服务器中,所以需要序列化成二进制流/串再存入。
2.1解决session不同服务不能共享问题,加入配置类
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 : xdt
* @createDate : 2024/12/31 10:01
*/
@Configuration
public class MallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setDomainName("xdt.com");
serializer.setCookieName("XdtSESSION");
return serializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
//因为有些类没加Serializer,所以这里使用json序列化,不使用Serializer
return new GenericJackson2JsonRedisSerializer();
}
}
单点登录
1、中央认证服务器
2、所有登录去服务器认证,登录成功后跳转回
3、只要有一个登录,其他都无需登录
4、全局统一sso.sessionId
这里使用三个域名做测试,一个服务端,两个客户端
一、创建服务器端
1、maven依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2、创建controller
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
//登录逻辑
@PostMapping("/doLogin")
public String doLogin(String username, String password, String url, HttpServletResponse response) {
//如果用户名和密码不为空就当登录成功
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
String uuid = UUID.randomUUID().toString().replace("_", "");
redisTemplate.opsForValue().set(uuid,username,30, TimeUnit.MINUTES);
Cookie ssoToken = new Cookie("sso_token",uuid);
response.addCookie(ssoToken);
//登录成功跳回之前的页面
return "redirect:"+url+"?token="+uuid;
}
//登录失败展示登录页
return "login";
}
//登录
@GetMapping("login.html")
public String loginPage(@RequestParam("redirect_url") String url, Model model,@CookieValue(value = "sso_token",required = false) String sso_token) {
if (StringUtils.hasText(sso_token)) {
//cookie有数据,说明有人登录了,留下痕迹。当然真实情况下得验证令牌sso_token是否有效,有效再跳转
String token = redisTemplate.opsForValue().get(sso_token);
if (token!=null) {
return "redirect:" + url + "?token=" + sso_token;
}
}
model.addAttribute("url",url);
return "login";
}
//获取用户信息
@ResponseBody
@GetMapping("userInfo")
public String userInfo(@RequestParam("token")String token){
String s = redisTemplate.opsForValue().get(token);
return s;
}
}
3、登录页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input name="username">
密码:<input name="password">
<input type="submit" value="登录">
<input type="hidden" name="url" th:value="${url}">
</form>
</body>
</html>
二、创建客户端
1、maven依赖
<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>
2、建立controller
application.properties文件的内容
sso.server.url=http://ssoserver.com:8080/login.html
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
//无需登录可访问
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
//登录才能访问
@GetMapping("/boss")
public String employees(Model model, HttpSession session,@RequestParam(value = "token",required = false) String token){
if (StringUtils.hasText(token)){
//带了token证明登录成功
//要去登录服务器获取当前token对应的用户信息,如果查询到了就放入session,否则重新到登录页面
//可以使用HttpUtils工具类发请求,这里就不引入了直接用spring的
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
String body = forEntity.getBody();
if (body==null){
return "redirect:"+ssoServerUrl+"?redirect_url=http://client2.com:8082/boss";
}
session.setAttribute("loginUser",body);
}
Object loginUser = session.getAttribute("loginUser");
if (loginUser==null){
//未登录,跳转登录服务器登录,url告诉单点登录服务器要跳回的地址
return "redirect:"+ssoServerUrl+"?redirect_url=http://client2.com:8082/boss";
}else {
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps",emps);
return "list";
}
}
}
3、用户详情页
<!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>
演示效果:
输入两个客户端需要认证才能访问的网页,自动跳转到授权服务的登录页面。
当用户输入正确的用户名和密码后,授权成功,跳转到需要访问的页面
刷新客户端1,发现已经登录
总结:起一个认证服务,其他服务登录先请求认证服务,认证服务判断自己域名下是否有cookie保存登录信息,如果有直接 返回,如果没有就登录并保存cookie重定向到申请地址。