別再讓你的微服務裸奔了,基於 Spring Session & Spring Security 微服務權限控制

  • 2019 年 10 月 30 日
  • 筆記

微服務架構

  • 網關:路由用戶請求到指定服務,轉發前端 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 發佈!