好好学习,天天向上
本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航
- 畅购商城(一):环境搭建
- 畅购商城(二):分布式文件系统FastDFS
- 畅购商城(三):商品管理
- 畅购商城(四):Lua、OpenResty、Canal实现广告缓存与同步
- 畅购商城(五):Elasticsearch实现商品搜索
- 畅购商城(六):商品搜索
- 畅购商城(七):Thymeleaf实现静态页
- 畅购商城(八):微服务网关和JWT令牌
- 畅购商城(九):Spring Security Oauth2
- 畅购商城(十):购物车
OAuth2.0对接用户微服务
上一篇文章中提到过,访问资源服务的时候,需要携带令牌去进行权限校验。那么用户微服务也是资源服务,所以需要对其进行配置,首先添加OAuth2.0的依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
然后把之前导出的public.key放到用户微服务的resources目录下。最后添加一个配置类即可,配置类上需要添加@EnableResourceServer注解,然后继承自ResourceServerConfigurerAdapter。
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String PUBLIC_KEY = "public.key";//公钥
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPublicKey());
return converter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
private String getPublicKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers("/user/add,user/load/*").permitAll() //配置 /user/add /user/load/*不需要权限
.anyRequest().authenticated(); //其他地址需要认证授权
}
}
这样配置类就配置好了,在添加了@EnableGlobalMethodSecurity注解后,就可以在指定的方法上面添加注解来进行权限控制。
比如:
@PreAuthorize("hasAnyAuthority('admin')") //表示该方法只有admin才能访问
@PutMapping(value = "/{id}")
public Result update(@RequestBody User user, @PathVariable String id) {
………………
这样用户微服务就配置好了。
网关配置
访问微服务的JWT令牌是放在请求头中一个叫“Authorization”的参数中。以“bearer”开头。因为请求是通过网关转发给相应的微服务,所以可以对网关进行配置。之前在网关微服务中写了一个过滤器叫AuthorizeFilter,里面的filter()方法写的是分别从请求头、参数、Cookie中获取token信息,然后进行校验。现在稍微修改一下,现在不校验了,只判断有无token信息并进行简单处理。
还有一点就是有些请求比如登录注册等不需要token的就直接放行。
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
…………
if (needlessToken(request.getURI().toString())) {
return chain.filter(exchange); //如果是不需要token的请求就直接放行
}
//还是没有Token就拦截
if (StringUtils.isEmpty(token)){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
} else {
if (!hasTokenInHeader) {
if (!(token.startsWith("brarer ") || token.startsWith("Bearer "))) {
token = "Bearer " + token;
}
request.mutate().header("Authorization",token);
}
}
//Token不为空就校验Token
// try {
// JwtUtil.parseJWT(token);
// } catch (Exception e) {
// //报异常说明Token是错误的,拦截
// response.setStatusCode(HttpStatus.UNAUTHORIZED);
// return response.setComplete();
// }
return chain.filter(exchange);
}
//判断指定的uri是否不需要token就可以访问,true表示不需要
public boolean needlessToken(String uri) {
String[] uris = new String[]{
"/api/user/add",
"/api/user/login"
};
for (String s : uris) {
if (s.equals(uri)) {
return true;
}
}
return false;
}
代码的意思就是如果请求头中有token的信息就不去管它,如果token在参数或者Cookie中,就看是不是以“**Bearer **”开头,不是的话就添加“Bearer ”,然后存入请求头中。如果是指定的不需要token的请求就直接放行。为什么现在不校验呢?因为校验的工作交给对应的微服务去处理了,网关不负责。
OAuth2.0从数据库加载数据
在之前的配置中,客户端的信息是配置在内存中的。用户也是在内存中指定的。现在来改造一下,从数据库中加载数据。首先是客户端信息,修改AuthorizationServerConfig中的configure()方法。
// 客户端信息配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).clients(clientDetails());
}
//客户端配置
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
我们给客户端配置了一个JdbcClientDetailsService。
然后就可以从oauth_client_details中加载数据了。
咦?好像没有在任何地方指定这张表呀,怎么就能从这张表里面加载数据???前面不是配了个JdbcClientDetailsService么。点进去看看
哦~ 原来是在JdbcClientDetailsService的内部指定好了呀。
现在就是从数据库中加载了客户端的信息,那怎么加载用户的信息呢?这里使用Feign接口去调用用户微服务查询出用户的信息。
//用户微服务中的UserController
@GetMapping({"/{id}","/load/{id}"})
public Result<User> findById(@PathVariable String id) {
//调用UserService实现根据主键查询User
User user = userService.findById(id);
return new Result<User>(true, StatusCode.OK, "查询成功", user);
}
我们给这个方法配置一个“load/{id}”的路径。之前已经配置过这个路径放行了。
然后在用户的api微服务中添加一个Feign接口。
@FeignClient("user")
@RequestMapping("/user")
public interface UserFeign {
//根据ID查询User数据
@GetMapping({"/load/{id}"})
Result<User> findById(@PathVariable String id);
}
这样在认证微服务的UserDetailsServiceImpl的loadUserByUsername()方法中就可以调用Feign去数据库中查询用户信息了。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/*客户端信息认证*/
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
if(authentication==null){
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
if(clientDetails!=null){
//秘钥
String clientSecret = clientDetails.getClientSecret();
//数据库查找方式
return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
/*用户信息认证*/
if (StringUtils.isEmpty(username)) {
return null;
}
com.robod.user.pojo.User user = userFeign.findById(username).getData(); //查询用户
if (user == null ) {
return null;
}
//根据用户名查询用户信息
String pwd = user.getPassword();
//创建User对象
String permissions = "goods_list,seckill_list";
UserJwt userDetails = new UserJwt(username,pwd,
AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
return userDetails;
}
这样就OK了,来测试一下。
这个robod用户是保存在数据库中的,可以正常登录,说明我们的配置是没有问题的。
微服务之间的令牌传递
在实现购物车功能之前,要先明确一个问题,只有登录过的用户才可以访问自己的购物车。所以我们在访问微服务的时候必须要携带令牌,但是还涉及到微服务之间的Feign接口调用,令牌该怎么传递过去呢?令牌不是放在名为“Authorization”的请求头中吗,所以加一个过滤器在Feign调用前调用,将当前请求的请求头信息封装到Feign请求的请求头中。但是呢,这个过滤器是很多微服务共用的,所以可以将这个过滤器放在common工程中,哪个微服务需要就直接注入到Spring容器中。
public class FeignHeaderInterceptor implements RequestInterceptor {
//Feign调用前调用
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();//所有请求头的名字集合
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
template.header(headerName,headerValue);
}
}
}
}
}
如果哪个微服务想要调用的话,就直接在启动类中注入即可。
@Bean
public FeignHeaderInterceptor feignHeaderInterceptor() {
return new FeignHeaderInterceptor();
}
这样就可以实现令牌在不同微服务之间的传递。
购物车
现在就可以来实现购物车的功能了,因为购物车毕竟是用来下单的,所以就将购物车功能写在订单微服务中。创建一个订单微服务changgou-service-order以及一个订单的api工程changgou-service-order-api。
从令牌中获取用户名
添加到购物车的流程就是用户从前端将商品的id和数量以及令牌传过来。然后解析令牌,拿到用户名。然后拿着商品的id用Feign调用Goods微服务将Sku和Spu查询出来,然后封装成一个OrderItem对象,存入Reids中。这个时候Feign接口的调用就涉及到微服务之间的令牌传递问题了,把FeignHeaderInterceptor注入即可。
这里还有一个问题,就是令牌解析,很简单,使用公钥解析即可,封装一个工具类com.robod.order.utils.TokenDecodeUtil
@Component
public class TokenDecodeUtil {
//公钥路径
private static final String PUBLIC_KEY_PATH = "public.key";
//公钥的内容
private static String publicKey="";
/***
* 获取用户信息
* @return
*/
public Map<String,String> getUserInfo() {
//获取授权信息
OAuth2AuthenticationDetails authentication =
(OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
//令牌解码
return decodeToken(authentication.getTokenValue());
}
/***
* 读取令牌数据
*/
public Map<String,String> decodeToken(String token){
//校验Jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(getPubKey()));
//获取Jwt原始内容
String claims = jwt.getClaims();
return JSON.parseObject(claims,Map.class);
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
public String getPubKey() {
if(!StringUtils.isEmpty(publicKey)){
return publicKey;
}
Resource resource = new ClassPathResource(PUBLIC_KEY_PATH);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
publicKey = br.lines().collect(Collectors.joining("\n"));
return publicKey;
} catch (IOException ioe) {
return null;
}
}
}
准备工作做好以后就可以编写相应的逻辑了。
添加到购物车
// CartController
@GetMapping("/add")
public Result add(long id,int num) {
String username = tokenDecodeUtil.getUserInfo().get("username");
cartService.add(id,num,username);
return new Result(true, StatusCode.OK,"成功添加到购物车");
}
//-----------------------------------------------------------------------
// CartServiceImpl
@Override
public void add(long id, int num, String username) {
BoundHashOperations boundHashOperations = redisTemplate.boundHashOps("Cart_" + username);
if (num <= 0){
boundHashOperations.delete(id);
Long size = boundHashOperations.size();
if (size == null || size<=0) {
redisTemplate.delete("Cart_" + username);
}
return;
}
Sku sku = skuFeign.findById(id).getData();
if (sku == null) {
throw new RuntimeException("未查询到商品信息");
}
Spu spu = spuFeign.findById(sku.getSpuId()).getData();
if (spu == null) {
throw new RuntimeException("数据库中数据异常");
}
OrderItem orderItem = createOrderItem(spu,sku,num);
boundHashOperations.put(id,orderItem);
}
private OrderItem createOrderItem(Spu spu,Sku sku,int num) {
OrderItem orderItem = new OrderItem();
orderItem.setCategoryId1(spu.getCategory1Id());
orderItem.setCategoryId2(spu.getCategory2Id());
orderItem.setCategoryId3(spu.getCategory3Id());
orderItem.setSpuId(spu.getId());
orderItem.setSkuId(sku.getId());
orderItem.setName(sku.getName());
orderItem.setNum(num);
orderItem.setPrice(sku.getPrice());
orderItem.setMoney(num * sku.getPrice());
orderItem.setImage(spu.getImage());
return orderItem;
}
但是现在访问的话还存在问题,FeignHeaderInterceptor里面ServletRequestAttributes的数据
这是因为现在的feign的隔离策略是THREAD(线程池隔离),这时候使用Feign的时候是单独开启一个线程的,不是之前的线程,所以获取不到数据。要想使用同一个线程去使用feign,可以把隔离策略设置成SEMAPHORE(信号量隔离)。在order微服务的配置文件中添加一段配置
#hystrix 配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE
这两种隔离策略的区别是
线程池隔离 | 信号量隔离 | |
---|---|---|
线程 | 与调用线程非相同线程 | 与调用线程相同(jetty线程) |
开销 | 排队、调度、上下文开销等 | 无线程切换,开销低 |
异步 | 支持 | 不支持 |
并发支持 | 支持(最大线程池大小) | 支持(最大信号量上限) |
这样就可以正常地添加数据到购物车了。
查询购物车
查询购物车的功能很简单,前端携带着令牌向服务器发送请求,Controller调用相应的方法解析令牌拿到用户名,然后调用Service层从Redis中获取到对应的数据。
// CartController
@GetMapping("/list")
public Result<List<OrderItem>> list() {
String username = tokenDecodeUtil.getUserInfo().get("username");
List<OrderItem> orderItems = cartService.list(username);
return new Result<>(true,StatusCode.OK,"查询购物车成功",orderItems);
}
//--------------------------------------------------------------------------
// CartServiceImpl
@Override
public List<OrderItem> list(String username) {
return (List<OrderItem>) redisTemplate.boundHashOps("Cart_" + username).values();
}
权限控制
虽然购物车的功能已经实现了,但是还存在一个问题,就是没有对用户进行权限校验,我们可以去限制某个方法只能由拥有特定权限的用户去访问,要是不进行权限控制的话,任何人都可以访问就会很不安全。这里我准备对购物车用到的四个方法添加“USER”权限的控制。
首先为订单微服务和商品微服务添加OAuth2.0的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
然后将之前提取出来的public.key文件添加到这两个微服务的resources目录下。最后添加资源服务的配置类。
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String PUBLIC_KEY = "public.key";//公钥
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPublicKey());
return converter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
private String getPublicKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
.anyRequest().permitAll();//.authenticated(); //其他地址需要认证授权
}
}
这里限制所有的请求都必须通过验证。然后我们在相应的方法上添加注解即可。
// SkuController
@GetMapping("/{id}")
@PreAuthorize("hasAnyAuthority('USER')")
public Result<Sku> findById(@PathVariable Long id){
// SpuController
@GetMapping("/{id}")
@PreAuthorize("hasAnyAuthority('USER')")
public Result<Spu> findById(@PathVariable Long id){
// CartController
@GetMapping("/add")
@PreAuthorize("hasAnyAuthority('USER')")
public Result add(long id,int num) {
@GetMapping("/list")
@PreAuthorize("hasAnyAuthority('USER')")
public Result<List<OrderItem>> list() {
这四个方法限制了只有拥有USER权限才可以访问,如果没有相应的权限请求就会被拒绝。
订单微服务对接网关
现在就差最后一步了,就是将订单微服务对接到网关,然后就可以通过网关将请求转发到订单微服务了。
在网关微服务的配置文件中添加订单微服务的路由配置
spring:
cloud:
routes:
- id: changgou_order_route
uri: http://localhost:18089
predicates:
-Path=/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,
/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,
/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**
filters:
- StripPrefix=1
这样就OK了。
总结
原本的代码中,OAuth2.0是从内存中获取数据,文章的开头先是改了代码从数据库中获取数据。然后实现了购物车的功能并实现了权限控制。最后将订单微服务对接到了网关中,实现了通过网关去访问相应的微服务。
如果我的文章对你有些帮助,不要忘了点赞,收藏,转发,关注。要是有什么好的意见欢迎在下方留言。让我们下期再见!