• 别再让你的微服务裸奔了,基于 Spring Session & Spring Security 微服务权限控制


    微服务架构

    • 网关:路由用户请求到指定服务,转发前端 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() 方法即可,实时生效。

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

    img

    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 发布!

  • 相关阅读:
    发短信集合类-阿里云短信涉及类
    第四方 fast快捷支付封装
    佰米支付封装
    支付宝支付封装【修改至2021.01.11】
    关于支付
    tp5下的文件上传与下载类
    发送短信集合类
    文件中设置开启访问权限
    SpringBoot注解分析
    HashMap底层实现原理及面试问题
  • 原文地址:https://www.cnblogs.com/springforall/p/11762374.html
Copyright © 2020-2023  润新知