业务流程说明如下:
1、客户端请求认证服务进行认证。
2、认证服务认证通过向浏览器cookie写入token(身份令牌)
认证服务请求用户中心查询用户信息。
认证服务请求Spring Security申请令牌。
认证服务将token(身份令牌)和jwt令牌存储至redis中。
认证服务向cookie写入 token(身份令牌)。
3、前端携带token请求认证服务获取jwt令牌
前端获取到jwt令牌并存储在sessionStorage。
前端从jwt令牌中解析中用户信息并显示在页面。
4、前端携带cookie中的token身份令牌及jwt令牌访问资源服务
前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt令牌
前端请求资源服务前在http header上添加jwt请求资源
5、网关校验token的合法性
用户请求必须携带 token身份令牌和jwt令牌
网关校验redis中token是否合法,已过期则要求用户重新登录
6、资源服务校验jwt的合法性并完成授权
资源服务校验jwt令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
BCrypt 随机加盐加密
早期使用md5对密码进行编码,每次算出的md5值都一样,这样非常不安全,Spring Security推荐使用
BCryptPasswordEncoder对密码加随机盐,每次的Hash值都不一样,安全性高。
1、BCryptPasswordEncoder案例程序如下:
@Test
public void testPasswrodEncoder(){
//原始密码
String password = "111111";
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//使用BCrypt加密,每次加密使用一个随机盐
for(int i=0;i<10;i++){
String encode = bCryptPasswordEncoder.encode(password);
System.out.println(encode);
//校验
boolean matches = bCryptPasswordEncoder.matches(password, encode);
System.out.println(matches);
}
}
spring cloud的应用方法:BCryptPasswordEncoder:
//采用bcrypt对密码进行编码
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
4 Zuul 网关 搭建实际案例工程(以后复制出来改)
4.1 需求分析
网关的作用相当于一个过虑器、拦截器,它可以拦截多个系统的请求。
本章节要使用网关校验用户的身份是否合法。
Spring Cloud Zuul是整合Netflix公司的Zuul开源项目实现的微服务网关,它实现了请求路由、负载均衡、校验过
虑等 功能。
官方:https://github.com/Netflix/zuul
Zuul与Nginx怎么配合使用?
Zuul与Nginx在实际项目中需要配合使用,如下图,Nginx的作用是反向代理、负载均衡,Zuul的作用是保障微服
务的安全访问,拦截微服务请求,校验合法性及负载均衡。
pom.xml
<!--网关的jar-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--eureka的jar-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xc-framework-model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xc-framework-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
application.yml
server:
port: 50201
servlet:
context-path: /api
spring:
application:
name: xc-govern-gateway
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
timeout: 5000 #连接超时 毫秒
jedis:
pool:
maxActive: 3
maxIdle: 3
minIdle: 1
maxWait: -1 #连接池最大等行时间 -1没有限制
zuul:
routes:
manage-course:
#表示只要是/api/course/开始的任意路径,都能通过该网关下发
path: /course/**
#该地址转发到哪个微服务名称,网关会从eureka中获取该服务名称下的服务实例的地址
serviceId: xc-service-manage-course
# 例子:将请求转发到http://localhost:31200/course
#url: http://www.baidu.com #也可指定url,此url也可以是外网地址\
strip-prefix: false #true:代理转发时去掉前缀,false:代理转发时不去掉前缀
#默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务
sensitiveHeaders: #什么都不设置,就是允许任何携带的消息通过
# ignoredHeaders: 默认为空表示不过虑任何头
xc-service-learning: #路由名称,名称任意,保持所有路由名称唯一
path: /learning/**
serviceId: xc-service-learning #指定服务id,从Eureka中找到服务的ip和端口
strip-prefix: false #是否去掉前缀?
sensitiveHeaders: #头信息,是否拦截,空,为不拦截
manage-cms:
path: /cms/**
serviceId: xc-service-manage-cms
strip-prefix: false
sensitiveHeaders:
manage-sys:
path: /sys/**
serviceId: xc-service-manage-cms
strip-prefix: false
sensitiveHeaders:
service-ucenter:
path: /ucenter/**
serviceId: xc-service-ucenter
sensitiveHeaders:
strip-prefix: false
xc-service-manage-order:
path: /order/**
serviceId: xc-service-manage-order
sensitiveHeaders:
strip-prefix: false
eureka:
client:
registerWithEureka: true #服务注册开关
fetchRegistry: true #服务发现开关
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/}
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ip-address: 127.0.0.1
instance-id: ${spring.application.name}:${server.port} #指定实例id
ribbon:
MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试,如果eureka中找不到服务则直接走断路器
MaxAutoRetriesNextServer: 3 #切换实例的重试次数
OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 6000 #请求处理的超时时间
GatewayApplication 启动类:
@SpringBootApplication
//此工程是一个zuul网关
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
AuthService
import com.xuecheng.framework.utils.CookieUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class AuthService {
@Autowired
StringRedisTemplate stringRedisTemplate;
//从头取出jwt令牌
public String getJwtFromHeader(HttpServletRequest request){
//取出头信息
String authorization = request.getHeader("Authorization");
if(StringUtils.isEmpty(authorization)){
return null;
}
if(!authorization.startsWith("Bearer ")){
return null;
}
//取到jwt令牌
String jwt = authorization.substring(7);
return jwt;
}
//从cookie取出token
//查询身份令牌
public String getTokenFromCookie(HttpServletRequest request){
Map<String, String> cookieMap = CookieUtil.readCookie(request, "uid");
String access_token = cookieMap.get("uid");
if(StringUtils.isEmpty(access_token)){
return null;
}
return access_token;
}
//查询令牌的有效期
public long getExpire(String access_token){
//key
String key = "user_token:"+access_token;
Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
return expire;
}
}
LoginFilter 过滤器的配置类:
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.govern.gateway.service.AuthService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** 身份校验过虑器,继承zuul过滤器
* @author Administrator
* @version 1.0
**/
@Component
public class LoginFilter extends ZuulFilter {
@Autowired
AuthService authService;
//过虑器的类型
@Override
public String filterType() {
/**
pre:请求在被路由之前执行
routing:在路由请求时调用
post:在routing和errror过滤器之后调用
error:处理请求时发生错误调用
*/
return "pre";
}
//过虑器序号,越小越被优先执行
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
//返回true表示要执行此过虑器,false表示不执行这个过滤器
return true;
}
//过虑器的内容
//测试的需求:过虑所有请求,判断头部信息是否有Authorization,如果没有则拒绝访问,否则转发到微服务。
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
//得到request
HttpServletRequest request = requestContext.getRequest();
//得到response
HttpServletResponse response = requestContext.getResponse();
//取cookie中的身份令牌
String tokenFromCookie = authService.getTokenFromCookie(request);
if(StringUtils.isEmpty(tokenFromCookie)){
//拒绝访问
access_denied();
return null;
}
//从header中取jwt
String jwtFromHeader = authService.getJwtFromHeader(request);
if(StringUtils.isEmpty(jwtFromHeader)){
//拒绝访问
access_denied();
return null;
}
//从redis取出jwt的过期时间
long expire = authService.getExpire(tokenFromCookie);
if(expire<0){
//拒绝访问
access_denied();
return null;
}
return null;
}
//拒绝访问
private void access_denied(){
RequestContext requestContext = RequestContext.getCurrentContext();
//得到response
HttpServletResponse response = requestContext.getResponse();
//拒绝访问的方法
requestContext.setSendZuulResponse(false);
//设置响应代码
requestContext.setResponseStatusCode(200);
//构建响应的信息
ResponseResult responseResult = new ResponseResult(CommonCode.UNAUTHENTICATED);
//转成json
String jsonString = JSON.toJSONString(responseResult);
requestContext.setResponseBody(jsonString);
//转成json,设置contentType
response.setContentType("application/json;charset=utf-8");
}
}
用户审核,的总体代码:参考Day18 有代码汇总