一、环境搭建

1、创建gulimall-auth-server模块

image.png

2、引入登录和注册页

image.png

3、修改域名

修改C:\Windows\System32\drivers\etc\hosts的域名
image.png

4、复制静态文件到nginx

在虚拟机的/mydata/nginx/html/static/下创建reg,并把静态资源放入reg文件夹下
在虚拟机的/mydata/nginx/html/static/下创建login,并把静态资源放入login文件夹下

5、在网关模块配置认证的路由

image.png

二、整合短信服务

1、配置参数

  1. spring:
  2. cloud:
  3. nacos:
  4. discovery:
  5. server-addr: 127.0.0.1:8848
  6. alicloud:
  7. access-key: LTAI5tF8VcyAsK2ghPxTQhGF
  8. secret-key:
  9. oss:
  10. endpoint: oss-cn-beijing.aliyuncs.com
  11. bucket: gulimall-adverseq
  12. sms:
  13. accessKeyId: LTAI4G73Dewcd8U5pC1dppWk
  14. secret: MSDVelqAqDtk9RW18ftGMrhC5TKzxs
  15. templateCode: SMS_189520818
  16. signName: 谷粒商城
  17. application:
  18. name: gulimall-third-party
  19. server:
  20. port: 30000

2、SmsApiController

  1. package com.atguigu.gulimall.thirdpart.controller;
  2. @RestController
  3. @CrossOrigin
  4. @RequestMapping("/sms")
  5. public class SmsApiController {
  6. @Autowired
  7. private SendSms sendSms;
  8. @Autowired
  9. private MyAccess access;
  10. @GetMapping("/sendcode")
  11. public R sendCode(@RequestParam("phone") String phone,@RequestParam("code")String code){
  12. sendSms.send(access,phone,code);
  13. return R.ok();
  14. }
  15. }
  16. // 获取配置文件参数
  17. package com.atguigu.gulimall.thirdpart.config;
  18. @ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
  19. @Component
  20. @Data
  21. public class MyAccess {
  22. private String accessKeyId;
  23. private String secret;
  24. private String templateCode;
  25. private String signName;
  26. }

3、SendSmsImpl

  1. package com.atguigu.gulimall.thirdpart.service.impl;
  2. @Service
  3. public class SendSmsImpl implements SendSms {
  4. @Override
  5. public boolean send(MyAccess access,String phoneNum,String code) {
  6. //连接阿里云
  7. DefaultProfile profile = DefaultProfile.getProfile("cn-qingdao", access.getAccessKeyId(), access.getSecret());
  8. IAcsClient client = new DefaultAcsClient(profile);
  9. //构建请求
  10. CommonRequest request = new CommonRequest();
  11. request.setSysMethod(MethodType.POST);
  12. request.setSysDomain("dysmsapi.aliyuncs.com");
  13. request.setSysVersion("2017-05-25");
  14. request.setSysAction("SendSms");
  15. //自定义参数(手机号,验证码,签名,模板)
  16. request.putQueryParameter("PhoneNumbers", phoneNum);
  17. request.putQueryParameter("SignName", access.getSignName());
  18. request.putQueryParameter("TemplateCode", access.getTemplateCode());
  19. //验证码
  20. request.putQueryParameter("TemplateParam",JSONObject.toJSONString(code));
  21. try {
  22. CommonResponse response = client.getCommonResponse(request);
  23. System.out.println(response.getData());
  24. return response.getHttpResponse().isSuccess();
  25. } catch (ServerException e) {
  26. e.printStackTrace();
  27. } catch (ClientException e) {
  28. e.printStackTrace();
  29. }
  30. return false;
  31. }
  32. }

4、创建验证码的常量类

  1. package com.atguigu.common.constant;
  2. public class AuthServerConstant {
  3. // 验证码前置
  4. public static final String SMS_CODE_CACHE_PREFIX="sms:code:";
  5. }

5、添加常量

  1. 添加验证码60秒内重复获取验证码的错误提示常量
  1. public enum BizCodeEnume {
  2. SMS_CODE_EXCEPTION(10002,"发送验证码太频繁,请稍后再试"),
  3. private int code;
  4. private String msg;
  5. BizCodeEnume(int code, String msg){
  6. this.code = code;
  7. this.msg = msg;
  8. }
  9. public int getCode() {
  10. return code;
  11. }
  12. public String getMsg() {
  13. return msg;
  14. }
  15. }

6、远程调用验证码接口

  1. package com.atguigu.gulimall.auth.feign.ThirdPartFeignService;
  2. @FeignClient("gulimall-thrid-party")
  3. public interface ThirdPartFeignService {
  4. @GetMapping("/sms/sendcode")
  5. public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
  6. }

7、接口防刷

  1. 由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。<br /> 1)在redis中以phone-code将电话号码和验证码进行存储并将当前时间与code一起存储<br /> 2)如果调用时以当前phone取出的v不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息<br /> 360s以后再次调用,需要删除之前存储的phone-code<br /> 4code存在一个过期时间,我们设置为10min10min内验证该验证码有效

8、LoginController

  1. /**
  2. * 发送手机验证码
  3. */
  4. @ResponseBody
  5. @GetMapping("/sms/sendcode")
  6. public R sendCode(@RequestParam("phone") String phone) {
  7. //TODO 接口防刷
  8. if(!StringUtils.isEmpty(phone) & phone != null) {
  9. String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone);
  10. if (!StringUtils.isEmpty(redisCode)) {
  11. long time = Long.parseLong(redisCode.split("_")[1]);
  12. if (System.currentTimeMillis() - time < 60000) {
  13. // 60秒内不能再发
  14. return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(), BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
  15. }
  16. }
  17. // 2.验证码的再次校验。redis:key->phone value->code sms:code:18312345678->45678
  18. // 防止同一个手机号在60秒内再次发送验证码,加时间戳后缀
  19. // String code1 = UUID.randomUUID().toString().substring(0, 5)+"_"+System.currentTimeMillis();
  20. String code = "123456" + "_" + System.currentTimeMillis();
  21. // redis缓存验证码
  22. stringRedisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, code, 10, TimeUnit.MINUTES);
  23. // 远程调用第三方服务发送验证码
  24. // thirdPartFeignService.sendCode(phone,code1);
  25. return R.ok();
  26. }else {
  27. return R.error(BizCodeEnume.PHONE_NULL_EXCEPTION.getCode(), BizCodeEnume.PHONE_NULL_EXCEPTION.getMsg());
  28. }
  29. }

9、测试

image.png
image.png

三、用户注册

1、添加视图控制器

image.png

2、导入参数校验依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency>

1)若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
2)若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员服务注册
3)会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面

3、UserRegistVo

  1. @Data
  2. public class UserRegistVo {
  3. @NotEmpty(message = "用户名不能为空")
  4. @Length(min =6, max = 18, message = "用户名必须是6~18位字符")
  5. private String userName;
  6. @NotEmpty(message = "密码不能为空")
  7. @Length(min =6, max = 18, message = "密码必须是6~18位字符")
  8. private String password;
  9. @NotEmpty(message = "手机号不能为空")
  10. @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "手机号码格式不正确")
  11. private String phone;
  12. @NotEmpty(message = "验证码不能为空")
  13. private String code;
  14. }

4、LoginController

  1. /**
  2. * 用户注册
  3. * 重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,
  4. * session里面的数据就会删掉
  5. *
  6. * @param vo
  7. * @param result
  8. * @param redirectAttributes 模拟重定向携带数据
  9. * @return 注册成功回到首页,或回到登录页
  10. */
  11. @PostMapping("/regist")
  12. public String regist(@Valid UserRegistVo vo, BindingResult result,
  13. RedirectAttributes redirectAttributes) {
  14. if (result.hasErrors()) {
  15. /**
  16. * 方法一
  17. * Map<String, String> errors = result.getFieldErrors().stream().map(fieldError ->{
  18. * String field = fieldError.getField();
  19. * String defaultMessage = fieldError.getDefaultMessage();
  20. * errors.put(field,defaultMessage);
  21. * return errors;
  22. * }).collect(Collector.asList());
  23. */
  24. // 方法二:
  25. // 1.1 如果校验不通过,则封装校验结果
  26. Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(fieldError -> {
  27. return fieldError.getField(); // Map的键
  28. }, fieldError -> {
  29. return fieldError.getDefaultMessage(); // Map的值
  30. }));
  31. // 1.2 将错误信息封装到session中,并在重定向的时候携带过去
  32. redirectAttributes.addFlashAttribute("errors", errors);
  33. /**
  34. * 使用 return "forward:/reg.html"; 会出现
  35. * 问题:Request method 'POST' not supported的问题
  36. * 原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的)
  37. */
  38. // return "reg"; //转发会出现重复提交的问题,不要以转发的方式
  39. // 1.3 如果校验出错,重定向到注册页(转发会重复提交表单)。但面临着数据不能携带的问题,就用RedirectAttributes可解决
  40. return "redirect:http://auth.gulimall.com/reg.html";
  41. }
  42. // 2 校验验证码
  43. String code = vo.getCode();
  44. String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
  45. if (!StringUtils.isEmpty(s)) {
  46. if (code.equals(s.split("_")[0])) {
  47. // 验证码通过
  48. // 删除验证码,令牌机制
  49. stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
  50. // 调用远程服务,注册
  51. R r = memberFeignService.regist(vo);
  52. if (r.getCode() == 0) {
  53. //注册成功回到登录页
  54. return "redirect:http://auth.gulimall.com/login.html";
  55. } else {
  56. Map<String, String> errors = new HashMap<>();
  57. errors.put("msg", r.getData2("msg", new TypeReference<String>() { }));
  58. redirectAttributes.addFlashAttribute("errors", errors);
  59. return "redirect:http://auth.gulimall.com/reg.html";
  60. }
  61. } else {
  62. Map<String, String> errors = new HashMap<>();
  63. errors.put("code", "验证码错误");
  64. redirectAttributes.addFlashAttribute("errors", errors);
  65. return "redirect:http://auth.gulimall.com/reg.html";
  66. }
  67. } else {
  68. Map<String, String> errors = new HashMap<>();
  69. errors.put("code", "验证码错误");
  70. redirectAttributes.addFlashAttribute("errors", errors);
  71. // 校验出错转发到注册页
  72. return "redirect:http://auth.gulimall.com/reg.html";
  73. }
  74. }

5、远程调用会员服务注册用户接口

  1. package com.atguigu.gulimall.auth.feign;
  2. @FeignClient("gulimall-member")
  3. public interface MemberFeignService {
  4. /**
  5. * 远程调用member的用户注册接口
  6. * @param vo
  7. * @return
  8. */
  9. @PostMapping("/member/member/regist")
  10. R regist(@RequestBody UserRegistVo vo);
  11. }

6、gulimall-member服务的MemberController

  1. /**
  2. * 用户注册
  3. * @param vo
  4. * @return
  5. */
  6. @PostMapping("/regist")
  7. public R regist(@RequestBody MemberRegistVo vo) {
  8. try {
  9. memberService.regist(vo);
  10. // 通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
  11. } catch (PhoneExistException e) {
  12. return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
  13. } catch (UserNameExistException e) {
  14. return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(), BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
  15. }
  16. return R.ok();
  17. }

7、自定义异常

  1. package com.atguigu.gulimall.member.exception;
  2. public class PhoneExistException extends RuntimeException {
  3. public PhoneExistException(){
  4. super("该手机号码已注册");
  5. }
  6. }
  1. package com.atguigu.gulimall.member.exception;
  2. public class UserNameExistException extends RuntimeException {
  3. public UserNameExistException(){
  4. super("用户名已存在");
  5. }
  6. }

8、MD5&MD5盐值与BCrypt加密

image.png

9、MemberServiceImpl

  1. @Override
  2. public void regist(MemberRegistVo vo) {
  3. MemberDao dao = this.baseMapper;
  4. MemberEntity entity = new MemberEntity();
  5. // 设置默认用户等级
  6. MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
  7. entity.setLevelId(levelEntity.getId());
  8. // 检查数据是否唯一,为了让controller感知异常,异常机制
  9. checkPhoneUnique(vo.getPhone());
  10. checkUserNameUnique(vo.getUserName());
  11. entity.setMobile(vo.getPhone());
  12. entity.setUsername(vo.getUserName());
  13. entity.setNickname(vo.getUserName());
  14. // 密码加密存储
  15. BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
  16. String encode = passwordEncoder.encode(vo.getPassword());
  17. entity.setPassword(encode);
  18. dao.insert(entity);
  19. // 其它默认信息
  20. }
  21. @Override
  22. public void checkPhoneUnique(String phone) throws PhoneExistException {
  23. MemberDao dao = this.baseMapper;
  24. Integer mobile = dao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
  25. if (mobile > 0) {
  26. throw new PhoneExistException();
  27. }
  28. }
  29. @Override
  30. public void checkUserNameUnique(String userName) throws UserNameExistException {
  31. MemberDao dao = this.baseMapper;
  32. Integer cout = dao.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
  33. if (cout > 0) {
  34. throw new UserNameExistException();
  35. }
  36. }

四、用户名密码登录

1、UserLoginVo

  1. package com.atguigu.gulimall.auth.vo;
  2. @Data
  3. public class UserLoginVo {
  4. private String loginacct;
  5. private String password;
  6. }

2、LoginController

  1. @PostMapping("/login")
  2. // 前端传来k,v参数不需要加@RequestBody
  3. public String login(UserLoginVo vo,
  4. RedirectAttributes redirectAttributes,
  5. HttpSession session) {
  6. //远程登录
  7. R r = memberFeignService.login(vo);
  8. if (r.getCode() == 0) {
  9. MemberRespVo data = r.getData(new TypeReference<MemberRespVo>() { });
  10. session.setAttribute(AuthServerConstant.LOGIN_USER, data);
  11. return "redirect:http://gulimall.com";
  12. } else {
  13. Map<String,String> errors = new HashMap<>();
  14. errors.put("msg",r.getData2("msg",new TypeReference<String>(){}));
  15. redirectAttributes.addFlashAttribute("errors",errors);
  16. return "redirect:http://auth.gulimall.com/login.html";
  17. }
  18. }

04:商业业务-认证服务 - 图9

3、远程调用member的用户登录接口

  1. package com.atguigu.gulimall.auth.feign;
  2. @FeignClient("gulimall-member")
  3. public interface MemberFeignService {
  4. /**
  5. * 远程调用member的用户登录接口
  6. * @param vo
  7. * @return
  8. */
  9. @PostMapping("/member/member/login")
  10. R login(@RequestBody UserLoginVo vo);
  11. }

4、会员服务的MemberController

  1. @PostMapping("/login")
  2. public R login(@RequestBody MemberLoginVo vo) {
  3. MemberEntity memberEntity = memberService.login(vo);
  4. if (memberEntity != null) {
  5. return R.ok().setData(memberEntity);
  6. } else {
  7. return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(), BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
  8. }
  9. }
  1. package com.atguigu.common.exception;
  2. public enum BizCodeEnume {
  3. LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15002,"账号或密码错误");
  4. private int code;
  5. private String msg;
  6. BizCodeEnume(int code, String msg){
  7. this.code = code;
  8. this.msg = msg;
  9. }
  10. public int getCode() {
  11. return code;
  12. }
  13. public String getMsg() {
  14. return msg;
  15. }
  16. }

5、MemberServiceImpl

  1. @Override
  2. public MemberEntity login(MemberLoginVo vo) {
  3. String loginacct = vo.getLoginacct();
  4. String password = vo.getPassword();
  5. //1.数据库查询
  6. MemberDao dao = this.baseMapper;
  7. QueryWrapper<MemberEntity> wrapper = new QueryWrapper<>();
  8. wrapper.eq("username", loginacct)
  9. .or()
  10. .eq("mobile", loginacct);
  11. MemberEntity entity = dao.selectOne(wrapper);
  12. if (entity == null) {
  13. return null;
  14. } else {
  15. String passwordDb = entity.getPassword();
  16. BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
  17. //密码匹配
  18. boolean matches = passwordEncoder.matches(password, passwordDb);
  19. if (matches) {
  20. return entity;
  21. } else {
  22. return null;
  23. }
  24. }
  25. }

五、社交登录

社交登录流程.png

六、SpringSession

1、session原理

image.png

2、分布式下session共享问题

image.png

3、Session共享问题解决

3.1 session复制

image.png

3.2 客户端存储

image.png

3.3 hash一致性

image.png

3.4 统一存储

image.png

3.5 不同服务、子域session共享

jsessionid这个cookie默认是当前系统域名的。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案:
04:商业业务-认证服务 - 图17

4、整合SpringSession

4.1 导入依赖

  1. <dependency>
  2. <groupId>org.springframework.session</groupId>
  3. <artifactId>spring-session-data-redis</artifactId>
  4. </dependency>

4.2 设置session的保存类型

  1. spring.session.store-type=redis session的保存类型
  2. server.servlet.session.timeout=30m session的过期时间

4.3 配置redis连接信息

  1. spring.redis.host=192.168.195.128
  2. spring.redis.port=6379

4.4 主配置类添加注解

  1. package com.atguigu.gulimall.auth;
  2. @EnableDiscoveryClient
  3. @SpringBootApplication
  4. // 整合redis存储session
  5. // 创建了一个springSessionRepositoryFilter
  6. // 负责将原生HttpSession替换为Spring Session的实现
  7. @EnableRedisHttpSession
  8. @EnableFeignClients
  9. public class GulimallAuthServerApplication {
  10. public static void main(String[] args) {
  11. SpringApplication.run(GulimallAuthServerApplication.class, args);
  12. }
  13. }

image.png

4.5 自定义配置

  • 由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化
  • 并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com ```java package com.atguigu.gulimall.auth.config;

@Configuration public class GulimallSessionConfig {

  1. // Cookie序列化器
  2. @Bean
  3. public CookieSerializer cookieSerializer(){
  4. DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
  5. // 设置session作用域
  6. cookieSerializer.setDomainName("gulimall.com");
  7. // 设置Cookie的名称
  8. cookieSerializer.setCookieName("GULISESSION");
  9. return cookieSerializer;
  10. }
  11. @Bean
  12. public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
  13. // 序列化机制
  14. return new GenericJackson2JsonRedisSerializer();
  15. }

}

  1. ![](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=)
  2. <a name="yjVep"></a>
  3. ## 5、SpringSession核心原理
  4. ![](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)
  5. <a name="DNPZS"></a>
  6. ## 6、分布式登录总结
  7. 首先判断session中是否有loginUser对象<br />1)没有loginUser对象,渲染login.html页面<br /> 用户输入账号密码后发送给urlauth.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持有着
  8. 额外说明:<br />问题1:我们有sessionId不就可以了吗?为什么还要在session中放到User对象?<br />为了其他服务可以根据这个user查数据库,只有session的话不能再次找到登录session的用户
  9. 问题2threadlocal的作用?<br />它是为了放到当前session的线程里,threadlocal就是这个作用,随着线程创建和消亡。把threadlocal定义为static的,这样当前会话的线程中任何代码地方都可以获取到。如果只是在session中的话,一是每次还得去redis查询,二是去调用service还得传入session参数,多麻烦啊
  10. 问题3cookie怎么回事?不是在config中定义了cookiekey和序列化器?<br />序列化器没什么好讲的,就是为了易读和来回转换。而cookiekey其实是无所谓的,只要两个项目里的key相同,然后访问同一个域名都带着该cookie即可。
  11. <a name="O7tgw"></a>
  12. # 七、单点登录
  13. ![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)
  14. <a name="Z2yFB"></a>
  15. ## 1、gulimall-test-sso-server
  16. <a name="Pg6wk"></a>
  17. ### 1.1 导入依赖
  18. ```java
  19. <?xml version="1.0" encoding="UTF-8"?>
  20. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  21. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  22. <modelVersion>4.0.0</modelVersion>
  23. <parent>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-parent</artifactId>
  26. <version>2.2.11.RELEASE</version>
  27. <relativePath/> <!-- lookup parent from repository -->
  28. </parent>
  29. <groupId>com.atguigu</groupId>
  30. <artifactId>gulimall-test-sso-server</artifactId>
  31. <version>0.0.1-SNAPSHOT</version>
  32. <name>gulimall-test-sso-server</name>
  33. <properties>
  34. <java.version>1.8</java.version>
  35. </properties>
  36. <dependencies>
  37. <dependency>
  38. <groupId>org.springframework.boot</groupId>
  39. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  40. </dependency>
  41. <dependency>
  42. <groupId>org.springframework.boot</groupId>
  43. <artifactId>spring-boot-starter-web</artifactId>
  44. </dependency>
  45. <dependency>
  46. <groupId>org.projectlombok</groupId>
  47. <artifactId>lombok</artifactId>
  48. <optional>true</optional>
  49. </dependency>
  50. </dependencies>
  51. <build>
  52. <plugins>
  53. <plugin>
  54. <groupId>org.springframework.boot</groupId>
  55. <artifactId>spring-boot-maven-plugin</artifactId>
  56. <configuration>
  57. <excludes>
  58. <exclude>
  59. <groupId>org.projectlombok</groupId>
  60. <artifactId>lombok</artifactId>
  61. </exclude>
  62. </excludes>
  63. </configuration>
  64. </plugin>
  65. </plugins>
  66. </build>
  67. </project>

1.1 LoginController

  1. package com.atguigu.gulimall.ssoserver.controller;
  2. @Controller
  3. public class LoginController {
  4. @Autowired
  5. StringRedisTemplate redisTemplate;
  6. @GetMapping("/userInfo")
  7. @ResponseBody
  8. public String userInfo(@RequestParam("token") String token){
  9. String s = redisTemplate.opsForValue().get(token);
  10. return s;
  11. }
  12. @GetMapping("/login.html")
  13. public String loginPage(@RequestParam("redirect_url") String url,
  14. @CookieValue(value = "sso_token",required = false) String sso_token,
  15. Model model) {
  16. // 判断是否登录过?依据是否拥有cookie sso_token,如果有直接返回之前的页面
  17. if(!StringUtils.isEmpty(sso_token)){
  18. return "redirect:" + url + "?token=" + sso_token;
  19. }
  20. model.addAttribute("url", url);
  21. return "login";
  22. }
  23. @PostMapping("/doLogin")
  24. public String doLogin(@RequestParam("username") String username,
  25. @RequestParam("password") String password,
  26. @RequestParam("url") String url,
  27. HttpServletResponse response) {
  28. // 登录成功跳转,跳回之前的页面
  29. if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
  30. // 保存登录成功的用户,例如redis
  31. String uuid = UUID.randomUUID().toString().replace("-", "");
  32. redisTemplate.opsForValue().set(uuid, username, 30, TimeUnit.MINUTES);
  33. Cookie sso_token = new Cookie("sso_token",uuid);
  34. response.addCookie(sso_token);
  35. return "redirect:" + url + "?token=" + uuid;
  36. }
  37. // 登录失败,展示登录页
  38. return "login";
  39. }
  40. }

1.2 login.html

  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <form action="/doLogin" method="post">
  9. 用户名: <input name="username" type="text"><br/>
  10. 密码: <input name="password" type="password"> <br/>
  11. <input type="hidden" name="url" th:value="${url}">
  12. <input type="submit" value="登录">
  13. </form>
  14. </body>
  15. </html>

2、gulimall-test-sso-client

  1. package com.atguigu.gulimall.ssoclient;
  2. @SpringBootApplication
  3. public class GulimallTestSsoClient2Application {
  4. public static void main(String[] args) {
  5. SpringApplication.run(GulimallTestSsoClient2Application.class, args);
  6. }
  7. }

2.1 application.properties

  1. server.port=8081
  2. sso.server.url=http://ssoserver.com:8080/login.html

2.2 HelloController

  1. package com.atguigu.gulimall.ssoclient.controller;
  2. @Controller
  3. public class HelloController {
  4. @Value("${sso.server.url}")
  5. String ssoServerUrl;
  6. // 无需登录即可访问
  7. @ResponseBody
  8. @GetMapping("/hello")
  9. public String hello(){
  10. return "hello";
  11. }
  12. /**
  13. * 可以感知登录服务器登录成功返回
  14. * ssoserver登录成功返回就会带上token
  15. * @return
  16. */
  17. @GetMapping("/employees")
  18. public String emploees(Model model, HttpSession session,
  19. @RequestParam(value = "token",required = false) String token){
  20. if(!StringUtils.isEmpty(token)){
  21. // 去sso服务器获取当前token真正对应的用户信息
  22. RestTemplate restTemplate = new RestTemplate();
  23. ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
  24. String body = forEntity.getBody();
  25. session.setAttribute("loginUser",body);
  26. }
  27. Object loginUser = session.getAttribute("loginUser");
  28. if(loginUser == null){
  29. // 没有登录,跳转到登录服务器进行登录
  30. return "redirect:"+ssoServerUrl+"?redirect_url=http://client1.com:8081/employees";
  31. }else {
  32. List<String> emps = new ArrayList<String>();
  33. emps.add("张三");
  34. emps.add("李四");
  35. emps.add("王五");
  36. model.addAttribute("emps", emps);
  37. return "list";
  38. }
  39. }
  40. }

2.3 list.html

  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <h1>[[${session.loginUser}]]</h1>
  9. <ul>
  10. <li th:each="emp:${emps}">姓名:[[${emp}]]</li>
  11. </ul>
  12. </body>
  13. </html>

3、gulimall-test-sso-client2

  1. package com.atguigu.gulimall.ssoclient;
  2. @SpringBootApplication
  3. public class GulimallTestSsoClient2Application {
  4. public static void main(String[] args) {
  5. SpringApplication.run(GulimallTestSsoClient2Application.class, args);
  6. }
  7. }

3.1 application.properties

  1. server.port=8082
  2. sso.server.url=http://ssoserver.com:8080/login.html

3.2 HelloController

  1. package com.atguigu.gulimall.ssoclient.controller;
  2. @Controller
  3. public class HelloController {
  4. @Value("${sso.server.url}")
  5. String ssoServerUrl;
  6. // 无需登录即可访问
  7. @ResponseBody
  8. @GetMapping("/hello")
  9. public String hello(){
  10. return "hello";
  11. }
  12. /**
  13. * 可以感知登录服务器登录成功返回
  14. * ssoserver登录成功返回就会带上token
  15. * @return
  16. */
  17. @GetMapping("/boss")
  18. public String emploees(@RequestParam(value = "token",required = false) String token,
  19. Model model, HttpSession session){
  20. if(!StringUtils.isEmpty(token)){
  21. // 去sso服务器获取当前token真正对应的用户信息
  22. RestTemplate restTemplate = new RestTemplate();
  23. ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
  24. String body = forEntity.getBody();
  25. session.setAttribute("loginUser",body);
  26. }
  27. Object loginUser = session.getAttribute("loginUser");
  28. if(loginUser == null){
  29. // 没有登录,跳转到登录服务器进行登录
  30. return "redirect:" + ssoServerUrl + "?redirect_url=http://client2.com:8082/boss";
  31. }else {
  32. List<String> emps = new ArrayList<String>();
  33. emps.add("Jack");
  34. emps.add("Tom");
  35. emps.add("Ada");
  36. model.addAttribute("emps", emps);
  37. return "list";
  38. }
  39. }
  40. }

3.3 list.html

  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <h1>欢迎:[[${session.loginUser}]]</h1>
  9. <ul>
  10. <li th:each="emp:${emps}">姓名:[[${emp}]]</li>
  11. </ul>
  12. </body>
  13. </html>