一、环境搭建
1、创建gulimall-auth-server模块
2、引入登录和注册页
3、修改域名
修改C:\Windows\System32\drivers\etc\hosts的域名
4、复制静态文件到nginx
在虚拟机的/mydata/nginx/html/static/下创建reg,并把静态资源放入reg文件夹下
在虚拟机的/mydata/nginx/html/static/下创建login,并把静态资源放入login文件夹下
5、在网关模块配置认证的路由
二、整合短信服务
1、配置参数
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alicloud:
access-key: LTAI5tF8VcyAsK2ghPxTQhGF
secret-key:
oss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket: gulimall-adverseq
sms:
accessKeyId: LTAI4G73Dewcd8U5pC1dppWk
secret: MSDVelqAqDtk9RW18ftGMrhC5TKzxs
templateCode: SMS_189520818
signName: 谷粒商城
application:
name: gulimall-third-party
server:
port: 30000
2、SmsApiController
package com.atguigu.gulimall.thirdpart.controller;
@RestController
@CrossOrigin
@RequestMapping("/sms")
public class SmsApiController {
@Autowired
private SendSms sendSms;
@Autowired
private MyAccess access;
@GetMapping("/sendcode")
public R sendCode(@RequestParam("phone") String phone,@RequestParam("code")String code){
sendSms.send(access,phone,code);
return R.ok();
}
}
// 获取配置文件参数
package com.atguigu.gulimall.thirdpart.config;
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Component
@Data
public class MyAccess {
private String accessKeyId;
private String secret;
private String templateCode;
private String signName;
}
3、SendSmsImpl
package com.atguigu.gulimall.thirdpart.service.impl;
@Service
public class SendSmsImpl implements SendSms {
@Override
public boolean send(MyAccess access,String phoneNum,String code) {
//连接阿里云
DefaultProfile profile = DefaultProfile.getProfile("cn-qingdao", access.getAccessKeyId(), access.getSecret());
IAcsClient client = new DefaultAcsClient(profile);
//构建请求
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
//自定义参数(手机号,验证码,签名,模板)
request.putQueryParameter("PhoneNumbers", phoneNum);
request.putQueryParameter("SignName", access.getSignName());
request.putQueryParameter("TemplateCode", access.getTemplateCode());
//验证码
request.putQueryParameter("TemplateParam",JSONObject.toJSONString(code));
try {
CommonResponse response = client.getCommonResponse(request);
System.out.println(response.getData());
return response.getHttpResponse().isSuccess();
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
}
return false;
}
}
4、创建验证码的常量类
package com.atguigu.common.constant;
public class AuthServerConstant {
// 验证码前置
public static final String SMS_CODE_CACHE_PREFIX="sms:code:";
}
5、添加常量
添加验证码60秒内重复获取验证码的错误提示常量
public enum BizCodeEnume {
SMS_CODE_EXCEPTION(10002,"发送验证码太频繁,请稍后再试"),
private int code;
private String msg;
BizCodeEnume(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
6、远程调用验证码接口
package com.atguigu.gulimall.auth.feign.ThirdPartFeignService;
@FeignClient("gulimall-thrid-party")
public interface ThirdPartFeignService {
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}
7、接口防刷
由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。<br /> 1)在redis中以phone-code将电话号码和验证码进行存储并将当前时间与code一起存储<br /> 2)如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息<br /> 3)60s以后再次调用,需要删除之前存储的phone-code<br /> 4)code存在一个过期时间,我们设置为10min,10min内验证该验证码有效
8、LoginController
/**
* 发送手机验证码
*/
@ResponseBody
@GetMapping("/sms/sendcode")
public R sendCode(@RequestParam("phone") String phone) {
//TODO 接口防刷
if(!StringUtils.isEmpty(phone) & phone != null) {
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) {
// 60秒内不能再发
return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
}
}
// 2.验证码的再次校验。redis:key->phone value->code sms:code:18312345678->45678
// 防止同一个手机号在60秒内再次发送验证码,加时间戳后缀
// String code1 = UUID.randomUUID().toString().substring(0, 5)+"_"+System.currentTimeMillis();
String code = "123456" + "_" + System.currentTimeMillis();
// redis缓存验证码
stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, code, 10, TimeUnit.MINUTES);
// 远程调用第三方服务发送验证码
// thirdPartFeignService.sendCode(phone,code1);
return R.ok();
}else {
return R.error(BizCodeEnume.PHONE_NULL_EXCEPTION.getCode(), BizCodeEnume.PHONE_NULL_EXCEPTION.getMsg());
}
}
9、测试
三、用户注册
1、添加视图控制器
2、导入参数校验依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
1)若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
2)若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员服务注册
3)会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面
3、UserRegistVo
@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;
}
4、LoginController
/**
* 用户注册
* 重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,
* session里面的数据就会删掉
*
* @param vo
* @param result
* @param redirectAttributes 模拟重定向携带数据
* @return 注册成功回到首页,或回到登录页
*/
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
/**
* 方法一
* Map<String, String> errors = result.getFieldErrors().stream().map(fieldError ->{
* String field = fieldError.getField();
* String defaultMessage = fieldError.getDefaultMessage();
* errors.put(field,defaultMessage);
* return errors;
* }).collect(Collector.asList());
*/
// 方法二:
// 1.1 如果校验不通过,则封装校验结果
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(fieldError -> {
return fieldError.getField(); // Map的键
}, fieldError -> {
return fieldError.getDefaultMessage(); // Map的值
}));
// 1.2 将错误信息封装到session中,并在重定向的时候携带过去
redirectAttributes.addFlashAttribute("errors", errors);
/**
* 使用 return "forward:/reg.html"; 会出现
* 问题:Request method 'POST' not supported的问题
* 原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的)
*/
// return "reg"; //转发会出现重复提交的问题,不要以转发的方式
// 1.3 如果校验出错,重定向到注册页(转发会重复提交表单)。但面临着数据不能携带的问题,就用RedirectAttributes可解决
return "redirect:http://auth.gulimall.com/reg.html";
}
// 2 校验验证码
String code = vo.getCode();
String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)) {
if (code.equals(s.split("_")[0])) {
// 验证码通过
// 删除验证码,令牌机制
stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
// 调用远程服务,注册
R r = memberFeignService.regist(vo);
if (r.getCode() == 0) {
//注册成功回到登录页
return "redirect:http://auth.gulimall.com/login.html";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData2("msg", new TypeReference<String>() { }));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
// 校验出错转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
}
5、远程调用会员服务注册用户接口
package com.atguigu.gulimall.auth.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 远程调用member的用户注册接口
* @param vo
* @return
*/
@PostMapping("/member/member/regist")
R regist(@RequestBody UserRegistVo vo);
}
6、gulimall-member服务的MemberController
/**
* 用户注册
* @param vo
* @return
*/
@PostMapping("/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();
}
7、自定义异常
package com.atguigu.gulimall.member.exception;
public class PhoneExistException extends RuntimeException {
public PhoneExistException(){
super("该手机号码已注册");
}
}
package com.atguigu.gulimall.member.exception;
public class UserNameExistException extends RuntimeException {
public UserNameExistException(){
super("用户名已存在");
}
}
8、MD5&MD5盐值与BCrypt加密
9、MemberServiceImpl
@Override
public void regist(MemberRegistVo vo) {
MemberDao dao = this.baseMapper;
MemberEntity entity = new MemberEntity();
// 设置默认用户等级
MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
entity.setLevelId(levelEntity.getId());
// 检查数据是否唯一,为了让controller感知异常,异常机制
checkPhoneUnique(vo.getPhone());
checkUserNameUnique(vo.getUserName());
entity.setMobile(vo.getPhone());
entity.setUsername(vo.getUserName());
entity.setNickname(vo.getUserName());
// 密码加密存储
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());
entity.setPassword(encode);
dao.insert(entity);
// 其它默认信息
}
@Override
public void checkPhoneUnique(String phone) throws PhoneExistException {
MemberDao dao = this.baseMapper;
Integer mobile = dao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if (mobile > 0) {
throw new PhoneExistException();
}
}
@Override
public void checkUserNameUnique(String userName) throws UserNameExistException {
MemberDao dao = this.baseMapper;
Integer cout = dao.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
if (cout > 0) {
throw new UserNameExistException();
}
}
四、用户名密码登录
1、UserLoginVo
package com.atguigu.gulimall.auth.vo;
@Data
public class UserLoginVo {
private String loginacct;
private String password;
}
2、LoginController
@PostMapping("/login")
// 前端传来k,v参数不需要加@RequestBody
public String login(UserLoginVo vo,
RedirectAttributes redirectAttributes,
HttpSession session) {
//远程登录
R r = memberFeignService.login(vo);
if (r.getCode() == 0) {
MemberRespVo data = r.getData(new TypeReference<MemberRespVo>() { });
session.setAttribute(AuthServerConstant.LOGIN_USER, data);
return "redirect:http://gulimall.com";
} else {
Map<String,String> errors = new HashMap<>();
errors.put("msg",r.getData2("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
3、远程调用member的用户登录接口
package com.atguigu.gulimall.auth.feign;
@FeignClient("gulimall-member")
public interface MemberFeignService {
/**
* 远程调用member的用户登录接口
* @param vo
* @return
*/
@PostMapping("/member/member/login")
R login(@RequestBody UserLoginVo vo);
}
4、会员服务的MemberController
@PostMapping("/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());
}
}
package com.atguigu.common.exception;
public enum BizCodeEnume {
LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15002,"账号或密码错误");
private int code;
private String msg;
BizCodeEnume(int code, String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
5、MemberServiceImpl
@Override
public MemberEntity login(MemberLoginVo vo) {
String loginacct = vo.getLoginacct();
String password = vo.getPassword();
//1.数据库查询
MemberDao dao = this.baseMapper;
QueryWrapper<MemberEntity> wrapper = new QueryWrapper<>();
wrapper.eq("username", loginacct)
.or()
.eq("mobile", loginacct);
MemberEntity entity = dao.selectOne(wrapper);
if (entity == null) {
return null;
} else {
String passwordDb = entity.getPassword();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//密码匹配
boolean matches = passwordEncoder.matches(password, passwordDb);
if (matches) {
return entity;
} else {
return null;
}
}
}
五、社交登录
六、SpringSession
1、session原理
2、分布式下session共享问题
3、Session共享问题解决
3.1 session复制
3.2 客户端存储
3.3 hash一致性
3.4 统一存储
3.5 不同服务、子域session共享
jsessionid这个cookie默认是当前系统域名的。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案:
4、整合SpringSession
4.1 导入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
4.2 设置session的保存类型
spring.session.store-type=redis session的保存类型
server.servlet.session.timeout=30m session的过期时间
4.3 配置redis连接信息
spring.redis.host=192.168.195.128
spring.redis.port=6379
4.4 主配置类添加注解
package com.atguigu.gulimall.auth;
@EnableDiscoveryClient
@SpringBootApplication
// 整合redis存储session
// 创建了一个springSessionRepositoryFilter
// 负责将原生HttpSession替换为Spring Session的实现
@EnableRedisHttpSession
@EnableFeignClients
public class GulimallAuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallAuthServerApplication.class, args);
}
}
4.5 自定义配置
- 由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化
- 并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com ```java package com.atguigu.gulimall.auth.config;
@Configuration public class GulimallSessionConfig {
// Cookie序列化器
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
// 设置session作用域
cookieSerializer.setDomainName("gulimall.com");
// 设置Cookie的名称
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
// 序列化机制
return new GenericJackson2JsonRedisSerializer();
}
}
![](https://cdn.nlark.com/yuque/0/2021/png/22523384/1635259497746-0a25e38c-95b2-41fa-b3a3-71b92f5f7093.png#crop=0&crop=0&crop=1&crop=1&from=url&id=kpuhM&margin=%5Bobject%20Object%5D&originHeight=845&originWidth=1387&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="yjVep"></a>
## 5、SpringSession核心原理
![](https://cdn.nlark.com/yuque/0/2021/png/22523384/1635259549526-c1cccb01-5650-4ed3-a618-b1dcc5e611e1.png#crop=0&crop=0&crop=1&crop=1&from=url&id=E1bOB&margin=%5Bobject%20Object%5D&originHeight=398&originWidth=1355&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22523384/1638880698213-d8956a2f-2c8a-46b7-9691-b21605a83991.png#clientId=uaad33c5d-2462-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=441&id=u03922f28&margin=%5Bobject%20Object%5D&name=image.png&originHeight=441&originWidth=997&originalType=binary&ratio=1&rotation=0&showTitle=false&size=424651&status=done&style=none&taskId=u4d07aa3b-2349-47b8-a135-a804e60c85b&title=&width=997)
<a name="DNPZS"></a>
## 6、分布式登录总结
首先判断session中是否有loginUser对象<br />1)没有loginUser对象,渲染login.html页面<br /> 用户输入账号密码后发送给url:auth.gulimall.com/login。根据表单传过来的VO对象,远程调用memberFeignService验证密码<br /> 如果验证失败,取出远程调用返回的错误信息,放到新的请求域,重定向到登录url<br /> 如果验证成功,远程服务就返回了对应的MemberRespVo对象,然后放到分布式redis-session中,key为"loginUser",重定向到首页gulimall.com,同时也会带着的GULISESSIONID<br /> 重定向到非auth项目后,先经过拦截器看session里有没有loginUser对象<br /> 有,放到静态threadLocal中,这样就可以操作本地内存,无需远程调用session<br /> 没有,重定向到登录页<br />2)有loginUser对象,代表登录过了,重定向到首页,session数据还依靠sessionID持有着
额外说明:<br />问题1:我们有sessionId不就可以了吗?为什么还要在session中放到User对象?<br />为了其他服务可以根据这个user查数据库,只有session的话不能再次找到登录session的用户
问题2:threadlocal的作用?<br />它是为了放到当前session的线程里,threadlocal就是这个作用,随着线程创建和消亡。把threadlocal定义为static的,这样当前会话的线程中任何代码地方都可以获取到。如果只是在session中的话,一是每次还得去redis查询,二是去调用service还得传入session参数,多麻烦啊
问题3:cookie怎么回事?不是在config中定义了cookie的key和序列化器?<br />序列化器没什么好讲的,就是为了易读和来回转换。而cookie的key其实是无所谓的,只要两个项目里的key相同,然后访问同一个域名都带着该cookie即可。
<a name="O7tgw"></a>
# 七、单点登录
![image.png](https://cdn.nlark.com/yuque/0/2021/png/22523384/1638881435012-3a138b67-0055-4477-97c4-ac05b553daf9.png#clientId=uaad33c5d-2462-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=391&id=u23d1380b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=391&originWidth=934&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64031&status=done&style=none&taskId=u18aae456-3c13-416e-8072-cb712334571&title=&width=934)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/22523384/1638965838676-952143e1-cb05-4830-9743-01d1f42d877b.png#clientId=ub0e8ea8c-09a0-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=492&id=u40c8bdd5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=492&originWidth=784&originalType=binary&ratio=1&rotation=0&showTitle=false&size=230765&status=done&style=none&taskId=u08470274-ad09-4f9c-b89b-5cc0daaff8d&title=&width=784)<br />![单点登录流程.png](https://cdn.nlark.com/yuque/0/2021/png/22523384/1638884900298-48a5fde5-807a-43a3-ae27-7cadd1696030.png#clientId=uaad33c5d-2462-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=1167&id=u79587eb2&margin=%5Bobject%20Object%5D&name=%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95%E6%B5%81%E7%A8%8B.png&originHeight=1167&originWidth=1002&originalType=binary&ratio=1&rotation=0&showTitle=false&size=160122&status=done&style=none&taskId=ud43f6f4d-6c47-4653-ad7e-0d15f7751c7&title=&width=1002)
<a name="Z2yFB"></a>
## 1、gulimall-test-sso-server
<a name="Pg6wk"></a>
### 1.1 导入依赖
```java
<?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.2.11.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu</groupId>
<artifactId>gulimall-test-sso-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-test-sso-server</name>
<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>
</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>
1.1 LoginController
package com.atguigu.gulimall.ssoserver.controller;
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
@GetMapping("/userInfo")
@ResponseBody
public String userInfo(@RequestParam("token") String token){
String s = redisTemplate.opsForValue().get(token);
return s;
}
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url,
@CookieValue(value = "sso_token",required = false) String sso_token,
Model model) {
// 判断是否登录过?依据是否拥有cookie sso_token,如果有直接返回之前的页面
if(!StringUtils.isEmpty(sso_token)){
return "redirect:" + url + "?token=" + sso_token;
}
model.addAttribute("url", url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("url") String url,
HttpServletResponse response) {
// 登录成功跳转,跳回之前的页面
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
// 保存登录成功的用户,例如redis
String uuid = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(uuid, username, 30, TimeUnit.MINUTES);
Cookie sso_token = new Cookie("sso_token",uuid);
response.addCookie(sso_token);
return "redirect:" + url + "?token=" + uuid;
}
// 登录失败,展示登录页
return "login";
}
}
1.2 login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名: <input name="username" type="text"><br/>
密码: <input name="password" type="password"> <br/>
<input type="hidden" name="url" th:value="${url}">
<input type="submit" value="登录">
</form>
</body>
</html>
2、gulimall-test-sso-client
package com.atguigu.gulimall.ssoclient;
@SpringBootApplication
public class GulimallTestSsoClient2Application {
public static void main(String[] args) {
SpringApplication.run(GulimallTestSsoClient2Application.class, args);
}
}
2.1 application.properties
server.port=8081
sso.server.url=http://ssoserver.com:8080/login.html
2.2 HelloController
package com.atguigu.gulimall.ssoclient.controller;
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
// 无需登录即可访问
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
/**
* 可以感知登录服务器登录成功返回
* ssoserver登录成功返回就会带上token
* @return
*/
@GetMapping("/employees")
public String emploees(Model model, HttpSession session,
@RequestParam(value = "token",required = false) String token){
if(!StringUtils.isEmpty(token)){
// 去sso服务器获取当前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:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
}else {
List<String> emps = new ArrayList<String>();
emps.add("张三");
emps.add("李四");
emps.add("王五");
model.addAttribute("emps", emps);
return "list";
}
}
}
2.3 list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>[[${session.loginUser}]]</h1>
<ul>
<li th:each="emp:${emps}">姓名:[[${emp}]]</li>
</ul>
</body>
</html>
3、gulimall-test-sso-client2
package com.atguigu.gulimall.ssoclient;
@SpringBootApplication
public class GulimallTestSsoClient2Application {
public static void main(String[] args) {
SpringApplication.run(GulimallTestSsoClient2Application.class, args);
}
}
3.1 application.properties
server.port=8082
sso.server.url=http://ssoserver.com:8080/login.html
3.2 HelloController
package com.atguigu.gulimall.ssoclient.controller;
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
// 无需登录即可访问
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello";
}
/**
* 可以感知登录服务器登录成功返回
* ssoserver登录成功返回就会带上token
* @return
*/
@GetMapping("/boss")
public String emploees(@RequestParam(value = "token",required = false) String token,
Model model, HttpSession session){
if(!StringUtils.isEmpty(token)){
// 去sso服务器获取当前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:" + ssoServerUrl + "?redirect_url=http://client2.com:8082/boss";
}else {
List<String> emps = new ArrayList<String>();
emps.add("Jack");
emps.add("Tom");
emps.add("Ada");
model.addAttribute("emps", emps);
return "list";
}
}
}
3.3 list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>欢迎:[[${session.loginUser}]]</h1>
<ul>
<li th:each="emp:${emps}">姓名:[[${emp}]]</li>
</ul>
</body>
</html>