学习目标:
早期单一服务器,用户认证。
缺点:
分布式,SSO(single sign on)模式
解决 :
用户身份信息独立管理,更好的分布式管理。
可以自己扩展安全策略
跨域不是问题
缺点:
业务流程图 {用户访问业务时,必须登录的流程}{单点登录的过程}
1、 用接收的用户名密码核对后台数据库
2、 核对通过,用uuid生成token
3、 将用户id加载到写入redis,redis的key为token,value为用户id。
4、 登录成功返回token与用户信息,将token与用户信息记录到cookie里面
5、 重定向用户到之前的来源地址。
数据库表:user_info,并添加一条数据!密码应该是加密的!
在gmall-service
模块下新增子模块:service-user。搭建方式如service-item
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);
}
}
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>gmall-service</artifactId>
<groupId>com.atguigu.gmall</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service-user</artifactId>
<build>
<finalName>service-user</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在resources目录下新增 bootstrap.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
在mybatis-plus-code
中执行代码生成器代码,将gmall_user
数据库中的 user_info,user_address 生成基础代码。
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<String, Object> 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();
}
/**
* <p>
* 读取控制台内容
* </p>
*/
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 + "!");
}
}
YAPI接口地址:
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();
}
}
业务接口:UserInfoService
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<UserInfo> {
/**
* 用户登录
* @param loginInfo
* @param request
* @return
*/
Result login(UserInfo loginInfo, HttpServletRequest request);
/**
* 用户退出系统
* @param token
*/
void logout(String token);
}
业务实现类:
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<UserInfoMapper, UserInfo> 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<UserInfo> 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<String, Object> 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);
}
}
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<UserInfo> {
}
在Nacos配置列表中,server-gateway-dev.yaml
进行编辑增加动态路由
- id: service-user
uri: lb://service-user
predicates:
- Path=/*/user/**
- id: web-passport
uri: lb://web-all
predicates:
- Host=passport.gmall.com
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;
/**
* <p>
* 用户认证接口
* </p>
*
*/
@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";
}
}
页面资源: \templates\login1.html
login1.html 没有公共头部信息
login.html 有公共头部信息
Html关键代码
<form class="sui-form">
<div class="input-prepend"><span class="add-on loginname"></span>
<input id="inputName" type="text" v-model="user.loginName" placeholder="邮箱/用户名/手机号" class="span2 input-xfat">
</div>
<div class="input-prepend"><span class="add-on loginpwd"></span>
<input id="inputPassword" type="password" v-model="user.passwd" placeholder="请输入密码" class="span2 input-xfat">
</div>
<div class="setting">
<label class="checkbox inline">
<input name="m1" type="checkbox" value="2" checked="">
自动登录
</label>
<span class="forget">忘记密码?</span>
</div>
<div class="logined">
<a class="sui-btn btn-block btn-xlarge btn-danger" href="javascript:" @click="submitLogin()">登 录</a>
</div>
</form>
<script src="/js/api/login.js"></script>
<script th:inline="javascript">
var item = new Vue({
el: '#profile',
data: {
originUrl: [[${originUrl}]],
user: {
loginName: '',
passwd: ''
}
},
created() {
},
methods: {
submitLogin() {
login.login(this.user).then(response => {
if (response.data.code == 200) {
//把token存在cookie中、也可以放在localStorage中
auth.setToken(response.data.data.token)
auth.setUserInfo(JSON.stringify(response.data.data))
console.log("originUrl:"+this.originUrl);
if(this.originUrl == ''){
window.location.href="http://www.gmall.com/index.html"
return ;
} else {
window.location.href = decodeURIComponent(this.originUrl)
}
} else {
alert(response.data.data.message)
}
})
}
}
})
</script>
web-all项目:common/header.html,common/head.html
功能:头部信息为公共信息,所有页面都具有相关的头部,所以我们可以单独提取出来,头部页面显示登录状态与关键字搜索等信息
提取头部信息我们会用到thymeleaf 两个标签:
th:fragment:定义代码块
th:include:将代码块片段包含的内容插入到使用了th:include的HTML标签中
1,定义头部代码块(/common/header.html),关键代码
<div id="nav-bottom" th:fragment="header">
<!--顶部-->
<div class="nav-top" id="header">
...
</div>
</div>
2,在其他页面引用头部代码块
<div th:include="common/header :: header"></div>
思路:登录成功后我们将用户信息写入了cookie,所以我们判断cookie中是否有用户信息,如果有则显示登录用户信息和退出按钮,我们采取vue的渲染方式
关键代码
Header.html 中
<ul class="fl">
<li class="f-item">尚品汇欢迎您!</li>
<li v-if="userInfo.nickName == ''" class="f-item">请<span><a href="javascript:" @click="login()">登录</a></span> <span><a href="#">免费注册</a></span></li>
<li v-if="userInfo.nickName != ''" class="f-item"><span>{{userInfo.nickName}}</span> <span><a href="javascript:" @click="logout()">退出</a></span></li>
</ul>
<script type="text/javascript" src="/js/plugins/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/js/plugins/jquery.cookie.js"></script>
<script src="/js/plugins/vue.js"></script>
<script src="/js/plugins/axios.js"></script>
<script src="/js/auth.js"></script>
<script src="/js/request.js"></script>
<script src="/js/api/login.js"></script>
<script th:inline="javascript">
var item = new Vue({
el: '#header',
data: {
userInfo: {
nickName: '',
name: ''
}
},
created() {
this.showInfo()
},
methods: {
showInfo() {
// debugger
if(auth.getUserInfo()) {
this.userInfo = auth.getUserInfo()
console.log("--------"+this.userInfo.nickName)
}
},
logout() {
//debugger
login.logout().then(response => {
console.log("已退出")
auth.removeToken()
auth.removeUserInfo()
//跳转页面
window.location.href = "/"
})
}
}
})
</script>
<div class="input-append">
<input id="keyword" type="text" v-model="keyword" class="input-error input-xxlarge" />
<button class="sui-btn btn-xlarge btn-danger" @click="search()" type="button">搜索</button>
</div>
<script th:inline="javascript">
var item = new Vue({
el: '#header',
data: {
keyword: [[${searchParam?.keyword}]],
userInfo: {
nickName: '',
name: ''
}
},
created() {
this.showInfo()
},
methods: {
showInfo() {
// debugger
if(auth.getUserInfo()) {
this.userInfo = auth.getUserInfo()
console.log("--------"+this.userInfo.nickName)
}
},
search() {
if(this.keyword == null) this.keyword = ''
window.location.href = 'http://list.gmall.com/search.html?keyword=' + this.keyword
},
login() {
window.location.href = 'http://passport.gmall.com/login.html?originUrl='+window.location.href
},
logout() {
//debugger
login.logout().then(response => {
console.log("已退出")
auth.removeToken()
auth.removeUserInfo()
//跳转页面
window.location.href = "/"
})
}
}
})
</script>
说明:[[${searchParam?.keyword}]],searchParam为搜索列表的搜索对象,如果存在searchParam对象,显示关键字的值
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:fragment="head">
<script type="text/javascript" src="/js/plugins/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/js/plugins/jquery.cookie.js"></script>
<script src="/js/plugins/vue.js"></script>
<script src="/js/plugins/axios.js"></script>
<script src="/js/auth.js"></script>
<script src="/js/request.js"></script>
</div>
</body>
</html>
引用
所有请求都会经过服务网关,服务网关对外暴露服务,不管是api异步请求还是web同步请求都走网关,在网关进行统一用户认证
既然要在网关进行用户认证,网关得知道对哪些url进行认证,所以我们得对url制定规则
Web页面同请求(如:*.html),我采取配置白名单的形式,凡是配置在白名单里面的请求都是需要用户认证的(注:也可以采取域名的形式,方式多多)
Api接口异步请求的,我们采取url规则匹配,如:/api/** 、/auth /**,如凡是满足该规则的都必须用户认证
所以在Nacos配置列表,修改server-gateway-dev.yaml
增加需要校验的html访问路径
authUrls:
url: trade.html,myOrder.html #,list.html, addCart.html # 用户访问该控制器的时候,会被拦截跳转到登录!
在gmall-gateway
模块的pom.xml中增加redis的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
由于在service-user
中存入Redis采用自定义序列化器,固在网关中同样需要配置
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<Object, Object> RedisTemplate(RedisConnectionFactory RedisConnectionFactory) {
RedisTemplate<Object, Object> 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;
}
}
gmall-gateway
项目中添加一个全局过滤器
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<String> authUrlList;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 网关过滤器逻辑
a. 限制用户通过浏览器访问内部数据接口
b. 限制用户在未登录的情况下访问带有/auth/ 这样的路径
c. 限制用户在未登录的情况下访问需要登录的微服务
d. 将用户Id,统一存储到header中!
* @param exchange 封装请求,响应对象
* @param chain 过滤器链
* @return
*/
@Override
public Mono<Void> 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<HttpCookie> 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<Void> outError(ServerHttpResponse response, ResultCodeEnum permission) {
//封装响应的结果对象
Result<Object> 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<String> mono = biz();
// System.out.println("业务代码3执行");
//
// //订阅上面任务执行结果
// mono.subscribe((ret)->{
// System.out.println(ret);
// });
//}
//
//
///**
// *
// * @return
// */
//private static Mono<String> biz() {
// //创建Mono异步操作-该业务有计算结果-将消息发送到"队列"
// Mono<String> 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);
}
}
在网关中如何获取用户信息:
1、从cookie中获取(如:web同步请求)
2、从header头信息中获取(如:异步请求)
如何判断用户信息合法:
登录时我们返回用户token,在服务网关中获取到token后,我在到redis中去查看用户id,如果用户id存在,则token合法,否则不合法,同时校验ip,防止token被盗用。
/**
* 获取登录用户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<HttpCookie> 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<Void> outError(ServerHttpResponse response, ResultCodeEnum permission) {
//封装响应的结果对象
Result<Object> 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));
}
http://localhost/api/product/inner/getSkuInfo/17
测试一:
未登录 :http://localhost/api/product/auth/hello
登录完成之后继续测试!
登录:http://localhost/api/product/auth/hello
使用localhost访问,你登录或者不登录,都会提示未登录!
测试二:
用户在未登录情况下测试:
http://item.gmall.com/api/product/auth/hello
在上面的访问链接的时候,如果用户登录了,那么还会继续提示未登录!
404 表示资源没有!没有提示未登录!
原因:
测试一:访问资源的时候,没有获取到userId
测试二:访问资源的时候,获取到了userId
因为:我们登录成功的时候,将token放入了cookie中。在放入cookie的时候,我们给cookie 设置了一个作用域。
return $.cookie('token', token, {domain: 'gmall.com', expires: 7, path: '/'})
测试一:使用的域名是localhost
测试二:使用item.gmall.com 包含gmall.com
所以测试二是正确的!以后我们访问的时候,不会通过localhost访问,都是通过域名访问的!
未登录直接访问:会弹出登录页面
http://list.gmall.com/list.html
会显示查询结果!