## 第10章-单点登录 **学习目标:** - 能够说出主流的认证方案-JWT/Token - 搭建用户微服务模块 - 完成基于Token+Redis实现认证 - 完成网关统一鉴权 # 1、单点登录业务介绍 ![img](assets/day10/wps1.png) 早期单一服务器,用户认证。 ![img](assets/day10/wps2.jpg) 缺点: - 单点性能压力,无法扩展 分布式,SSO(single sign on)模式 ![img](assets/day10/wps3.jpg) 解决 : - 用户身份信息独立管理,更好的分布式管理。 - 可以自己扩展安全策略 - 跨域不是问题 缺点: - 认证服务器访问压力较大。 业务流程图 {用户访问业务时,必须登录的流程}{单点登录的过程} ![img](assets/day10/wps4.jpg) # 2、用户模块 ## 2.1 实现思路 1、 用接收的用户名密码核对后台数据库 2、 核对通过,用uuid生成token 3、 将用户id加载到写入redis,redis的key为token,value为用户id。 4、 登录成功返回token与用户信息,将token与用户信息记录到cookie里面 5、 重定向用户到之前的来源地址。 数据库表:user_info,并添加一条数据!**密码应该是加密的!** ## 2.2 搭建认证中心模块service-user ### 2.2.1 搭建service-user服务 在`gmall-service`模块下新增子模块:service-user。搭建方式如service-item ![image-20221206174842858](assets/day10/image-20221206174842858.png) ### 2.2.2 启动类 ```java package com.atguigu.gmall; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class UserApp { public static void main(String[] args) { SpringApplication.run(UserApp.class, args); } } ``` ### 2.2.3 配置pom.xml ```xml gmall-service com.atguigu.gmall 1.0 4.0.0 service-user service-user org.springframework.boot spring-boot-maven-plugin ``` ### 2.2.4 添加配置文件 在resources目录下新增 bootstrap.properties 文件 ```properties spring.application.name=service-user spring.profiles.active=dev spring.cloud.nacos.discovery.server-addr=192.168.200.128:8848 spring.cloud.nacos.config.server-addr=192.168.200.128:8848 spring.cloud.nacos.config.prefix=${spring.application.name} spring.cloud.nacos.config.file-extension=yaml spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml ``` ### 2.2.5 生成基础代码 在`mybatis-plus-code`中执行代码生成器代码,将`gmall_user`数据库中的 user_info,user_address 生成基础代码。 ```java package com.atguigu.mybatispluscode; import com.atguigu.gmall.base.model.BaseEntity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.*; /** * 可参考:https://blog.csdn.net/weixin_44541136/article/details/121202871 */ public class CodeGenerator { public static void main(String[] args) { // 设置代码生成的位置 // String projectPath = System.getProperty("user.dir"); String projectPath = "D:\\code\\workspace2023\\sph\\gmall-parent\\gmall-service"; // 设置父模块名称 String parentModuleName = "com.atguigu.gmall"; // 设置子模块名称 String moduleName = "user"; String subPath = "/service-" + moduleName; // 设置数据库连接 String databaseUrl = "jdbc:mysql://127.0.0.1:3306/gmall_" + moduleName + "?useUnicode=true&useSSL=false&characterEncoding=utf8"; // 数据库用户名 String username = "root"; // 数据库密码 String password = "root"; // 设置表名前缀,例如表为tb_UserInfo,这里设置表前缀为"tb_",生成实体类的时候会自动去除前缀,最终生成UserInfo String tablePrefix = ""; // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); // 最终输出目录 gc.setOutputDir(projectPath + subPath + "/src/main/java"); gc.setAuthor("atguigu"); //作者 gc.setOpen(false); //是否打开输出目录 gc.setSwagger2(true); //实体属性 Swagger2 注解 gc.setDateType(DateType.ONLY_DATE); //时间类型为 Date LocalDateTime // 设置主键类型 ASSIGN_ID为分布式全局唯一ID AUTO:数据库自增 gc.setIdType(IdType.AUTO); //gc.setIdType(IdType.ASSIGN_ID); // 是否覆盖已有文件 gc.setFileOverride(false); //去掉Service接口的首字母I gc.setServiceName("%sService"); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl(databaseUrl); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.jdbc.Driver"); dsc.setUsername(username); dsc.setPassword(password); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); // pc.setModuleName(scanner("模块名")); //设置实体类包名 pc.setEntity("model"); pc.setParent(parentModuleName); pc.setModuleName(moduleName); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { //自定义属性注入 //在.ftl(或者是.vm)模板中,通过${cfg.abc}获取属性 @Override public void initMap() { Map map = new HashMap<>(); map.put("parentName", parentModuleName); map.put("moduleName", moduleName); this.setMap(map); } }; mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setEntity("templates/entity2.java"); //指定自定义模板路径, 位置:/resources/templates/entity2.java.ftl(或者是.vm) templateConfig.setMapper("templates/mapper2.java"); templateConfig.setService("templates/service2.java"); templateConfig.setServiceImpl("templates/serviceImpl2.java"); templateConfig.setController("templates/controller2.java"); mpg.setTemplate(templateConfig); //TODO 根据需要来设置!!!例如:禁用模版的方式禁止生成实体类 templateConfig.disable(TemplateType.ENTITY); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setSuperEntityClass(BaseEntity.class); //写于父类中的公共字段 strategy.setSuperEntityColumns("id", "create_time", "update_time", "is_deleted"); strategy.setNaming(NamingStrategy.underline_to_camel); //数据库表映射到实体的命名策略 strategy.setColumnNaming(NamingStrategy.underline_to_camel); //数据库表字段映射到实体的命名策略 strategy.setEntityLombokModel(true); //【实体】是否为lombok模型(默认 false) strategy.setRestControllerStyle(true); strategy.setEntityTableFieldAnnotationEnable(true); //是否生成实体时,生成字段注解 strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix(tablePrefix); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } /** *

* 读取控制台内容 *

*/ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } } ``` ### 2.2.4 封装登录接口 > YAPI接口地址: > > - 登录 http://192.168.200.128:3000/project/11/interface/api/803 > - 退出 http://192.168.200.128:3000/project/11/interface/api/811 #### 2.2.4.1 控制器 ```java package com.atguigu.gmall.user.controller; import com.atguigu.gmall.common.result.Result; import com.atguigu.gmall.user.model.UserInfo; import io.swagger.annotations.Api; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import com.atguigu.gmall.user.service.UserInfoService; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * 用户表 前端控制器 * * @author atguigu * @since 2022-12-15 */ @Api(tags = "用户表控制器") @RestController @RequestMapping("/api/user/passport") public class PassportController { @Autowired private UserInfoService userInfoService; /** * 用户登录 * @param loginInfo * @return */ @PostMapping("/login") public Result login(@RequestBody UserInfo loginInfo, HttpServletRequest request){ return userInfoService.login(loginInfo, request); } /** * 用户退出 * @param token 请求头中token令牌 * @return */ @GetMapping("/logout") public Result logout(@RequestHeader("token") String token){ userInfoService.logout(token); return Result.ok(); } } ``` #### 2.2.4.2 业务层 业务接口:UserInfoService ```java package com.atguigu.gmall.user.service; import com.atguigu.gmall.common.result.Result; import com.atguigu.gmall.user.model.UserInfo; import com.baomidou.mybatisplus.extension.service.IService; import javax.servlet.http.HttpServletRequest; /** * 用户表 业务接口类 * @author atguigu * @since 2023-01-10 */ public interface UserInfoService extends IService { /** * 用户登录 * @param loginInfo * @param request * @return */ Result login(UserInfo loginInfo, HttpServletRequest request); /** * 用户退出系统 * @param token */ void logout(String token); } ``` 业务实现类: ```java package com.atguigu.gmall.user.service.impl; import com.alibaba.fastjson.JSONObject; import com.atguigu.gmall.common.constant.RedisConst; import com.atguigu.gmall.common.result.Result; import com.atguigu.gmall.common.util.IpUtil; import com.atguigu.gmall.user.model.UserInfo; import com.atguigu.gmall.user.mapper.UserInfoMapper; import com.atguigu.gmall.user.service.UserInfoService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * 用户表 业务实现类 * * @author atguigu * @since 2023-01-10 */ @Service public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService { //@Autowired //private UserInfoMapper userInfoMapper; @Autowired private RedisTemplate redisTemplate; /** * 用户登录 * * @param loginInfo * @param request * @return */ @Override public Result login(UserInfo loginInfo, HttpServletRequest request) { //1.验证用户信息是否合法-根据根据用户名密码查询用户记录--注意:数据库中密码为密文 //1.1 对用户填写密码进行md5加密 String userPwd = DigestUtils.md5DigestAsHex(loginInfo.getPasswd().getBytes()); //1.2 根据用户账户+用户填写加密后密码 进行查询 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserInfo::getLoginName, loginInfo.getLoginName()); queryWrapper.eq(UserInfo::getPasswd, userPwd); UserInfo userInfo = getOne(queryWrapper); if (userInfo == null) { return Result.fail().message("用户名或者密码错误!"); } //2.如果用户认证通过,将用户信息以及Token存入Redis 设置有限期7天 //2.1 生成存入redis 令牌UUID String uuid = UUID.randomUUID().toString().replaceAll("-", ""); String redisKey = RedisConst.USER_LOGIN_KEY_PREFIX + uuid; //2.2 生成存入redis中用户信息 JSONObject jsonObject = new JSONObject(); jsonObject.put("userId", userInfo.getId()); jsonObject.put("ip", IpUtil.getIpAddress(request)); redisTemplate.opsForValue().set(redisKey, jsonObject, RedisConst.USERKEY_TIMEOUT, TimeUnit.SECONDS); //3.按照接口文档要求封装响应业务数据{token:"UUID令牌", data:用户信息} HashMap data = new HashMap<>(); data.put("token", uuid); data.put("nickName", userInfo.getNickName()); return Result.ok(data); } /** * 用户退出,将登录后存储在Redis中token删除即可 * * @param token */ @Override public void logout(String token) { String redisKey = RedisConst.USER_LOGIN_KEY_PREFIX + token; redisTemplate.delete(redisKey); } } ``` #### 2.2.4.3 持久层 ```java package com.atguigu.gmall.user.mapper; import com.atguigu.gmall.model.user.UserInfo; import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface UserInfoMapper extends BaseMapper { } ``` ## 2.3 配置网关路由 在Nacos配置列表中,`server-gateway-dev.yaml`进行编辑增加动态路由 ```yaml - id: service-user uri: lb://service-user predicates: - Path=/*/user/** - id: web-passport uri: lb://web-all predicates: - Host=passport.gmall.com ``` ## 2.4 在web-all模块添加实现 ### 2.4.1 在web-all 项目中跳转页面 ```java package com.atguigu.gmall.web.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; /** *

* 用户认证接口 *

* */ @Controller public class PassportController { /** * 渲染登录页面 * @param originUrl 登录前用户访问地址,登录成功后重定向到该地址 * @return */ @GetMapping("/login.html") public String login(Model model, @RequestParam("originUrl") String originUrl){ model.addAttribute("originUrl", originUrl); return "login.html"; } } ``` ### 2.4.2 登录页面 页面资源: \templates\login1.html login1.html 没有公共头部信息 login.html 有公共头部信息 ![img](assets/day10/wps5.jpg) Html关键代码 ```html
忘记密码?
``` ## 2.5 头部信息处理 web-all项目:common/header.html,common/head.html 功能:头部信息为公共信息,所有页面都具有相关的头部,所以我们可以单独提取出来,头部页面显示登录状态与关键字搜索等信息 ### 2.5.1 提取头部信息 提取头部信息我们会用到thymeleaf 两个标签: > **th****:fragment****:定义代码块** > > **th****:include****:将代码块片段包含的内容插入到使用了th:include的HTML标签中** 1,定义头部代码块(/common/header.html),关键代码 ```html ``` 2,在其他页面引用头部代码块 ```html
``` ### 2.5.2 头部登录状态处理 思路:登录成功后我们将用户信息写入了cookie,所以我们判断cookie中是否有用户信息,如果有则显示登录用户信息和退出按钮,我们采取vue的渲染方式 关键代码 Header.html 中 ```html ``` ### 2.4.3 头部关键字搜索 ```html
``` 说明:[[${searchParam?.keyword}]],searchParam为搜索列表的搜索对象,如果存在searchParam对象,显示关键字的值 ### 2.5.4 头部公共js ```html
``` 引用 ![img](assets/day10/wps6.jpg) # 3、用户认证与服务网关整合 ## 3.1 实现思路 1. 所有请求都会经过服务网关,服务网关对外暴露服务,不管是api异步请求还是web同步请求都走网关,在网关进行统一用户认证 2. 既然要在网关进行用户认证,网关得知道对哪些url进行认证,所以我们得对url制定规则 3. Web页面同请求(如:*.html),我采取配置白名单的形式,凡是配置在白名单里面的请求都是需要用户认证的(注:也可以采取域名的形式,方式多多) 4. Api接口异步请求的,我们采取url规则匹配,如:/api/** 、/auth /**,如凡是满足该规则的都必须用户认证 所以在Nacos配置列表,修改` server-gateway-dev.yaml`增加需要校验的html访问路径 ```yaml authUrls: url: trade.html,myOrder.html #,list.html, addCart.html # 用户访问该控制器的时候,会被拦截跳转到登录! ``` ## 3.2 在服务网关添加fillter 1. 在`gmall-gateway`模块的pom.xml中增加redis的依赖 ```xml org.springframework.boot spring-boot-starter-data-redis-reactive ``` 2. 由于在`service-user`中存入Redis采用自定义序列化器,固在网关中同样需要配置 ```java package com.atguigu.gmall.gateway.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @EnableCaching public class RedisConfig { @Primary @Bean public RedisTemplate RedisTemplate(RedisConnectionFactory RedisConnectionFactory) { RedisTemplate RedisTemplate = new RedisTemplate<>(); RedisTemplate.setConnectionFactory(RedisConnectionFactory); //使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化) 对存储的对象进行JSON序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 序列化key value RedisTemplate.setKeySerializer(new StringRedisSerializer()); RedisTemplate.setValueSerializer(jackson2JsonRedisSerializer); RedisTemplate.setHashKeySerializer(new StringRedisSerializer()); RedisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); RedisTemplate.afterPropertiesSet(); return RedisTemplate; } } ``` 3. `gmall-gateway` 项目中添加一个全局过滤器 ```java package com.atguigu.gmall.gateway.filter; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.atguigu.gmall.common.result.Result; import com.atguigu.gmall.common.result.ResultCodeEnum; import com.atguigu.gmall.common.util.IpUtil; import jdk.nashorn.internal.runtime.regexp.JoniRegExp; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.CollectionUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.List; /** * @author: atguigu * @create: 2023-01-10 14:33 */ @Component public class AuthFitler implements GlobalFilter, Ordered { @Autowired private RedisTemplate redisTemplate; /** * 需要校验请求地址,要求用户是登录状态才可以访问 */ @Value("${authUrls.url}") private List authUrlList; private AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * 网关过滤器逻辑 a. 限制用户通过浏览器访问内部数据接口 b. 限制用户在未登录的情况下访问带有/auth/ 这样的路径 c. 限制用户在未登录的情况下访问需要登录的微服务 d. 将用户Id,统一存储到header中! * @param exchange 封装请求,响应对象 * @param chain 过滤器链 * @return */ @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //1.限制所有的用户做访问内部微服务间提供带"/inner"接口 直接拒绝 //1.1 获取用户请求url地址 String path = request.getURI().getPath(); //对于样式;JS;图片文件请求直接放行即可 if (antPathMatcher.match("/**/css/**", path) || antPathMatcher.match("/**/js/**", path) || antPathMatcher.match("/**/img/**", path)) { return chain.filter(exchange); } //1.2 通过路径匹配器进行判断 if (antPathMatcher.match("/**/inner/**", path)) { return outError(response, ResultCodeEnum.PERMISSION); } //2.尝试从Redis中获取用户ID String userId = getUserId(request); //3.限制用户在未登录情况下 访问 /auth 或者 "/api" 接口地址 返回拒绝 if (antPathMatcher.match("/**/auth/**", path)) { if (StringUtils.isBlank(userId)) { return outError(response, ResultCodeEnum.LOGIN_AUTH); } } //4.从yml文件读取需要登录后才能访问接口以及地址 判断用户登录状态 重定向到登录页面 //4.1 遍历需要校验登录状态访问地址列表 if (!CollectionUtils.isEmpty(authUrlList)) { for (String s : authUrlList) { if (antPathMatcher.match("/" + s + "*", path) && StringUtils.isBlank(userId)) { //说明用户未登录 访问地址要求登录 设置重定向 需要将http状态码设置为301 response.setStatusCode(HttpStatus.SEE_OTHER); //通过Response对象 响应头设置重定向登录地址 response.getHeaders().set(HttpHeaders.LOCATION, "http://www.gmall.com/login.html?originUrl=" + request.getURI().toString()); //结束 return response.setComplete(); } } } //5.将获取到用户ID设置到请求头中,将用户ID传输到目标微服务 if (StringUtils.isNotBlank(userId)) { request.mutate().header("userId", userId); } return chain.filter(exchange); } /** * 获取登录用户ID * 前端如果访问是静态html文件,令牌采用cookie中提交 * 前端如果访问是ajax请求,令牌采用请求头中提交 * * @param request * @return 正常:用户ID 令牌被窃取:-1 */ private String getUserId(ServerHttpRequest request) { //1.从请求对象中获取前端提交头信息或者Cookie获取令牌 String token = ""; token = request.getHeaders().getFirst("token"); if (StringUtils.isBlank(token)) { List cookieList = request.getCookies().get("token"); if (!CollectionUtils.isEmpty(cookieList)) { token = cookieList.get(0).getValue(); } } //2.查询Redis中令牌绑定的用户信息 用户信息存在同时 IP 地址也需要相同 String redisKey = "user:login:" + token; JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(redisKey); if (jsonObject != null) { String ip = jsonObject.getString("ip"); String userIp = IpUtil.getGatwayIpAddress(request); if (userIp.equals(ip)) { return jsonObject.getString("userId"); } else { //token被盗取风险 return "-1"; } } return ""; } /** * 响应给客户端错误结果 * * @param response 设置响应的结果对象 * @param permission 封装提示信息 * @return */ private Mono outError(ServerHttpResponse response, ResultCodeEnum permission) { //封装响应的结果对象 Result result = Result.fail().message(permission.getMessage()); String resultJSON = JSON.toJSONString(result); //通过response设置响应数据格式 response.getHeaders().add("Content-type", "application/json;charset=utf-8"); DataBuffer wrap = response.bufferFactory().wrap(resultJSON.getBytes()); return response.writeWith(Mono.just(wrap)); } /** * 过滤器执行顺序 * * @return */ @Override public int getOrder() { return 0; } //public static void main(String[] args) { // System.out.println("业务代码1执行"); // Mono mono = biz(); // System.out.println("业务代码3执行"); // // //订阅上面任务执行结果 // mono.subscribe((ret)->{ // System.out.println(ret); // }); //} // // ///** // * // * @return // */ //private static Mono biz() { // //创建Mono异步操作-该业务有计算结果-将消息发送到"队列" // Mono mono = Mono.fromCallable(() -> { // Thread.sleep(2000); // System.out.println("业务2代码执行"); // return "atguigu-业务2代码"; // }); // return mono; //} public static void main(String[] args) { //Spring提供地址通配符匹配对象 AntPathMatcher antPathMatcher = new AntPathMatcher(); boolean match = antPathMatcher.match("/list.html*", "/list.html?category3Id=61"); System.out.println(match); } } ``` ## 3.3 在服务网关中判断用户登录状态 在网关中如何获取用户信息: 1、从cookie中获取(如:web同步请求) 2、从header头信息中获取(如:异步请求) 如何判断用户信息合法: 登录时我们返回用户token,在服务网关中获取到token后,我在到redis中去查看用户id,如果用户id存在,则token合法,否则不合法,同时校验ip,防止token被盗用。 ### 3.3.1 取用户信息 ```java /** * 获取登录用户ID * 前端如果访问是静态html文件,令牌采用cookie中提交 * 前端如果访问是ajax请求,令牌采用请求头中提交 * * @param request * @return 正常:用户ID 令牌被窃取:-1 */ private String getUserId(ServerHttpRequest request) { //1.从请求对象中获取前端提交头信息或者Cookie获取令牌 String token = ""; token = request.getHeaders().getFirst("token"); if (StringUtils.isBlank(token)) { List cookieList = request.getCookies().get("token"); if (!CollectionUtils.isEmpty(cookieList)) { token = cookieList.get(0).getValue(); } } //2.查询Redis中令牌绑定的用户信息 用户信息存在同时 IP 地址也需要相同 String redisKey = "user:login:" + token; JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(redisKey); if (jsonObject != null) { String ip = jsonObject.getString("ip"); String userIp = IpUtil.getGatwayIpAddress(request); if (userIp.equals(ip)) { return jsonObject.getString("userId"); } else { //token被盗取风险 return "-1"; } } return ""; } ``` ### 3.3.2 输出信息out 方法 ```java /** * 响应给客户端错误结果 * * @param response 设置响应的结果对象 * @param permission 封装提示信息 * @return */ private Mono outError(ServerHttpResponse response, ResultCodeEnum permission) { //封装响应的结果对象 Result result = Result.fail().message(permission.getMessage()); String resultJSON = JSON.toJSONString(result); //通过response设置响应数据格式 response.getHeaders().add("Content-type", "application/json;charset=utf-8"); DataBuffer wrap = response.bufferFactory().wrap(resultJSON.getBytes()); return response.writeWith(Mono.just(wrap)); } ``` ### 3.3.3 测试 1. 通过网关访问内部接口,则不能访问! http://localhost/api/product/inner/getSkuInfo/17 ![img](assets/day10/wps7.jpg) 2. 测试登录权限 测试一: 未登录 :http://localhost/api/product/auth/hello ![img](assets/day10/wps8.jpg) 登录完成之后继续测试! 登录:http://localhost/api/product/auth/hello ![img](assets/day10/wps9.jpg) 使用localhost访问,你登录或者不登录,都会提示未登录! 测试二: 用户在未登录情况下测试: http://item.gmall.com/api/product/auth/hello ![img](assets/day10/wps10.jpg) 在上面的访问链接的时候,如果用户登录了,那么还会继续提示未登录! ![img](assets/day10/wps11.jpg) 404 表示资源没有!没有提示未登录! 原因: 测试一:访问资源的时候,没有获取到userId 测试二:访问资源的时候,获取到了userId 因为:我们登录成功的时候,将token放入了cookie中。在放入cookie的时候,我们给cookie 设置了一个作用域。 return $.cookie('token', token, {domain: 'gmall.com', expires: 7, path: '/'}) 测试一:使用的域名是localhost 测试二:使用item.gmall.com 包含gmall.com 所以测试二是正确的!以后我们访问的时候,不会通过localhost访问,都是通过域名访问的! 3. 验证Url 访问的是控制器 未登录直接访问:会弹出登录页面 http://list.gmall.com/list.html 4. 登录之后,然后在访问 会显示查询结果! http://list.gmall.com/list.html