学习更多的知识,整理不易,拒绝白嫖,记得三连哦

关注公众号:java星星 获取全套课件资料

1. 用户管理提供数据接口

1.1. 数据验证功能

根据接口文档知:

  • 请求方式:GET
  • 请求路径:check/{param}/{type}
  • 请求参数:param,type
  • 返回结果:true或false

1.1.2. UserController

/*** 校验数据是否可用* @param data* @param type* @return*/
@GetMapping("check/{data}/{type}")
public ResponseVo<Boolean> checkData(@PathVariable("data") String data, @PathVariable("type") Integer type) {Boolean b = this.userService.checkData(data, type);return ResponseVo.ok(b);
}

1.1.2. UserService

@Autowired
private UserMapper userMapper;@Override
public Boolean checkData(String data, Integer type) {QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();switch (type) {case 1:wrapper.eq("username", data);break;case 2:wrapper.eq("phone", data);break;case 3:wrapper.eq("email", data);break;default:return null;}return this.userMapper.selectCount(wrapper) == 0;
}

1.1.3. 测试

我们在数据库插入一条假数据:

然后在浏览器调用接口,测试:


1.2. 发送短信功能

参考接口文档,这里的业务逻辑是这样的:

  • 1)我们接收页面发送来的手机号码
  • 2)生成一个随机验证码
  • 3)将验证码保存在服务端
  • 4)发送短信,将验证码发送到用户手机

那么问题来了:验证码保存在哪里呢?

验证码有一定有效期,一般是5分钟,我们可以利用Redis的过期机制来保存。

具体实现略。。。。

1.3. 注册功能

基本逻辑:

  • 1)校验短信验证码
  • 2)生成盐
  • 3)对密码加密
  • 4)写入数据库
  • 5)删除Redis中的验证码

1.3.1. UserController

/*** 注册* @param userEntity* @param code* @return*/
@PostMapping("register")
public ResponseVo<Object> register(UserEntity userEntity, @RequestParam("code") String code) {this.userService.register(userEntity, code);return ResponseVo.ok(null);
}

1.3.2. UserService

public void register(UserEntity userEntity, String code) {// 校验短信验证码// String cacheCode = this.redisTemplate.opsForValue().get(KEY_PREFIX + userEntity.getPhone());// if (!StringUtils.equals(code, cacheCode)) {//     return false;// }// 生成盐String salt = StringUtils.replace(UUID.randomUUID().toString(), "-", "");userEntity.setSalt(salt);// 对密码加密userEntity.setPassword(DigestUtils.md5Hex(salt + DigestUtils.md5Hex(userEntity.getPassword())));// 设置创建时间等userEntity.setCreateTime(new Date());userEntity.setLevelId(1l);userEntity.setStatus(1);userEntity.setIntegration(0);userEntity.setGrowth(0);userEntity.setNickname(userEntity.getUserName());// 添加到数据库boolean b = this.save(userEntity);// if(b){// 注册成功,删除redis中的记录// this.redisTemplate.delete(KEY_PREFIX + memberEntity.getPhone());// }
}

1.3.3. 测试

我们通过PostMan测试:

查看数据库:

查看redis中的信息也被删除

1.4. 查询用户

请求方式:GET

请求路径:/ums/user/query

请求参数:username/phone/email password

响应数据:用户的json格式

1.4.1. controller

    @GetMapping("query")public ResponseVo<UserEntity> queryUser(@RequestParam("loginName")String loginName,@RequestParam("password")String password){UserEntity userEntity = this.userService.queryUser(loginName, password);return ResponseVo.ok(userEntity);}

1.4.2. service

@Override
public UserEntity queryUser(String loginName, String password) {// 1.根据登录名查询用户信息(拿到盐)UserEntity userEntity = this.getOne(new QueryWrapper<UserEntity>().eq("username", loginName).or().eq("phone", loginName).or().eq("email", loginName));// 2.判断用户是否为空if (userEntity == null){throw new UserException("账户输入不合法!");}// 3.对密码加盐加密,并和数据库中的密码进行比较password = DigestUtils.md5Hex(password + userEntity.getSalt());if (!StringUtils.equals(userEntity.getPassword(), password)){throw new UserException("密码输入错误!");}// 4.返回用户信息return userEntity;
}

要注意,查询时也要对密码进行加密后判断是否一致。

1.4.3. 测试

1.5. 搭建接口工程

创建gmall-ums-interface工程:

pom.xml中的依赖,参照其他interface工程。并在gmall-ums和gmall-auth工程中引入该接口工程

GmallUmsApi:

public interface GmallUmsApi {/*** 根据登录名和密码查询用户* @param username* @param password* @return*/@GetMapping("ums/user/query")public ResponseVo<UserEntity> queryUser(@RequestParam("loginName") String loginName,@RequestParam("password") String password);
}

2. 单点登录(SSO)

SSO英文全称Single Sign On,单点登录。

SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

2.1. cookie问题

电商平台通常由多个微服务组成,每个微服务都有独立的域名,而cookie是有作用域的。

查看浏览器控制台:

domain:作用域名

domain参数 atguigu.com sso.atguigu.com order.atguigu.com
atguigu.com
sso.atguigu.com × ×
order.atguigu.com × ×

domain有两点要注意:

​ 1. domain参数可以设置父域名以及自身,但不能设置其它域名,包括子域名,否则cookie不起作用。

​ 2. cookie的作用域是domain本身以及domain下的所有子域名。

Cookie的路径(Path):

​ response.addCookie默认放在当前路径下,访问当前路径下的所有请求都会带

​ 设置/标识项目根路径,访问项目任何位置都会携带

2.2. 演示案例

把课前资料中的sso演示工程 《sso-example》导入idea,并且启动。

在hosts文件中配置域名的映射:

追加配置如下:

127.0.0.1 client.atguigu.com
127.0.0.1 sso.atguigu.com

2.2.1. 测试一:不能访问兄弟域名cookie

访问:http://client.atguigu.com:8080/hello

由于没有登录会重定向到登录页面:

输入用户名密码(任意)点击登录,又回到了上述页面。

查看浏览器cookie,发现:

sso.atguigu.com下已经有token信息。那么为什么又回到了登录页面呢?

这是由于点击登录时,cookie放入了sso.atguigu.com这个作用域,client域下没有cookie导致,再次访问client时,client认为没有登录,又重定向到登录页面

2.2.2. 测试二:可以访问父域名的cookie

修改sso-service工程LoginController类的login方法,把cookie的作用域设置为atguigu.com

重启sso-service。

访问:http://client.atguigu.com:8080/hello

依然重定向到登录页面:


输入任意内容,点击登录:

可以登录成功!!

2.2.3. 测试三:cookie的作用路径

修改sso-service工程LoginController类的login方法,把cookie的作用路径设置为/hello


重启sso-service服务,并清理掉cookie信息。

在浏览器中访问:http://client.atguigu.com:8080/hello

依然重定向到登录页面,输入任意内容,点击登录:

可以登录成功,但是cookie的作用路径是/hello。

此时访问:http://client.atguigu.com:8080/hello1

又会跳转到登录页面。原因:cookie只能在/hello路径及其子路径下可以正常访问。

2.3. 有状态登录

为了保证客户端cookie的安全性,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。

例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。

缺点是什么?

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,无法进行水平扩展
  • 客户端请求依赖服务端,多次请求必须访问同一台服务器

即使使用redis保存用户的信息,也会损耗服务器资源。

2.4. 无状态登录

微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:

  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

带来的好处是什么呢?

  • 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩
  • 减小服务端存储压力

2.5. 无状态登录流程

无状态登录的流程:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务的对token进行解密,判断是否有效。

流程图:

整个登录过程中,最关键的点是什么?

token的安全性

token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。

采用何种方式加密才是安全可靠的呢?

我们将采用JWT + RSA非对称加密

3. jwt实现无状态登录

JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io

GitHub上jwt的java客户端:https://github.com/jwtk/jjwt

3.1. 数据格式

JWT包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    • token类型:JWT
    • 加密方式:base64(HS256)
  • Payload:载荷,就是有效数据,一般包含下面信息:

    • 用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
    • 注册声明:如token的签发时间,过期时间,签发人等

    这部分也会采用base64编码,得到第二部分数据

  • Signature:签名,是整个数据的认证信息。根据前两步的数据,再加上指定的密钥(secret)(不要泄漏,最好周期性更换),通过base64编码生成。用于验证整个数据完整和可靠性

3.2. JWT交互流程

流程图:

步骤翻译:

  • 1、用户登录
  • 2、服务的认证,通过后根据secret生成token
  • 3、将生成的token返回给浏览器
  • 4、用户每次请求携带token
  • 5、服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
  • 6、处理请求,返回响应结果

因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。

3.3. 非对称加密

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:

  • 对称加密,如AES

    • 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
    • 优势:算法公开、计算量小、加密速度快、加密效率高
    • 缺陷:双方都使用同样密钥,安全性得不到保证
  • 非对称加密,如RSA
    • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端

      • 私钥加密,持有公钥才可以解密
      • 公钥加密,持有私钥才可解密
    • 优点:安全,难以破解
    • 缺点:算法比较耗时
  • 不可逆加密,如MD5,SHA
    • 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

RSA算法历史:

1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA

4. 搭建授权中心

用户鉴权:

  • 接收用户的登录请求,通过用户中心的接口进行校验,通过后生成JWT
  • 使用私钥生成JWT并返回

有一些生成jwt,解析jwt这样行为的工具类,以后在其它微服务中也会用到,因此放在gmall-core中。

4.1. 创建工程

pom.xml中添加gmall-common及gmall-ums-interface的依赖

启动类:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class GmallAuthApplication {public static void main(String[] args) {SpringApplication.run(GmallAuthApplication.class, args);}}

bootstrap.yml:

spring:application:name: auth-servicecloud:nacos:config:server-addr: 127.0.0.1:8848

application.yml:

server:port: 18089
spring:cloud:nacos:discovery:server-addr: localhost:8848sentinel:transport:dashboard: localhost:8080port: 8179zipkin:base-url: http://localhost:9411/sender:type: webdiscovery-client-enabled: falsesleuth:sampler:probability: 1thymeleaf:cache: false
feign:sentinel:enabled: true

网关工程gmall-gateway添加用户授权的网关路由:

在nginx中添加:sso.gmall.com

在hosts文件中添加sso.gmall.com的域名映射。

注意:不要忘记重启网关,重新加载nginx配置。

4.2. JWT工具类

gmall-common工程中已经封装了jwt相关的工具类:

并在gmall-common中的pom.xml中引入了jwt相关的依赖:

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.6</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.6</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.6</version><scope>runtime</scope>
</dependency>
<dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId><version>2.10.3</version>
</dependency>

4.3. 测试工具类

public class JwtTest {// 别忘了创建D:\\project\rsa目录private static final String pubKeyPath = "D:\\project\\rsa\\rsa.pub";private static final String priKeyPath = "D:\\project\\rsa\\rsa.pri";private PublicKey publicKey;private PrivateKey privateKey;@Testpublic void testRsa() throws Exception {RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");}//    @BeforeEachpublic void testGetRsa() throws Exception {this.publicKey = RsaUtils.getPublicKey(pubKeyPath);this.privateKey = RsaUtils.getPrivateKey(priKeyPath);}@Testpublic void testGenerateToken() throws Exception {Map<String, Object> map = new HashMap<>();map.put("id", "11");map.put("username", "liuyan");// 生成tokenString token = JwtUtils.generateToken(map, privateKey, 5);System.out.println("token = " + token);}@Testpublic void testParseToken() throws Exception {String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjExIiwidXNlcm5hbWUiOiJsaXV5YW4iLCJleHAiOjE1ODYwOTg0MDd9.Gjt968x1OhFVUSDvnKK_TdNgau6wFCLXF98Teosidf__FewtOW3ytA5I1H9jU3DVzrhDfZl0fFfxNJrPPb75_WNKj06f6lB2yRy8fbazzVDrtzsBcPqEa1HeVoNA3NmUVQNlPC3ckYhZ-yu9BT3km3lY0eGum_jPivBHLsXLMbFnSnpXIjYi3kguJfXXRZYKuanGttCV6t7uCWd10GWhEBbXhIi81houaALr2cDWtqHUBC6FbJ0oVdxAaZixwnZJm_vSUjmYjM062H3CJwX44WCxLZXhSRCWhWo3HGpSU2LuUyfd_IJw8MDdI5w31P3dRczAjMjMykAhGBlOCGwy7Q";// 解析tokenMap<String, Object> map = JwtUtils.getInfoFromToken(token, publicKey);System.out.println("id: " + map.get("id"));System.out.println("userName: " + map.get("username"));}
}

测试生成公钥和私钥,我们运行testRsa方法:注意需要把@Before方法注释掉

运行之后,查看目标目录:

测试testGenerateToken生成token:注意把@BeforeEach的注释去掉的

测试解析token:

正常情况:

任意改动一下:

4.4. 配置公钥和私钥

我们需要在授权中心生成真正的公钥和私钥。可以把相关配置内容配置到gmall-auth工程的application.yml中或者配置中心:

auth:jwt:pubKeyPath: D:\\project-1010\\rsa\\rsa.pubpriKeyPath: D:\\project-1010\\rsa\\rsa.prisecret: 30489ouerweljrLROE@#)(@$*343jlsdfcookieName: GMALL-TOKENexpire: 180unick: unick

然后编写属性类读取jwt配置,并从秘钥配置文件中读取出响应的公钥及私钥,加载这些数据:

内容如下:

@Data
@Slf4j
@ConfigurationProperties(prefix = "auth.jwt")
public class JwtProperties {private String pubKeyPath;private String priKeyPath;private String secret;private String cookieName;private Integer expire;private String unick;private PublicKey publicKey;private PrivateKey privateKey;/*** 该方法在构造方法执行之后执行*/@PostConstructpublic void init(){try {File pubFile = new File(pubKeyPath);File priFile = new File(priKeyPath);// 如果公钥或者私钥不存在,重新生成公钥和私钥if (!pubFile.exists() || !priFile.exists()) {RsaUtil.generateKey(pubKeyPath, priKeyPath, secret);}this.publicKey = RsaUtil.getPublicKey(pubKeyPath);this.privateKey = RsaUtil.getPrivateKey(priKeyPath);} catch (Exception e) {log.error("生成公钥和私钥出错");e.printStackTrace();}}
}

5. 完成登录功能

5.1. 跳转到登录页

参照京东,当点击登录跳转到登录页面时,如下:

会记录跳转到登录页面前的页面地址,登录成功后要回到原来的页面。

把课前资料动态页面中的common目录及login.html拷贝到templates目录下

添加AuthController,并添加页面跳转方法如下:

@Controller
public class AuthController {@GetMapping("toLogin.html")public String toLogin(@RequestParam("returnUrl")String returnUrl, Model model){// 把登录前的页面地址,记录到登录页面,以备将来登录成功,回到登录前的页面model.addAttribute("returnUrl", returnUrl);return "login";}
}

在login.html页面会记录returnUrl地址,将来登录成功后重定向到该地址:

在浏览器输入:http://sso.gmall.com/toLogin.html?returnUrl=http://www.gmall.com

效果如下:

5.2. 完成登录功能

接下来,我们需要在gmall-auth编写登录代码。基本流程如下:

  • 客户端携带用户名和密码请求登录 ,并携带登录前页面的路径
  • 授权中心调用用户中心接口,根据用户名和密码查询用户信息
  • 用户名密码不正确,不能获取用户,登录失败
  • 如果校验成功,则生成JWT,jwt要防止别人盗取
  • 把jwt放入cookie
  • 为了方便页面展示登录用户昵称,向cookie中单独写入昵称(例如京东cookie中的的unick
  • 重定向 回到登录前的页面

实现后的项目结构如下:

5.2.1. AuthController

编写授权接口,我们接收登录名和密码及登陆前的页面地址,登录成功后重定向到登陆前页面。

  • 请求方式:post
  • 请求路径:/login
  • 请求参数:loginName和password
  • 返回结果:无

代码:

@Controller
public class AuthController {@Autowiredprivate AuthService authService;@Autowiredprivate JwtProperties jwtProperties;@GetMapping("toLogin.html")public String toLogin(@RequestParam("returnUrl")String returnUrl, Model model){// 把登录前的页面地址,记录到登录页面,以备将来登录成功,回到登录前的页面model.addAttribute("returnUrl", returnUrl);return "login";}@PostMapping("login")public String login(@RequestParam("loginName")String loginName,@RequestParam("password")String password,@RequestParam("returnUrl")String returnUrl,HttpServletRequest request, HttpServletResponse response){this.authService.accredit(loginName, password, request, response);// 登录成功重定向到登录前页面return "redirect:" + returnUrl;}
}

5.2.2. AuthService

在gmall-auth:

@Service
@EnableConfigurationProperties({JwtProperties.class})
public class AuthService {@Autowiredprivate GmallUmsClient umsClient;@Autowiredprivate JwtProperties jwtProperties;public void accredit(String loginName, String password, HttpServletRequest request, HttpServletResponse response) {try {// 1. 完成远程请求,获取用户信息ResponseVo<UserEntity> userEntityResponseVo = this.umsClient.queryUser(loginName, password);UserEntity userEntity = userEntityResponseVo.getData();// 2. 判断用户信息是否为空if (userEntity == null) {throw new UserException("用户名或者密码有误!");}// 3. 把用户id及用户名放入载荷Map<String, Object> map = new HashMap<>();map.put("userId", userEntity.getId());map.put("username", userEntity.getUsername());// 4. 为了防止jwt被别人盗取,载荷中加入用户ip地址String ipAddress = IpUtil.getIpAddress(request);map.put("ip", ipAddress);// 5. 制作jwt类型的token信息String token = JwtUtil.generateToken(map, this.jwtProperties.getPrivateKey(), this.jwtProperties.getExpire());// 6. 把jwt放入cookie中CookieUtil.setCookie(request, response, this.jwtProperties.getCookieName(), token, this.jwtProperties.getExpire() * 60);// 7.用户昵称放入cookie中,方便页面展示昵称CookieUtil.setCookie(request, response, this.jwtProperties.getUnick(), userEntity.getNickname(), this.jwtProperties.getExpire() * 60);} catch (Exception e) {e.printStackTrace();throw new UserException("用户名或者密码出错!");}}
}

5.2.3. GmallUmsClient

接下来我们肯定要对用户密码进行校验,所以我们需要通过FeignClient去访问 ums-service微服务:

在gmall-auth中引入gmall-ums-interface依赖:

<dependency><groupId>com.atguigu</groupId><artifactId>gmall-ums-interface</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>

编写GmallUmsClient:

@FeignClient("ums-service")
public interface GmallUmsClient extends GmallUmsApi {}

5.2.4. 测试

此时,在登录页面输入正确的用户名和密码信息,可以登录成功并跳转到首页:

但是查看cookie信息发现cookie为空。

再查看Headers中的Set-Cookie信息中的Domain发现是ip地址,不是地址栏的域名。

由于浏览器地址栏地址:http://www.gmall.com,而设置cookie的domain域是ip地址,相当于兄弟域名。

兄弟之间不能操作cookie。导致cookie没有写入成功!

为什么是Set-Cookie的Domain是IP地址?

5.3. 解决cookie写入问题

解决cookie写入问题,要注意两点:

  1. cookie中的domain域必须和地址栏(或者是父域名)一致。
  2. cors跨域满足携带cookie的生效条件
    • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。(网关中已设置)
    • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名。(网关中已设置具体域名)
    • 浏览器发起ajax需要指定withCredentials 为true。(前端工程:gmall-admin\src\utils\httpRequest.js文件已经设置)

5.3.1. 跟踪CookieUtils

在gmall-auth中AuthController的authentication方法打一个断点:

F7进入setCookie方法:

发现它调用了重载的setCookie方法,再次F7:

142行可以发现获取domian域并设置domain域。F7进入getDomainName方法,查看最终获取的domain是什么


160行获取的serverName是ip地址。也就是说这时候只能获取ip地址了,获取不到域名信息。

5.3.2. domain地址变化原因

那么问题来了:为什么我们这里的请求serverName变成了pi地址了呢?

这是因为在地址栏输入域名时,经过了两次转发:

  • 我们使用了nginx反向代理,当监听到sso.gmall.com的时候,会自动将请求转发至代理ip地址,即gateway服务器地址。
  • 而后请求到达我们的gateway网关,gateway网关就会根据路径匹配,我们的请求是/api/auth,根据规则被转发到了auth服务地址 ,即我们的授权中心。

每次转发都会丢失域名信息。

5.3.3. nginx转发时要携带域名

首先nginx转发请求给网关时,要携带域名信息。需要在nginx配置文件中配置代理头信息:

proxy_set_header Host $host;

修改完成之后,重新加载nginx配置:nginx -s reload

这样就解决了nginx转发时的域名问题。

5.3.4. 网关转发时要携带域名

在网关转发请求给服务时,要携带地址信息:

spring.cloud.gateway.x-forwarded.host-enabled=true

修改之后再次重启,测试

发现serverName依然时ip地址。

这是因为网关转发后,会把域名通过X-Forwarded-Host头信息转发给服务。所以,需要修改160行的代码,如下:

String serverName = request.getHeader("X-Forwarded-Host");

5.3.5. 再次登录测试

完美!!!!!

5.4. 公共页头显示用户名

js实现:

<script th:inline="javascript">var item = new Vue({el: '#header',data: {keyword: [[${searchParam?.keyword}]],nickName: ''},created() {this.showInfo()},methods: {showInfo() {// debuggerif(auth.getUserInfo()) {this.nickName = auth.getUserInfo();}},search() {if(this.keyword == null) this.keyword = ''window.location.href = 'http://search.gmall.com/search?keyword=' + this.keyword},login() {window.location.href = 'http://sso.gmall.com/toLogin.html?returnUrl='+window.location.href},logout() {//debuggerauth.removeToken()auth.removeUserInfo()//跳转页面window.location.href = "/"}}})
</script>

6. 网关过滤器验证登录状态

gateway网关过滤器包含两种:全局过滤器局部过滤器

6.1. 自定义全局过滤器

自定义全局过滤器非常简单:实现GlobalFilter接口即可,无差别拦截所有微服务的请求

内容如下:

@Component
public class TestGatewayFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {System.out.println("无需配置,拦截所有经过网关的请求!!");// 放行return chain.filter(exchange);}/*** 通过实现Orderer接口的getOrder方法控制全局过滤器的执行顺序* @return*/@Overridepublic int getOrder() {return 0;}
}

6.2. 自定义局部过滤器

自定义局部过滤器稍微麻烦一点:

  1. 需要编写过滤器工厂类继承AbstractGatewayFilterFactory抽象类
  2. 在需要过滤的微服务路由中配置该过滤器

可以做到定点拦截。

6.2.1. 过滤器工厂AuthGatewayFilterFactory

@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {@Overridepublic GatewayFilter apply(Object config) {// 实现GatewaFilter接口return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {System.out.println("自定义过滤器!");// 放行return chain.filter(exchange);}};}
}

6.2.2. 在配置文件中使用

现在拿gmall-auth工程尝试使用吧

过滤器名称就是Auth,即自定义过滤器工厂类名称 去掉 GatewayFilterFactory

6.2.3. 读取过滤器配置内容

此时,虽然可以使用这个拦截器了,但是我们的拦截器还是光秃秃的,不能指定内容。

如果像下面一样指定拦截路径,并在过滤器中获取拦截路径,再去判断当前路径是否需要拦截

假设如下:

改造AuthGatewayFilterFactory过滤器工厂类如下:

@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.PathConfig> {/*** 一定要重写构造方法* 告诉父类,这里使用PathConfig对象接收配置内容*/public AuthGatewayFilterFactory() {super(PathConfig.class);}@Overridepublic GatewayFilter apply(PathConfig config) {// 实现GatewaFilter接口return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {System.out.println("自定义过滤器!" + config);// 放行return chain.filter(exchange);}};}/*** 指定字段顺序* 可以通过不同的字段分别读取:/toLogin.html,/login* 在这里希望通过一个集合字段读取所有的路径* @return*/@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("authPaths");}/*** 指定读取字段的结果集类型* 默认通过map的方式,把配置读取到不同字段*  例如:/toLogin.html,/login*      由于只指定了一个字段,只能接收/toLogin.html* @return*/@Overridepublic ShortcutType shortcutType() {return ShortcutType.GATHER_LIST;}/*** 读取配置的内部类*/@Datapublic static class PathConfig{private List<String> authPaths;}
}

测试效果如下:已经可以拿到配置内容

6.3. 通过自定义局部过滤器完成登录验证

接下来,我们在gmall-gateway编写过滤器,对用户的token进行校验,如果发现未登录,则进行拦截。

思路:

  1. 判断请求路径在不在拦截名单中,不在直接放行
  2. 获取请求中的token。同步请求从cookie中获取,异步请求从header中获取(走cookie太重,一个网站往往有很多cookie,如果通过携带cookie的方式传递token,网络传输压力太大)
  3. 判断token是否为空。为空直接拦截
  4. 如果不为空,解析jwt获取登录信息
  5. 判断是否被盗用。通过登录信息中的ip和当前请求的ip比较
  6. 传递登录信息给后续服务。后续各服务就不用再去解析了
  7. 放行

6.3.1. 引入jwt相关配置

既然是登录拦截,一定需要公钥解析jwt,我们在gmall-gateway中配置。

首先在pom.xml中,引入所需要的依赖:

<dependency><groupId>com.atguigu</groupId><artifactId>gmall-common</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>

然后编写application.yml属性文件,添加如下内容:

auth:jwt:pubKeyPath: D:\\project\\rsa\\rsa.pub # 公钥地址cookieName: GMALL-TOKEN

编写属性类,读取公钥:

jwtProperties内容如下:

@Data
@Slf4j
@ConfigurationProperties(prefix = "auth.jwt")
public class JwtProperties {private String pubKeyPath;// 公钥private PublicKey publicKey; // 公钥private String cookieName;@PostConstructpublic void init(){try {// 获取公钥和私钥this.publicKey = RsaUtils.getPublicKey(pubKeyPath);} catch (Exception e) {log.error("初始化公钥失败!", e);throw new RuntimeException();}}}

6.3.2. 编写代码实现登录拦截

改造AuthGatewayFilterFactory

@Component
@EnableConfigurationProperties({JwtProperties.class})
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.PathConfig> {@Autowiredprivate JwtProperties properties;/*** 一定要重写构造方法* 告诉父类,这里使用PathConfig对象接收配置内容*/public AuthGatewayFilterFactory() {super(PathConfig.class);}@Overridepublic GatewayFilter apply(PathConfig config) {// 实现GatewaFilter接口return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取request和response,注意:不是HttpServletRequest及HttpServletResponseServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();// 获取当前请求的path路径String path = request.getURI().getPath();// 1.判断请求路径在不在拦截名单中,不在直接放行Boolean flag = false;for (String authPath : config.getAuthPaths()) {// 如果白名单中有一个包含当前路径if (path.indexOf(authPath) != -1){flag = true;break;}}// 不在拦截名单中,放行if (!flag){return chain.filter(exchange);}// 2.获取请求中的tokenString token = "";// 异步请求,通过头信息获取tokenList<String> tokenList = request.getHeaders().get("token");if(!CollectionUtils.isEmpty(tokenList)) {token = tokenList.get(0);} else {// 同步请求通过cookieMultiValueMap<String, HttpCookie> cookies = request.getCookies();if (CollectionUtils.isEmpty(cookies) || !cookies.containsKey(properties.getCookieName())) {// 拦截// 重定向到登录// 303状态码表示由于请求对应的资源存在着另一个URI,应使用重定向获取请求的资源response.setStatusCode(HttpStatus.SEE_OTHER);response.getHeaders().set(HttpHeaders.LOCATION, "http://sso.gmall.com/toLogin.html?returnUrl="+request.getURI());// 设置响应状态码为未认证
//                        response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}// 获取cookie中的jwtHttpCookie cookie = cookies.getFirst(properties.getCookieName());token = cookie.getValue();}// 3.判断token是否为空if (StringUtils.isEmpty(token)) {// 去登录response.setStatusCode(HttpStatus.SEE_OTHER);response.getHeaders().set(HttpHeaders.LOCATION, "http://sso.gmall.com/toLogin.html?returnUrl="+request.getURI());return response.setComplete();}try {// 4.解析jwt,获取登录信息Map<String, Object> map = JwtUtil.getInfoFromToken(token, properties.getPublicKey());// 5.判断token是否被盗用String ip = map.get("ip").toString();// 当前请求的ipString curIp = IpUtil.getIpAddressAtGateway(request);if (!StringUtils.equals(ip, curIp)){// 去登陆response.setStatusCode(HttpStatus.SEE_OTHER);response.getHeaders().set(HttpHeaders.LOCATION, "http://sso.gmall.com/toLogin.html?returnUrl="+request.getURI());return response.setComplete();}// 6.传递登录信息给后续服务String userId = map.get("userId").toString();// 将userId转变成request对象。mutate:转变的意思request.mutate().header("userId", userId).build();exchange.mutate().request(request).build();// 放行return chain.filter(exchange);} catch (Exception e) {e.printStackTrace();// 7.异常,去登录response.setStatusCode(HttpStatus.SEE_OTHER);response.getHeaders().set(HttpHeaders.LOCATION, "http://sso.gmall.com/toLogin.html?returnUrl="+request.getURI());return response.setComplete();}}};}/*** 指定字段顺序* 可以通过不同的字段分别读取:/toLogin.html,/login* 在这里希望通过一个集合字段读取所有的路径* @return*/@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("authPaths");}/*** 指定读取字段的结果集类型* 默认通过map的方式,把配置读取到不同字段*  例如:/toLogin.html,/login*      由于只指定了一个字段,只能接收/toLogin.html* @return*/@Overridepublic ShortcutType shortcutType() {return ShortcutType.GATHER_LIST;}/*** 读取配置的内部类*/@Datapublic static class PathConfig{private List<String> authPaths;}
}

6.3.3. 配置过滤器并测试

在网关配置文件中的配置如下:

在浏览器上访问:http://sso.gmall.com/xxx

重定向到了登录页面

输入用户名及密码登录后

出现404,说明登录情况下网关放行了,由于没有该路径对应的接口,所有出现了404

6.4. 常见异常解决

如果网关报如下错误:

原因:springCloud-gateway内部集成的是webflux而不是servlet,所以需要排除servlet相关的依赖。

tomcat是servlet容器

尚硅谷2020微服务分布式电商项目《谷粒商城》-单点登录(jwt)相关推荐

  1. 尚硅谷2020微服务分布式电商项目《谷粒商城》学习笔记

    尚硅谷2020微服务分布式电商项目<谷粒商城> 项目简介 资料 百度云 链接:https://pan.baidu.com/s/1eGCTi6pLtKbDCwBs-zCOzQ 提取码:1pm ...

  2. 尚硅谷2020微服务分布式电商项目《谷粒商城》-商品搜索

    关注公众号:java星星 获取全套课件资料 1. 导入商品数据 1.1. 搭建搜索工程 pom.xml内容如下: <?xml version="1.0" encoding=& ...

  3. 尚硅谷2020微服务分布式电商项目《谷粒商城》-支付、秒杀

    学习更多的知识,整理不易,拒绝白嫖,记得三连哦 关注公众号:java星星 获取全套课件资料 1. 支付 订单搞定之后就是支付了,首先搭建支付工程. 1.1. 搭建环境 pom.xml <?xml ...

  4. 谷粒商城 - 微服务分布式电商项目

    谷粒商城 1.项目背景 谷粒商城项目是尚硅谷研究院最新推出的完整大型分布式架构电商平台,技术全面.业务深入,全网无出其右.技术涵盖:微服务架构 + 分布式 + 全栈 + 集群 + 部署 + 自动化运维 ...

  5. 微服务分布式电商项目《谷粒商城》学习笔记

    文章目录 一.基本架构图 二.配置 三.项目搭建 四.数据库 1.开启虚拟机,在windows下通过navicat连接上 2.人人开源:https://gitee.com/renrenio 五.微服务 ...

  6. 分布式电商项目 谷粒商城 学习笔记<2>

    文章目录 六.三级分类 1.按照父子类的结构获取所有分类 2.跨域问题的解决 1.使用nginx部署为同一域 2.让服务器告诉预检请求能跨域 3.过滤器优先级问题 4.删除 5.增加修改拖拽 七.品牌 ...

  7. 基于微服务的电商系统架构

    分层 微服务设计 微服务微内核 基于微服务的电商系统架构 转载于:https://www.cnblogs.com/davidwang456/articles/9221369.html

  8. JavaEE大型分布式电商项目 上海淘淘商城 29期

    上海29期_张志君老师_淘淘商城_大型分布式电商项目 JavaEE大型分布式电商项目 淘淘商城 29期 需要的加qq:350226234,备注:程序员学习视频 ==================== ...

  9. 分布式电商项目五:使用人人开源搭建前后分离的后台管理系统

    分布式电商项目五:使用人人开源搭建前后分离的后台管理系统 现在我们开始搭建一个后台管理系统,使用的是码云上面的开源项目:人人开源 需要使用两个开源项目:fast和fast-vue. 使用git把需要的 ...

最新文章

  1. 赠书福利 | Tidio AI 趋势报告:约42%受访者能够接受机器人伴侣
  2. JPA使用原生SQL查询
  3. Citrix XenApp Hotfix Rollup Pack部署最佳实践
  4. rtsp协议_如何在RTSP协议视频智能平台EasyNVR未登录的情况下调用通道直播的接口?...
  5. 写5个不同的自己的函数,来截取一个全路径的文件的扩展名,允许封装php库中已有的函数。
  6. Code Blocks 10 05的安装及使用
  7. 使用StarUML生成live555类图
  8. sql语句多条件查询语句拼接
  9. 0x0000011b解决办法
  10. dropbox无法访问后国内网盘对比选择
  11. Python案例1—人民币与美元的汇率兑换V_8.0
  12. 现代循环神经网络 - 机器翻译与数据集
  13. 《IT项目经理进阶之道》简介
  14. 最惨大学生,大学四年,啥也不会
  15. linux 无法解析配置文件,Linux无法解析域名的解决办法
  16. python习题计算a+aa+aaa+aaaa的结果 lintcode题目
  17. HTML+CSS+JavaScript实现登陆注册进入动态相册
  18. win系列服务器,windows服务器系列系统
  19. esp8266~lwip突破MTU最大1500限制
  20. 因为电脑分区合并引发的磁盘动态无效问题

热门文章

  1. 十个健脑绝招 锻炼大脑 提高记忆(转)
  2. 华为软件开发实习面经
  3. 3GPP协议对应的协议文档
  4. mongodb去除重复的值
  5. 怎样理解WaaS?看zCloud如何走上自治智能数据库云管平台的道路
  6. 怎么编辑本地的html文件,本地的HTML文件怎么打开
  7. java计算机毕业设计公司薪酬管理系统(附源码、数据库)
  8. OPC UA客户端工具UaExpert使用
  9. LED火焰灯方案-LED火焰灯方案,太阳能火把灯IC
  10. Ubuntu历史与发展过程