1. 程式人生 > >別再讓你的微服務裸奔了,基於 Spring Session & Spring Security 微服務許可權控制

別再讓你的微服務裸奔了,基於 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() 方法即可,實時生效。

© 著作權歸作者所有,轉載或內容合作請聯絡作者

● 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 釋出!