微服务架构

  • 网关:路由用户请求到指定服务,转发前端 Cookie 中包含的 Session 信息;
  • 用户服务:用户登录认证(Authentication),用户授权(Authority),用户管理(Redis Session Management)
  • 其他服务:依赖 Redis 中用户信息进行接口请求验证

用户 - 角色 - 权限表结构设计

  • 权限表

    权限表最小粒度的控制单个功能,例如用户管理、资源管理,表结构示例:
id authority description
1 ROLE_ADMIN_USER 管理所有用户
2 ROLE_ADMIN_RESOURCE 管理所有资源
3 ROLE_A_1 访问 ServiceA 的某接口的权限
4 ROLE_A_2 访问 ServiceA 的另一个接口的权限
5 ROLE_B_1 访问 ServiceB 的某接口的权限
6 ROLE_B_2 访问 ServiceB 的另一个接口的权限
  • 角色 - 权限表

    自定义角色,组合各种权限,例如超级管理员拥有所有权限,表结构示例:
id name authority_ids
1 超级管理员 1,2,3,4,5,6
2 管理员A 3,4
3 管理员B 5,6
4 普通用户 NULL
  • 用户 - 角色表

    用户绑定一个或多个角色,即分配各种权限,示例表结构:
user_id role_id
1 1
1 4
2 2

用户服务设计

Maven 依赖(所有服务)

 <!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency> <!-- Spring Session Redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

应用配置 application.yml 示例:

# Spring Session 配置
spring.session.store-type=redis
server.servlet.session.persistent=true
server.servlet.session.timeout=7d
server.servlet.session.cookie.max-age=7d # Redis 配置
spring.redis.host=<redis-host>
spring.redis.port=6379 # MySQL 配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://<mysql-host>:3306/test
spring.datasource.username=<username>
spring.datasource.password=<passowrd>

用户登录认证(authentication)与授权(authority)

Slf4j
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final UserService userService; CustomAuthenticationFilter(String defaultFilterProcessesUrl, UserService userService) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl, HttpMethod.POST.name()));
this.userService = userService;
} @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
JSONObject requestBody = getRequestBody(request);
String username = requestBody.getString("username");
String password = requestBody.getString("password");
UserDO user = userService.getByUsername(username);
if (user != null && validateUsernameAndPassword(username, password, user)){
// 查询用户的 authority
List<SimpleGrantedAuthority> userAuthorities = userService.getSimpleGrantedAuthority(user.getId());
return new UsernamePasswordAuthenticationToken(user.getId(), null, userAuthorities);
}
throw new AuthenticationServiceException("登录失败");
} /**
* 获取请求体
*/
private JSONObject getRequestBody(HttpServletRequest request) throws AuthenticationException{
try {
StringBuilder stringBuilder = new StringBuilder();
InputStream inputStream = request.getInputStream();
byte[] bs = new byte[StreamUtils.BUFFER_SIZE];
int len;
while ((len = inputStream.read(bs)) != -1) {
stringBuilder.append(new String(bs, 0, len));
}
return JSON.parseObject(stringBuilder.toString());
} catch (IOException e) {
log.error("get request body error.");
}
throw new AuthenticationServiceException(HttpRequestStatusEnum.INVALID_REQUEST.getMessage());
} /**
* 校验用户名和密码
*/
private boolean validateUsernameAndPassword(String username, String password, UserDO user) throws AuthenticationException {
return username == user.getUsername() && password == user.getPassword();
} } @EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private static final String LOGIN_URL = "/user/login"; private static final String LOGOUT_URL = "/user/logout"; private final UserService userService; @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(LOGIN_URL).permitAll()
.anyRequest().authenticated()
.and()
.logout().logoutUrl(LOGOUT_URL).clearAuthentication(true).permitAll()
.and()
.csrf().disable(); http.addFilterAt(bipAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.rememberMe().alwaysRemember(true);
} /**
* 自定义认证过滤器
*/
private CustomAuthenticationFilter customAuthenticationFilter() {
CustomAuthenticationFilter authenticationFilter = new CustomAuthenticationFilter(LOGIN_URL, userService);
return authenticationFilter;
} }

其他服务设计

应用配置 application.yml 示例:

# Spring Session 配置
spring.session.store-type=redis # Redis 配置
spring.redis.host=<redis-host>
spring.redis.port=6379

全局安全配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
} }

用户认证信息获取

用户通过用户服务登录成功后,用户信息会被缓存到 Redis,缓存的信息与 CustomAuthenticationFilterattemptAuthentication() 方法返回的对象有关,如上所以,返回的对象是 new UsernamePasswordAuthenticationToken(user.getId(), null, userAuthorities),即 Redis 缓存了用户的 ID 和用户的权力(authorities)。

UsernamePasswordAuthenticationToken 构造函数的第一个参数是 Object 对象,所以可以自定义缓存对象。

在微服务各个模块获取用户的这些信息的方法如下:

@GetMapping()
public WebResponse test(@AuthenticationPrincipal UsernamePasswordAuthenticationToken authenticationToken){
// 略
}

权限控制

  • 启用基于方法的权限注解
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class Application { public static void main(String[] args) {
SpringApplication.run(Application.class, args);
} }
  • 简单权限校验

    例如,删除角色的接口,仅允许拥有 ROLE_ADMIN_USER 权限的用户访问。
/**
* 删除角色
*/
@PostMapping("/delete")
@PreAuthorize("hasRole('ADMIN_USER')")
public WebResponse deleteRole(@RequestBody RoleBean roleBean){
// 略
}

@PreAuthorize("hasRole('<authority>')") 可作用于微服务中的各个模块

  • 自定义权限校验

    如上所示,hasRole() 方法是 Spring Security 内嵌的,如需自定义,可以使用 Expression-Based Access Control,示例:
/**
* 自定义校验服务
*/
@Service
public class CustomService{ public boolean check(UsernamePasswordAuthenticationToken authenticationToken, String extraParam){
// 略
} } /**
* 删除角色
*/
@PostMapping()
@PreAuthorize("@customService.check(authentication, #userBean.username)")
public WebResponse custom(@RequestBody UserBean userBean){
// 略
}

authentication 属于内置对象, # 获取入参的值

  • 任意用户权限动态修改

    原理上,用户的权限信息保存在 Redis 中,修改用户权限就需要操作 Redis,示例:
@Service
@AllArgsConstructor
public class HttpSessionService<S extends Session> { private final FindByIndexNameSessionRepository<S> sessionRepository; /**
* 重置用户权限
*/
public void resetAuthorities(Long userId, List<GrantedAuthority> authorities){
UsernamePasswordAuthenticationToken newToken = new UsernamePasswordAuthenticationToken(userId, null, authorities);
Map<String, S> redisSessionMap = sessionRepository.findByPrincipalName(String.valueOf(userId));
redisSessionMap.values().forEach(session -> {
SecurityContextImpl securityContext = session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
securityContext.setAuthentication(newToken);
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext);
sessionRepository.save(session);
});
} }

修改用户权限,仅需调用 httpSessionService.resetAuthorities() 方法即可,实时生效。

© 著作权归作者所有,转载或内容合作请联系作者

Spring Cloud Gateway - 快速开始

APM工具寻找了一圈,发现SkyWalking才是我的真爱

Spring Boot 注入外部配置到应用内部的静态变量

将 HTML 转化为 PDF新姿势

Java 使用 UnixSocket 调用 Docker API

Fastjson致命缺陷

Service Mesh - gRPC 本地联调远程服务

使用 Thymeleaf 动态渲染 HTML

Fastjson致命缺陷

Spring Boot 2 集成log4j2日志框架

Java面试通关要点汇总集之核心篇参考答案

Java面试通关要点汇总集之框架篇参考答案

Spring Security 实战干货:如何保护用户密码

Spring Boot RabbitMQ - 优先级队列

原文链接:https://mp.weixin.qq.com/s?__biz=MzU0MDEwMjgwNA==&mid=2247486167&idx=2&sn=76dba01d16b7147c9b1dfb7cbf2d8d28&chksm=fb3f132ccc489a3ad2ea05314823d660c40e8af90dcd35800422899958f98b4a258d23badba8&token=280305379&lang=zh_CN#rd

本文由博客一文多发平台 OpenWrite 发布!

别再让你的微服务裸奔了,基于 Spring Session & Spring Security 微服务权限控制的更多相关文章

  1. .NET Core微服务之路:基于Consul最少集群实现服务的注册与发现(二)

    重温Consul最少化集群的搭建  

  2. Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

    技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢. 当然,因为是REST API接口,外部客户端直接调用各个微服务是没有问 ...

  3. 几种常见的微服务架构方案简述——ZeroC IceGrid、Spring Cloud、基于消息队列

    微服务架构是当前很热门的一个概念,它不是凭空产生的,是技术发展的必然结果.虽然微服务架构没有公认的技术标准和规范草案,但业界已经有一些很有影响力的开源微服务架构平台,架构师可以根据公司的技术实力并结合 ...

  4. Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十一):服务网关(Zuul)

    在线演示 演示地址:http://139.196.87.48:9002/kitty 用户名:admin 密码:admin 技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡 ...

  5. Spring Cloud(六)服务网关 zuul 快速入门

    服务网关是微服务架构中一个不可或缺的部分.通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由.均衡负载功能之外,它还具备了权限控制等功能.Spring Cloud Netflix中 ...

  6. 综合使用spring cloud技术实现微服务应用

    在之前的章节,我们已经实现了配置服务器.注册服务器.微服务服务端,实现了服务注册与发现.这一章将实现微服务的客户端,以及联调.实现整个spring cloud框架核心应用. 本文属于<7天学会s ...

  7. spring Boot+spring Cloud实现微服务详细教程第二篇

    上一篇文章已经说明了一下,关于spring boot创建maven项目的简单步骤,相信很多熟悉Maven+Eclipse作为开发常用工具的朋友们都一目了然,这篇文章主要讲解一下,构建spring bo ...

  8. 微服务架构的基础框架选择:Spring Cloud还是Dubbo?

    最近一段时间不论互联网还是传统行业,凡是涉及信息技术范畴的圈子几乎都在讨论微服务架构.近期也看到各大技术社区开始组织一些沙龙和论坛来分享Spring Cloud的相关实施经验,这对于最近正在整理Spr ...

  9. .NET Core微服务之基于Steeltoe使用Spring Cloud Config统一管理配置

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 =>  Steeltoe目录快速导航: 1. 基于Steeltoe使用Spring Cloud Eureka 2. 基于Steelt ...

随机推荐

  1. [Leetcode] 第324题 摆动排序II

    一.题目描述 给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序. 示例 1: 输入: nums ...

  2. WebStorm2017.3.4版本 注册码

    http://idea.singee77.com http://im.js.cn:8888

  3. [Python]字典的简单用法

    Python中的字典与现实中字典类似,从字典中可以找到“鱼”字:鱼类是体被骨鳞.以鳃呼吸.通过尾部和躯干部的摆动以及鳍的协调作用游泳和凭上下颌摄食的变温水生脊椎动物.类比到Python的字典中,把“鱼 ...

  4. HTML5-常用正则表达式

    有关H5正则表达式的一些常用式子,希望热爱编程的同学们多多指教,还有也希望可以关注收藏本站哦!❤^_^❤ 一.校验数字的表达式 1. 数字:^[0-9]*$ 2. n位的数字:^\d{n}$ 3. 至 ...

  5. Python将自己写的模块进行打包

    将项目打包成模块的想法来自于flask文档教程,这不是在PyCon上和阿明合了照嘛,这不得多看看人家的东西.有兴趣的可以看看文档的项目可安装化部分,作者将flask项目打包成一个包,使其可以再任何地方 ...

  6. 蓝松SDK - 卡点视频制作介绍

    ---恢复内容开始--- 说明:卡点视频:是指随音频的节拍来不断的切换照片做成的一种 动感视频效果.卡点是卡的音乐中节奏切换的时间点, 在这些时间点上动态切换一个图片, 并给图片做各种动画,从而形成或 ...

  7. Linux端口占用情况查看

    1,查看8010端口是否被占用[root@cloud ~]# netstat -an|grep 8010tcp 0 0 0.0.0.0:8010 0.0.0.0:* LISTEN 2,查看8010是被 ...

  8. C++——多文件结构和编译预处理命令

    [toc] 一.多文件结构 1.一个工程可以划分为多个源文件 类声明文件(.h文件) 类实现文件(.cpp文件) 类的使用文件(main函数所在的.cpp文件) 2.利用工程来组合各个文件 //Poi ...

  9. JAVA TCP/IP网络通讯编程(一)

    一个实例通过client端和server端通讯 客户端发送:“我是客户端,请多关照” 服务端回复:“收到来自于"+s.getInetAddress().getHostName()+" ...

  10. _self.$scopedSlots.default is not a function报错

    问题: 当同一页面有elementUI的多个table表格时,如果用到v-if来动态展示表格,切换时出现如下报错: 原因: 是因为表格是element-ui通过循环产生的,而vue在dom重新渲染时有 ...